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/query/readiness.js
CHANGED
|
@@ -49,6 +49,24 @@ import {
|
|
|
49
49
|
* @property {number[]} briefSince
|
|
50
50
|
* @property {import('./impact.js').ImpactNode[]} impacted
|
|
51
51
|
* @property {string[]} impactedFbs
|
|
52
|
+
* @property {Record<string, string>} currentDocHashes
|
|
53
|
+
* Live per-document hashes (ledgers keyed as `ledger:<name>`).
|
|
54
|
+
* Threaded from `computeDelta` so the Readiness tab can render a
|
|
55
|
+
* document-level hash diff for each changed / added id (spec §5
|
|
56
|
+
* block 6; the frozen body is not persisted in the freeze record
|
|
57
|
+
* in 0.29.0, so the diff is hash-only for now).
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {object} CheckResult
|
|
62
|
+
* @property {string} name
|
|
63
|
+
* @property {boolean} ok
|
|
64
|
+
* @property {'delta'|'tree'} over
|
|
65
|
+
* @property {number} pass
|
|
66
|
+
* @property {number} total
|
|
67
|
+
* @property {Array<{ id: string, why: string }>} failing
|
|
68
|
+
* @property {'productOwner'|'engineer'} persona
|
|
69
|
+
* @property {string} question
|
|
52
70
|
*/
|
|
53
71
|
|
|
54
72
|
/**
|
|
@@ -56,10 +74,47 @@ import {
|
|
|
56
74
|
* @property {string} stage
|
|
57
75
|
* @property {string} gate
|
|
58
76
|
* @property {'passed'|'failing'|'acknowledged'|'notApplicable'} state
|
|
59
|
-
* @property {
|
|
77
|
+
* @property {CheckResult[]} checks
|
|
60
78
|
* @property {string} [reason]
|
|
61
79
|
*/
|
|
62
80
|
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {object} Blocker
|
|
83
|
+
* @property {string} stage
|
|
84
|
+
* @property {string} gate
|
|
85
|
+
* @property {string} check
|
|
86
|
+
* @property {'productOwner'|'engineer'} persona
|
|
87
|
+
* @property {'delta'|'tree'} over
|
|
88
|
+
* @property {number} failingCount
|
|
89
|
+
* @property {string[]} ids
|
|
90
|
+
* @property {string} question
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @typedef {object} LevelVerdict
|
|
95
|
+
* @property {boolean} ok
|
|
96
|
+
* @property {Blocker[]} blockedBy
|
|
97
|
+
* @property {NextAction | null} nextAction
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @typedef {object} ReadinessLevels
|
|
102
|
+
* @property {LevelVerdict} intentComplete
|
|
103
|
+
* @property {LevelVerdict} readyToBuild
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @typedef {object} PersonaGroup
|
|
108
|
+
* @property {Blocker[]} blockers
|
|
109
|
+
* @property {NextAction | null} nextAction
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @typedef {object} ReadinessPersonas
|
|
114
|
+
* @property {PersonaGroup} productOwner
|
|
115
|
+
* @property {PersonaGroup} engineer
|
|
116
|
+
*/
|
|
117
|
+
|
|
63
118
|
/**
|
|
64
119
|
* @typedef {object} NextAction
|
|
65
120
|
* @property {string} stage
|
|
@@ -77,6 +132,8 @@ import {
|
|
|
77
132
|
* @property {{ tree: import('./coverage.js').CoverageResult, delta: import('./coverage.js').CoverageResult[] }} coverage
|
|
78
133
|
* @property {Array<Record<string, unknown>>} decisions
|
|
79
134
|
* @property {boolean} freezeable
|
|
135
|
+
* @property {ReadinessLevels} levels
|
|
136
|
+
* @property {ReadinessPersonas} personas
|
|
80
137
|
*/
|
|
81
138
|
|
|
82
139
|
/**
|
|
@@ -198,6 +255,14 @@ export function computeReadiness(tree, args = {}) {
|
|
|
198
255
|
const freezeable = stages.every((s) => s.state === 'passed' || s.state === 'acknowledged' || s.state === 'notApplicable');
|
|
199
256
|
const nextAction = freezeable ? null : deriveNextAction(stages);
|
|
200
257
|
|
|
258
|
+
// 7b. Fold `levels` and `personas` from stages[].checks[] alone
|
|
259
|
+
// (ADR-4126, proposal section 2.1). Nothing is written. L1
|
|
260
|
+
// intent-complete is "every productOwner check ok across every
|
|
261
|
+
// stage"; L2 ready-to-build equals freezeable with blockers over
|
|
262
|
+
// failing stages only.
|
|
263
|
+
const levels = deriveLevels(stages, freezeable, nextAction);
|
|
264
|
+
const personas = derivePersonas(stages, levels);
|
|
265
|
+
|
|
201
266
|
// 8. Build the tree summary line inputs.
|
|
202
267
|
const queue = computeQueue(tree);
|
|
203
268
|
const fbsTotal = (tree.fbsItems ?? []).length;
|
|
@@ -219,12 +284,15 @@ export function computeReadiness(tree, args = {}) {
|
|
|
219
284
|
briefSince: delta.briefSince,
|
|
220
285
|
impacted: impactedNodes,
|
|
221
286
|
impactedFbs,
|
|
287
|
+
currentDocHashes: delta.currentDocHashes ?? {},
|
|
222
288
|
},
|
|
223
289
|
stages,
|
|
224
290
|
nextAction,
|
|
225
291
|
coverage: { tree: coverageTree, delta: coverageDelta },
|
|
226
292
|
decisions: openDecisions,
|
|
227
293
|
freezeable,
|
|
294
|
+
levels,
|
|
295
|
+
personas,
|
|
228
296
|
};
|
|
229
297
|
}
|
|
230
298
|
|
|
@@ -266,33 +334,206 @@ function collectDeltaReqAncestors(tree, pivotIds) {
|
|
|
266
334
|
|
|
267
335
|
/**
|
|
268
336
|
* Pick the first failing stage in D1..D8 order and the first failing
|
|
269
|
-
* check inside it, then compose a `NextAction`.
|
|
337
|
+
* check inside it, then compose a `NextAction`. With no `persona`
|
|
338
|
+
* passed, behaves exactly as 0.28.4 did (first failing stage, first
|
|
339
|
+
* failing check, largest failing list). With a `persona` ('productOwner'
|
|
340
|
+
* | 'engineer'), picks the first failing stage that holds a failing
|
|
341
|
+
* check of that persona, then the largest failing-list check among
|
|
342
|
+
* them; the command carries `--level intent` on the productOwner
|
|
343
|
+
* variant and no `--level` flag otherwise (proposal section 2.1).
|
|
344
|
+
*
|
|
345
|
+
* The productOwner next action is defined over every stage holding a
|
|
346
|
+
* failing PO check (not just stages with state `failing`), because
|
|
347
|
+
* `levels.intentComplete.blockedBy` ignores stage state. In practice
|
|
348
|
+
* PO checks live on blocking stages (D1, D2, D4, D7) so any failing
|
|
349
|
+
* PO check also makes its stage `failing`, but the compute does not
|
|
350
|
+
* rely on that coincidence.
|
|
270
351
|
*
|
|
271
352
|
* @param {StageResult[]} stages
|
|
353
|
+
* @param {'productOwner' | 'engineer'} [persona]
|
|
272
354
|
* @returns {NextAction | null}
|
|
273
355
|
*/
|
|
274
|
-
export function deriveNextAction(stages) {
|
|
356
|
+
export function deriveNextAction(stages, persona) {
|
|
275
357
|
const byStage = new Map(stages.map((s) => [s.stage, s]));
|
|
276
358
|
for (const stageId of STAGE_ORDER) {
|
|
277
359
|
const s = byStage.get(stageId);
|
|
278
|
-
if (!s
|
|
360
|
+
if (!s) continue;
|
|
361
|
+
// Without a persona filter: today's rule (stage must be
|
|
362
|
+
// `failing`; this skips `acknowledged` warn-with-ack stages).
|
|
363
|
+
// With a persona filter: scan any stage that holds a failing
|
|
364
|
+
// check of that persona. PO checks only live on blocking stages
|
|
365
|
+
// so this coincides with "stage failing" in practice, but it
|
|
366
|
+
// keeps the compute honest if that ever changes.
|
|
367
|
+
if (!persona && s.state !== 'failing') continue;
|
|
368
|
+
const failing = s.checks.filter((c) => !c.ok && (persona ? c.persona === persona : true));
|
|
369
|
+
if (failing.length === 0) continue;
|
|
279
370
|
// First failing check; tie-break by the largest failing list so
|
|
280
371
|
// the operator sees the most impactful gap first.
|
|
281
|
-
const failing = s.checks.filter((c) => !c.ok);
|
|
282
|
-
if (failing.length === 0) continue;
|
|
283
372
|
const check = failing.reduce((a, b) => (b.failing.length > a.failing.length ? b : a), failing[0]);
|
|
284
373
|
const ids = [...new Set(check.failing.map((f) => f.id))].slice(0, 20);
|
|
285
374
|
const shortName = STAGE_SHORT_NAMES[stageId] ?? stageId;
|
|
375
|
+
const command = persona === 'productOwner'
|
|
376
|
+
? `rcf define readiness --level intent --check ${shortName}`
|
|
377
|
+
: `rcf define readiness --check ${shortName}`;
|
|
286
378
|
return {
|
|
287
379
|
stage: stageId,
|
|
288
380
|
check: check.name,
|
|
289
381
|
ids,
|
|
290
|
-
command
|
|
382
|
+
command,
|
|
291
383
|
};
|
|
292
384
|
}
|
|
293
385
|
return null;
|
|
294
386
|
}
|
|
295
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Build a `Blocker` row from a stage + check pair. `ids` are
|
|
390
|
+
* deduplicated and capped at 20 to match the display convention
|
|
391
|
+
* `nextAction.ids` uses (proposal section 2.1).
|
|
392
|
+
*
|
|
393
|
+
* @param {StageResult} stage
|
|
394
|
+
* @param {CheckResult} check
|
|
395
|
+
* @returns {Blocker}
|
|
396
|
+
*/
|
|
397
|
+
function makeBlocker(stage, check) {
|
|
398
|
+
return {
|
|
399
|
+
stage: stage.stage,
|
|
400
|
+
gate: stage.gate,
|
|
401
|
+
check: check.name,
|
|
402
|
+
persona: check.persona,
|
|
403
|
+
over: check.over,
|
|
404
|
+
failingCount: check.failing.length,
|
|
405
|
+
ids: [...new Set(check.failing.map((f) => f.id))].slice(0, 20),
|
|
406
|
+
question: check.question,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Fold the two levels from `stages[].checks[]` (ADR-4126, proposal
|
|
412
|
+
* section 2.1). L1 ignores stage state and lists every failing PO
|
|
413
|
+
* check; L2 lists every failing check in a stage whose state is
|
|
414
|
+
* `failing` (checks inside an `acknowledged` stage are not blockers).
|
|
415
|
+
*
|
|
416
|
+
* @param {StageResult[]} stages
|
|
417
|
+
* @param {boolean} freezeable
|
|
418
|
+
* @param {NextAction | null} nextAction
|
|
419
|
+
* @returns {ReadinessLevels}
|
|
420
|
+
*/
|
|
421
|
+
export function deriveLevels(stages, freezeable, nextAction) {
|
|
422
|
+
/** @type {Blocker[]} */
|
|
423
|
+
const poBlockers = [];
|
|
424
|
+
/** @type {Blocker[]} */
|
|
425
|
+
const buildBlockers = [];
|
|
426
|
+
for (const stage of stages) {
|
|
427
|
+
for (const check of stage.checks) {
|
|
428
|
+
if (check.ok) continue;
|
|
429
|
+
if (check.persona === 'productOwner') {
|
|
430
|
+
poBlockers.push(makeBlocker(stage, check));
|
|
431
|
+
}
|
|
432
|
+
if (stage.state === 'failing') {
|
|
433
|
+
buildBlockers.push(makeBlocker(stage, check));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const intentOk = poBlockers.length === 0;
|
|
438
|
+
return {
|
|
439
|
+
intentComplete: {
|
|
440
|
+
ok: intentOk,
|
|
441
|
+
blockedBy: poBlockers,
|
|
442
|
+
nextAction: intentOk ? null : deriveNextAction(stages, 'productOwner'),
|
|
443
|
+
},
|
|
444
|
+
readyToBuild: {
|
|
445
|
+
ok: freezeable,
|
|
446
|
+
blockedBy: buildBlockers,
|
|
447
|
+
nextAction,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Fold the `personas` block (ADR-4126, proposal section 3.3).
|
|
454
|
+
* `productOwner.blockers` deep-equals `levels.intentComplete.blockedBy`;
|
|
455
|
+
* `engineer.blockers` is the failing engineer checks in failing stages
|
|
456
|
+
* (the L2 blockedBy minus the PO entries).
|
|
457
|
+
*
|
|
458
|
+
* @param {StageResult[]} stages
|
|
459
|
+
* @param {ReadinessLevels} levels
|
|
460
|
+
* @returns {ReadinessPersonas}
|
|
461
|
+
*/
|
|
462
|
+
export function derivePersonas(stages, levels) {
|
|
463
|
+
const engineerBlockers = levels.readyToBuild.blockedBy.filter((b) => b.persona === 'engineer');
|
|
464
|
+
return {
|
|
465
|
+
productOwner: {
|
|
466
|
+
blockers: levels.intentComplete.blockedBy,
|
|
467
|
+
nextAction: levels.intentComplete.nextAction,
|
|
468
|
+
},
|
|
469
|
+
engineer: {
|
|
470
|
+
blockers: engineerBlockers,
|
|
471
|
+
nextAction: deriveNextAction(stages, 'engineer'),
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Validate a `--level` flag value against the closed set
|
|
478
|
+
* `intent | build`. Returns the value, `null` when the flag was
|
|
479
|
+
* absent, or throws a usage error whose message is formatted exactly
|
|
480
|
+
* like the `--check` usage line so the CLI can forward it verbatim.
|
|
481
|
+
*
|
|
482
|
+
* @param {string | undefined} raw
|
|
483
|
+
* @returns {'intent' | 'build' | null}
|
|
484
|
+
*/
|
|
485
|
+
export function parseLevelFlag(raw) {
|
|
486
|
+
if (raw === undefined || raw === null) return null;
|
|
487
|
+
if (raw === 'intent' || raw === 'build') return raw;
|
|
488
|
+
throw new Error(`unknown --level ${raw} (expected intent | build)`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Validate a `--persona` flag value against the closed set
|
|
493
|
+
* `productOwner | engineer`. Returns the value, `null` when the flag
|
|
494
|
+
* was absent, or throws a usage error with the same shape.
|
|
495
|
+
*
|
|
496
|
+
* @param {string | undefined} raw
|
|
497
|
+
* @returns {'productOwner' | 'engineer' | null}
|
|
498
|
+
*/
|
|
499
|
+
export function parsePersonaFlag(raw) {
|
|
500
|
+
if (raw === undefined || raw === null) return null;
|
|
501
|
+
if (raw === 'productOwner' || raw === 'engineer') return raw;
|
|
502
|
+
throw new Error(`unknown --persona ${raw} (expected productOwner | engineer)`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Format the two verdict lines shared by the CLI text report and
|
|
507
|
+
* the Readiness tab (proposal section 2.3). The strings are built
|
|
508
|
+
* from `levels` alone so the two surfaces cannot disagree.
|
|
509
|
+
*
|
|
510
|
+
* @param {ReadinessResult} result
|
|
511
|
+
* @returns {{ intentComplete: string, readyToBuild: string }}
|
|
512
|
+
*/
|
|
513
|
+
export function formatVerdictLines(result) {
|
|
514
|
+
const l = result.levels;
|
|
515
|
+
const intentLine = l.intentComplete.ok
|
|
516
|
+
? 'Intent-complete: yes. Every product-owner question is answered.'
|
|
517
|
+
: (() => {
|
|
518
|
+
const n = l.intentComplete.blockedBy.length;
|
|
519
|
+
const detail = l.intentComplete.blockedBy
|
|
520
|
+
.map((b) => `${b.stage}/${b.check}`)
|
|
521
|
+
.join(', ');
|
|
522
|
+
return `Intent-complete: no; ${n} question${n === 1 ? '' : 's'} for the product owner (${detail}).`;
|
|
523
|
+
})();
|
|
524
|
+
const buildLine = l.readyToBuild.ok
|
|
525
|
+
? `Ready-to-build: yes. Freezeable at ${shortHash(result.tree.currentTreeHash)}.`
|
|
526
|
+
: (() => {
|
|
527
|
+
const blockers = l.readyToBuild.blockedBy;
|
|
528
|
+
const stages = [...new Set(blockers.map((b) => b.stage))].sort();
|
|
529
|
+
const total = blockers.length;
|
|
530
|
+
const p = blockers.filter((b) => b.persona === 'productOwner').length;
|
|
531
|
+
const e = blockers.filter((b) => b.persona === 'engineer').length;
|
|
532
|
+
return `Ready-to-build: no; blocked on ${stages.join(', ')} (${total} check${total === 1 ? '' : 's'}: ${p} product owner, ${e} engineer).`;
|
|
533
|
+
})();
|
|
534
|
+
return { intentComplete: intentLine, readyToBuild: buildLine };
|
|
535
|
+
}
|
|
536
|
+
|
|
296
537
|
/**
|
|
297
538
|
* Short 8-character prefix of a `sha256:<hex>` string, or '(none)' when
|
|
298
539
|
* absent. Kept exported so text formatters share one convention.
|
package/src/server/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { createServer } from 'node:http';
|
|
|
14
14
|
import { readFile } from 'node:fs/promises';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
|
|
17
|
-
import { LIVE_CLIENT_PATH, STYLE_CSS_PATH, VENDORED_MERMAID_PATH, renderModelToPage } from '../view/index.js';
|
|
17
|
+
import { LIVE_CLIENT_PATH, PAGE_INIT_PATH, STYLE_CSS_PATH, TEST_HOST_CLIENT_PATH, VENDORED_MERMAID_PATH, renderModelToPage } from '../view/index.js';
|
|
18
18
|
import { watch as defaultWatch } from '../watch/index.js';
|
|
19
19
|
import { createRouter } from './routes.js';
|
|
20
20
|
import { createScopeHandler } from './scope-endpoint.js';
|
|
@@ -29,6 +29,10 @@ import { createSseHub } from './sse.js';
|
|
|
29
29
|
* @property {number} [debounceMs=50]
|
|
30
30
|
* @property {(line: string) => void} [log] - stderr sink
|
|
31
31
|
* @property {typeof defaultWatch} [watchImpl] - injectable watch primitive for tests
|
|
32
|
+
* @property {boolean} [testHost=false] - viewer UI refresh PR 9 (TAC-4134,
|
|
33
|
+
* ADR-4136): when true, mount the wespa host fixture at /test-host.html
|
|
34
|
+
* plus its embed-client script at /test-host.js. Off by default so
|
|
35
|
+
* wespa's proxy allow-list is unaffected.
|
|
32
36
|
*/
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -70,6 +74,7 @@ export async function startServer(args) {
|
|
|
70
74
|
const debounceMs = typeof args.debounceMs === 'number' ? args.debounceMs : 50;
|
|
71
75
|
const log = typeof args.log === 'function' ? args.log : () => {};
|
|
72
76
|
const watchImpl = typeof args.watchImpl === 'function' ? args.watchImpl : defaultWatch;
|
|
77
|
+
const testHost = args.testHost === true;
|
|
73
78
|
|
|
74
79
|
/** @type {{ version: number, fullPageHtml: string, contentHtml: string, errors: import('#core/errors').RcfError[] } | null} */
|
|
75
80
|
let state = null;
|
|
@@ -97,6 +102,11 @@ export async function startServer(args) {
|
|
|
97
102
|
contentHtml: result.contentHtml,
|
|
98
103
|
errors: result.errors,
|
|
99
104
|
pmPartials: result.pmPartials,
|
|
105
|
+
// Viewer UI refresh PR 7 (TAC-4132, ADR-4134): the ID lookup
|
|
106
|
+
// index is built once per rewalk and snapshotted on `state`
|
|
107
|
+
// so `GET /index.json` serves from memory and the SSE
|
|
108
|
+
// `tree-update` version lets the client invalidate its cache.
|
|
109
|
+
indexJson: result.indexJson,
|
|
100
110
|
};
|
|
101
111
|
sse.broadcast('tree-update', { version, contentHtml: result.contentHtml });
|
|
102
112
|
if (result.errors && result.errors.length > 0) {
|
|
@@ -122,10 +132,17 @@ export async function startServer(args) {
|
|
|
122
132
|
// so a checkout branch switch during the server's lifetime cannot
|
|
123
133
|
// change what gets served. The tree walker is the only path that
|
|
124
134
|
// re-reads disk on rcf/ change; every other asset is fixed at boot.
|
|
125
|
-
const [styleAsset, mermaidAsset, liveClientAsset] = await Promise.all([
|
|
135
|
+
const [styleAsset, mermaidAsset, liveClientAsset, pageInitAsset, testHostClientAsset] = await Promise.all([
|
|
126
136
|
loadStaticAsset(STYLE_CSS_PATH, 'text/css; charset=utf-8'),
|
|
127
137
|
loadStaticAsset(VENDORED_MERMAID_PATH, 'application/javascript; charset=utf-8'),
|
|
128
138
|
loadStaticAsset(LIVE_CLIENT_PATH, 'application/javascript; charset=utf-8'),
|
|
139
|
+
loadStaticAsset(PAGE_INIT_PATH, 'application/javascript; charset=utf-8'),
|
|
140
|
+
// Viewer UI refresh PR 9 (TAC-4134): the wespa host fixture client
|
|
141
|
+
// only loads when testHost is on. Keeping the read behind the gate
|
|
142
|
+
// means a startup under default options never touches the file.
|
|
143
|
+
testHost
|
|
144
|
+
? loadStaticAsset(TEST_HOST_CLIENT_PATH, 'application/javascript; charset=utf-8')
|
|
145
|
+
: Promise.resolve(null),
|
|
129
146
|
]);
|
|
130
147
|
|
|
131
148
|
const scopeHandler = createScopeHandler({ projectRoot });
|
|
@@ -138,10 +155,15 @@ export async function startServer(args) {
|
|
|
138
155
|
styleAsset,
|
|
139
156
|
mermaidAsset,
|
|
140
157
|
liveClientAsset,
|
|
158
|
+
pageInitAsset,
|
|
141
159
|
stylePath: STYLE_CSS_PATH,
|
|
142
160
|
mermaidPath: VENDORED_MERMAID_PATH,
|
|
143
161
|
liveClientPath: LIVE_CLIENT_PATH,
|
|
162
|
+
pageInitPath: PAGE_INIT_PATH,
|
|
144
163
|
scope: scopeHandler,
|
|
164
|
+
testHost,
|
|
165
|
+
testHostClientAsset,
|
|
166
|
+
testHostClientPath: testHost ? TEST_HOST_CLIENT_PATH : undefined,
|
|
145
167
|
});
|
|
146
168
|
|
|
147
169
|
const server = createServer(router);
|
package/src/server/routes.js
CHANGED
|
@@ -4,7 +4,28 @@
|
|
|
4
4
|
// GET /style.css -> shipped stylesheet
|
|
5
5
|
// GET /mermaid.min.js-> vendored mermaid runtime
|
|
6
6
|
// GET /live-client.js-> phase 3.8 live client script
|
|
7
|
+
// GET /page-init.js -> viewer UI refresh PR 1 shell / router script
|
|
8
|
+
// (Dex / wespa 2026-10-02 #263; release-noted so
|
|
9
|
+
// the proxy's route allow-list is extended).
|
|
7
10
|
// GET /scope.json -> deep-link scope resolver (w-2026-08-30-dave-020)
|
|
11
|
+
// GET /index.json -> viewer UI refresh PR 7 ID lookup index built
|
|
12
|
+
// from BuiltTreeModel on every rewalk; `{ rows }`
|
|
13
|
+
// with one row per document id plus one per AC
|
|
14
|
+
// (ADR-4134; release-noted).
|
|
15
|
+
// GET /_fixtures/components -> viewer UI refresh PR 2 component
|
|
16
|
+
// fixture page (release-noted). Static page, no
|
|
17
|
+
// live-walk coupling, so it is always available
|
|
18
|
+
// even before the first rewalk completes.
|
|
19
|
+
// GET /test-host.html -> viewer UI refresh PR 9 wespa host fixture
|
|
20
|
+
// page (TAC-4134, ADR-4136). Only served when
|
|
21
|
+
// `deps.testHost === true`; 404 otherwise so
|
|
22
|
+
// wespa's reverse-proxy allow-list is unaffected.
|
|
23
|
+
// Mimics the wespa shell (56px header, resizable
|
|
24
|
+
// sidebar) and iframes the viewer at `./?embed=1
|
|
25
|
+
// &theme=light`. Test-suite proof only; not a
|
|
26
|
+
// shipped UI.
|
|
27
|
+
// GET /test-host.js -> viewer UI refresh PR 9 wespa host fixture
|
|
28
|
+
// client script. Same testHost gate as the HTML.
|
|
8
29
|
// Everything else -> 404 text/plain.
|
|
9
30
|
//
|
|
10
31
|
// No CORS headers, no cache headers on static assets beyond what the
|
|
@@ -12,6 +33,9 @@
|
|
|
12
33
|
|
|
13
34
|
import { readFile, stat } from 'node:fs/promises';
|
|
14
35
|
|
|
36
|
+
import { renderComponentsFixturePage } from '../view/components-fixture.js';
|
|
37
|
+
import { renderTestHostPage } from '../view/test-host/fixture.js';
|
|
38
|
+
|
|
15
39
|
// Issue #248 (0.28.3): the shipped static assets (style.css,
|
|
16
40
|
// mermaid.min.js, live-client.js) MUST be resolved once at server
|
|
17
41
|
// startup, not read from disk on every HTTP request. If the checkout
|
|
@@ -32,12 +56,13 @@ const MIME = {
|
|
|
32
56
|
html: 'text/html; charset=utf-8',
|
|
33
57
|
css: 'text/css; charset=utf-8',
|
|
34
58
|
js: 'application/javascript; charset=utf-8',
|
|
59
|
+
json: 'application/json; charset=utf-8',
|
|
35
60
|
txt: 'text/plain; charset=utf-8',
|
|
36
61
|
};
|
|
37
62
|
|
|
38
63
|
/**
|
|
39
64
|
* @typedef {object} RouterDeps
|
|
40
|
-
* @property {() => { fullPageHtml: string, contentHtml: string, version: number } | null} currentState
|
|
65
|
+
* @property {() => { fullPageHtml: string, contentHtml: string, version: number, indexJson?: string } | null} currentState
|
|
41
66
|
* @property {ReturnType<typeof import('./sse.js').createSseHub>} sse
|
|
42
67
|
* @property {string} stylePath
|
|
43
68
|
* @property {string} mermaidPath
|
|
@@ -95,6 +120,10 @@ export function createRouter(deps) {
|
|
|
95
120
|
serveAsset(res, deps.liveClientAsset, deps.liveClientPath, MIME.js).catch((err) => fail(res, err));
|
|
96
121
|
return;
|
|
97
122
|
}
|
|
123
|
+
if (path === '/page-init.js') {
|
|
124
|
+
serveAsset(res, deps.pageInitAsset, deps.pageInitPath, MIME.js).catch((err) => fail(res, err));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
98
127
|
if (path.startsWith('/product-map/')) {
|
|
99
128
|
const state = deps.currentState();
|
|
100
129
|
if (!state || !state.pmPartials) {
|
|
@@ -113,6 +142,44 @@ export function createRouter(deps) {
|
|
|
113
142
|
res.end(html);
|
|
114
143
|
return;
|
|
115
144
|
}
|
|
145
|
+
if (path === '/_fixtures/components') {
|
|
146
|
+
res.writeHead(200, { 'content-type': MIME.html, 'cache-control': 'no-store' });
|
|
147
|
+
res.end(renderComponentsFixturePage());
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (path === '/test-host.html' && deps.testHost) {
|
|
151
|
+
// Viewer UI refresh PR 9 (TAC-4134, ADR-4136): the wespa host
|
|
152
|
+
// fixture is only mounted when the server was started with
|
|
153
|
+
// testHost: true. No X-Frame-Options, no CSP; the viewer's
|
|
154
|
+
// framing lint asserts the same on GET /.
|
|
155
|
+
res.writeHead(200, { 'content-type': MIME.html, 'cache-control': 'no-store' });
|
|
156
|
+
res.end(renderTestHostPage());
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (path === '/test-host.js' && deps.testHost) {
|
|
160
|
+
serveAsset(res, deps.testHostClientAsset, deps.testHostClientPath, MIME.js).catch((err) => fail(res, err));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (path === '/index.json') {
|
|
164
|
+
// Viewer UI refresh PR 7 (TAC-4132, ADR-4134): the ID lookup
|
|
165
|
+
// index is built once per rewalk and lives on `state.indexJson`
|
|
166
|
+
// as a pre-stringified payload, so this handler never touches
|
|
167
|
+
// disk and content-length matches the exact bytes served.
|
|
168
|
+
const state = deps.currentState();
|
|
169
|
+
if (!state || typeof state.indexJson !== 'string') {
|
|
170
|
+
res.writeHead(503, { 'content-type': MIME.txt });
|
|
171
|
+
res.end('view server initialising\n');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const body = state.indexJson;
|
|
175
|
+
res.writeHead(200, {
|
|
176
|
+
'content-type': MIME.json,
|
|
177
|
+
'content-length': String(Buffer.byteLength(body)),
|
|
178
|
+
'cache-control': 'no-store',
|
|
179
|
+
});
|
|
180
|
+
res.end(body);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
116
183
|
if (path === '/scope.json') {
|
|
117
184
|
if (typeof deps.scope !== 'function') {
|
|
118
185
|
res.writeHead(404, { 'content-type': MIME.txt });
|
|
@@ -75,6 +75,23 @@ escalation.)_
|
|
|
75
75
|
|
|
76
76
|
unstated
|
|
77
77
|
|
|
78
|
+
## Surface
|
|
79
|
+
|
|
80
|
+
_(how the agent frames its review-ready output when a slice closes.
|
|
81
|
+
One of the three values below on a single line; the seeded default is
|
|
82
|
+
\`prDiff\` and a hand-edit is one word away.)_
|
|
83
|
+
|
|
84
|
+
- \`viewer\`: the slice is reviewed in a rendered viewer surface (an
|
|
85
|
+
HTML preview, a Readiness tab, a dashboard). The agent lands on a
|
|
86
|
+
URL the operator opens.
|
|
87
|
+
- \`runningApp\`: the slice is reviewed by running the app and
|
|
88
|
+
observing behaviour. The agent lands on a one-line reproduction
|
|
89
|
+
recipe.
|
|
90
|
+
- \`prDiff\`: the slice is reviewed as a pull-request diff on the
|
|
91
|
+
shared repo. The agent lands on a PR link with the chain IDs listed.
|
|
92
|
+
|
|
93
|
+
prDiff
|
|
94
|
+
|
|
78
95
|
## Project-scoped notes
|
|
79
96
|
|
|
80
97
|
_(anything specific to this project that would be useful for the agent
|