@dzhechkov/harness-core 0.8.11 → 0.8.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/.dz-manifest.json +399 -139
  2. package/LICENSE +13 -0
  3. package/README.md +420 -6
  4. package/dist/agents-policy.d.ts +15 -1
  5. package/dist/agents-policy.d.ts.map +1 -1
  6. package/dist/agents-policy.js +27 -1
  7. package/dist/agents-policy.js.map +1 -1
  8. package/dist/amendment-trace.d.ts +72 -0
  9. package/dist/amendment-trace.d.ts.map +1 -1
  10. package/dist/amendment-trace.js +385 -17
  11. package/dist/amendment-trace.js.map +1 -1
  12. package/dist/backlog-public.d.ts +153 -0
  13. package/dist/backlog-public.d.ts.map +1 -0
  14. package/dist/backlog-public.js +415 -0
  15. package/dist/backlog-public.js.map +1 -0
  16. package/dist/backlog-transitions.d.ts +48 -0
  17. package/dist/backlog-transitions.d.ts.map +1 -0
  18. package/dist/backlog-transitions.js +64 -0
  19. package/dist/backlog-transitions.js.map +1 -0
  20. package/dist/backlog.d.ts.map +1 -1
  21. package/dist/backlog.js +13 -0
  22. package/dist/backlog.js.map +1 -1
  23. package/dist/claim-check.d.ts.map +1 -1
  24. package/dist/claim-check.js +24 -2
  25. package/dist/claim-check.js.map +1 -1
  26. package/dist/claude-hooks-assets.d.ts +93 -0
  27. package/dist/claude-hooks-assets.d.ts.map +1 -0
  28. package/dist/claude-hooks-assets.js +208 -0
  29. package/dist/claude-hooks-assets.js.map +1 -0
  30. package/dist/clean-room-smoke.d.ts +83 -0
  31. package/dist/clean-room-smoke.d.ts.map +1 -0
  32. package/dist/clean-room-smoke.js +138 -0
  33. package/dist/clean-room-smoke.js.map +1 -0
  34. package/dist/cmd-usage.d.ts.map +1 -1
  35. package/dist/cmd-usage.js +36 -6
  36. package/dist/cmd-usage.js.map +1 -1
  37. package/dist/codex-hooks-assets.d.ts +25 -7
  38. package/dist/codex-hooks-assets.d.ts.map +1 -1
  39. package/dist/codex-hooks-assets.js +138 -30
  40. package/dist/codex-hooks-assets.js.map +1 -1
  41. package/dist/codex-hooks.d.ts +21 -1
  42. package/dist/codex-hooks.d.ts.map +1 -1
  43. package/dist/codex-hooks.js +21 -1
  44. package/dist/codex-hooks.js.map +1 -1
  45. package/dist/course-staleness.d.ts +19 -0
  46. package/dist/course-staleness.d.ts.map +1 -0
  47. package/dist/course-staleness.js +95 -0
  48. package/dist/course-staleness.js.map +1 -0
  49. package/dist/destructive-guard-hook.d.ts +40 -0
  50. package/dist/destructive-guard-hook.d.ts.map +1 -0
  51. package/dist/destructive-guard-hook.js +109 -0
  52. package/dist/destructive-guard-hook.js.map +1 -0
  53. package/dist/destructive-guard.d.ts +27 -0
  54. package/dist/destructive-guard.d.ts.map +1 -0
  55. package/dist/destructive-guard.js +2808 -0
  56. package/dist/destructive-guard.js.map +1 -0
  57. package/dist/discrimination-gate.d.ts +28 -3
  58. package/dist/discrimination-gate.d.ts.map +1 -1
  59. package/dist/discrimination-gate.js +76 -16
  60. package/dist/discrimination-gate.js.map +1 -1
  61. package/dist/feature-adr-routing.d.ts +95 -1
  62. package/dist/feature-adr-routing.d.ts.map +1 -1
  63. package/dist/feature-adr-routing.js +193 -27
  64. package/dist/feature-adr-routing.js.map +1 -1
  65. package/dist/guard.d.ts +90 -0
  66. package/dist/guard.d.ts.map +1 -1
  67. package/dist/guard.js +271 -0
  68. package/dist/guard.js.map +1 -1
  69. package/dist/harness-core-location.d.ts +18 -0
  70. package/dist/harness-core-location.d.ts.map +1 -0
  71. package/dist/harness-core-location.js +42 -0
  72. package/dist/harness-core-location.js.map +1 -0
  73. package/dist/index.d.ts +24 -6
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +24 -3
  76. package/dist/index.js.map +1 -1
  77. package/dist/lead-shift-gate.d.ts +95 -0
  78. package/dist/lead-shift-gate.d.ts.map +1 -0
  79. package/dist/lead-shift-gate.js +100 -0
  80. package/dist/lead-shift-gate.js.map +1 -0
  81. package/dist/ledger-backfill.d.ts +11 -1
  82. package/dist/ledger-backfill.d.ts.map +1 -1
  83. package/dist/ledger-backfill.js +19 -0
  84. package/dist/ledger-backfill.js.map +1 -1
  85. package/dist/loop-blobs.generated.d.ts +1 -1
  86. package/dist/loop-blobs.generated.d.ts.map +1 -1
  87. package/dist/loop-blobs.generated.js +12 -3
  88. package/dist/loop-blobs.generated.js.map +1 -1
  89. package/dist/loop-lint.d.ts.map +1 -1
  90. package/dist/loop-lint.js +56 -7
  91. package/dist/loop-lint.js.map +1 -1
  92. package/dist/loop-plan-graph.d.ts +1 -3
  93. package/dist/loop-plan-graph.d.ts.map +1 -1
  94. package/dist/loop-plan-graph.js +70 -1
  95. package/dist/loop-plan-graph.js.map +1 -1
  96. package/dist/loop-trace.d.ts.map +1 -1
  97. package/dist/loop-trace.js +16 -2
  98. package/dist/loop-trace.js.map +1 -1
  99. package/dist/managed-hooks.d.ts +5 -6
  100. package/dist/managed-hooks.d.ts.map +1 -1
  101. package/dist/managed-hooks.js +2 -2
  102. package/dist/managed-hooks.js.map +1 -1
  103. package/dist/model-recommender.d.ts.map +1 -1
  104. package/dist/model-recommender.js +14 -3
  105. package/dist/model-recommender.js.map +1 -1
  106. package/dist/operations.d.ts.map +1 -1
  107. package/dist/operations.js +82 -0
  108. package/dist/operations.js.map +1 -1
  109. package/dist/patterns.d.ts +23 -0
  110. package/dist/patterns.d.ts.map +1 -1
  111. package/dist/patterns.js +10 -0
  112. package/dist/patterns.js.map +1 -1
  113. package/dist/publish.d.ts +11 -0
  114. package/dist/publish.d.ts.map +1 -1
  115. package/dist/publish.js +16 -2
  116. package/dist/publish.js.map +1 -1
  117. package/dist/registry.d.ts.map +1 -1
  118. package/dist/registry.js +3 -2
  119. package/dist/registry.js.map +1 -1
  120. package/dist/score.d.ts.map +1 -1
  121. package/dist/score.js +15 -4
  122. package/dist/score.js.map +1 -1
  123. package/dist/session-retro.d.ts +121 -2
  124. package/dist/session-retro.d.ts.map +1 -1
  125. package/dist/session-retro.js +454 -11
  126. package/dist/session-retro.js.map +1 -1
  127. package/dist/setup.d.ts +28 -0
  128. package/dist/setup.d.ts.map +1 -1
  129. package/dist/setup.js +223 -6
  130. package/dist/setup.js.map +1 -1
  131. package/dist/sign.d.ts.map +1 -1
  132. package/dist/sign.js +18 -1
  133. package/dist/sign.js.map +1 -1
  134. package/dist/skills-verify.d.ts +34 -1
  135. package/dist/skills-verify.d.ts.map +1 -1
  136. package/dist/skills-verify.js +82 -0
  137. package/dist/skills-verify.js.map +1 -1
  138. package/dist/stage-line.d.ts +68 -0
  139. package/dist/stage-line.d.ts.map +1 -0
  140. package/dist/stage-line.js +129 -0
  141. package/dist/stage-line.js.map +1 -0
  142. package/dist/statusline.d.ts +99 -0
  143. package/dist/statusline.d.ts.map +1 -1
  144. package/dist/statusline.js +310 -44
  145. package/dist/statusline.js.map +1 -1
  146. package/dist/store-counts.d.ts +26 -0
  147. package/dist/store-counts.d.ts.map +1 -0
  148. package/dist/store-counts.js +125 -0
  149. package/dist/store-counts.js.map +1 -0
  150. package/dist/store-guard.d.ts +106 -0
  151. package/dist/store-guard.d.ts.map +1 -0
  152. package/dist/store-guard.js +294 -0
  153. package/dist/store-guard.js.map +1 -0
  154. package/dist/swarm-brief.d.ts +95 -0
  155. package/dist/swarm-brief.d.ts.map +1 -0
  156. package/dist/swarm-brief.js +660 -0
  157. package/dist/swarm-brief.js.map +1 -0
  158. package/dist/trace-bundle.d.ts +8 -0
  159. package/dist/trace-bundle.d.ts.map +1 -1
  160. package/dist/trace-bundle.js +11 -0
  161. package/dist/trace-bundle.js.map +1 -1
  162. package/package.json +12 -11
  163. package/sbom.json +804 -154
  164. package/src/agents-policy.ts +46 -2
  165. package/src/amendment-trace.ts +441 -19
  166. package/src/backlog-public.ts +503 -0
  167. package/src/backlog-transitions.ts +77 -0
  168. package/src/backlog.ts +12 -0
  169. package/src/claim-check.ts +25 -2
  170. package/src/claude-hooks-assets.ts +227 -0
  171. package/src/clean-room-smoke.ts +195 -0
  172. package/src/cmd-usage.ts +29 -5
  173. package/src/codex-hooks-assets.ts +140 -30
  174. package/src/codex-hooks.ts +21 -1
  175. package/src/course-staleness.ts +125 -0
  176. package/src/destructive-guard-hook.ts +151 -0
  177. package/src/destructive-guard.ts +3027 -0
  178. package/src/discrimination-gate.ts +98 -19
  179. package/src/feature-adr-routing.ts +220 -22
  180. package/src/guard.ts +318 -0
  181. package/src/harness-core-location.ts +44 -0
  182. package/src/index.ts +111 -3
  183. package/src/lead-shift-gate.ts +145 -0
  184. package/src/ledger-backfill.ts +20 -1
  185. package/src/loop-blobs.generated.ts +12 -3
  186. package/src/loop-lint.ts +52 -7
  187. package/src/loop-plan-graph.ts +66 -1
  188. package/src/loop-trace.ts +13 -1
  189. package/src/managed-hooks.ts +5 -6
  190. package/src/model-recommender.ts +14 -3
  191. package/src/operations.ts +75 -0
  192. package/src/patterns.ts +33 -0
  193. package/src/publish.ts +27 -2
  194. package/src/registry.ts +3 -2
  195. package/src/score.ts +16 -4
  196. package/src/session-retro.ts +466 -11
  197. package/src/setup.ts +246 -9
  198. package/src/sign.ts +18 -1
  199. package/src/skills-verify.ts +99 -1
  200. package/src/stage-line.ts +151 -0
  201. package/src/statusline.ts +396 -47
  202. package/src/store-counts.ts +154 -0
  203. package/src/store-guard.ts +388 -0
  204. package/src/swarm-brief.ts +661 -0
  205. package/src/trace-bundle.ts +10 -0
@@ -13,6 +13,7 @@
13
13
  *
14
14
  * @packageDocumentation
15
15
  */
16
+ import { type StoreHealthVerdict } from './store-guard.js';
16
17
  /**
17
18
  * Live learning state for one in-flight `/feature-adr` run — the per-run visibility panel
18
19
  * that surfaces the Pattern memory loop (POOL learned, RECALLED for this run, STORED this run).
@@ -36,13 +37,31 @@ export interface FeatureAdrState {
36
37
  readonly reinforced?: number;
37
38
  /** ISO timestamp of the write — drives the freshness window on the render path. */
38
39
  readonly ts: string;
40
+ /** Identity of one pipeline invocation. The monotonic guard never crosses two distinct runs. */
41
+ readonly runId?: string;
39
42
  /** Optional run mode (e.g. "reference", "full-qe", "full-qe-extended"). */
40
43
  readonly mode?: string;
44
+ /** Complexity tier of the run (S/M/L/XL) — drives done/total on the phase line. Carried forward
45
+ * by the write path when a later record omits it. */
46
+ readonly tier?: string;
47
+ /** ISO timestamp of the CURRENT phase's start — reset by the write path when the step label
48
+ * changes, carried forward while it stays the same. Drives the phase line's elapsed minutes. */
49
+ readonly phaseStartTs?: string;
41
50
  }
42
51
  /** A snapshot of dz's self-learning state for one project (all fields best-effort). */
43
52
  export interface StatuslineData {
44
53
  /** Count of learned patterns in the project's unified memory store. */
45
54
  readonly patterns: number;
55
+ /** Exact lexical-tier availability split; omitted when the enhanced readonly count cannot be established. */
56
+ readonly patternBreakdown?: {
57
+ readonly source: 'lexical';
58
+ readonly active: number;
59
+ readonly quarantined: number;
60
+ /** True once quarantine contains at least one third of the lexical pool. */
61
+ readonly attention: boolean;
62
+ /** Absolute lexical/vector quarantine-label delta, present only above the tolerated drift threshold. */
63
+ readonly tierDelta?: number;
64
+ };
46
65
  /** Count of learned patterns that the live recall hook has actually injected at least once. */
47
66
  readonly usedPatterns?: number;
48
67
  /** Number of sources registered in the durable cross-project knowledge brain. */
@@ -51,6 +70,18 @@ export interface StatuslineData {
51
70
  readonly consolidatedAgeH?: number;
52
71
  /** Live `/feature-adr` learning state — present ONLY when a fresh run is in flight. */
53
72
  readonly featureAdr?: FeatureAdrState;
73
+ /** Persistent owner-facing store warning; healthy/no-mark/error paths omit it. */
74
+ readonly storeHealth?: StatuslineStoreHealth;
75
+ }
76
+ /** Render-ready details for a non-healthy store verdict. */
77
+ export interface StatuslineStoreHealth {
78
+ readonly verdict: Exclude<StoreHealthVerdict, 'ok' | 'no-mark'>;
79
+ /** Previous maximum for the affected store tier. */
80
+ readonly previousMax?: number;
81
+ /** Snapshot directory used by cold-start recovery guidance. */
82
+ readonly snapshotPath?: string;
83
+ /** Basenames of store files whose readonly count failed. */
84
+ readonly unreadableFiles?: readonly string[];
54
85
  }
55
86
  /** Directory of the per-slug live `/feature-adr` learning-state slots. */
56
87
  export declare function featureAdrStateDir(projectRoot: string): string;
@@ -82,6 +113,44 @@ export interface WriteFeatureAdrStateInput {
82
113
  readonly stored: number;
83
114
  readonly reinforced?: number;
84
115
  readonly mode?: string;
116
+ /** Identity of one pipeline invocation. Empty strings are not identities. */
117
+ readonly runId?: string;
118
+ /** Complexity tier (S/M/L/XL). Invalid/absent → the previous slot's tier is carried forward. */
119
+ readonly tier?: string;
120
+ /**
121
+ * TEST-ONLY seam (ADR-002 C2 RED half): run the slot transaction WITHOUT the
122
+ * `fa-phase-slot` named lock, so `statusline-phase-lock.test.ts` can reproduce the lost update
123
+ * the lock prevents. Never set by any shipped caller — `statusline-phase-lock.test.ts` asserts
124
+ * `grep -c _unsafeSkipLock` on the CLI source is 0, so this cannot be reached from `dz`.
125
+ */
126
+ readonly _unsafeSkipLock?: boolean;
127
+ /**
128
+ * TEST-ONLY seam, the twin of `_unsafeSkipLock`: busy-hold this many ms INSIDE the transition,
129
+ * between reading the previous slot and writing the new one. Without it the read-modify-write
130
+ * window is sub-millisecond and the RED half would be reproducing a race by LUCK — a flaky test
131
+ * is not a proof. With it the interleaving is deterministic in both halves: unlocked, two writers
132
+ * provably observe the same previous slot; locked, they provably do not. Never set by any shipped
133
+ * caller (same grep assertion as `_unsafeSkipLock`).
134
+ */
135
+ readonly _unsafeHoldMs?: number;
136
+ /**
137
+ * TEST-ONLY seam: fail after phase telemetry has been appended but before the slot is written.
138
+ * This makes the partial-write receipt deterministic without fabricating a phase row from a
139
+ * missing previous slot. Never set by a shipped caller.
140
+ */
141
+ readonly _unsafeFailSlotWrite?: boolean;
142
+ }
143
+ /**
144
+ * The outcome of a slot write. `state` is the slot as written; `refused` is a SHORT machine-ish
145
+ * reason when the operation did not complete. `refusedEffect` says whether no disk effect is known
146
+ * (`none`, callback never began) or a partial effect is possible (`unknown`, callback began).
147
+ * Exactly one of `state` and `refused` is present. The CLI prints `refused` on stderr.
148
+ */
149
+ export interface WriteFeatureAdrStateResult {
150
+ readonly state?: FeatureAdrState;
151
+ readonly refused?: string;
152
+ /** Known disk effect of a refusal: `none` before the transaction, `unknown` once it began. */
153
+ readonly refusedEffect?: 'none' | 'unknown';
85
154
  }
86
155
  /**
87
156
  * Record the live `/feature-adr` learning state — called by the pipeline at Steps 0/8/9. Computes
@@ -92,6 +161,13 @@ export interface WriteFeatureAdrStateInput {
92
161
  * @param now Injectable clock (epoch ms) for the write timestamp — defaults to `Date.now()`.
93
162
  */
94
163
  export declare function writeFeatureAdrState(projectRoot: string, input: WriteFeatureAdrStateInput, now?: number): FeatureAdrState | undefined;
164
+ /**
165
+ * The same write as {@link writeFeatureAdrState}, but it SAYS WHY it did not complete and what is
166
+ * known about its disk effect. A refusal before the transition returns `refusedEffect:'none'`; once
167
+ * the callback begins, a refusal returns `refusedEffect:'unknown'` because an append or slot write
168
+ * may already have landed. The pipeline still never breaks — the caller decides, this never throws.
169
+ */
170
+ export declare function writeFeatureAdrStateDetailed(projectRoot: string, input: WriteFeatureAdrStateInput, now?: number): WriteFeatureAdrStateResult;
95
171
  /**
96
172
  * Gather dz's self-learning counts for one project. FAST + best-effort: every read
97
173
  * is guarded so a missing/corrupt `.dz`, absent native module, or locked store
@@ -101,4 +177,27 @@ export declare function writeFeatureAdrState(projectRoot: string, input: WriteFe
101
177
  * @param now Injectable clock (epoch ms) for the consolidation age — defaults to `Date.now()`.
102
178
  */
103
179
  export declare function statuslineData(projectRoot: string, now?: number): StatuslineData;
180
+ /**
181
+ * Render the /feature-adr PHASE line (line format B, fa-phase-statusline ADR-001) — the second
182
+ * status-bar line:
183
+ *
184
+ * `📐 <slug> [<tier>] <done>/<total> ▶ <step label> · <N>м/—`
185
+ *
186
+ * States, in precedence order:
187
+ * - `undefined` (no line): a loop slot, the terminal label `done`, or ≥90 min of slot silence;
188
+ * - `⚠ <N>м без отчёта` from 30 min of slot silence (counted from the last report `ts`);
189
+ * - a label starting `⛔` or `⏸` replaces the `▶ <label>` portion VERBATIM (failure/waiting states
190
+ * are reported by the pipeline through the label itself, never synthesised here);
191
+ * - otherwise `▶ <label>` with elapsed minutes counted from the PHASE start (`phaseStartTs`).
192
+ *
193
+ * done/total is the label's step-number position in its tier's active-step list; an unknown tier,
194
+ * an unparseable label, or a number outside the list renders `?/?` — the panel never guesses.
195
+ * The estimate slot is fixed-width at the tail: v1 defers estimates entirely, so it is always `/—`
196
+ * (a future median would be computed on the WRITE path and stored in the slot as a ready number —
197
+ * this render path stays a pure function over the slot, no DB, no subprocess, no features/* sweep).
198
+ *
199
+ * @param state A fresh slot from {@link readFeatureAdrState} (the ONLY source of "in progress").
200
+ * @param now Injectable clock (epoch ms) — defaults to `Date.now()`.
201
+ */
202
+ export declare function renderFeatureAdrPhaseLine(state: FeatureAdrState, now?: number): string | undefined;
104
203
  //# sourceMappingURL=statusline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"statusline.d.ts","sourceRoot":"","sources":["../src/statusline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,gGAAgG;IAChG,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;IACvC,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gGAAgG;IAChG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+FAA+F;IAC/F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,uFAAuF;IACvF,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC;CACvC;AAYD,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE9D;AAYD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAI9E;AA8FD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,eAAe,GAAG,SAAS,CA0E9G;AAED,0FAA0F;AAC1F,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,yBAAyB,EAChC,GAAG,GAAE,MAAmB,GACvB,eAAe,GAAG,SAAS,CA0C7B;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,cAAc,CAyC5F"}
1
+ {"version":3,"file":"statusline.d.ts","sourceRoot":"","sources":["../src/statusline.ts"],"names":[],"mappings":"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"}
@@ -13,11 +13,15 @@
13
13
  *
14
14
  * @packageDocumentation
15
15
  */
16
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
17
- import { join, resolve } from 'node:path';
16
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
17
+ import { basename, join, resolve } from 'node:path';
18
18
  import { createRequire } from 'node:module';
19
19
  import { listBrain } from './brain.js';
20
+ import { withNamedLockSync } from './named-lock.js';
20
21
  import { RECALL_USAGE_LOG_RELATIVE, aggregateRecallUsage, parseRecallUsageLog } from './recall-usage.js';
22
+ import { countLearningStoreRowsReadonly, countSqliteRowsReadonly } from './store-counts.js';
23
+ import { checkStoreHealth, readStoreMark, storeSnapshotPath, } from './store-guard.js';
24
+ const QUARANTINE_TIER_DRIFT_TOLERANCE = 5;
21
25
  /** Path of the SQLite pattern store (the Tier-3 backend). */
22
26
  function sqlitePatternPath(projectRoot) {
23
27
  return join(projectRoot, '.dz', 'memory', 'patterns.sqlite');
@@ -50,33 +54,29 @@ export function featureAdrStatePath(projectRoot, slug) {
50
54
  }
51
55
  /**
52
56
  * Freshness window for the `/feature-adr` panel: a run older than this is considered finished, so
53
- * its state must NOT keep showing a stale panel in the status bar. 30 minutes (in ms).
57
+ * its state must NOT keep showing a stale panel in the status bar. 90 minutes (in ms).
58
+ *
59
+ * fa-phase-statusline (ADR-001 D4): extended 30→90 min as an EXPLICIT behaviour change so the phase
60
+ * line can fade in stages — the renderer shows `⚠ <N>м без отчёта` from 30 min of slot silence and
61
+ * nothing at all from 90. One window for the reader; the staging lives in
62
+ * {@link renderFeatureAdrPhaseLine}.
54
63
  */
55
- const FEATURE_ADR_FRESH_MS = 30 * 60 * 1_000;
64
+ const FEATURE_ADR_FRESH_MS = 90 * 60 * 1_000;
65
+ /** Slot silence (ms) after which the phase line renders `⚠ <N>м без отчёта` instead of progress. */
66
+ const FEATURE_ADR_WARN_MS = 30 * 60 * 1_000;
67
+ /** Valid complexity tiers for the phase line — anything else renders as `[?]` with `?/?`. */
68
+ const FEATURE_ADR_TIERS = new Set(['S', 'M', 'L', 'XL']);
56
69
  /**
57
- * Count learned patterns via a direct **readonly** `COUNT(*)` with a short
58
- * `busy_timeout` no FTS rebuild, no schema write, no waiting on a live writer.
59
- * Returns `undefined` (not `0`) when the fast path is unavailable so the caller
60
- * can fall back to a readonly jsonl line-count; any error → `undefined`.
70
+ * Active-step lists per tier (mirrors the Step-0 router bands in the /feature-adr SKILL):
71
+ * done/total on the phase line is the label's step-number POSITION in its tier's list. A label
72
+ * whose number is not in the list renders `?/?` the panel never guesses progress.
61
73
  */
62
- function countSqlitePatternsReadonly(sqlitePath) {
63
- try {
64
- const require = createRequire(import.meta.url);
65
- const Database = require('better-sqlite3');
66
- const db = new Database(sqlitePath, { readonly: true });
67
- try {
68
- db.pragma('busy_timeout = 100'); // back off fast — never block the 300ms refresh on a write lock
69
- const row = db.prepare('SELECT COUNT(*) as cnt FROM memory_records').get();
70
- return typeof row?.cnt === 'number' ? row.cnt : undefined;
71
- }
72
- finally {
73
- db.close();
74
- }
75
- }
76
- catch {
77
- return undefined;
78
- }
79
- }
74
+ const FEATURE_ADR_ACTIVE_STEPS = {
75
+ S: [0, 1, 6, 7, 8],
76
+ M: [0, 1, 3, 3.5, 5, 6, 7, 8],
77
+ L: [0, 1, 2, 3, 3.5, 4, 5, 6, 7, 8, 9],
78
+ XL: [0, 1, 2, 3, 3.5, 4, 5, 6, 7, 8, 9],
79
+ };
80
80
  /**
81
81
  * Count the legacy/default `.dz/patterns.jsonl` store by reading the FILE directly — READONLY, no
82
82
  * backend open, no FTS rebuild, no schema write. Used only when the SQLite readonly COUNT is
@@ -103,7 +103,7 @@ function countJsonlPatternsReadonly(projectRoot) {
103
103
  function countLearnedPatterns(projectRoot) {
104
104
  const sqlitePath = sqlitePatternPath(projectRoot);
105
105
  if (existsSync(sqlitePath)) {
106
- const fast = countSqlitePatternsReadonly(sqlitePath);
106
+ const fast = countSqliteRowsReadonly(sqlitePath, 'memory_records');
107
107
  if (fast !== undefined)
108
108
  return fast;
109
109
  }
@@ -193,6 +193,8 @@ export function readFeatureAdrState(projectRoot, now = Date.now()) {
193
193
  return undefined;
194
194
  if (typeof parsed.step !== 'string' || parsed.step.length === 0)
195
195
  return undefined;
196
+ if (parsed.step.trim() === 'done')
197
+ return undefined; // terminal slots never enter arbitration
196
198
  if (typeof parsed.ts !== 'string')
197
199
  return undefined;
198
200
  const tsMs = Date.parse(parsed.ts);
@@ -215,7 +217,11 @@ export function readFeatureAdrState(projectRoot, now = Date.now()) {
215
217
  stored: num(parsed.stored),
216
218
  ...(num(parsed.reinforced) > 0 ? { reinforced: num(parsed.reinforced) } : {}),
217
219
  ts: parsed.ts,
220
+ ...(typeof parsed.runId === 'string' && parsed.runId.trim() !== '' ? { runId: parsed.runId } : {}),
218
221
  ...(typeof parsed.mode === 'string' && parsed.mode.length > 0 ? { mode: parsed.mode } : {}),
222
+ ...(typeof parsed.tier === 'string' && FEATURE_ADR_TIERS.has(parsed.tier) ? { tier: parsed.tier } : {}),
223
+ ...(typeof parsed.phaseStartTs === 'string' && !Number.isNaN(Date.parse(parsed.phaseStartTs))
224
+ ? { phaseStartTs: parsed.phaseStartTs } : {}),
219
225
  };
220
226
  return { state, tsMs, rank: kind === 'feature-adr' ? 1 : 0 };
221
227
  }
@@ -236,6 +242,56 @@ export function readFeatureAdrState(projectRoot, now = Date.now()) {
236
242
  }
237
243
  return winner?.state;
238
244
  }
245
+ /**
246
+ * Read ONE slot file raw (no freshness window, no arbitration) — the write path's "previous state".
247
+ * Best-effort: absent/corrupt → `undefined`.
248
+ */
249
+ function readPreviousSlot(root, slug) {
250
+ try {
251
+ const parsed = JSON.parse(readFileSync(featureAdrStatePath(root, slug), 'utf-8'));
252
+ if (typeof parsed.step !== 'string' || typeof parsed.ts !== 'string')
253
+ return undefined;
254
+ return parsed;
255
+ }
256
+ catch {
257
+ return undefined;
258
+ }
259
+ }
260
+ /**
261
+ * fa-phase-statusline (ADR-001 D3): when a step label CHANGES, the phase that just ended leaves one
262
+ * telemetry row in the EXISTING `.dz/feature-adr/run-cost-ledger.jsonl`:
263
+ * `{"_schema":"feature-adr-phase/1","kind":"phase",slug,tier,mode,step,wallSec,ts}`.
264
+ * DELIBERATE field choices, both load-bearing:
265
+ * - `ts`, never `date` — cadence.ts skips ledger rows without a string `date`, so shipment dating
266
+ * is untouched by construction (pinned in cadence.test.ts);
267
+ * - `wallSec` (wall seconds of the ended phase), never `minutes` — 1-3 minute phases keep resolution.
268
+ * The ledger is NEVER created here: phase rows enrich the existing witness file, they are not a new
269
+ * store. Append-only (`appendFileSync`, one line) — not a read-modify-write, so no named lock owed.
270
+ * Best-effort: any failure is swallowed (the panel write must never break the pipeline).
271
+ */
272
+ function appendPhaseRow(root, prev, resolved, now) {
273
+ try {
274
+ const ledger = join(root, '.dz', 'feature-adr', 'run-cost-ledger.jsonl');
275
+ if (!existsSync(ledger))
276
+ return; // append to the EXISTING ledger only — never create one
277
+ const startedMs = Date.parse(prev.phaseStartTs ?? prev.ts ?? '');
278
+ if (Number.isNaN(startedMs))
279
+ return; // an unmeasurable phase leaves no row — never a guess
280
+ const wallSec = Math.max(0, Math.round((now - startedMs) / 1000));
281
+ const row = {
282
+ _schema: 'feature-adr-phase/1',
283
+ kind: 'phase',
284
+ slug: resolved.slug,
285
+ tier: resolved.tier ?? null,
286
+ mode: prev.mode ?? resolved.mode ?? null,
287
+ step: prev.step,
288
+ wallSec,
289
+ ts: new Date(now).toISOString(),
290
+ };
291
+ appendFileSync(ledger, `${JSON.stringify(row)}\n`);
292
+ }
293
+ catch { /* best-effort telemetry — never blocks the slot write */ }
294
+ }
239
295
  /**
240
296
  * Record the live `/feature-adr` learning state — called by the pipeline at Steps 0/8/9. Computes
241
297
  * `pool` as the total learned-pattern count (via the same readonly {@link countLearnedPatterns} the
@@ -245,7 +301,17 @@ export function readFeatureAdrState(projectRoot, now = Date.now()) {
245
301
  * @param now Injectable clock (epoch ms) for the write timestamp — defaults to `Date.now()`.
246
302
  */
247
303
  export function writeFeatureAdrState(projectRoot, input, now = Date.now()) {
304
+ return writeFeatureAdrStateDetailed(projectRoot, input, now).state;
305
+ }
306
+ /**
307
+ * The same write as {@link writeFeatureAdrState}, but it SAYS WHY it did not complete and what is
308
+ * known about its disk effect. A refusal before the transition returns `refusedEffect:'none'`; once
309
+ * the callback begins, a refusal returns `refusedEffect:'unknown'` because an append or slot write
310
+ * may already have landed. The pipeline still never breaks — the caller decides, this never throws.
311
+ */
312
+ export function writeFeatureAdrStateDetailed(projectRoot, input, now = Date.now()) {
248
313
  const root = resolve(projectRoot);
314
+ let transactionStarted = false;
249
315
  let pool = 0;
250
316
  try {
251
317
  pool = countLearnedPatterns(root);
@@ -253,22 +319,12 @@ export function writeFeatureAdrState(projectRoot, input, now = Date.now()) {
253
319
  catch {
254
320
  pool = 0;
255
321
  }
256
- const state = {
257
- kind: input.kind === 'loop' ? 'loop' : 'feature-adr',
258
- slug: input.slug,
259
- step: input.step,
260
- pool,
261
- recalled: Number.isFinite(input.recalled) ? input.recalled : 0,
262
- stored: Number.isFinite(input.stored) ? input.stored : 0,
263
- ...(input.reinforced !== undefined && Number.isFinite(input.reinforced) ? { reinforced: input.reinforced } : {}),
264
- ts: new Date(now).toISOString(),
265
- ...(input.mode !== undefined && input.mode.length > 0 ? { mode: input.mode } : {}),
266
- };
267
322
  try {
268
323
  const dir = featureAdrStateDir(root);
269
324
  mkdirSync(dir, { recursive: true });
270
325
  // Housekeeping belongs only on this write path, never the ~300ms render path. Every file is
271
326
  // independently guarded so an unreadable/racing entry cannot prevent the live state write.
327
+ // Kept OUTSIDE the lock below: it is per-file guarded and takes no part in the transition race.
272
328
  try {
273
329
  const cutoff = Date.now() - 24 * 60 * 60 * 1_000;
274
330
  for (const name of readdirSync(dir)) {
@@ -283,13 +339,99 @@ export function writeFeatureAdrState(projectRoot, input, now = Date.now()) {
283
339
  }
284
340
  }
285
341
  catch { /* best-effort directory cleanup */ }
286
- const path = featureAdrStatePath(root, input.slug);
287
- writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`);
342
+ // fa-phase-statusline QE fix (P2, cross-family review of 656d6903): read/compare/append/write
343
+ // is a read-modify-write over a shared slot — two concurrent design-sibling ckpt agents both
344
+ // read the same previous slot, both see a label change, and both append a phase row, breaking
345
+ // the one-row-per-transition promise. So the whole transaction runs under the repo's named
346
+ // per-store lock (cross-runtime-concurrency rule) — a short, fully synchronous critical
347
+ // section, no subprocess, no model turn. A lock timeout throws → the outer catch refuses the
348
+ // write (returns undefined): no new claim rather than a raced one.
349
+ const transition = () => {
350
+ transactionStarted = true;
351
+ // ADR-001 D3/D5: the previous slot is the phase clock. Same step label → the phase start
352
+ // carries forward; a CHANGED label ends the previous phase (one telemetry row) and starts a
353
+ // new one now. Tier is stored state, carried forward when a record omits it.
354
+ const prev = readPreviousSlot(root, input.slug);
355
+ const tier = typeof input.tier === 'string' && FEATURE_ADR_TIERS.has(input.tier)
356
+ ? input.tier
357
+ : (typeof prev?.tier === 'string' && FEATURE_ADR_TIERS.has(prev.tier) ? prev.tier : undefined);
358
+ const nowIso = new Date(now).toISOString();
359
+ const prevRunId = typeof prev?.runId === 'string' && prev.runId.trim() !== '' ? prev.runId.trim() : undefined;
360
+ const runId = typeof input.runId === 'string' && input.runId.trim() !== '' ? input.runId.trim() : undefined;
361
+ const sameRun = prevRunId !== undefined && runId !== undefined && prevRunId === runId;
362
+ const legacyRunScope = prevRunId === undefined && runId === undefined;
363
+ const sameRunScope = sameRun || legacyRunScope;
364
+ // MONOTONIC STEP GUARD (P1, cross-family review of 656d6903): the pipeline's pre-existing
365
+ // fallback recorder fires AFTER the router checkpoint, so "Step 0 recall" used to overwrite
366
+ // "Step 1 Design" and the panel deterministically regressed to Step 0 for the whole design
367
+ // fan (with the design time mis-attributed to Step 0 in telemetry). A plain step label whose
368
+ // number goes BACKWARDS within the SAME identified invocation is a stale duplicate of an
369
+ // earlier step: its counters/tier/mode/ts are absorbed, but the step and phase clock stand,
370
+ // and no phase row is written. Two absent identities retain the legacy fresh-slot behavior.
371
+ // Distinct identities, a one-sided identity, an explicit gate/waiting label (⛔/⏸), or an
372
+ // unparseable label never authorizes the guard to cross an unproven run boundary.
373
+ let step = input.step;
374
+ const isGateLabel = input.step.trimStart().startsWith('⛔') || input.step.trimStart().startsWith('⏸');
375
+ if (prev !== undefined && input.kind !== 'loop' && prev.kind !== 'loop' && !isGateLabel) {
376
+ const prevTsMs = Date.parse(prev.ts ?? '');
377
+ const prevFresh = !Number.isNaN(prevTsMs) && now - prevTsMs <= FEATURE_ADR_FRESH_MS;
378
+ const prevNum = parseStepNumber(prev.step ?? '');
379
+ const newNum = parseStepNumber(input.step);
380
+ const guardThisRun = sameRun || (legacyRunScope && prevFresh);
381
+ if (guardThisRun && prevNum !== undefined && newNum !== undefined && newNum < prevNum) {
382
+ step = prev.step; // stale duplicate — the run is further along than this record
383
+ }
384
+ }
385
+ // TEST-ONLY hold (see WriteFeatureAdrStateInput._unsafeHoldMs): widens the read→write window
386
+ // so the lost update is deterministic instead of lucky. Zero cost when unset.
387
+ if (typeof input._unsafeHoldMs === 'number' && input._unsafeHoldMs > 0) {
388
+ const until = Date.now() + input._unsafeHoldMs;
389
+ while (Date.now() < until) { /* deliberate busy hold — test seam only */ }
390
+ }
391
+ const samePhase = prev !== undefined && sameRunScope && prev.step === step;
392
+ const phaseStartTs = samePhase ? (prev.phaseStartTs ?? prev.ts ?? nowIso) : nowIso;
393
+ // Phase telemetry is a /feature-adr concept — loop slots never leave phase rows in the ledger.
394
+ if (prev !== undefined && sameRunScope && !samePhase && input.kind !== 'loop' && prev.kind !== 'loop') {
395
+ appendPhaseRow(root, prev, { slug: input.slug, ...(tier !== undefined ? { tier } : {}), ...(input.mode !== undefined && input.mode.length > 0 ? { mode: input.mode } : {}) }, now);
396
+ }
397
+ const state = {
398
+ kind: input.kind === 'loop' ? 'loop' : 'feature-adr',
399
+ slug: input.slug,
400
+ step,
401
+ pool,
402
+ recalled: Number.isFinite(input.recalled) ? input.recalled : 0,
403
+ stored: Number.isFinite(input.stored) ? input.stored : 0,
404
+ ...(input.reinforced !== undefined && Number.isFinite(input.reinforced) ? { reinforced: input.reinforced } : {}),
405
+ ts: nowIso,
406
+ ...(runId !== undefined ? { runId } : {}),
407
+ ...(input.mode !== undefined && input.mode.length > 0 ? { mode: input.mode } : {}),
408
+ ...(tier !== undefined ? { tier } : {}),
409
+ phaseStartTs,
410
+ };
411
+ if (input._unsafeFailSlotWrite === true) {
412
+ throw new Error('test-only slot write failure after phase append');
413
+ }
414
+ writeFileSync(featureAdrStatePath(root, input.slug), `${JSON.stringify(state, null, 2)}\n`);
415
+ return state;
416
+ };
417
+ // The seam is TEST-ONLY (see WriteFeatureAdrStateInput._unsafeSkipLock): it exists so the RED
418
+ // half of the lock test can reproduce the lost update. Shipped callers never pass it.
419
+ const written = input._unsafeSkipLock === true
420
+ ? transition()
421
+ : withNamedLockSync(root, 'fa-phase-slot', transition);
422
+ return { state: written };
288
423
  }
289
- catch {
290
- return undefined;
424
+ catch (err) {
425
+ // A refusal is REPORTED, never mistaken for success. A lock timeout before the callback proves
426
+ // effect `none`; once the callback began, append/write progress is unknowable after an error.
427
+ const reason = err instanceof Error && err.message.trim() !== '' ? err.message : String(err);
428
+ return { refused: reason, refusedEffect: transactionStarted ? 'unknown' : 'none' };
291
429
  }
292
- return state;
430
+ }
431
+ /** First `Step <n>` number in a label, or `undefined` — shared by the guard and the renderer. */
432
+ function parseStepNumber(label) {
433
+ const m = /Step\s+(\d+(?:\.\d+)?)/.exec(label);
434
+ return m !== null ? Number(m[1]) : undefined;
293
435
  }
294
436
  /**
295
437
  * Gather dz's self-learning counts for one project. FAST + best-effort: every read
@@ -302,12 +444,85 @@ export function writeFeatureAdrState(projectRoot, input, now = Date.now()) {
302
444
  export function statuslineData(projectRoot, now = Date.now()) {
303
445
  const root = resolve(projectRoot);
304
446
  let patterns = 0;
447
+ let storeRows;
305
448
  try {
306
- patterns = countLearnedPatterns(root);
449
+ storeRows = countLearningStoreRowsReadonly(root);
450
+ patterns = typeof storeRows.lexicalRows === 'number'
451
+ ? storeRows.lexicalRows
452
+ : countJsonlPatternsReadonly(root);
307
453
  }
308
454
  catch {
309
455
  patterns = 0;
310
456
  }
457
+ let storeHealth;
458
+ try {
459
+ if (storeRows === undefined)
460
+ throw new Error('store row counts unavailable');
461
+ const rows = storeRows;
462
+ const mark = readStoreMark(root);
463
+ const health = checkStoreHealth({
464
+ projectRoot: root,
465
+ lexicalRows: rows.lexicalRows,
466
+ lexicalSource: rows.lexicalSource,
467
+ vectorRows: rows.vectorRows,
468
+ mark,
469
+ });
470
+ if (health.verdict === 'collapsed') {
471
+ const vectorAffected = health.reason.startsWith('vector ');
472
+ const previousMax = vectorAffected ? mark?.vectorMax : mark?.lexicalMax;
473
+ storeHealth = {
474
+ verdict: health.verdict,
475
+ ...(previousMax === undefined ? {} : { previousMax }),
476
+ };
477
+ }
478
+ else if (health.verdict === 'cold-start-over-existing') {
479
+ const previousMax = mark === undefined
480
+ ? undefined
481
+ : rows.lexicalRows === 0 && mark.lexicalMax > 0 ? mark.lexicalMax : mark.vectorMax;
482
+ storeHealth = {
483
+ verdict: health.verdict,
484
+ ...(previousMax === undefined ? {} : { previousMax }),
485
+ snapshotPath: storeSnapshotPath(root),
486
+ };
487
+ }
488
+ else if (health.verdict === 'unreadable') {
489
+ storeHealth = {
490
+ verdict: health.verdict,
491
+ unreadableFiles: [
492
+ ...(rows.lexicalRows === 'unreadable' ? [basename(rows.lexicalSourcePath)] : []),
493
+ ...(rows.vectorRows === 'unreadable' && rows.vectorSourcePath !== undefined
494
+ ? [basename(rows.vectorSourcePath)] : []),
495
+ ],
496
+ };
497
+ }
498
+ else if (health.verdict === 'source-changed') {
499
+ storeHealth = { verdict: health.verdict };
500
+ }
501
+ }
502
+ catch {
503
+ storeHealth = undefined;
504
+ }
505
+ let patternBreakdown;
506
+ try {
507
+ if (storeRows !== undefined
508
+ && typeof storeRows.lexicalRows === 'number'
509
+ && typeof storeRows.lexicalQuarantinedRows === 'number') {
510
+ const quarantined = storeRows.lexicalQuarantinedRows;
511
+ const tierDelta = typeof storeRows.vectorQuarantinedRows === 'number'
512
+ ? Math.abs(quarantined - storeRows.vectorQuarantinedRows)
513
+ : undefined;
514
+ patternBreakdown = {
515
+ source: 'lexical',
516
+ active: storeRows.lexicalRows - quarantined,
517
+ quarantined,
518
+ attention: quarantined > 0 && quarantined * 3 >= storeRows.lexicalRows,
519
+ ...(tierDelta !== undefined && tierDelta > QUARANTINE_TIER_DRIFT_TOLERANCE ? { tierDelta } : {}),
520
+ };
521
+ }
522
+ }
523
+ catch {
524
+ patternBreakdown = undefined;
525
+ }
311
526
  let brainSources = 0;
312
527
  try {
313
528
  brainSources = listBrain().length;
@@ -333,10 +548,61 @@ export function statuslineData(projectRoot, now = Date.now()) {
333
548
  }
334
549
  return {
335
550
  patterns,
551
+ ...(patternBreakdown !== undefined ? { patternBreakdown } : {}),
336
552
  ...(usedPatterns !== undefined ? { usedPatterns } : {}),
337
553
  brainSources,
338
554
  ...(ageH !== undefined ? { consolidatedAgeH: ageH } : {}),
339
555
  ...(featureAdr !== undefined ? { featureAdr } : {}),
556
+ ...(storeHealth !== undefined ? { storeHealth } : {}),
340
557
  };
341
558
  }
559
+ /**
560
+ * Render the /feature-adr PHASE line (line format B, fa-phase-statusline ADR-001) — the second
561
+ * status-bar line:
562
+ *
563
+ * `📐 <slug> [<tier>] <done>/<total> ▶ <step label> · <N>м/—`
564
+ *
565
+ * States, in precedence order:
566
+ * - `undefined` (no line): a loop slot, the terminal label `done`, or ≥90 min of slot silence;
567
+ * - `⚠ <N>м без отчёта` from 30 min of slot silence (counted from the last report `ts`);
568
+ * - a label starting `⛔` or `⏸` replaces the `▶ <label>` portion VERBATIM (failure/waiting states
569
+ * are reported by the pipeline through the label itself, never synthesised here);
570
+ * - otherwise `▶ <label>` with elapsed minutes counted from the PHASE start (`phaseStartTs`).
571
+ *
572
+ * done/total is the label's step-number position in its tier's active-step list; an unknown tier,
573
+ * an unparseable label, or a number outside the list renders `?/?` — the panel never guesses.
574
+ * The estimate slot is fixed-width at the tail: v1 defers estimates entirely, so it is always `/—`
575
+ * (a future median would be computed on the WRITE path and stored in the slot as a ready number —
576
+ * this render path stays a pure function over the slot, no DB, no subprocess, no features/* sweep).
577
+ *
578
+ * @param state A fresh slot from {@link readFeatureAdrState} (the ONLY source of "in progress").
579
+ * @param now Injectable clock (epoch ms) — defaults to `Date.now()`.
580
+ */
581
+ export function renderFeatureAdrPhaseLine(state, now = Date.now()) {
582
+ if (state.kind === 'loop')
583
+ return undefined;
584
+ const label = state.step.trim();
585
+ if (label === 'done')
586
+ return undefined; // a finished run leaves the bar immediately
587
+ const tsMs = Date.parse(state.ts);
588
+ if (Number.isNaN(tsMs))
589
+ return undefined;
590
+ const silenceMs = now - tsMs;
591
+ const silenceMin = Math.floor(silenceMs / 60_000);
592
+ if (silenceMs >= FEATURE_ADR_FRESH_MS)
593
+ return undefined; // fully faded (90 min)
594
+ const tier = state.tier !== undefined && FEATURE_ADR_TIERS.has(state.tier) ? state.tier : undefined;
595
+ const steps = tier !== undefined ? FEATURE_ADR_ACTIVE_STEPS[tier] : undefined;
596
+ const stepNum = parseStepNumber(label);
597
+ const idx = steps !== undefined && stepNum !== undefined ? steps.indexOf(stepNum) : -1;
598
+ const ratio = idx >= 0 && steps !== undefined ? `${idx}/${steps.length}` : '?/?';
599
+ const head = `📐 ${state.slug} [${tier ?? '?'}] ${ratio}`;
600
+ if (silenceMs >= FEATURE_ADR_WARN_MS)
601
+ return `${head} ⚠ ${silenceMin}м без отчёта`;
602
+ const phaseStartMs = Date.parse(state.phaseStartTs ?? state.ts);
603
+ const elapsedMin = Number.isNaN(phaseStartMs) ? 0 : Math.max(0, Math.floor((now - phaseStartMs) / 60_000));
604
+ // ⛔ (a gate failure) and ⏸ (a waiting state) labels carry their own marker — verbatim, no ▶.
605
+ const stepPart = label.startsWith('⛔') || label.startsWith('⏸') ? label : `▶ ${label}`;
606
+ return `${head} ${stepPart} · ${elapsedMin}м/—`;
607
+ }
342
608
  //# sourceMappingURL=statusline.js.map