@productbrain/cli 0.1.0-beta.1440 → 0.1.0-beta.1463

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.
@@ -0,0 +1,250 @@
1
+ /**
2
+ * SYNCED from packages/scoreboard-core/src/index.ts — DO NOT EDIT HERE.
3
+ * Canonical SSOT for the flywheel scoreboard diagnosis (consumed by BOTH the published
4
+ * `pb` CLI and the Cortex page). The CLI cannot import the workspace package at publish time
5
+ * (tsc, no bundler), so it carries this committed copy. Edit the source, then run:
6
+ * node scripts/sync-scoreboard-core.mjs
7
+ * A content-hash drift gate (scripts/check-mcp-lib-drift.mjs) fails CI if this drifts.
8
+ */
9
+ // User-facing copy carries NO raw Chain IDs (chain-id-leak guard): the CLI prints `cause` /
10
+ // `nextAction` / `reading` verbatim to any workspace's terminal, so an ID here would leak our
11
+ // internal governance refs cross-tenant. The governing entry (the no-flip-threshold decision) is
12
+ // cited in code comments only, never in these strings.
13
+ const UNPINNED = 'threshold unpinned — pin against the live distribution';
14
+ function pct(n) {
15
+ return n === null ? 'n/a' : `${(n * 100).toFixed(0)}%`;
16
+ }
17
+ function ms(n) {
18
+ if (n === null)
19
+ return 'n/a';
20
+ const h = n / (1000 * 60 * 60);
21
+ if (h < 1)
22
+ return `${(n / (1000 * 60)).toFixed(0)}m`;
23
+ if (h < 48)
24
+ return `${h.toFixed(1)}h`;
25
+ return `${(h / 24).toFixed(1)}d`;
26
+ }
27
+ /**
28
+ * M1 — retrieval truth-state mix. The no-domain-share flip (INS-1775 floor) is UNPINNED, so the
29
+ * status is `awaiting` whenever there is data to judge (we never invent the 50% floor). With zero
30
+ * serves it is `unmeasured` (nothing served yet). The named cause for a high no-domain share is
31
+ * "domains unratified".
32
+ */
33
+ export function diagnoseM1(m1) {
34
+ const reading = `served=${m1.mix.total} · audited=${pct(m1.auditedShare)} · draft=${pct(m1.draftShare)} · ` +
35
+ `no-domain=${pct(m1.noDomainShare)} (window ${ms(m1.windowMs)})`;
36
+ if (m1.mix.total === 0) {
37
+ return {
38
+ metric: 'M1',
39
+ label: 'Retrieval truth-state mix',
40
+ status: 'unmeasured',
41
+ reading,
42
+ cause: 'no governance served in the window yet — nothing to classify',
43
+ nextAction: 'drive an orient/start_pb serve, then re-read',
44
+ };
45
+ }
46
+ return {
47
+ metric: 'M1',
48
+ label: 'Retrieval truth-state mix',
49
+ status: 'awaiting',
50
+ reading,
51
+ cause: `no-domain-share floor ${UNPINNED}`,
52
+ // INS-1775 records the ≈57% no-domain baseline — cited here in a comment, not the user string.
53
+ nextAction: 'pin the no-domain floor (≈57% measured baseline); if no-domain stays high, domains are unratified → ratify the inferred-domain binding entries',
54
+ };
55
+ }
56
+ /**
57
+ * M2 — ranking agreement (tests the unify integrity). This one HAS a clean structural signal:
58
+ * post-unify, full parity (divergentBuckets === 0) is the EXPECTED steady state — that's `ok`.
59
+ * A non-zero divergence IS the regression signal — `degraded` — and the M2-magnitude flip stays
60
+ * unpinned, but the structural ok/degraded does not need a number (0 vs non-0 is exact).
61
+ */
62
+ export function diagnoseM2(m2) {
63
+ const reading = `divergentBuckets=${m2.divergentBuckets} · fullParity=${m2.fullParity}${m2.task ? ` · task="${m2.task}"` : ''}`;
64
+ if (m2.fullParity && m2.divergentBuckets === 0) {
65
+ return {
66
+ metric: 'M2',
67
+ label: 'Ranking agreement (unify integrity)',
68
+ status: 'ok',
69
+ reading,
70
+ cause: 'orient ↔ start_pb render identical set+order+tier — the unify holds',
71
+ nextAction: '',
72
+ };
73
+ }
74
+ return {
75
+ metric: 'M2',
76
+ label: 'Ranking agreement (unify integrity)',
77
+ status: 'degraded',
78
+ reading,
79
+ cause: 'the two surfaces re-forked — one is selecting/ordering governance outside the shared ranker',
80
+ // The ranking-unify tension is TEN-2431 — cited in the comment, kept out of the user string.
81
+ nextAction: 'diff getOrientEntries vs getBindingGovernanceView; restore the shared ranker as the single source',
82
+ };
83
+ }
84
+ /**
85
+ * M3 — draft-to-SSOT rate. The in-session-ratify floor (ASM-45 rolling-20 window) is UNPINNED,
86
+ * so a judged reading is `awaiting`. Zero captures in the window → `unmeasured`.
87
+ */
88
+ export function diagnoseM3(m3) {
89
+ const reading = `captured=${m3.capturedCount} · in-session=${pct(m3.inSessionRatifiedRate)} · ` +
90
+ `ever=${pct(m3.everRatifiedRate)} (over ${m3.sessionsConsidered}/${m3.sessionWindow} sessions)`;
91
+ if (m3.capturedCount === 0) {
92
+ return {
93
+ metric: 'M3',
94
+ label: 'Draft-to-SSOT rate',
95
+ status: 'unmeasured',
96
+ reading,
97
+ cause: 'no captures in the rolling session window — nothing to rate',
98
+ nextAction: 'capture in-session (pb capture), then re-read after the human accepts in Cortex',
99
+ };
100
+ }
101
+ return {
102
+ metric: 'M3',
103
+ label: 'Draft-to-SSOT rate',
104
+ status: 'awaiting',
105
+ reading,
106
+ // ASM-45 (the draft-graveyard hypothesis + rolling-20-session design) governs this metric —
107
+ // cited in comments; the user strings stay ID-free.
108
+ cause: `in-session ratify floor ${UNPINNED} (rolling-20-session design)`,
109
+ nextAction: 'pin the in-session floor (X); if rate is below it, the draft-graveyard is confirmed → accept/promote captures in Cortex',
110
+ };
111
+ }
112
+ /**
113
+ * M4 — first-time-good (rework proxy). DEFERRED (TEN-2433) and ABSENT from the data payload.
114
+ * ALWAYS surfaced as an explicit `awaiting` tile — never a fake green, never a number. (Pure —
115
+ * takes no input; the tile is standing.)
116
+ */
117
+ export function diagnoseM4() {
118
+ return {
119
+ metric: 'M4',
120
+ // The baseline-arm evidence sprint is EXP-19 (kept out of the user-facing label per the
121
+ // chain-id-leak guard; the entry id still rides nextAction, which is not a user-facing sink).
122
+ label: 'First-time-good (value)',
123
+ status: 'awaiting',
124
+ reading: 'no baseline arm yet — renders only as a PB-vs-baseline delta, never a lone number',
125
+ // The served↔rework git/CI join is deferred (TEN-2433); the baseline arm comes from the
126
+ // paired blind-judged A/B sprint (EXP-19). Both cited in comments, kept out of the user copy.
127
+ cause: 'awaiting baseline — the served↔rework join is not built; deferred from the data layer',
128
+ nextAction: 'run the paired blind-judged A/B sprint to populate the baseline arm',
129
+ };
130
+ }
131
+ /**
132
+ * M5 — projection staleness, computed as LOCAL-PULL drift at the CLI (DEC-1284). The staleness-age
133
+ * flip is UNPINNED, so a determinable reading is `awaiting`. When the local `.productbrain/`
134
+ * projection (or the server materialization) is unavailable, the status is `unmeasured` with the
135
+ * reason — fail-soft, never a fake ok.
136
+ */
137
+ export function diagnoseM5(m5) {
138
+ if (m5.unavailableReason || m5.localProjectionMtime === null) {
139
+ return {
140
+ metric: 'M5',
141
+ label: 'Projection staleness (local-pull drift)',
142
+ status: 'unmeasured',
143
+ reading: m5.unavailableReason ?? 'local .productbrain/ projection not found',
144
+ cause: 'cannot stat the local projection — local-pull drift is undeterminable here',
145
+ nextAction: 'run from a worktree with a materialized .productbrain/ (pb handshake), then re-read',
146
+ };
147
+ }
148
+ // The local projection exists, but with NO server materialization (e.g. missing/expired
149
+ // setup_receipt) there is no reference to diff against — the local-pull DRIFT (server−local,
150
+ // this metric's whole definition) is undeterminable. Report `unmeasured` (honesty over flattery)
151
+ // rather than `awaiting` with a `local-lag=n/a` that implies the drift is merely awaiting a
152
+ // threshold. local-age is still surfaced as the one signal we can honestly read.
153
+ if (m5.serverMaterializedAt === null || m5.localLagMs === null) {
154
+ return {
155
+ metric: 'M5',
156
+ label: 'Projection staleness (local-pull drift)',
157
+ status: 'unmeasured',
158
+ reading: `local-age=${ms(m5.localAgeMs)} · server materialization: none (drift undeterminable)`,
159
+ cause: 'no server materialization to diff against — local-pull drift (server−local) cannot be computed',
160
+ nextAction: 'materialize the projection server-side (pb handshake), then re-read to measure drift',
161
+ };
162
+ }
163
+ const lag = m5.localLagMs;
164
+ const age = m5.localAgeMs;
165
+ const reading = `local-lag=${ms(lag)} (server−local) · local-age=${ms(age)}`;
166
+ return {
167
+ metric: 'M5',
168
+ label: 'Projection staleness (local-pull drift)',
169
+ status: 'awaiting',
170
+ reading,
171
+ cause: `staleness-age threshold ${UNPINNED}; this is the one metric where PB can be staler than the never-drifting CLAUDE.md file`,
172
+ nextAction: 're-run pb handshake to re-pull the projection if the lag/age looks high',
173
+ };
174
+ }
175
+ /**
176
+ * M5 (server view) — the Cortex page has NO local filesystem, so it cannot compute the spec's
177
+ * local-pull drift (DEC-1284). It reads only the server-side projection-generation building block
178
+ * (`materialization.latestMaterializedAt` / age) the public action already returns — adding NO new
179
+ * truth (spec §2d). This is deliberately a DIFFERENT, HONEST framing from `diagnoseM5`: it is
180
+ * SERVER-generation age, never claimed to be the local-pull staleness the CLI measures. The
181
+ * staleness-age flip is UNPINNED → `awaiting` when there is a materialization; `unmeasured` when
182
+ * the workspace has never materialized.
183
+ */
184
+ export function diagnoseM5FromServer(materialization) {
185
+ if (materialization.latestMaterializedAt === null) {
186
+ return {
187
+ metric: 'M5',
188
+ label: 'Projection staleness (server generation age)',
189
+ status: 'unmeasured',
190
+ reading: 'no materialization receipt yet — the projection has never been generated server-side',
191
+ cause: 'nothing to age — the workspace has not materialized its projection',
192
+ nextAction: 'run pb handshake to materialize the projection, then re-read',
193
+ };
194
+ }
195
+ const age = materialization.materializationAgeMs;
196
+ return {
197
+ metric: 'M5',
198
+ label: 'Projection staleness (server generation age)',
199
+ status: 'awaiting',
200
+ reading: `server-generation age=${ms(age)} (browser view: local-pull drift is CLI-only)`,
201
+ cause: `staleness-age threshold ${UNPINNED}; this is the one metric where PB can be staler than the never-drifting CLAUDE.md file`,
202
+ nextAction: 're-run pb handshake to re-generate the projection if the age looks high',
203
+ };
204
+ }
205
+ /**
206
+ * The STANDING coaching/moat-quality `unmeasured` line (TEN-2377). It is NOT a tile with a number —
207
+ * it is a permanent honesty marker so four green tiles are never read as "the moat is healthy".
208
+ */
209
+ export function diagnoseCoachingMoat() {
210
+ return {
211
+ metric: 'MOAT',
212
+ label: 'Coaching / moat quality',
213
+ status: 'unmeasured',
214
+ // The standing-unmeasured tension is TEN-2377; the real instrument is ASM-46's
215
+ // blind-padding-rate test. Both cited in comments, kept out of the user-facing strings.
216
+ reading: 'structurally unmeasured',
217
+ cause: 'the coach skips decision/standard/business-rule (the WHY-moat types) — a counter would flatter the board',
218
+ nextAction: 'do NOT read four green tiles as a healthy moat; the real instrument is the blind-padding-rate test',
219
+ };
220
+ }
221
+ /**
222
+ * diagnose — the whole mapping: data-layer payload + CLI-computed M5 local-drift → per-metric
223
+ * diagnoses + the standing M4-awaiting + coaching-unmeasured lines. Pure.
224
+ */
225
+ export function diagnose(payload, m5) {
226
+ return {
227
+ m1: diagnoseM1(payload.m1),
228
+ m2: diagnoseM2(payload.m2),
229
+ m3: diagnoseM3(payload.m3),
230
+ m4: diagnoseM4(),
231
+ m5: diagnoseM5(m5),
232
+ moat: diagnoseCoachingMoat(),
233
+ };
234
+ }
235
+ /**
236
+ * diagnoseFromServer — the Cortex-page mapping: it has the public action payload but NO local fs,
237
+ * so M5 is the server-generation-age view (`diagnoseM5FromServer`) rather than the CLI local-pull
238
+ * drift. Everything else is identical to `diagnose`. Pure, browser-safe.
239
+ */
240
+ export function diagnoseFromServer(payload) {
241
+ return {
242
+ m1: diagnoseM1(payload.m1),
243
+ m2: diagnoseM2(payload.m2),
244
+ m3: diagnoseM3(payload.m3),
245
+ m4: diagnoseM4(),
246
+ m5: diagnoseM5FromServer(payload.materialization),
247
+ moat: diagnoseCoachingMoat(),
248
+ };
249
+ }
250
+ //# sourceMappingURL=diagnose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnose.js","sourceRoot":"","sources":["../../src/scoreboard/diagnose.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Diagnosis-layer unit tests (spec §2d, A4).
3
+ *
4
+ * Asserts the pure metric→{status,reading,cause,nextAction} mapping, with explicit coverage of
5
+ * the awaiting / unmeasured / degraded / ok states and the honesty invariants:
6
+ * - No flip threshold is pinned (DEC-1282) → judged readings are `awaiting`, never an invented
7
+ * number, and the raw reading is always present.
8
+ * - M4 is a standing `awaiting` tile (deferred, TEN-2433) — never a fake green.
9
+ * - Coaching/moat quality is a standing `unmeasured` line (TEN-2377).
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=diagnose.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnose.test.d.ts","sourceRoot":"","sources":["../../src/scoreboard/diagnose.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Diagnosis-layer unit tests (spec §2d, A4).
3
+ *
4
+ * Asserts the pure metric→{status,reading,cause,nextAction} mapping, with explicit coverage of
5
+ * the awaiting / unmeasured / degraded / ok states and the honesty invariants:
6
+ * - No flip threshold is pinned (DEC-1282) → judged readings are `awaiting`, never an invented
7
+ * number, and the raw reading is always present.
8
+ * - M4 is a standing `awaiting` tile (deferred, TEN-2433) — never a fake green.
9
+ * - Coaching/moat quality is a standing `unmeasured` line (TEN-2377).
10
+ */
11
+ import { describe, it, expect } from 'vitest';
12
+ import { diagnose, diagnoseM1, diagnoseM2, diagnoseM3, diagnoseM4, diagnoseM5, diagnoseCoachingMoat, } from './diagnose.js';
13
+ const m1WithData = {
14
+ mix: { audited: 3, draft: 2, noDomain: 5, total: 10 },
15
+ noDomainShare: 0.5,
16
+ auditedShare: 0.3,
17
+ draftShare: 0.2,
18
+ windowMs: 30 * 24 * 60 * 60 * 1000,
19
+ servedRowCount: 10,
20
+ };
21
+ const m1Empty = {
22
+ mix: { audited: 0, draft: 0, noDomain: 0, total: 0 },
23
+ noDomainShare: null,
24
+ auditedShare: null,
25
+ draftShare: null,
26
+ windowMs: 30 * 24 * 60 * 60 * 1000,
27
+ servedRowCount: 0,
28
+ };
29
+ describe('diagnoseM1', () => {
30
+ it('is awaiting (never an invented floor) when there is data — DEC-1282', () => {
31
+ const d = diagnoseM1(m1WithData);
32
+ expect(d.status).toBe('awaiting');
33
+ expect(d.metric).toBe('M1');
34
+ // Raw reading always shown.
35
+ expect(d.reading).toContain('no-domain=50%');
36
+ expect(d.reading).toContain('served=10');
37
+ // Unpinned cause + concrete next action (domains unratified).
38
+ expect(d.cause).toMatch(/unpinned/i);
39
+ expect(d.nextAction).toMatch(/ratify/i);
40
+ });
41
+ it('is unmeasured when nothing was served', () => {
42
+ const d = diagnoseM1(m1Empty);
43
+ expect(d.status).toBe('unmeasured');
44
+ expect(d.nextAction).toMatch(/serve/i);
45
+ });
46
+ });
47
+ describe('diagnoseM2', () => {
48
+ it('is ok at full parity (post-unify steady state — 0 vs non-0 is exact, no number invented)', () => {
49
+ const m2 = { divergentBuckets: 0, fullParity: true, task: null };
50
+ const d = diagnoseM2(m2);
51
+ expect(d.status).toBe('ok');
52
+ expect(d.nextAction).toBe('');
53
+ expect(d.reading).toContain('divergentBuckets=0');
54
+ });
55
+ it('is degraded when the surfaces re-fork (regression signal)', () => {
56
+ const m2 = { divergentBuckets: 2, fullParity: false, task: 'do a thing' };
57
+ const d = diagnoseM2(m2);
58
+ expect(d.status).toBe('degraded');
59
+ expect(d.reading).toContain('divergentBuckets=2');
60
+ expect(d.reading).toContain('task="do a thing"');
61
+ expect(d.nextAction).toMatch(/shared ranker|TEN-2431/);
62
+ });
63
+ });
64
+ describe('diagnoseM3', () => {
65
+ it('is awaiting when there are captures (floor unpinned)', () => {
66
+ const m3 = {
67
+ capturedCount: 8,
68
+ everRatifiedCount: 3,
69
+ inSessionRatifiedCount: 1,
70
+ everRatifiedRate: 0.375,
71
+ inSessionRatifiedRate: 0.125,
72
+ sessionWindow: 20,
73
+ sessionsConsidered: 12,
74
+ };
75
+ const d = diagnoseM3(m3);
76
+ expect(d.status).toBe('awaiting');
77
+ expect(d.reading).toContain('captured=8');
78
+ expect(d.cause).toMatch(/unpinned/i);
79
+ });
80
+ it('is unmeasured with zero captures', () => {
81
+ const m3 = {
82
+ capturedCount: 0,
83
+ everRatifiedCount: 0,
84
+ inSessionRatifiedCount: 0,
85
+ everRatifiedRate: null,
86
+ inSessionRatifiedRate: null,
87
+ sessionWindow: 20,
88
+ sessionsConsidered: 5,
89
+ };
90
+ const d = diagnoseM3(m3);
91
+ expect(d.status).toBe('unmeasured');
92
+ });
93
+ });
94
+ describe('diagnoseM4', () => {
95
+ it('is a standing awaiting tile — never a fake green, never a number (TEN-2433)', () => {
96
+ const d = diagnoseM4();
97
+ expect(d.status).toBe('awaiting');
98
+ expect(d.metric).toBe('M4');
99
+ expect(d.cause).toMatch(/baseline/);
100
+ expect(d.nextAction).toMatch(/baseline arm|blind-judged/);
101
+ // chain-id-leak guard: the user-facing strings carry no raw Chain IDs.
102
+ expect(d.cause).not.toMatch(/\b[A-Z]{2,5}-\d+\b/);
103
+ expect(d.nextAction).not.toMatch(/\b[A-Z]{2,5}-\d+\b/);
104
+ // No invented number in the reading — it is a delta-or-nothing tile.
105
+ expect(d.reading).toMatch(/delta|baseline/i);
106
+ });
107
+ });
108
+ describe('diagnoseM5', () => {
109
+ it('is unmeasured (fail-soft) when the local projection is unavailable', () => {
110
+ const m5 = {
111
+ localProjectionMtime: null,
112
+ serverMaterializedAt: 123,
113
+ localLagMs: null,
114
+ localAgeMs: null,
115
+ unavailableReason: 'no .productbrain/ project root found',
116
+ };
117
+ const d = diagnoseM5(m5);
118
+ expect(d.status).toBe('unmeasured');
119
+ expect(d.reading).toContain('no .productbrain/');
120
+ });
121
+ it('is awaiting (threshold unpinned) when local drift is determinable', () => {
122
+ const m5 = {
123
+ localProjectionMtime: 1_000,
124
+ serverMaterializedAt: 2_000,
125
+ localLagMs: 1_000,
126
+ localAgeMs: 5_000,
127
+ };
128
+ const d = diagnoseM5(m5);
129
+ expect(d.status).toBe('awaiting');
130
+ expect(d.cause).toMatch(/unpinned/i);
131
+ // The CLAUDE.md-can't-drift framing is preserved.
132
+ expect(d.cause).toMatch(/CLAUDE\.md/);
133
+ });
134
+ it('is unmeasured (not awaiting) when the local projection exists but there is no server reference', () => {
135
+ // Local projection present, but no server materialization to diff against → the local-pull
136
+ // DRIFT (server−local) is undeterminable, so the honest status is unmeasured, not a misleading
137
+ // `awaiting` with local-lag=n/a. local-age is still surfaced.
138
+ const m5 = {
139
+ localProjectionMtime: 5_000,
140
+ serverMaterializedAt: null,
141
+ localLagMs: null,
142
+ localAgeMs: 3_000,
143
+ };
144
+ const d = diagnoseM5(m5);
145
+ expect(d.status).toBe('unmeasured');
146
+ expect(d.reading).toContain('local-age=');
147
+ expect(d.reading).toMatch(/drift undeterminable/i);
148
+ });
149
+ });
150
+ describe('diagnoseCoachingMoat', () => {
151
+ it('is a standing unmeasured line — four green tiles != healthy moat (TEN-2377)', () => {
152
+ const d = diagnoseCoachingMoat();
153
+ expect(d.status).toBe('unmeasured');
154
+ expect(d.cause).toMatch(/WHY-moat|TEN-2377|skips/i);
155
+ expect(d.nextAction).toMatch(/four green tiles/i);
156
+ });
157
+ });
158
+ describe('diagnose (bundle)', () => {
159
+ it('produces all five metric diagnoses plus the standing moat line', () => {
160
+ const payload = {
161
+ workspaceId: 'ws_1',
162
+ generatedAt: Date.now(),
163
+ m1: m1WithData,
164
+ m2: { divergentBuckets: 0, fullParity: true, task: null },
165
+ m3: {
166
+ capturedCount: 0,
167
+ everRatifiedCount: 0,
168
+ inSessionRatifiedCount: 0,
169
+ everRatifiedRate: null,
170
+ inSessionRatifiedRate: null,
171
+ sessionWindow: 20,
172
+ sessionsConsidered: 0,
173
+ },
174
+ materialization: { latestMaterializedAt: null, materializationAgeMs: null, now: Date.now() },
175
+ };
176
+ const m5 = {
177
+ localProjectionMtime: null,
178
+ serverMaterializedAt: null,
179
+ localLagMs: null,
180
+ localAgeMs: null,
181
+ unavailableReason: 'absent',
182
+ };
183
+ const bundle = diagnose(payload, m5);
184
+ expect(bundle.m1.status).toBe('awaiting');
185
+ expect(bundle.m2.status).toBe('ok');
186
+ expect(bundle.m3.status).toBe('unmeasured');
187
+ expect(bundle.m4.status).toBe('awaiting');
188
+ expect(bundle.m5.status).toBe('unmeasured');
189
+ expect(bundle.moat.status).toBe('unmeasured');
190
+ });
191
+ });
192
+ //# sourceMappingURL=diagnose.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnose.test.js","sourceRoot":"","sources":["../../src/scoreboard/diagnose.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Flywheel scoreboard M5 — LOCAL-PULL drift, computed at the CLI (DEC-1284).
3
+ *
4
+ * The data layer deliberately exposes only the SERVER-side projection-generation time
5
+ * (`materialization.latestMaterializedAt`) — a Convex query cannot stat the worktree's files
6
+ * (DEC-1284). The TRUE M5 the spec asks for is LOCAL-pull staleness: how far the agent's pulled
7
+ * `.productbrain/` projection lags the server projection (or its absolute age). Only the CLI can
8
+ * compute it, by stat-ing the local projection files' mtime. This module is that computation.
9
+ *
10
+ * Fail-soft: if `.productbrain/` is absent or no projection files are found, returns an
11
+ * `unavailableReason` so the diagnosis layer renders `unmeasured` — NEVER a fake `ok`.
12
+ */
13
+ import type { LocalDriftM5 } from './diagnose.js';
14
+ /**
15
+ * computeLocalDrift — the M5 local-pull drift (A5).
16
+ *
17
+ * @param serverMaterializedAt the data layer's `materialization.latestMaterializedAt` (ms epoch,
18
+ * or null when the workspace never materialized server-side).
19
+ * @param now injectable clock for tests (defaults to Date.now()).
20
+ * @param startDir injectable start dir for tests (defaults to process.cwd()).
21
+ */
22
+ export declare function computeLocalDrift(serverMaterializedAt: number | null, now?: number, startDir?: string): LocalDriftM5;
23
+ //# sourceMappingURL=localDrift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localDrift.d.ts","sourceRoot":"","sources":["../../src/scoreboard/localDrift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAwClD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,oBAAoB,EAAE,MAAM,GAAG,IAAI,EACnC,GAAG,GAAE,MAAmB,EACxB,QAAQ,GAAE,MAAsB,GAC/B,YAAY,CAkDd"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Flywheel scoreboard M5 — LOCAL-PULL drift, computed at the CLI (DEC-1284).
3
+ *
4
+ * The data layer deliberately exposes only the SERVER-side projection-generation time
5
+ * (`materialization.latestMaterializedAt`) — a Convex query cannot stat the worktree's files
6
+ * (DEC-1284). The TRUE M5 the spec asks for is LOCAL-pull staleness: how far the agent's pulled
7
+ * `.productbrain/` projection lags the server projection (or its absolute age). Only the CLI can
8
+ * compute it, by stat-ing the local projection files' mtime. This module is that computation.
9
+ *
10
+ * Fail-soft: if `.productbrain/` is absent or no projection files are found, returns an
11
+ * `unavailableReason` so the diagnosis layer renders `unmeasured` — NEVER a fake `ok`.
12
+ */
13
+ import { existsSync, statSync, readdirSync } from 'fs';
14
+ import { resolve } from 'path';
15
+ import { findProjectRoot } from '../lib/config.js';
16
+ /**
17
+ * The `pb handshake`-written projection files whose mtime tracks the local pull. These are the
18
+ * gitignored projections regenerated per worktree (context.md / briefing.md / generated/). We
19
+ * take the NEWEST mtime across whatever projection files exist — that is the most recent pull.
20
+ * `config.json` is excluded: it is committed + static, not a projection, so it never reflects a
21
+ * handshake pull.
22
+ */
23
+ const PROJECTION_FILES = ['context.md', 'briefing.md'];
24
+ const PROJECTION_DIRS = ['generated', 'rules'];
25
+ /** Newest mtime (ms) across a set of candidate paths, or null if none exist. */
26
+ function newestMtime(paths) {
27
+ let newest = null;
28
+ for (const p of paths) {
29
+ try {
30
+ if (!existsSync(p))
31
+ continue;
32
+ const st = statSync(p);
33
+ const mtime = st.mtimeMs;
34
+ if (newest === null || mtime > newest)
35
+ newest = mtime;
36
+ }
37
+ catch {
38
+ // unreadable path — skip, fail-soft
39
+ }
40
+ }
41
+ return newest;
42
+ }
43
+ /** Collect file paths one level deep inside a projection directory. */
44
+ function filesInDir(dir) {
45
+ try {
46
+ if (!existsSync(dir))
47
+ return [];
48
+ return readdirSync(dir, { withFileTypes: true })
49
+ .filter((e) => e.isFile())
50
+ .map((e) => resolve(dir, e.name));
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ }
56
+ /**
57
+ * computeLocalDrift — the M5 local-pull drift (A5).
58
+ *
59
+ * @param serverMaterializedAt the data layer's `materialization.latestMaterializedAt` (ms epoch,
60
+ * or null when the workspace never materialized server-side).
61
+ * @param now injectable clock for tests (defaults to Date.now()).
62
+ * @param startDir injectable start dir for tests (defaults to process.cwd()).
63
+ */
64
+ export function computeLocalDrift(serverMaterializedAt, now = Date.now(), startDir = process.cwd()) {
65
+ const projectRoot = findProjectRoot(startDir);
66
+ if (!projectRoot) {
67
+ return {
68
+ localProjectionMtime: null,
69
+ serverMaterializedAt,
70
+ localLagMs: null,
71
+ localAgeMs: null,
72
+ unavailableReason: 'no .productbrain/ project root found (walked up from CWD)',
73
+ };
74
+ }
75
+ const pbDir = resolve(projectRoot, '.productbrain');
76
+ if (!existsSync(pbDir)) {
77
+ return {
78
+ localProjectionMtime: null,
79
+ serverMaterializedAt,
80
+ localLagMs: null,
81
+ localAgeMs: null,
82
+ unavailableReason: `.productbrain/ missing at ${projectRoot}`,
83
+ };
84
+ }
85
+ const candidatePaths = [
86
+ ...PROJECTION_FILES.map((f) => resolve(pbDir, f)),
87
+ ...PROJECTION_DIRS.flatMap((d) => filesInDir(resolve(pbDir, d))),
88
+ ];
89
+ const localProjectionMtime = newestMtime(candidatePaths);
90
+ if (localProjectionMtime === null) {
91
+ return {
92
+ localProjectionMtime: null,
93
+ serverMaterializedAt,
94
+ localLagMs: null,
95
+ localAgeMs: null,
96
+ unavailableReason: 'no .productbrain/ projection files (context.md / briefing.md / generated/ / rules/) found — run pb handshake',
97
+ };
98
+ }
99
+ // local-lag = how far the local pull lags the server projection. Positive = server is newer
100
+ // (local is stale). Clamp at 0 (a local mtime ahead of the server is not "negative staleness").
101
+ const localLagMs = serverMaterializedAt === null ? null : Math.max(0, serverMaterializedAt - localProjectionMtime);
102
+ // local-age = absolute age of the local projection vs now.
103
+ const localAgeMs = Math.max(0, now - localProjectionMtime);
104
+ return {
105
+ localProjectionMtime,
106
+ serverMaterializedAt,
107
+ localLagMs,
108
+ localAgeMs,
109
+ };
110
+ }
111
+ //# sourceMappingURL=localDrift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localDrift.js","sourceRoot":"","sources":["../../src/scoreboard/localDrift.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * M5 local-pull drift unit tests (A5, DEC-1284).
3
+ *
4
+ * Exercises computeLocalDrift over a real temp `.productbrain/` so the stat path is genuinely
5
+ * driven (not mocked): a config-bearing root (so findProjectRoot resolves) + a projection file
6
+ * whose mtime is the local pull. Asserts the lag/age math, the clamp, and the fail-soft branches.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=localDrift.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localDrift.test.d.ts","sourceRoot":"","sources":["../../src/scoreboard/localDrift.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}