@@ -45,8 +45,48 @@ export function clampCadence(kind: WatchKind, minutes: number): number {
4545 return options . find ( ( option ) => option >= minutes ) ?? options [ options . length - 1 ] ! ;
4646}
4747
48- /** Swap the condition for its sibling variant (§3), carrying everything else. */
48+ /**
49+ * The note, restated from the spec. The note is "why the user asked for it" and
50+ * the wake narration quotes it — so once the user CHANGES the condition (or its
51+ * number), the original words no longer describe the watch and must be rewritten.
52+ * Edits that keep the condition (window, cadence) keep the user's own words.
53+ */
54+ export function noteFor ( spec : WatchSpec ) : string {
55+ switch ( spec . kind ) {
56+ case "run_start" :
57+ return `tell me when run ${ spec . runId } starts` ;
58+ case "run_finished" :
59+ return `tell me when run ${ spec . runId } finishes` ;
60+ case "run_failed" :
61+ return `tell me if run ${ spec . runId } fails` ;
62+ case "backlog_drain" :
63+ return `tell me when the ${ spec . queue } queue drains` ;
64+ case "queue_depth_above" :
65+ return `tell me if the ${ spec . queue } queue grows above ${ spec . threshold } ` ;
66+ case "queue_depth_below" :
67+ return `tell me when the ${ spec . queue } queue is back below ${ spec . threshold } ` ;
68+ case "queue_stalled" :
69+ return `tell me if the ${ spec . queue } queue stops moving` ;
70+ case "queue_oldest_age" :
71+ return `tell me if runs in ${ spec . queue } wait longer than ${ spec . thresholdMinutes } minutes` ;
72+ case "error_recurrence" :
73+ return `ping me if error ${ spec . fingerprint } happens again` ;
74+ case "health_recovery" :
75+ return "tell me when health is back to normal" ;
76+ }
77+ }
78+
79+ /**
80+ * Swap the condition for its sibling variant (§3), carrying everything else —
81+ * except the note, which is restated to describe the NEW condition.
82+ */
4983export function withVariant ( draft : WatchDraft , kind : WatchKind ) : WatchDraft {
84+ const next = variantSpec ( draft , kind ) ;
85+ if ( next === draft . spec ) return draft ;
86+ return { ...draft , spec : { ...next , note : noteFor ( next ) } as WatchSpec } ;
87+ }
88+
89+ function variantSpec ( draft : WatchDraft , kind : WatchKind ) : WatchSpec {
5090 const { spec } = draft ;
5191 const common = {
5292 note : spec . note ,
@@ -59,36 +99,36 @@ export function withVariant(draft: WatchDraft, kind: WatchKind): WatchDraft {
5999 case "run_failed" :
60100 case "run_start" : {
61101 const runId = "runId" in spec ? spec . runId : "" ;
62- return { ...draft , spec : { ... common , kind, runId } as WatchSpec } ;
102+ return { ...common , kind, runId } as WatchSpec ;
63103 }
64104 case "backlog_drain" : {
65105 const queue = "queue" in spec ? spec . queue : "" ;
66- return { ...draft , spec : { ... common , kind, queue } as WatchSpec } ;
106+ return { ...common , kind, queue } as WatchSpec ;
67107 }
68108 case "queue_depth_above" :
69109 case "queue_depth_below" : {
70110 const queue = "queue" in spec ? spec . queue : "" ;
71111 // The number carries across the two threshold questions: someone who typed
72112 // 500 for "above" means the same 500 when they flip to "back below".
73113 const threshold = "threshold" in spec ? spec . threshold : WATCH_DEFAULT_QUEUE_THRESHOLD ;
74- return { ...draft , spec : { ... common , kind, queue, threshold } as WatchSpec } ;
114+ return { ...common , kind, queue, threshold } as WatchSpec ;
75115 }
76116 case "queue_stalled" : {
77117 const queue = "queue" in spec ? spec . queue : "" ;
78118 // K is not user-facing in this iteration (§3): the default is the product
79119 // decision, and the card never shows a field for it.
80120 const ticks = "ticks" in spec ? spec . ticks : WATCH_STALL_TICKS_DEFAULT ;
81- return { ...draft , spec : { ... common , kind, queue, ticks } as WatchSpec } ;
121+ return { ...common , kind, queue, ticks } as WatchSpec ;
82122 }
83123 case "queue_oldest_age" : {
84124 const queue = "queue" in spec ? spec . queue : "" ;
85125 const thresholdMinutes =
86126 "thresholdMinutes" in spec ? spec . thresholdMinutes : WATCH_DEFAULT_QUEUE_AGE_MINUTES ;
87- return { ...draft , spec : { ... common , kind, queue, thresholdMinutes } as WatchSpec } ;
127+ return { ...common , kind, queue, thresholdMinutes } as WatchSpec ;
88128 }
89129 // The kinds with no second question keep the draft untouched.
90130 default :
91- return draft ;
131+ return draft . spec ;
92132 }
93133}
94134
@@ -125,7 +165,9 @@ export function withThreshold(draft: WatchDraft, threshold: number): WatchDraft
125165 if ( draft . spec . kind !== "queue_depth_above" && draft . spec . kind !== "queue_depth_below" ) {
126166 return draft ;
127167 }
128- return { ...draft , spec : { ...draft . spec , threshold } } ;
168+ // The note quotes the number, so a new number restates the note.
169+ const spec = { ...draft . spec , threshold } ;
170+ return { ...draft , spec : { ...spec , note : noteFor ( spec ) } } ;
129171}
130172
131173/**
@@ -134,7 +176,9 @@ export function withThreshold(draft: WatchDraft, threshold: number): WatchDraft
134176 */
135177export function withAgeMinutes ( draft : WatchDraft , thresholdMinutes : number ) : WatchDraft {
136178 if ( draft . spec . kind !== "queue_oldest_age" ) return draft ;
137- return { ...draft , spec : { ...draft . spec , thresholdMinutes } } ;
179+ // The note quotes the number, so a new number restates the note.
180+ const spec = { ...draft . spec , thresholdMinutes } ;
181+ return { ...draft , spec : { ...spec , note : noteFor ( spec ) } } ;
138182}
139183
140184/**
0 commit comments