rcf-lite 0.28.4 → 0.29.0
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/CHANGELOG.md +107 -1
- package/bin/rcf.js +4 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/define-draft-shapes.md +89 -0
- package/guidance/define-intake.md +97 -0
- package/guidance/define-intent.md +107 -0
- package/guidance/discover-prototype.md +67 -0
- package/guidance/discovery-prototypes.md +69 -0
- package/guidance/harness-template.md +34 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +34 -0
- package/guidance/manifest.json +9 -2
- package/package.json +1 -1
- package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
- package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
- package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
- package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
- package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
- package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
- package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
- package/rcf/adrs/adr-4134-index-json-route.json +35 -0
- package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
- package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
- package/rcf/code-nodes/cn-725.json +18 -2
- package/rcf/code-nodes/cn-726.json +10 -2
- package/rcf/code-nodes/cn-727.json +13 -4
- package/rcf/code-nodes/cn-728.json +18 -2
- package/rcf/code-nodes/cn-729.json +10 -2
- package/rcf/code-nodes/cn-730.json +13 -3
- package/rcf/code-nodes/cn-731.json +20 -0
- package/rcf/code-nodes/cn-732.json +14 -0
- package/rcf/code-nodes/cn-733.json +14 -0
- package/rcf/code-nodes/cn-734.json +14 -0
- package/rcf/code-nodes/cn-735.json +20 -0
- package/rcf/code-nodes/cn-736.json +14 -0
- package/rcf/code-nodes/cn-737.json +14 -0
- package/rcf/code-nodes/cn-738.json +14 -0
- package/rcf/code-nodes/cn-739.json +18 -0
- package/rcf/code-nodes/cn-740.json +19 -0
- package/rcf/code-nodes/cn-741.json +16 -0
- package/rcf/code-nodes/cn-742.json +16 -0
- package/rcf/code-nodes/cn-743.json +15 -0
- package/rcf/code-nodes/cn-744.json +22 -0
- package/rcf/code-nodes/cn-745.json +16 -0
- package/rcf/code-nodes/cn-746.json +14 -0
- package/rcf/code-nodes/cn-747.json +18 -0
- package/rcf/code-nodes/cn-748.json +16 -0
- package/rcf/code-nodes/cn-749.json +16 -0
- package/rcf/code-nodes/cn-750.json +15 -0
- package/rcf/code-nodes/cn-751.json +18 -0
- package/rcf/code-nodes/cn-752.json +14 -0
- package/rcf/fbs/fbs-196.json +13 -5
- package/rcf/fbs/fbs-198.json +42 -0
- package/rcf/fbs/fbs-199.json +59 -0
- package/rcf/fbs/fbs-200.json +46 -0
- package/rcf/fbs/fbs-201.json +49 -0
- package/rcf/fbs/fbs-202.json +110 -0
- package/rcf/fbs/fbs-203.json +100 -0
- package/rcf/requirements/req-002.json +2 -2
- package/rcf/requirements/req-173.json +2 -2
- package/rcf/requirements/req-174.json +10 -4
- package/rcf/requirements/req-175.json +10 -4
- package/rcf/requirements/req-176.json +25 -0
- package/rcf/requirements/req-180.json +31 -0
- package/rcf/requirements/req-181.json +31 -0
- package/rcf/requirements/req-182.json +25 -0
- package/rcf/requirements/req-186.json +25 -0
- package/rcf/requirements/req-187.json +31 -0
- package/rcf/requirements/req-188.json +25 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +15 -3
- package/rcf/tacs/tac-4122-define-gates.json +40 -4
- package/rcf/tacs/tac-4123-define-readiness.json +39 -3
- package/rcf/tacs/tac-4124-define-freeze.json +28 -0
- package/rcf/tacs/tac-4127-readiness-view.json +49 -0
- package/rcf/tacs/tac-4128-define-guidance.json +53 -0
- package/rcf/tacs/tac-4129-define-questions.json +45 -0
- package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
- package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
- package/rcf/tacs/tac-4132-id-lookup.json +42 -0
- package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
- package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
- package/rcf/test-suites/ts-016.json +3 -3
- package/rcf/test-suites/ts-215.json +148 -4
- package/rcf/test-suites/ts-216.json +19 -3
- package/rcf/test-suites/ts-218.json +78 -0
- package/rcf/test-suites/ts-219.json +70 -0
- package/rcf/test-suites/ts-220.json +78 -0
- package/rcf/test-suites/ts-221.json +78 -0
- package/rcf/test-suites/ts-222.json +100 -0
- package/rcf/test-suites/ts-223.json +94 -0
- package/rcf/test-suites/ts-224.json +70 -0
- package/rcf/test-suites/ts-225.json +78 -0
- package/rcf/test-suites/ts-226.json +158 -0
- package/rcf/test-suites/ts-227.json +38 -0
- package/rcf/test-suites/ts-228.json +86 -0
- package/rcf/test-suites/ts-229.json +83 -0
- package/rcf/user-stories/us-17302.json +50 -0
- package/rcf/user-stories/us-17401.json +14 -2
- package/rcf/user-stories/us-17402.json +55 -0
- package/rcf/user-stories/us-17403.json +60 -0
- package/rcf/user-stories/us-17502.json +60 -0
- package/rcf/user-stories/us-17503.json +65 -0
- package/rcf/user-stories/us-17601.json +58 -0
- package/rcf/user-stories/us-18001.json +55 -0
- package/rcf/user-stories/us-18002.json +35 -0
- package/rcf/user-stories/us-18101.json +35 -0
- package/rcf/user-stories/us-18202.json +40 -0
- package/rcf/user-stories/us-18601.json +55 -0
- package/rcf/user-stories/us-18602.json +50 -0
- package/rcf/user-stories/us-18603.json +45 -0
- package/rcf/user-stories/us-18701.json +50 -0
- package/rcf/user-stories/us-18702.json +50 -0
- package/rcf/user-stories/us-18801.json +45 -0
- package/rcf/user-stories/us-204.json +59 -0
- package/rcf/user-stories/us-205.json +64 -0
- package/rcf/user-stories/us-206.json +54 -0
- package/releases/releases.yaml +11 -1
- package/src/cli/freeze.js +730 -0
- package/src/cli/help.js +12 -0
- package/src/cli/ledger.js +228 -12
- package/src/cli/questions.js +288 -0
- package/src/cli/readiness.js +267 -49
- package/src/cli/view.js +29 -0
- package/src/define/freeze-record.js +30 -15
- package/src/define/ledgers.js +210 -11
- package/src/intake/index.js +1 -1
- package/src/intake/orchestrator.js +117 -0
- package/src/mcp/tools.js +354 -0
- package/src/query/delta.js +6 -0
- package/src/query/gates.js +314 -39
- package/src/query/index.js +16 -2
- package/src/query/questions.js +438 -0
- package/src/query/readiness.js +248 -7
- package/src/server/index.js +24 -2
- package/src/server/routes.js +68 -1
- package/src/setup/identity-seed.js +17 -0
- package/src/view/build-dag.js +563 -0
- package/src/view/components/badge.js +41 -0
- package/src/view/components/diff.js +72 -0
- package/src/view/components/doc-row.js +62 -0
- package/src/view/components/empty-state.js +35 -0
- package/src/view/components/entity-selector.js +137 -0
- package/src/view/components/filter-bar.js +97 -0
- package/src/view/components/findings-list.js +49 -0
- package/src/view/components/index.js +12 -0
- package/src/view/components/pill.js +46 -0
- package/src/view/components/sub-tab-strip.js +43 -0
- package/src/view/components/toast.js +25 -0
- package/src/view/components-fixture.js +265 -0
- package/src/view/doc-renderers/architecture.js +304 -0
- package/src/view/doc-renderers/build.js +160 -0
- package/src/view/doc-renderers/index.js +2 -0
- package/src/view/doc-renderers/prd.js +35 -6
- package/src/view/html-page.js +700 -602
- package/src/view/index.js +96 -3
- package/src/view/live-client.js +39 -6
- package/src/view/needs-work.js +79 -0
- package/src/view/page-init.js +2193 -0
- package/src/view/readiness/phrasebook.js +115 -0
- package/src/view/readiness/po-layer.js +288 -0
- package/src/view/readiness/question-adapter.js +329 -0
- package/src/view/readiness/stage-legend.js +71 -0
- package/src/view/readiness.js +505 -0
- package/src/view/style.css +2017 -382
- package/src/view/test-host/embed-client.js +121 -0
- package/src/view/test-host/fixture.js +183 -0
- package/src/view/test-host/lints.js +220 -0
- package/src/view/view-index.js +323 -0
package/src/cli/readiness.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
// `rcf define readiness` subcommand handler (REQ-175; proposal
|
|
2
|
-
// 2026-09-22 §2.4, §6.2 v3
|
|
2
|
+
// 2026-09-22 §2.4, §6.2 v3; spec 2026-10-01 §2.3, §3, §4 for the
|
|
3
|
+
// 0.29.0 PR B additions).
|
|
3
4
|
//
|
|
4
|
-
// Prints the readiness object (§2.4) or its JSON envelope.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// Prints the readiness object (§2.4) or its JSON envelope. The 0.29.0
|
|
6
|
+
// train added two verdict lines at the top of the text report (shared
|
|
7
|
+
// with the Readiness tab via `formatVerdictLines`) and two new flags:
|
|
8
|
+
// `--level intent|build` which drives the exit code against
|
|
9
|
+
// `levels.<which>.ok` (stricter than `--check all` on warn-with-ack
|
|
10
|
+
// failures, per spec section 3.2 decision 4; the `[warn]` line still
|
|
11
|
+
// prints), and `--persona productOwner|engineer` which is a display
|
|
12
|
+
// filter and never affects the exit code or JSON content. `--check`
|
|
13
|
+
// behaviour is unchanged. The producer is wrapped with
|
|
14
|
+
// `runWithAdmissibilityGate` (NV-BL-SR-03 addendum) so a chain that
|
|
15
|
+
// refuses admissibility short-circuits with the same refusal envelope
|
|
16
|
+
// every other query verb has.
|
|
11
17
|
|
|
12
18
|
import { parseArgs } from 'node:util';
|
|
13
19
|
import { readFile } from 'node:fs/promises';
|
|
@@ -25,12 +31,20 @@ import {
|
|
|
25
31
|
STAGE_SHORT_NAMES,
|
|
26
32
|
stagePolicy,
|
|
27
33
|
} from '../query/gates.js';
|
|
28
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
computeReadiness,
|
|
36
|
+
formatTreeLine,
|
|
37
|
+
formatVerdictLines,
|
|
38
|
+
parseLevelFlag,
|
|
39
|
+
parsePersonaFlag,
|
|
40
|
+
} from '../query/readiness.js';
|
|
29
41
|
import { runWithAdmissibilityGate } from '../query/index.js';
|
|
30
42
|
|
|
31
43
|
const OPTION_SPEC = {
|
|
32
44
|
json: { type: 'boolean' },
|
|
33
45
|
check: { type: 'string' },
|
|
46
|
+
level: { type: 'string' },
|
|
47
|
+
persona: { type: 'string' },
|
|
34
48
|
help: { type: 'boolean' },
|
|
35
49
|
};
|
|
36
50
|
|
|
@@ -45,8 +59,15 @@ eight stage-gate checks (D1..D8), tree coverage and open decisions
|
|
|
45
59
|
into one object. The viewer's Readiness tab (slice 5) will render the
|
|
46
60
|
same object with no schema translation.
|
|
47
61
|
|
|
62
|
+
The text report prints two verdict lines at the top (shared with the
|
|
63
|
+
Readiness tab via formatVerdictLines): intent-complete (every
|
|
64
|
+
product-owner question answered) and ready-to-build (equals
|
|
65
|
+
freezeable). Blockers are grouped by persona, ordered by the
|
|
66
|
+
profile.md register marker.
|
|
67
|
+
|
|
48
68
|
Options:
|
|
49
69
|
--json Emit the full readiness object as JSON.
|
|
70
|
+
--persona is ignored under --json.
|
|
50
71
|
--check <stage> Narrow the output to one stage. Accepts a
|
|
51
72
|
stage id (D1..D8) or its short name:
|
|
52
73
|
brief | skeleton | shapes | stories |
|
|
@@ -57,6 +78,25 @@ Options:
|
|
|
57
78
|
unacknowledged warn-with-ack failure
|
|
58
79
|
(D3 / D5 / D6). Use --check all to print
|
|
59
80
|
every stage under this exit-code policy.
|
|
81
|
+
--level <intent|build> Pick which verdict the exit code follows.
|
|
82
|
+
--level intent exits 4 when
|
|
83
|
+
levels.intentComplete.ok is false
|
|
84
|
+
(any failing product-owner check); exits
|
|
85
|
+
0 otherwise. --level build exits 4 when
|
|
86
|
+
levels.readyToBuild.ok is false, which is
|
|
87
|
+
stricter than --check all on unacked
|
|
88
|
+
warn-with-ack failures (the [warn] line
|
|
89
|
+
still prints). Combined with --check, the
|
|
90
|
+
invocation exits 4 if either policy
|
|
91
|
+
says 4. With neither flag the exit code
|
|
92
|
+
stays 0.
|
|
93
|
+
--persona <po|engineer> Display filter for the text report.
|
|
94
|
+
Values: productOwner | engineer. Collapses
|
|
95
|
+
the opposite-persona block to one line
|
|
96
|
+
with its check count and restricts the
|
|
97
|
+
per-stage detail to matching-persona
|
|
98
|
+
checks. Never affects the exit code or
|
|
99
|
+
the JSON content.
|
|
60
100
|
--help Print this help.
|
|
61
101
|
`;
|
|
62
102
|
|
|
@@ -86,6 +126,34 @@ async function readProfileText(projectRoot) {
|
|
|
86
126
|
}
|
|
87
127
|
}
|
|
88
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Pick the profile register from the text, returning one of
|
|
131
|
+
* `productOwner` | `engineer` | `unstated`. The marker is a bare
|
|
132
|
+
* word match (same convention as `checkD1Brief`); the first marker
|
|
133
|
+
* found in the text wins. Absent or unrecognised yields `unstated`,
|
|
134
|
+
* which the text report orders as PO first (spec section 4).
|
|
135
|
+
*
|
|
136
|
+
* @param {string | null} text
|
|
137
|
+
* @returns {'productOwner' | 'engineer' | 'unstated'}
|
|
138
|
+
*/
|
|
139
|
+
function pickRegister(text) {
|
|
140
|
+
if (typeof text !== 'string' || text.length === 0) return 'unstated';
|
|
141
|
+
// Scan in file-order so the first marker in the document wins;
|
|
142
|
+
// this matches how a human reads `profile.md`.
|
|
143
|
+
const markers = ['productOwner', 'engineer', 'unstated'];
|
|
144
|
+
let best = null;
|
|
145
|
+
let bestIdx = Infinity;
|
|
146
|
+
for (const m of markers) {
|
|
147
|
+
const idx = text.indexOf(m);
|
|
148
|
+
if (idx === -1) continue;
|
|
149
|
+
if (idx < bestIdx) {
|
|
150
|
+
bestIdx = idx;
|
|
151
|
+
best = m;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return /** @type {any} */ (best) ?? 'unstated';
|
|
155
|
+
}
|
|
156
|
+
|
|
89
157
|
/**
|
|
90
158
|
* @param {string[]} argv
|
|
91
159
|
* @param {object} [deps]
|
|
@@ -123,6 +191,24 @@ export async function main(argv, deps = {}) {
|
|
|
123
191
|
}
|
|
124
192
|
}
|
|
125
193
|
|
|
194
|
+
/** @type {'intent' | 'build' | null} */
|
|
195
|
+
let level;
|
|
196
|
+
try {
|
|
197
|
+
level = parseLevelFlag(flags.level);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
stderr.write(`[error] usage readiness: ${/** @type {Error} */ (err).message}\n`);
|
|
200
|
+
return 2;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @type {'productOwner' | 'engineer' | null} */
|
|
204
|
+
let persona;
|
|
205
|
+
try {
|
|
206
|
+
persona = parsePersonaFlag(flags.persona);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
stderr.write(`[error] usage readiness: ${/** @type {Error} */ (err).message}\n`);
|
|
209
|
+
return 2;
|
|
210
|
+
}
|
|
211
|
+
|
|
126
212
|
const projectRoot = await findProjectRoot(cwd);
|
|
127
213
|
if (!projectRoot) {
|
|
128
214
|
stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
|
|
@@ -177,39 +263,53 @@ export async function main(argv, deps = {}) {
|
|
|
177
263
|
|
|
178
264
|
if (flags.json) {
|
|
179
265
|
// JSON emission carries the full object plus the wall-time hint
|
|
180
|
-
// as a stable side-band on `_meta
|
|
181
|
-
//
|
|
182
|
-
|
|
266
|
+
// and the invocation level as a stable side-band on `_meta`.
|
|
267
|
+
// `_meta.level` is null when the flag was absent (spec section
|
|
268
|
+
// 3.3): consumers can tell "no --level given" from "--level
|
|
269
|
+
// build" explicitly, which matters for the ticket artefact.
|
|
270
|
+
const envelope = { ...result, _meta: { wallMs, level } };
|
|
183
271
|
stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
184
|
-
return
|
|
272
|
+
return decideExitCode(result, checkStage, level, stderr);
|
|
185
273
|
}
|
|
186
274
|
|
|
187
|
-
renderText(stdout, result, wallMs, checkStage);
|
|
188
|
-
|
|
189
|
-
return decideExitCode(result, checkStage, stderr);
|
|
190
|
-
}
|
|
191
|
-
return 0;
|
|
275
|
+
renderText(stdout, stderr, result, wallMs, checkStage, level, persona, profileText);
|
|
276
|
+
return decideExitCode(result, checkStage, level, stderr);
|
|
192
277
|
}
|
|
193
278
|
|
|
194
279
|
/**
|
|
195
|
-
* Emit the text summary
|
|
196
|
-
*
|
|
197
|
-
*
|
|
280
|
+
* Emit the text summary (spec section 4):
|
|
281
|
+
* 1. Tree line.
|
|
282
|
+
* 2. Verdict pair (intent-complete, ready-to-build).
|
|
283
|
+
* 3. Next action, chip line, delta, coverage, decisions, freezeable
|
|
284
|
+
* (today's lines; omitted when --check narrows to one stage).
|
|
285
|
+
* 4. Product-owner block and engineer block, ordered by the
|
|
286
|
+
* profile.md register (PO first by default; engineer first when
|
|
287
|
+
* the register is `engineer`; `unstated`/absent is PO first).
|
|
288
|
+
* With --persona the opposite-persona block collapses to one
|
|
289
|
+
* line; with --check <stage> the persona blocks are limited to
|
|
290
|
+
* that stage.
|
|
291
|
+
* 5. Per-stage detail block (today's output; persona is now inside
|
|
292
|
+
* the parenthesis after `over`). With --persona only the
|
|
293
|
+
* matching-persona checks are rendered.
|
|
198
294
|
*
|
|
199
295
|
* @param {NodeJS.WritableStream} stdout
|
|
296
|
+
* @param {NodeJS.WritableStream} stderr
|
|
200
297
|
* @param {import('../query/readiness.js').ReadinessResult} result
|
|
201
298
|
* @param {number} wallMs
|
|
202
299
|
* @param {string | null} checkStage
|
|
300
|
+
* @param {'intent' | 'build' | null} level
|
|
301
|
+
* @param {'productOwner' | 'engineer' | null} persona
|
|
302
|
+
* @param {string | null} profileText
|
|
203
303
|
*/
|
|
204
|
-
function renderText(stdout, result, wallMs, checkStage) {
|
|
304
|
+
function renderText(stdout, stderr, result, wallMs, checkStage, level, persona, profileText) {
|
|
205
305
|
stdout.write(`${formatTreeLine(result)}\n`);
|
|
306
|
+
|
|
307
|
+
const verdicts = formatVerdictLines(result);
|
|
308
|
+
stdout.write(`${verdicts.intentComplete}\n`);
|
|
309
|
+
stdout.write(`${verdicts.readyToBuild}\n`);
|
|
310
|
+
|
|
206
311
|
if (!checkStage || checkStage === 'all') {
|
|
207
|
-
|
|
208
|
-
const ids = result.nextAction.ids.length > 0 ? `; ids: ${result.nextAction.ids.join(', ')}` : '';
|
|
209
|
-
stdout.write(`Next action: ${result.nextAction.stage} / ${result.nextAction.check}${ids}. Run \`${result.nextAction.command}\` after editing.\n`);
|
|
210
|
-
} else {
|
|
211
|
-
stdout.write('Next action: none; every gate passed, acknowledged or notApplicable. Freezeable.\n');
|
|
212
|
-
}
|
|
312
|
+
renderNextActions(stdout, result, level);
|
|
213
313
|
stdout.write(`Chips: ${formatChipLine(result)}\n`);
|
|
214
314
|
stdout.write(`Delta: changed ${result.delta.changed.length}, added ${result.delta.added.length}, removed ${result.delta.removed.length}, briefSince ${result.delta.briefSince.length}, impacted ${result.delta.impacted.length}, impactedFbs ${result.delta.impactedFbs.length}.\n`);
|
|
215
315
|
stdout.write(`Coverage: tree ${result.coverage.tree.totals.covered}/${result.coverage.tree.totals.requirements} covered (strict), ${result.coverage.delta.length} REQ ancestor(s) scoped from the delta.\n`);
|
|
@@ -217,12 +317,16 @@ function renderText(stdout, result, wallMs, checkStage) {
|
|
|
217
317
|
stdout.write(`Freezeable: ${result.freezeable ? 'yes' : 'no'}. Compute: ${wallMs} ms.\n`);
|
|
218
318
|
}
|
|
219
319
|
|
|
320
|
+
const register = pickRegister(profileText);
|
|
321
|
+
renderPersonaBlocks(stdout, result, persona, register, checkStage);
|
|
322
|
+
|
|
220
323
|
for (const stage of result.stages) {
|
|
221
324
|
if (checkStage && checkStage !== 'all' && stage.stage !== checkStage) continue;
|
|
222
|
-
stdout.write(`\n${stage.stage} (${stage.gate})
|
|
325
|
+
stdout.write(`\n${stage.stage} (${stage.gate}): ${STATE_LABEL[stage.state] ?? stage.state}${stage.reason ? ` (${stage.reason})` : ''}\n`);
|
|
223
326
|
for (const c of stage.checks) {
|
|
327
|
+
if (persona && c.persona !== persona) continue;
|
|
224
328
|
const mark = c.ok ? 'ok' : 'FAIL';
|
|
225
|
-
stdout.write(` [${mark}] ${c.name} (${c.over}): ${c.pass}/${c.total}\n`);
|
|
329
|
+
stdout.write(` [${mark}] ${c.name} (${c.over}, ${c.persona}): ${c.pass}/${c.total}\n`);
|
|
226
330
|
for (const f of c.failing.slice(0, 20)) {
|
|
227
331
|
stdout.write(` - ${f.id}: ${f.why}\n`);
|
|
228
332
|
}
|
|
@@ -233,6 +337,83 @@ function renderText(stdout, result, wallMs, checkStage) {
|
|
|
233
337
|
}
|
|
234
338
|
}
|
|
235
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Render the per-persona next-action lines (spec section 4). When a
|
|
342
|
+
* `--level` is given the matching persona's line heads the pair; when
|
|
343
|
+
* no `--level` is given the pair follows the register order. Each
|
|
344
|
+
* line is `Next action (<persona>): <stage> / <check>; ids: ...`.
|
|
345
|
+
*
|
|
346
|
+
* @param {NodeJS.WritableStream} stdout
|
|
347
|
+
* @param {import('../query/readiness.js').ReadinessResult} result
|
|
348
|
+
* @param {'intent' | 'build' | null} level
|
|
349
|
+
*/
|
|
350
|
+
function renderNextActions(stdout, result, level) {
|
|
351
|
+
const po = result.personas.productOwner.nextAction;
|
|
352
|
+
const eng = result.personas.engineer.nextAction;
|
|
353
|
+
const writeAction = (label, action) => {
|
|
354
|
+
if (!action) {
|
|
355
|
+
stdout.write(`Next action (${label}): none.\n`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const ids = action.ids.length > 0 ? `; ids: ${action.ids.join(', ')}` : '';
|
|
359
|
+
stdout.write(`Next action (${label}): ${action.stage} / ${action.check}${ids}. Run \`${action.command}\` after editing.\n`);
|
|
360
|
+
};
|
|
361
|
+
writeAction('product owner', po);
|
|
362
|
+
writeAction('engineer', eng);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Render the two persona-grouped blocker blocks (spec section 4).
|
|
367
|
+
* `register` orders the pair; `persona` (the display filter) collapses
|
|
368
|
+
* the opposite block to one count line; `checkStage` (when not null
|
|
369
|
+
* or 'all') restricts both blocks to blockers in that stage.
|
|
370
|
+
*
|
|
371
|
+
* @param {NodeJS.WritableStream} stdout
|
|
372
|
+
* @param {import('../query/readiness.js').ReadinessResult} result
|
|
373
|
+
* @param {'productOwner' | 'engineer' | null} persona
|
|
374
|
+
* @param {'productOwner' | 'engineer' | 'unstated'} register
|
|
375
|
+
* @param {string | null} checkStage
|
|
376
|
+
*/
|
|
377
|
+
function renderPersonaBlocks(stdout, result, persona, register, checkStage) {
|
|
378
|
+
const stageFilter = (bs) => (checkStage && checkStage !== 'all'
|
|
379
|
+
? bs.filter((b) => b.stage === checkStage)
|
|
380
|
+
: bs);
|
|
381
|
+
// L1 blockedBy for the PO list (ignores stage state); L2 blockedBy
|
|
382
|
+
// minus PO entries for the engineer list, matching
|
|
383
|
+
// `personas.engineer.blockers`.
|
|
384
|
+
const poBlockers = stageFilter(result.personas.productOwner.blockers);
|
|
385
|
+
const engineerBlockers = stageFilter(result.personas.engineer.blockers);
|
|
386
|
+
|
|
387
|
+
const order = register === 'engineer'
|
|
388
|
+
? [['engineer', engineerBlockers], ['productOwner', poBlockers]]
|
|
389
|
+
: [['productOwner', poBlockers], ['engineer', engineerBlockers]];
|
|
390
|
+
|
|
391
|
+
for (const [p, blockers] of order) {
|
|
392
|
+
stdout.write('\n');
|
|
393
|
+
const headerLabel = p === 'productOwner' ? 'Product owner' : 'Engineer';
|
|
394
|
+
const noun = p === 'productOwner' ? 'question' : 'check blocking';
|
|
395
|
+
const nounPlural = p === 'productOwner' ? 'questions' : 'checks blocking';
|
|
396
|
+
if (persona && p !== persona) {
|
|
397
|
+
stdout.write(`${headerLabel}: ${blockers.length} ${blockers.length === 1 ? noun : nounPlural} (hidden; run without --persona)\n`);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (blockers.length === 0) {
|
|
401
|
+
stdout.write(`${headerLabel}: 0 ${nounPlural}.\n`);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
stdout.write(`${headerLabel}: ${blockers.length} ${blockers.length === 1 ? noun : nounPlural}\n`);
|
|
405
|
+
for (const b of blockers) {
|
|
406
|
+
stdout.write(` ${b.stage} ${b.check}: ${b.question} ${b.failingCount}/${b.failingCount}\n`);
|
|
407
|
+
for (const id of b.ids.slice(0, 20)) {
|
|
408
|
+
stdout.write(` - ${id}\n`);
|
|
409
|
+
}
|
|
410
|
+
if (b.failingCount > b.ids.length) {
|
|
411
|
+
stdout.write(` ... ${b.failingCount - b.ids.length} more suppressed\n`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
236
417
|
/**
|
|
237
418
|
* Compose the chip line: "D1:passed D2:failing D3:acknowledged ..."
|
|
238
419
|
*
|
|
@@ -244,34 +425,71 @@ function formatChipLine(result) {
|
|
|
244
425
|
}
|
|
245
426
|
|
|
246
427
|
/**
|
|
247
|
-
* Decide the exit code for
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
428
|
+
* Decide the exit code for an invocation (spec section 3.2 matrix):
|
|
429
|
+
* - No `--check` and no `--level`: always 0.
|
|
430
|
+
* - `--check` alone: today's stage policy (4 on blocking failure,
|
|
431
|
+
* 0 with `[warn]` on warn-with-ack failure).
|
|
432
|
+
* - `--level intent`: 4 iff `levels.intentComplete.ok === false`.
|
|
433
|
+
* - `--level build`: 4 iff `levels.readyToBuild.ok === false`
|
|
434
|
+
* (stricter than `--check all`; the warn line still prints).
|
|
435
|
+
* - Both: 4 if either policy says 4.
|
|
251
436
|
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* warn-with-ack failure).
|
|
437
|
+
* Warn lines are emitted whenever a warn-with-ack stage is failing,
|
|
438
|
+
* independent of exit code, so an operator always sees why.
|
|
255
439
|
*
|
|
256
440
|
* @param {import('../query/readiness.js').ReadinessResult} result
|
|
257
|
-
* @param {string} checkStage
|
|
441
|
+
* @param {string | null} checkStage
|
|
442
|
+
* @param {'intent' | 'build' | null} level
|
|
258
443
|
* @param {NodeJS.WritableStream} stderr
|
|
259
444
|
* @returns {number}
|
|
260
445
|
*/
|
|
261
|
-
function decideExitCode(result, checkStage, stderr) {
|
|
262
|
-
const stages = checkStage === 'all'
|
|
263
|
-
? result.stages
|
|
264
|
-
: result.stages.filter((s) => s.stage === checkStage);
|
|
446
|
+
function decideExitCode(result, checkStage, level, stderr) {
|
|
265
447
|
let exitCode = 0;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
448
|
+
|
|
449
|
+
// --check policy (unchanged from PR A).
|
|
450
|
+
if (checkStage) {
|
|
451
|
+
const stages = checkStage === 'all'
|
|
452
|
+
? result.stages
|
|
453
|
+
: result.stages.filter((s) => s.stage === checkStage);
|
|
454
|
+
for (const s of stages) {
|
|
455
|
+
if (s.state !== 'failing') continue;
|
|
456
|
+
const policy = stagePolicy(s.stage);
|
|
457
|
+
if (policy === 'blocking') {
|
|
458
|
+
exitCode = 4;
|
|
459
|
+
} else {
|
|
460
|
+
stderr.write(`[warn] readiness: ${s.stage} (${s.gate}) is failing without an acknowledgement at the current tree hash. Run \`rcf define freeze --ack ${s.gate} --reason "<text>"\` to acknowledge, or edit the tree to clear the failure.\n`);
|
|
461
|
+
}
|
|
273
462
|
}
|
|
274
463
|
}
|
|
464
|
+
|
|
465
|
+
// --level policy. When explicit, exit 4 if that level says "no".
|
|
466
|
+
// --level build is stricter than --check all: it exits 4 even on an
|
|
467
|
+
// unacknowledged warn-with-ack failure (spec decision 4). The warn
|
|
468
|
+
// lines are still emitted (above when --check all is also set, or
|
|
469
|
+
// here when --level build is given alone).
|
|
470
|
+
if (level === 'intent' && result.levels.intentComplete.ok === false) {
|
|
471
|
+
exitCode = 4;
|
|
472
|
+
} else if (level === 'build' && result.levels.readyToBuild.ok === false) {
|
|
473
|
+
exitCode = 4;
|
|
474
|
+
// When --level build is set without --check all, we have not yet
|
|
475
|
+
// emitted the per-stage warn lines. Emit them here so an operator
|
|
476
|
+
// still sees which warn-with-ack stage drove the "no".
|
|
477
|
+
if (!checkStage || checkStage !== 'all') {
|
|
478
|
+
const seen = new Set();
|
|
479
|
+
if (checkStage) {
|
|
480
|
+
// --level build + --check <stage>: the --check branch already
|
|
481
|
+
// emitted that stage's warn line (if any). Avoid duplicates.
|
|
482
|
+
seen.add(checkStage);
|
|
483
|
+
}
|
|
484
|
+
for (const s of result.stages) {
|
|
485
|
+
if (seen.has(s.stage)) continue;
|
|
486
|
+
if (s.state !== 'failing') continue;
|
|
487
|
+
if (stagePolicy(s.stage) === 'blocking') continue;
|
|
488
|
+
stderr.write(`[warn] readiness: ${s.stage} (${s.gate}) is failing without an acknowledgement at the current tree hash. Run \`rcf define freeze --ack ${s.gate} --reason "<text>"\` to acknowledge, or edit the tree to clear the failure.\n`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
275
493
|
return exitCode;
|
|
276
494
|
}
|
|
277
495
|
|
package/src/cli/view.js
CHANGED
|
@@ -92,6 +92,13 @@ Options:
|
|
|
92
92
|
and CI is unset).
|
|
93
93
|
--verbose Log each watch event and each SSE broadcast to
|
|
94
94
|
stderr.
|
|
95
|
+
--test-host Mount the wespa host fixture at /test-host.html
|
|
96
|
+
plus its client script at /test-host.js. Off by
|
|
97
|
+
default; the fixture is a test-suite proof that
|
|
98
|
+
the viewer honours Dex's embedding contract (viewer
|
|
99
|
+
UI refresh PR 9; TAC-4134, ADR-4136), not a shipped
|
|
100
|
+
UI, so wespa's reverse-proxy allow-list is
|
|
101
|
+
unaffected. Can also be set via RCF_VIEW_TEST_HOST=1.
|
|
95
102
|
--help Print this help and exit.
|
|
96
103
|
|
|
97
104
|
Security posture:
|
|
@@ -130,6 +137,7 @@ export function parseArgs(argv) {
|
|
|
130
137
|
help: false,
|
|
131
138
|
noOpen: false,
|
|
132
139
|
port: null,
|
|
140
|
+
testHost: false,
|
|
133
141
|
};
|
|
134
142
|
const errors = [];
|
|
135
143
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -148,6 +156,13 @@ export function parseArgs(argv) {
|
|
|
148
156
|
case '-h':
|
|
149
157
|
opts.help = true;
|
|
150
158
|
break;
|
|
159
|
+
case '--test-host':
|
|
160
|
+
// Viewer UI refresh PR 9 (TAC-4134, ADR-4136): mount the wespa
|
|
161
|
+
// host fixture at /test-host.html plus its client at
|
|
162
|
+
// /test-host.js. Off by default; the fixture is a test-suite
|
|
163
|
+
// proof, not a shipped UI.
|
|
164
|
+
opts.testHost = true;
|
|
165
|
+
break;
|
|
151
166
|
case '--port': {
|
|
152
167
|
const next = argv[i + 1];
|
|
153
168
|
if (next === undefined || next.startsWith('--')) {
|
|
@@ -170,6 +185,19 @@ export function parseArgs(argv) {
|
|
|
170
185
|
return { opts, errors };
|
|
171
186
|
}
|
|
172
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Resolve the testHost gate: `--test-host` beats `RCF_VIEW_TEST_HOST=1`
|
|
190
|
+
* env beats default (false). Viewer UI refresh PR 9 (TAC-4134).
|
|
191
|
+
*
|
|
192
|
+
* @param {boolean} flagTestHost
|
|
193
|
+
* @param {NodeJS.ProcessEnv} env
|
|
194
|
+
* @returns {boolean}
|
|
195
|
+
*/
|
|
196
|
+
export function resolveTestHost(flagTestHost, env) {
|
|
197
|
+
if (flagTestHost === true) return true;
|
|
198
|
+
return env.RCF_VIEW_TEST_HOST === '1';
|
|
199
|
+
}
|
|
200
|
+
|
|
173
201
|
/**
|
|
174
202
|
* Resolve port precedence: --port beats RCF_VIEW_PORT env beats default.
|
|
175
203
|
*
|
|
@@ -294,6 +322,7 @@ export async function main(argv, deps = {}) {
|
|
|
294
322
|
projectRoot,
|
|
295
323
|
port: resolvedPort,
|
|
296
324
|
log: logSink,
|
|
325
|
+
testHost: resolveTestHost(opts.testHost, env),
|
|
297
326
|
});
|
|
298
327
|
} catch (err) {
|
|
299
328
|
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'EADDRINUSE') {
|
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
// override: null | { reason, by, at } // required (nullable)
|
|
28
28
|
// }
|
|
29
29
|
//
|
|
30
|
+
// `override` is required-nullable: the validator throws when the key
|
|
31
|
+
// is missing; every writer stamps `override: null` on a fresh freeze.
|
|
32
|
+
// The loader tolerates older records that lack the key by defaulting
|
|
33
|
+
// to null on read, so a pre-slice-3 freeze.json survives a load
|
|
34
|
+
// without a migration step (the next write converges the on-disk
|
|
35
|
+
// shape).
|
|
36
|
+
//
|
|
30
37
|
// The record is EXTENSIBLE: later slices add fields (2 gates the freeze
|
|
31
38
|
// verb wrote at freeze time, 4 the `bundle --next` refusal read at
|
|
32
39
|
// override time). This slice validates the fields it consumes and
|
|
@@ -149,24 +156,23 @@ export function validateFreezeRecord(raw, filePath) {
|
|
|
149
156
|
);
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
require('override');
|
|
160
|
+
const override = body.override;
|
|
161
|
+
if (override !== null) {
|
|
162
|
+
if (override === undefined || typeof override !== 'object' || Array.isArray(override)) {
|
|
163
|
+
throw new FreezeRecordError(
|
|
164
|
+
"Freeze record 'override' must be null or { reason, by, at }.",
|
|
165
|
+
{ filePath, field: 'override' },
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const o = /** @type {Record<string, unknown>} */ (override);
|
|
169
|
+
for (const f of ['reason', 'by', 'at']) {
|
|
170
|
+
if (typeof o[f] !== 'string' || o[f].length === 0) {
|
|
156
171
|
throw new FreezeRecordError(
|
|
157
|
-
|
|
158
|
-
{ filePath, field:
|
|
172
|
+
`Freeze record 'override.${f}' must be a non-empty string.`,
|
|
173
|
+
{ filePath, field: `override.${f}` },
|
|
159
174
|
);
|
|
160
175
|
}
|
|
161
|
-
const o = /** @type {Record<string, unknown>} */ (override);
|
|
162
|
-
for (const f of ['reason', 'by', 'at']) {
|
|
163
|
-
if (typeof o[f] !== 'string' || o[f].length === 0) {
|
|
164
|
-
throw new FreezeRecordError(
|
|
165
|
-
`Freeze record 'override.${f}' must be a non-empty string.`,
|
|
166
|
-
{ filePath, field: `override.${f}` },
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
176
|
}
|
|
171
177
|
}
|
|
172
178
|
|
|
@@ -204,6 +210,15 @@ export async function loadFreezeRecord({ projectRoot }) {
|
|
|
204
210
|
{ filePath: relPath, code: 'parseFailure' },
|
|
205
211
|
);
|
|
206
212
|
}
|
|
213
|
+
// Back-compat: older freeze records were written before override
|
|
214
|
+
// became a required-nullable field. Fill in null on read so a
|
|
215
|
+
// pre-slice-3 record survives; the writer stamps override: null on
|
|
216
|
+
// every fresh freeze so the on-disk shape converges on the current
|
|
217
|
+
// schema without a migration step.
|
|
218
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
219
|
+
&& !Object.prototype.hasOwnProperty.call(parsed, 'override')) {
|
|
220
|
+
/** @type {Record<string, unknown>} */ (parsed).override = null;
|
|
221
|
+
}
|
|
207
222
|
return validateFreezeRecord(parsed, relPath);
|
|
208
223
|
}
|
|
209
224
|
|