@dzhechkov/harness-core 0.3.63 → 0.3.67

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.
package/src/statusline.ts CHANGED
@@ -14,12 +14,35 @@
14
14
  * @packageDocumentation
15
15
  */
16
16
 
17
- import { existsSync, readFileSync, statSync } from 'node:fs';
18
- import { join, resolve } from 'node:path';
17
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
18
+ import { dirname, join, resolve } from 'node:path';
19
19
  import { createRequire } from 'node:module';
20
20
 
21
21
  import { listBrain } from './brain.js';
22
22
 
23
+ /**
24
+ * Live learning state for one in-flight `/feature-adr` run — the per-run visibility panel
25
+ * that surfaces the Pattern memory loop (POOL learned, RECALLED for this run, STORED this run).
26
+ * Written by the pipeline at Steps 0/8/9 via `writeFeatureAdrState`; read back on the render
27
+ * path (readonly, best-effort) by `readFeatureAdrState`.
28
+ */
29
+ export interface FeatureAdrState {
30
+ /** The feature slug the pipeline is working on (kebab-case). */
31
+ readonly slug: string;
32
+ /** Human-readable step label (e.g. "Step 0", "Step 8 QE"). */
33
+ readonly step: string;
34
+ /** Total learned-pattern POOL (all patterns available to recall from) at write time. */
35
+ readonly pool: number;
36
+ /** How many patterns this run RECALLED / used to inform its work. */
37
+ readonly recalled: number;
38
+ /** How many NEW patterns this run STORED back into the pool. */
39
+ readonly stored: number;
40
+ /** ISO timestamp of the write — drives the freshness window on the render path. */
41
+ readonly ts: string;
42
+ /** Optional run mode (e.g. "reference", "full-qe", "full-qe-extended"). */
43
+ readonly mode?: string;
44
+ }
45
+
23
46
  /** A snapshot of dz's self-learning state for one project (all fields best-effort). */
24
47
  export interface StatuslineData {
25
48
  /** Count of learned patterns in the project's unified memory store. */
@@ -28,6 +51,8 @@ export interface StatuslineData {
28
51
  readonly brainSources: number;
29
52
  /** Hours since the last `dz consolidate` run, if a watermark is present. */
30
53
  readonly consolidatedAgeH?: number;
54
+ /** Live `/feature-adr` learning state — present ONLY when a fresh run is in flight. */
55
+ readonly featureAdr?: FeatureAdrState;
31
56
  }
32
57
 
33
58
  /** Path of the SQLite pattern store (the Tier-3 backend). */
@@ -40,6 +65,17 @@ function consolidateWatermarkPath(projectRoot: string): string {
40
65
  return join(projectRoot, '.dz', 'memory', 'consolidate.json');
41
66
  }
42
67
 
68
+ /** Path of the live `/feature-adr` learning-state file (per-run panel source). */
69
+ export function featureAdrStatePath(projectRoot: string): string {
70
+ return join(projectRoot, '.dz', 'feature-adr', 'learning-state.json');
71
+ }
72
+
73
+ /**
74
+ * Freshness window for the `/feature-adr` panel: a run older than this is considered finished, so
75
+ * its state must NOT keep showing a stale panel in the status bar. 30 minutes (in ms).
76
+ */
77
+ const FEATURE_ADR_FRESH_MS = 30 * 60 * 1_000;
78
+
43
79
  interface ReadonlyCountDb {
44
80
  pragma: (s: string) => void;
45
81
  prepare: (q: string) => { get: (...a: unknown[]) => unknown };
@@ -114,6 +150,94 @@ function consolidatedAgeHours(projectRoot: string, now: number): number | undefi
114
150
  }
115
151
  }
116
152
 
153
+ /**
154
+ * Read the live `/feature-adr` learning state for one project — the source of the per-run panel.
155
+ *
156
+ * RENDER-PATH DISCIPLINE (statusline pattern #1): this runs inside the ~300ms status-bar refresh, so
157
+ * it is a plain **readonly** file read, **best-effort**, and NEVER throws — an absent, unreadable, or
158
+ * corrupt state file collapses to `undefined`, not an exception.
159
+ *
160
+ * FRESHNESS: a run whose `ts` is older than {@link FEATURE_ADR_FRESH_MS} is treated as finished and
161
+ * returns `undefined`, so a stale run can never keep a panel pinned in the status bar.
162
+ *
163
+ * @param projectRoot Absolute (or cwd-relative) project directory.
164
+ * @param now Injectable clock (epoch ms) for the freshness check — defaults to `Date.now()`.
165
+ */
166
+ export function readFeatureAdrState(projectRoot: string, now: number = Date.now()): FeatureAdrState | undefined {
167
+ const path = featureAdrStatePath(resolve(projectRoot));
168
+ if (!existsSync(path)) return undefined;
169
+ try {
170
+ const parsed = JSON.parse(readFileSync(path, 'utf-8')) as Partial<FeatureAdrState>;
171
+ if (typeof parsed.slug !== 'string' || parsed.slug.length === 0) return undefined;
172
+ if (typeof parsed.step !== 'string' || parsed.step.length === 0) return undefined;
173
+ if (typeof parsed.ts !== 'string') return undefined;
174
+ const tsMs = Date.parse(parsed.ts);
175
+ if (Number.isNaN(tsMs)) return undefined;
176
+ if (now - tsMs > FEATURE_ADR_FRESH_MS) return undefined; // stale run — do not surface a panel
177
+ const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
178
+ const state: FeatureAdrState = {
179
+ slug: parsed.slug,
180
+ step: parsed.step,
181
+ pool: num(parsed.pool),
182
+ recalled: num(parsed.recalled),
183
+ stored: num(parsed.stored),
184
+ ts: parsed.ts,
185
+ ...(typeof parsed.mode === 'string' && parsed.mode.length > 0 ? { mode: parsed.mode } : {}),
186
+ };
187
+ return state;
188
+ } catch {
189
+ return undefined;
190
+ }
191
+ }
192
+
193
+ /** Fields the `/feature-adr` pipeline supplies when recording its live learning state. */
194
+ export interface WriteFeatureAdrStateInput {
195
+ readonly slug: string;
196
+ readonly step: string;
197
+ readonly recalled: number;
198
+ readonly stored: number;
199
+ readonly mode?: string;
200
+ }
201
+
202
+ /**
203
+ * Record the live `/feature-adr` learning state — called by the pipeline at Steps 0/8/9. Computes
204
+ * `pool` as the total learned-pattern count (via the same readonly {@link countLearnedPatterns} the
205
+ * panel uses) and writes the JSON with a fresh `ts`. Best-effort: returns the written state, or
206
+ * `undefined` on any I/O error (this must never break the pipeline).
207
+ *
208
+ * @param now Injectable clock (epoch ms) for the write timestamp — defaults to `Date.now()`.
209
+ */
210
+ export function writeFeatureAdrState(
211
+ projectRoot: string,
212
+ input: WriteFeatureAdrStateInput,
213
+ now: number = Date.now(),
214
+ ): FeatureAdrState | undefined {
215
+ const root = resolve(projectRoot);
216
+ let pool = 0;
217
+ try {
218
+ pool = countLearnedPatterns(root);
219
+ } catch {
220
+ pool = 0;
221
+ }
222
+ const state: FeatureAdrState = {
223
+ slug: input.slug,
224
+ step: input.step,
225
+ pool,
226
+ recalled: Number.isFinite(input.recalled) ? input.recalled : 0,
227
+ stored: Number.isFinite(input.stored) ? input.stored : 0,
228
+ ts: new Date(now).toISOString(),
229
+ ...(input.mode !== undefined && input.mode.length > 0 ? { mode: input.mode } : {}),
230
+ };
231
+ try {
232
+ const path = featureAdrStatePath(root);
233
+ mkdirSync(dirname(path), { recursive: true });
234
+ writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`);
235
+ } catch {
236
+ return undefined;
237
+ }
238
+ return state;
239
+ }
240
+
117
241
  /**
118
242
  * Gather dz's self-learning counts for one project. FAST + best-effort: every read
119
243
  * is guarded so a missing/corrupt `.dz`, absent native module, or locked store
@@ -140,5 +264,19 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
140
264
  }
141
265
 
142
266
  const ageH = consolidatedAgeHours(root, now);
143
- return ageH !== undefined ? { patterns, brainSources, consolidatedAgeH: ageH } : { patterns, brainSources };
267
+
268
+ // Live /feature-adr panel — attached ONLY when a fresh run is in flight (readonly, never throws).
269
+ let featureAdr: FeatureAdrState | undefined;
270
+ try {
271
+ featureAdr = readFeatureAdrState(root, now);
272
+ } catch {
273
+ featureAdr = undefined;
274
+ }
275
+
276
+ return {
277
+ patterns,
278
+ brainSources,
279
+ ...(ageH !== undefined ? { consolidatedAgeH: ageH } : {}),
280
+ ...(featureAdr !== undefined ? { featureAdr } : {}),
281
+ };
144
282
  }