rcf-lite 0.32.5 → 0.33.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 +33 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/discovery/discovery-viewer-command-page-1-layout.html +191 -0
- package/guidance/discovery/discovery-viewer-command-page-2-coverage.html +193 -0
- package/guidance/discovery/discovery-viewer-command-page-3-trace-matrix.html +195 -0
- package/guidance/discovery/discovery-viewer-command-page-4-tables.html +196 -0
- package/guidance/discovery/discovery-viewer-command-page-5-subtabs.html +224 -0
- package/guidance/discovery/discovery-viewer-command-page-6-one-page.html +280 -0
- package/guidance/discovery/discovery-viewer-command-page.md +301 -0
- package/package.json +1 -1
- package/rcf/adrs/adr-4139-readiness-tables-read-only-no-commands.json +30 -0
- package/rcf/adrs/adr-4140-view-query-routes-per-pivot-matrix-no-mermaid.json +30 -0
- package/rcf/adrs/adr-4141-verify-modes.json +25 -0
- package/rcf/code-nodes/cn-771.json +3 -2
- package/rcf/code-nodes/cn-779.json +19 -0
- package/rcf/code-nodes/cn-780.json +14 -0
- package/rcf/code-nodes/cn-781.json +14 -0
- package/rcf/code-nodes/cn-782.json +15 -0
- package/rcf/code-nodes/cn-783.json +17 -0
- package/rcf/code-nodes/cn-784.json +20 -0
- package/rcf/code-nodes/cn-785.json +14 -0
- package/rcf/code-nodes/cn-786.json +14 -0
- package/rcf/code-nodes/cn-787.json +18 -0
- package/rcf/code-nodes/cn-788.json +14 -0
- package/rcf/code-nodes/cn-789.json +14 -0
- package/rcf/code-nodes/cn-790.json +15 -0
- package/rcf/code-nodes/cn-791.json +14 -0
- package/rcf/code-nodes/cn-792.json +17 -0
- package/rcf/code-nodes/cn-793.json +17 -0
- package/rcf/code-nodes/cn-794.json +18 -0
- package/rcf/code-nodes/cn-795.json +15 -0
- package/rcf/code-nodes/cn-796.json +14 -0
- package/rcf/code-nodes/cn-797.json +14 -0
- package/rcf/code-nodes/cn-798.json +14 -0
- package/rcf/define/brief-ledger.json +47 -0
- package/rcf/define/decisions-ledger.json +113 -0
- package/rcf/fbs/fbs-204.json +46 -0
- package/rcf/fbs/fbs-205.json +46 -0
- package/rcf/fbs/fbs-206.json +49 -0
- package/rcf/fbs/fbs-207.json +43 -0
- package/rcf/fbs/fbs-208.json +47 -0
- package/rcf/fbs/fbs-209.json +22 -0
- package/rcf/requirements/req-002.json +5 -2
- package/rcf/requirements/req-180.json +6 -3
- package/rcf/tacs/tac-4127-readiness-view.json +2 -2
- package/rcf/tacs/tac-4133-readiness-po-layer.json +2 -2
- package/rcf/tacs/tac-4135-readiness-command-page.json +94 -0
- package/rcf/tacs/tac-4136-view-query-routes.json +68 -0
- package/rcf/tacs/tac-4141-verify-modes.json +25 -0
- package/rcf/test-suites/ts-223.json +7 -7
- package/rcf/test-suites/ts-237.json +78 -0
- package/rcf/test-suites/ts-238.json +70 -0
- package/rcf/test-suites/ts-239.json +78 -0
- package/rcf/test-suites/ts-240.json +70 -0
- package/rcf/test-suites/ts-241.json +70 -0
- package/rcf/test-suites/ts-242.json +46 -0
- package/rcf/user-stories/us-16503.json +51 -0
- package/rcf/user-stories/us-18001.json +4 -4
- package/rcf/user-stories/us-18003.json +79 -0
- package/rcf/user-stories/us-18004.json +70 -0
- package/rcf/user-stories/us-18005.json +79 -0
- package/rcf/user-stories/us-208.json +74 -0
- package/rcf/user-stories/us-209.json +70 -0
- package/releases/releases.yaml +10 -1
- package/src/cli/finalise.js +141 -2
- package/src/cli/view.js +13 -1
- package/src/finalise/spawn.js +2 -0
- package/src/server/index.js +104 -39
- package/src/server/routes.js +169 -0
- package/src/verify/cli/run.js +40 -0
- package/src/verify/engine/deterministic.js +150 -0
- package/src/verify/engine/index.js +27 -4
- package/src/view/doc-renderers/req.js +11 -9
- package/src/view/html-page.js +15 -11
- package/src/view/index.js +7 -1
- package/src/view/live-client.js +4 -0
- package/src/view/page-init.js +842 -1
- package/src/view/readiness/composite-id.js +35 -0
- package/src/view/readiness/coverage-summary.js +293 -0
- package/src/view/readiness/delta-counts.js +156 -0
- package/src/view/readiness/grid.js +175 -0
- package/src/view/readiness/po-layer.js +110 -220
- package/src/view/readiness/tables.js +500 -0
- package/src/view/readiness/thin-reqs.js +237 -0
- package/src/view/readiness/trace-matrix.js +392 -0
- package/src/view/readiness.js +359 -370
- package/src/view/style.css +594 -0
- package/src/watch/index.js +75 -17
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"createdAt": "2026-10-07T13:46:54.539Z",
|
|
3
|
+
"updatedAt": "2026-10-07T13:46:54.539Z",
|
|
4
|
+
"description": "User story under REQ-002 for the traceability view (discovery sections 4 rank 8, 9 and 12, section 6.5; FBS-208; TAC-4136, ADR-4140). The Readiness tab's Trace sub-tab renders a matrix for a pivot read from entity= in the hash: rows are the stories and criteria under the pivot, columns are suites, cases, build specs and components, cells are filled where the chain reaches and coloured by resolution from coverage. Every id in the readiness tables carries a Trace action and the lookup modal's readiness hits land on the same hash. The client caches per state.version in memory and re-renders after an SSE swap. The per-requirement Mermaid slice on the Requirements tab is retired in the same change; no Mermaid is user-facing on these surfaces.",
|
|
5
|
+
"asA": "a product owner showing traceability or an engineer checking a requirement's reach",
|
|
6
|
+
"iWant": "a matrix from the chosen requirement down to its tests, with the unresolved cells visible, reachable from any id on the page",
|
|
7
|
+
"soThat": "the viewer demonstrates the traceability the method sells without a terminal",
|
|
8
|
+
"tacIds": [
|
|
9
|
+
"TAC-4136-view-query-routes"
|
|
10
|
+
],
|
|
11
|
+
"acceptanceCriteria": [
|
|
12
|
+
{
|
|
13
|
+
"id": "AC-209-1",
|
|
14
|
+
"description": "[happy] Given the Trace sub-tab and a pivot id in the hash, when the client fetches \"./trace.json\" for it, then a matrix renders with one TraceMatrixRow per story and criterion under the pivot and columns for suites, cases, build specs and components, each TraceMatrixCell.reached true where the forward trace reaches the column from the row's criterion.",
|
|
15
|
+
"testable": true,
|
|
16
|
+
"ownerRef": {
|
|
17
|
+
"tacId": "TAC-4136-view-query-routes",
|
|
18
|
+
"field": "interfaces[renderTraceMatrix]"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "AC-209-2",
|
|
23
|
+
"description": "[happy] Given coverage data for the pivot's criteria, when a cell renders, then TraceMatrixCell.resolution is \"resolving\" on the --sv-success token, \"unresolved\" on the --sv-warning token or \"none\" on the --sv-muted token, with no new colour token introduced.",
|
|
24
|
+
"testable": true,
|
|
25
|
+
"ownerRef": {
|
|
26
|
+
"tacId": "TAC-4136-view-query-routes",
|
|
27
|
+
"field": "interfaces[TraceMatrixCell]"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "AC-209-3",
|
|
32
|
+
"description": "[happy] Given any id rendered in the questions, blocking, thin-requirements or coverage tables, when the row renders, then it carries a \"Trace\" action linking to \"#tab=readiness&sub=trace&entity=<id>\", and a lookup modal hit on a readiness row navigates to the same hash preserving embed and theme.",
|
|
33
|
+
"testable": true,
|
|
34
|
+
"ownerRef": {
|
|
35
|
+
"tacId": "TAC-4136-view-query-routes",
|
|
36
|
+
"field": "interfaces[wireTraceMatrix]"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "AC-209-4",
|
|
41
|
+
"description": "[edge] Given an SSE tree-update that swaps #rcf-live-content while the Trace sub-tab shows a matrix, when the swap lands, then the matrix is re-rendered from the client cache keyed by state.version and marked stale until the next fetch completes, never dropped.",
|
|
42
|
+
"testable": true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "AC-209-5",
|
|
46
|
+
"description": "[must-not] Given the Requirements tab, when a requirement renders, then no Mermaid slice diagram is rendered for it (the per-requirement slice and its details element are gone) and no Mermaid is rendered anywhere on the Readiness tab.",
|
|
47
|
+
"testable": true
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "AC-209-6",
|
|
51
|
+
"description": "[failure] Given a pivot the route reports with found false, when the Trace sub-tab renders, then an empty state names the id and offers the lookup modal, and no partial matrix is drawn.",
|
|
52
|
+
"testable": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "AC-209-7",
|
|
56
|
+
"description": "[edge] Given a pivot whose forward trace yields more than 200 TraceMatrixRow entries, when the matrix renders, then a row filter by story id or criterion id is shown, the full TraceMatrixRow.cells set is retained in memory and nothing is truncated silently.",
|
|
57
|
+
"testable": true,
|
|
58
|
+
"ownerRef": {
|
|
59
|
+
"tacId": "TAC-4136-view-query-routes",
|
|
60
|
+
"field": "interfaces[TraceMatrixRow]"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"usId": "US-209",
|
|
65
|
+
"prdId": "PRD-001",
|
|
66
|
+
"reqId": "REQ-002",
|
|
67
|
+
"version": "0.1.0",
|
|
68
|
+
"status": "draft",
|
|
69
|
+
"title": "the viewer renders a trace matrix for a selected node and offers Trace on every id, retiring the Mermaid slice"
|
|
70
|
+
}
|
package/releases/releases.yaml
CHANGED
|
@@ -40,8 +40,17 @@
|
|
|
40
40
|
# `npm install rcf-lite`.
|
|
41
41
|
|
|
42
42
|
feedVersion: 1
|
|
43
|
-
latest: "0.
|
|
43
|
+
latest: "0.33.0"
|
|
44
44
|
releases:
|
|
45
|
+
- version: "0.33.0"
|
|
46
|
+
date: "2026-10-09"
|
|
47
|
+
breaking: false
|
|
48
|
+
headlines:
|
|
49
|
+
- "rcf-lite 0.33.0 makes the viewer's Readiness tab the command page: questions, blocking items, a coverage summary, a stage grid with sub-tabs, delta counts and a trace matrix, backed by three new trace, impact and coverage JSON routes with a per-version memo on the viewer server. `rcf verify run` and `rcf build finalise` gain a deterministic mode so a dispatched worker that cannot spawn the agent CLI can still produce a report and refuse promotion honestly. The live viewer survives write-temp-then-rename saves (vim backupcopy=no, JetBrains safe-write) and never silently drops a change that lands mid-rewalk. The @stravica-ai/rcf-schemas 0.6.3 pin is unchanged."
|
|
50
|
+
- "For embedders: three new viewer routes (/trace.json, /impact.json, /coverage.json) must be added to the reverse-proxy allow-list for /rcf-viewer/* before the version pin bump. The embed contract and the rcf-view-theme postMessage are unchanged."
|
|
51
|
+
minAgentAction: null
|
|
52
|
+
notesUrl: "https://stravica.ai/docs/rcf/changelog/"
|
|
53
|
+
|
|
45
54
|
- version: "0.32.5"
|
|
46
55
|
date: "2026-10-07"
|
|
47
56
|
breaking: false
|
package/src/cli/finalise.js
CHANGED
|
@@ -30,11 +30,18 @@ import { resolve as resolvePath } from 'node:path';
|
|
|
30
30
|
import process from 'node:process';
|
|
31
31
|
import { parseArgs } from 'node:util';
|
|
32
32
|
|
|
33
|
+
import { writeFile } from 'node:fs/promises';
|
|
34
|
+
|
|
33
35
|
import { isRcfError, rcfError, writeUnexpectedFailure } from '#core/errors';
|
|
34
36
|
import { updateDocument, walkTree } from '#core/store';
|
|
35
37
|
|
|
36
38
|
import { findProjectRoot } from '../view/index.js';
|
|
37
39
|
import { kindOf } from '../query/index.js';
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_VERIFY_MODE,
|
|
42
|
+
VERIFY_MODES,
|
|
43
|
+
isVerifyMode,
|
|
44
|
+
} from '../verify/engine/deterministic.js';
|
|
38
45
|
import {
|
|
39
46
|
buildVerifyArgs,
|
|
40
47
|
composeShipWithoutVerifiedRecord,
|
|
@@ -111,6 +118,12 @@ const OPTION_SPEC = {
|
|
|
111
118
|
// deploy), the check is skipped and a warn line records that
|
|
112
119
|
// finalise could not resolve the branch state.
|
|
113
120
|
'allow-pre-merge': { type: 'boolean' },
|
|
121
|
+
// Issue 330 (2026-10-08): verify mode passed through to the fresh
|
|
122
|
+
// `rcf verify run` subprocess. Default is agentScreenshotCritique (the
|
|
123
|
+
// parent-session mode). A dispatched worker runs --mode deterministic
|
|
124
|
+
// because the harness refuses to let a worker spawn `claude` as a
|
|
125
|
+
// subagent, which agentScreenshotCritique requires.
|
|
126
|
+
mode: { type: 'string' },
|
|
114
127
|
quiet: { type: 'boolean' },
|
|
115
128
|
help: { type: 'boolean' },
|
|
116
129
|
};
|
|
@@ -176,6 +189,18 @@ Options:
|
|
|
176
189
|
dev-loop dry runs; the decision is logged
|
|
177
190
|
on stdout. Absent a git checkout the check
|
|
178
191
|
degrades to a warn line.
|
|
192
|
+
--mode <mode> Verify mode (default: agentScreenshotCritique):
|
|
193
|
+
agentScreenshotCritique parent-session mode
|
|
194
|
+
(spawns the agent CLI as a subagent); only
|
|
195
|
+
runs from a session that may invoke the
|
|
196
|
+
agent CLI.
|
|
197
|
+
deterministic model-free, in-process mode a
|
|
198
|
+
dispatched worker can run. Produces the
|
|
199
|
+
same report schema with a per-AC skip
|
|
200
|
+
ledger; the finalise gate refuses
|
|
201
|
+
promotion on a deterministic run that
|
|
202
|
+
skipped any AC (ADR-4112 verified
|
|
203
|
+
invariant).
|
|
179
204
|
--quiet Suppress non-error confirmations
|
|
180
205
|
--help Print this help
|
|
181
206
|
|
|
@@ -292,6 +317,14 @@ export async function main(argv, deps = {}) {
|
|
|
292
317
|
if (typeof flags.provision === 'string' && flags.provision.startsWith('-')) {
|
|
293
318
|
return usage('--provision takes a file path, not a flag or inline value (credentials are never accepted inline)');
|
|
294
319
|
}
|
|
320
|
+
// Issue 330: resolve --mode. Unknown value refuses exit 2 so a typo
|
|
321
|
+
// ("--mode determinstic") does not silently fall through to the
|
|
322
|
+
// default agent mode a dispatched worker cannot run.
|
|
323
|
+
const modeFlag = flags.mode;
|
|
324
|
+
if (modeFlag !== undefined && !isVerifyMode(modeFlag)) {
|
|
325
|
+
return usage(`--mode must be one of ${VERIFY_MODES.join(' | ')} (got "${modeFlag}")`);
|
|
326
|
+
}
|
|
327
|
+
const mode = modeFlag ?? DEFAULT_VERIFY_MODE;
|
|
295
328
|
|
|
296
329
|
const projectRoot = await findProjectRoot(cwd);
|
|
297
330
|
if (!projectRoot) {
|
|
@@ -361,10 +394,48 @@ export async function main(argv, deps = {}) {
|
|
|
361
394
|
provision: flags.provision,
|
|
362
395
|
chain: flags.chain,
|
|
363
396
|
persona: flags.persona,
|
|
397
|
+
mode,
|
|
364
398
|
});
|
|
365
399
|
|
|
400
|
+
// Issue 330 partial-report preseed. Verify is a fresh subprocess and
|
|
401
|
+
// may fail to start (ENOENT, blocked by harness), be killed, or exit
|
|
402
|
+
// non-zero before it has had the chance to write its own report. In
|
|
403
|
+
// every one of those cases the ingest step below would otherwise read
|
|
404
|
+
// ENOENT and finalise would print "could not read the verify report"
|
|
405
|
+
// with no clue about what the mode or the reason was. Preseeding the
|
|
406
|
+
// report path with a `{ status: "aborted" }` stub guarantees ingest
|
|
407
|
+
// always has something to read; a successful verify run overwrites
|
|
408
|
+
// the stub with its real report in the same path.
|
|
409
|
+
const finaliseStartedAt = new Date().toISOString();
|
|
410
|
+
const partialStub = {
|
|
411
|
+
schemaVersion: '1',
|
|
412
|
+
verdict: 'LAUNCH-FAILURE',
|
|
413
|
+
verdictAuthority: null,
|
|
414
|
+
run: {
|
|
415
|
+
profile,
|
|
416
|
+
url: flags.url,
|
|
417
|
+
startedAt: finaliseStartedAt,
|
|
418
|
+
finishedAt: null,
|
|
419
|
+
runStats: { mode, status: 'pending', reason: 'verify not yet started' },
|
|
420
|
+
},
|
|
421
|
+
findings: [],
|
|
422
|
+
blockedAcs: [],
|
|
423
|
+
launchFailure: {
|
|
424
|
+
message: 'rcf verify did not write a report (process was killed, errored before write, or did not start)',
|
|
425
|
+
status: 'aborted',
|
|
426
|
+
reason: 'preseed',
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
const writer = deps.writeFile ?? writeFile;
|
|
430
|
+
try {
|
|
431
|
+
await writer(outPath, `${JSON.stringify(partialStub, null, 2)}\n`, 'utf8');
|
|
432
|
+
} catch (err) {
|
|
433
|
+
stderr.write(`[finalise] could not preseed partial report at ${outPath}: ${err.message}\n`);
|
|
434
|
+
return 1;
|
|
435
|
+
}
|
|
436
|
+
|
|
366
437
|
if (!quiet) {
|
|
367
|
-
stdout.write(`[finalise] launching rcf verify run (fresh subprocess) against ${flags.url} [profile=${profile}]...\n`);
|
|
438
|
+
stdout.write(`[finalise] launching rcf verify run (fresh subprocess) against ${flags.url} [profile=${profile}, mode=${mode}]...\n`);
|
|
368
439
|
}
|
|
369
440
|
|
|
370
441
|
let spawnResult;
|
|
@@ -398,7 +469,13 @@ export async function main(argv, deps = {}) {
|
|
|
398
469
|
// §9 independence guarantee). An unreadable report on a pass is treated as a
|
|
399
470
|
// HOLD, never a silent promotion (mirrors ingest's "unreadable is not a pass").
|
|
400
471
|
const passLoaded = await (deps.loadReport ? deps.loadReport(outPath, deps) : loadReport(outPath, deps));
|
|
401
|
-
|
|
472
|
+
// Issue 330: a passing exit code with ONLY the preseed stub on disk
|
|
473
|
+
// means verify exited 0 without writing its own report; treat
|
|
474
|
+
// that as unreadable (never a silent promotion). Mirrors the
|
|
475
|
+
// "unreadable is not a pass" NIT-3 contract.
|
|
476
|
+
const passPreseedOnly = passLoaded.ok
|
|
477
|
+
&& passLoaded.report?.launchFailure?.reason === 'preseed';
|
|
478
|
+
const authority = (passLoaded.ok && !passPreseedOnly) ? passLoaded.report.verdictAuthority : undefined;
|
|
402
479
|
if (authority !== 'ship') {
|
|
403
480
|
stderr.write(`[finalise] HOLD: rcf verify passed but this run carries '${authority ?? 'unknown'}' authority, `
|
|
404
481
|
+ `not 'ship'; ${fbsId} left '${currentStatus}'.\n`);
|
|
@@ -406,8 +483,32 @@ export async function main(argv, deps = {}) {
|
|
|
406
483
|
+ 'runtime is edge-identical to prod - carries ship authority and can promote to verified. '
|
|
407
484
|
+ `Report: ${outPath}\n`);
|
|
408
485
|
if (!passLoaded.ok) stderr.write(`[finalise] (the verify report could not be read: ${passLoaded.reason})\n`);
|
|
486
|
+
else if (passPreseedOnly) stderr.write('[finalise] (the verify report could not be read: verify exited 0 without replacing the preseed stub)\n');
|
|
409
487
|
return 4;
|
|
410
488
|
}
|
|
489
|
+
// Issue 330 (2026-10-08) deterministic-mode refusal, keyed off the
|
|
490
|
+
// ADR-4112 verified invariant. A deterministic-mode run never
|
|
491
|
+
// actively exercises a critique-only AC (every testable AC is
|
|
492
|
+
// recorded on blockedAcs[] with reason `critique-only`). Promoting
|
|
493
|
+
// to `verified` off such a run would quietly contradict the ADR
|
|
494
|
+
// promise. Refuse with a clear counts line; the operator either
|
|
495
|
+
// re-runs the gate under agentScreenshotCritique from a parent
|
|
496
|
+
// session, or extends the chain with a per-AC probe shape the
|
|
497
|
+
// deterministic mode can judge.
|
|
498
|
+
if (passLoaded.ok) {
|
|
499
|
+
const runStats = passLoaded.report?.run?.runStats
|
|
500
|
+
?? passLoaded.report?.runStats
|
|
501
|
+
?? null;
|
|
502
|
+
if (runStats && runStats.mode === 'deterministic') {
|
|
503
|
+
const counts = runStats.counts ?? { verified: 0, failed: 0, skipped: 0, total: 0 };
|
|
504
|
+
if ((counts.skipped ?? 0) > 0 || (counts.verified ?? 0) < (counts.total ?? 0)) {
|
|
505
|
+
stderr.write(`[finalise] gate NOT promoted: deterministic mode verified ${counts.verified ?? 0} of ${counts.total ?? 0} ACs (skipped ${counts.skipped ?? 0} critique-only); critique mode required to promote ${fbsId} to 'verified'. ADR-4112 verified invariant.\n`);
|
|
506
|
+
stderr.write("Re-run from a parent session that may spawn the agent CLI ('rcf build finalise ... --mode agentScreenshotCritique'), or extend the chain so every AC carries a deterministic probe shape.\n");
|
|
507
|
+
stderr.write(`Report: ${outPath}\n`);
|
|
508
|
+
return 4;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
411
512
|
// 0.7.0 verification-integrity §5.2: the finalise gate reads the
|
|
412
513
|
// verify report's per-AC verdicts and refuses to promote to
|
|
413
514
|
// 'verified' on MOCK-ONLY-DECLARED / BLOCKED-BY-DECLARATION unless
|
|
@@ -583,9 +684,47 @@ export async function main(argv, deps = {}) {
|
|
|
583
684
|
// Gate NOT passed. The FBS stays put; surface the findings from the report
|
|
584
685
|
// (the §5.4 verify -> fix -> re-verify seam), then fail non-zero.
|
|
585
686
|
stderr.write(`[finalise] gate NOT passed (rcf verify exit ${spawnResult.code}); ${fbsId} left '${currentStatus}'.\n`);
|
|
687
|
+
// Issue 330: if the subprocess exited without replacing the preseed
|
|
688
|
+
// partial-report stub, overwrite it with an `aborted` record naming
|
|
689
|
+
// the mode and the exit code so a reader sees why there is no
|
|
690
|
+
// verdict, not an ENOENT.
|
|
586
691
|
const loaded = await (deps.loadReport ? deps.loadReport(outPath, deps) : loadReport(outPath, deps));
|
|
692
|
+
const finishedAtAborted = new Date().toISOString();
|
|
693
|
+
if (loaded.ok && loaded.report?.launchFailure?.reason === 'preseed') {
|
|
694
|
+
const abortedReport = {
|
|
695
|
+
...loaded.report,
|
|
696
|
+
verdict: 'LAUNCH-FAILURE',
|
|
697
|
+
verdictAuthority: null,
|
|
698
|
+
run: {
|
|
699
|
+
...(loaded.report.run ?? {}),
|
|
700
|
+
finishedAt: finishedAtAborted,
|
|
701
|
+
runStats: {
|
|
702
|
+
...((loaded.report.run && loaded.report.run.runStats) || {}),
|
|
703
|
+
mode,
|
|
704
|
+
status: 'aborted',
|
|
705
|
+
reason: `rcf verify exited ${spawnResult.code} without writing a report`,
|
|
706
|
+
exitCode: spawnResult.code,
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
launchFailure: {
|
|
710
|
+
message: `rcf verify exited ${spawnResult.code} without writing a report (mode=${mode})`,
|
|
711
|
+
status: 'aborted',
|
|
712
|
+
reason: `rcf verify exited ${spawnResult.code} without writing a report`,
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
try {
|
|
716
|
+
await writer(outPath, `${JSON.stringify(abortedReport, null, 2)}\n`, 'utf8');
|
|
717
|
+
loaded.report = abortedReport;
|
|
718
|
+
} catch (err) {
|
|
719
|
+
stderr.write(`[finalise] could not overwrite preseed stub at ${outPath}: ${err.message}\n`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
587
722
|
if (loaded.ok) {
|
|
588
723
|
stderr.write(summariseReport(loaded.report));
|
|
724
|
+
const abortStats = loaded.report?.run?.runStats ?? loaded.report?.runStats ?? null;
|
|
725
|
+
if (abortStats && abortStats.status === 'aborted') {
|
|
726
|
+
stderr.write(`[finalise] mode=${abortStats.mode ?? mode}: ${abortStats.reason ?? 'verify produced no report'}\n`);
|
|
727
|
+
}
|
|
589
728
|
stderr.write(`Full report (build-lite ingestible, chain-node-addressed): ${outPath}\n`);
|
|
590
729
|
} else {
|
|
591
730
|
stderr.write(`[finalise] could not read the verify report: ${loaded.reason}\n`);
|
package/src/cli/view.js
CHANGED
|
@@ -272,6 +272,10 @@ export async function main(argv, deps = {}) {
|
|
|
272
272
|
const stdout = deps.stdout ?? process.stdout;
|
|
273
273
|
const stderr = deps.stderr ?? process.stderr;
|
|
274
274
|
const onSignal = deps.onSignal ?? ((sig, handler) => process.on(sig, handler));
|
|
275
|
+
// Issue #333 item 2: tests inject a `startServerImpl` to observe the
|
|
276
|
+
// sinks the CLI wires without racing a real walker; the default is
|
|
277
|
+
// the real `startServer`.
|
|
278
|
+
const startServerImpl = deps.startServerImpl ?? startServer;
|
|
275
279
|
|
|
276
280
|
// Track C+D §9: subverb routing. When the first argv token is
|
|
277
281
|
// `start | status | stop | logs`, dispatch to the lifecycle handler
|
|
@@ -315,13 +319,21 @@ export async function main(argv, deps = {}) {
|
|
|
315
319
|
}
|
|
316
320
|
|
|
317
321
|
const logSink = opts.verbose ? (line) => stderr.write(`${line}\n`) : () => {};
|
|
322
|
+
// Issue #333 item 2: a host that spawns `rcf audit view` as a
|
|
323
|
+
// supervised child without --verbose still needs to see fatal watcher
|
|
324
|
+
// and walker exceptions, otherwise the viewer looks alive while
|
|
325
|
+
// live-updates have silently stopped. `logError` is unconditional;
|
|
326
|
+
// the chatty per-event lines keep flowing through `log` only when
|
|
327
|
+
// --verbose is on.
|
|
328
|
+
const logErrorSink = (line) => stderr.write(`${line}\n`);
|
|
318
329
|
|
|
319
330
|
let server;
|
|
320
331
|
try {
|
|
321
|
-
server = await
|
|
332
|
+
server = await startServerImpl({
|
|
322
333
|
projectRoot,
|
|
323
334
|
port: resolvedPort,
|
|
324
335
|
log: logSink,
|
|
336
|
+
logError: logErrorSink,
|
|
325
337
|
testHost: resolveTestHost(opts.testHost, env),
|
|
326
338
|
});
|
|
327
339
|
} catch (err) {
|
package/src/finalise/spawn.js
CHANGED
|
@@ -31,6 +31,7 @@ import { isolationEnv } from '#core/isolation';
|
|
|
31
31
|
* @param {string} [opts.provision] - provisioning FILE path
|
|
32
32
|
* @param {string} [opts.chain] - PRD/chain ref
|
|
33
33
|
* @param {string} [opts.persona]
|
|
34
|
+
* @param {string} [opts.mode] - verify mode (issue 330): agentScreenshotCritique | deterministic
|
|
34
35
|
* @returns {string[]}
|
|
35
36
|
*/
|
|
36
37
|
export function buildVerifyArgs(opts) {
|
|
@@ -47,6 +48,7 @@ export function buildVerifyArgs(opts) {
|
|
|
47
48
|
if (opts.parityEnv) args.push('--parity-env');
|
|
48
49
|
if (opts.provision) args.push('--provision', opts.provision);
|
|
49
50
|
if (opts.persona) args.push('--persona', opts.persona);
|
|
51
|
+
if (opts.mode) args.push('--mode', opts.mode);
|
|
50
52
|
return args;
|
|
51
53
|
}
|
|
52
54
|
|
package/src/server/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// (D11); EADDRINUSE is a hard failure (D10).
|
|
5
5
|
//
|
|
6
6
|
// Public surface:
|
|
7
|
-
// startServer({ projectRoot, port, host, log, heartbeatMs, watchImpl })
|
|
7
|
+
// startServer({ projectRoot, port, host, log, logError, heartbeatMs, watchImpl, renderImpl })
|
|
8
8
|
// -> Promise<{ url, port, close, hub, currentState }>
|
|
9
9
|
// close() drains SSE with a `shutdown` event and releases the port. The
|
|
10
10
|
// caller (bin/rcf-view.js) is responsible for the 2s force-exit budget
|
|
@@ -27,8 +27,16 @@ import { createSseHub } from './sse.js';
|
|
|
27
27
|
* @property {string} [host='127.0.0.1']
|
|
28
28
|
* @property {number} [heartbeatMs=30000]
|
|
29
29
|
* @property {number} [debounceMs=50]
|
|
30
|
-
* @property {(line: string) => void} [log] -
|
|
30
|
+
* @property {(line: string) => void} [log] - verbose-gated chatty sink;
|
|
31
|
+
* defaults to a noop so a caller that does not want chatter can omit it.
|
|
32
|
+
* @property {(line: string) => void} [logError] - fatal-error sink that is
|
|
33
|
+
* always written to, independent of `log`. Issue #333 item 2: the CLI
|
|
34
|
+
* wires this to stderr unconditionally so a host that spawned
|
|
35
|
+
* `rcf audit view` without --verbose still sees a dead watcher or a
|
|
36
|
+
* failed walker. Defaults to `log` for back-compat.
|
|
31
37
|
* @property {typeof defaultWatch} [watchImpl] - injectable watch primitive for tests
|
|
38
|
+
* @property {typeof renderModelToPage} [renderImpl] - injectable walker
|
|
39
|
+
* for tests; defaults to the real `renderModelToPage`.
|
|
32
40
|
* @property {boolean} [testHost=false] - viewer UI refresh PR 9 (TAC-4134,
|
|
33
41
|
* ADR-4136): when true, mount the wespa host fixture at /test-host.html
|
|
34
42
|
* plus its embed-client script at /test-host.js. Off by default so
|
|
@@ -73,54 +81,96 @@ export async function startServer(args) {
|
|
|
73
81
|
const heartbeatMs = typeof args.heartbeatMs === 'number' ? args.heartbeatMs : 30000;
|
|
74
82
|
const debounceMs = typeof args.debounceMs === 'number' ? args.debounceMs : 50;
|
|
75
83
|
const log = typeof args.log === 'function' ? args.log : () => {};
|
|
84
|
+
// Issue #333 item 2: fatal watcher/walker exceptions always surface to
|
|
85
|
+
// stderr regardless of --verbose; chatty per-event lines stay gated on
|
|
86
|
+
// `log`. Callers that pass only `log` keep the pre-fix behaviour (one
|
|
87
|
+
// sink for both chatty and fatal), which the back-compat test asserts.
|
|
88
|
+
const logError = typeof args.logError === 'function' ? args.logError : log;
|
|
76
89
|
const watchImpl = typeof args.watchImpl === 'function' ? args.watchImpl : defaultWatch;
|
|
90
|
+
// Issue #333 item 1: tests inject a controllable render promise to
|
|
91
|
+
// observe the trailing-rewalk coalescing without racing a real walk.
|
|
92
|
+
const renderImpl = typeof args.renderImpl === 'function' ? args.renderImpl : renderModelToPage;
|
|
77
93
|
const testHost = args.testHost === true;
|
|
78
94
|
|
|
79
|
-
/** @type {{ version: number, fullPageHtml: string, contentHtml: string, errors: import('#core/errors').RcfError[] } | null} */
|
|
95
|
+
/** @type {{ version: number, fullPageHtml: string, contentHtml: string, errors: import('#core/errors').RcfError[], tree?: import('#core/store/walker.js').TreeModel, testPointers?: Map<string, import('#core/store/tp-resolve.js').TestPointerResolution> } | null} */
|
|
80
96
|
let state = null;
|
|
81
97
|
let version = 0;
|
|
82
98
|
let rewalkInFlight = null;
|
|
99
|
+
// Issue #333 item 1: a change that arrives while a walk is in flight
|
|
100
|
+
// queues exactly one trailing walk so the post-in-flight on-disk state
|
|
101
|
+
// is picked up; any number of changes during the in-flight walk collapse
|
|
102
|
+
// onto that single trailing walk (never dropped, never re-run per call).
|
|
103
|
+
let rewalkTrailing = false;
|
|
83
104
|
let closed = false;
|
|
84
105
|
|
|
85
106
|
const sse = createSseHub({ heartbeatMs, log });
|
|
86
107
|
|
|
87
|
-
|
|
88
|
-
|
|
108
|
+
// FBS-207 (TAC-4136 queryCache + AC-208-6): per-version memo for the
|
|
109
|
+
// three JSON query routes keyed by `${version}:${route}:${pivot}:${d
|
|
110
|
+
// irection|scope}`. Replaced wholesale on every rewalk so a new tree
|
|
111
|
+
// never serves stale compute. One compute per key per version; never
|
|
112
|
+
// persisted; never written under rcf/. Page-lifetime: a reconnected
|
|
113
|
+
// client sees a fresh cache.
|
|
114
|
+
/** @type {Map<string, string>} */
|
|
115
|
+
let queryCache = new Map();
|
|
116
|
+
|
|
117
|
+
async function runWalk() {
|
|
118
|
+
try {
|
|
119
|
+
const result = await renderImpl({ projectRoot });
|
|
120
|
+
if (closed) return;
|
|
121
|
+
version += 1;
|
|
122
|
+
state = {
|
|
123
|
+
version,
|
|
124
|
+
fullPageHtml: result.fullPageHtml,
|
|
125
|
+
contentHtml: result.contentHtml,
|
|
126
|
+
errors: result.errors,
|
|
127
|
+
pmPartials: result.pmPartials,
|
|
128
|
+
// Viewer UI refresh PR 7 (TAC-4132, ADR-4134): the ID lookup
|
|
129
|
+
// index is built once per rewalk and snapshotted on `state`
|
|
130
|
+
// so `GET /index.json` serves from memory and the SSE
|
|
131
|
+
// `tree-update` version lets the client invalidate its cache.
|
|
132
|
+
indexJson: result.indexJson,
|
|
133
|
+
// FBS-207 (TAC-4136, AC-208-6): the walked tree and resolved
|
|
134
|
+
// test-pointer map snapshotted on state so /trace.json,
|
|
135
|
+
// /impact.json and /coverage.json compute from the same tree
|
|
136
|
+
// the page was rendered from. `tree` was already produced by
|
|
137
|
+
// renderModelToPage; the server now carries it onto state.
|
|
138
|
+
tree: result.tree,
|
|
139
|
+
testPointers: result.testPointers,
|
|
140
|
+
};
|
|
141
|
+
// FBS-207 (AC-208-6): a new version drops every previous
|
|
142
|
+
// version's compute entries wholesale, as TAC-4136 specifies.
|
|
143
|
+
queryCache = new Map();
|
|
144
|
+
sse.broadcast('tree-update', { version, contentHtml: result.contentHtml });
|
|
145
|
+
if (result.errors && result.errors.length > 0) {
|
|
146
|
+
sse.broadcast('walker-error', { errors: result.errors });
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
// Fatal walker exception: always to stderr via `logError` so a
|
|
150
|
+
// host spawning `rcf audit view` without --verbose still sees it
|
|
151
|
+
// (issue #333 item 2).
|
|
152
|
+
logError(`[server] walker failed: ${/** @type {Error} */ (err).message}`);
|
|
153
|
+
sse.broadcast('walker-error', {
|
|
154
|
+
errors: [{ kind: 'ioFailure', message: /** @type {Error} */ (err).message }],
|
|
155
|
+
});
|
|
156
|
+
} finally {
|
|
157
|
+
const trailing = rewalkTrailing;
|
|
158
|
+
rewalkTrailing = false;
|
|
159
|
+
rewalkInFlight = trailing && !closed ? runWalk() : null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function rewalk() {
|
|
164
|
+
if (closed) return Promise.resolve();
|
|
89
165
|
if (rewalkInFlight) {
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
166
|
+
// A walk is already running. Flag that a trailing walk is wanted
|
|
167
|
+
// and return a promise that resolves when the trailing walk (not
|
|
168
|
+
// the in-flight one) has completed. Any concurrent callers coalesce
|
|
169
|
+
// onto the same trailing walk - the final on-disk state wins.
|
|
170
|
+
rewalkTrailing = true;
|
|
171
|
+
return rewalkInFlight.then(() => rewalkInFlight ?? undefined);
|
|
93
172
|
}
|
|
94
|
-
rewalkInFlight = (
|
|
95
|
-
try {
|
|
96
|
-
const result = await renderModelToPage({ projectRoot });
|
|
97
|
-
if (closed) return;
|
|
98
|
-
version += 1;
|
|
99
|
-
state = {
|
|
100
|
-
version,
|
|
101
|
-
fullPageHtml: result.fullPageHtml,
|
|
102
|
-
contentHtml: result.contentHtml,
|
|
103
|
-
errors: result.errors,
|
|
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,
|
|
110
|
-
};
|
|
111
|
-
sse.broadcast('tree-update', { version, contentHtml: result.contentHtml });
|
|
112
|
-
if (result.errors && result.errors.length > 0) {
|
|
113
|
-
sse.broadcast('walker-error', { errors: result.errors });
|
|
114
|
-
}
|
|
115
|
-
} catch (err) {
|
|
116
|
-
log(`[server] walker failed: ${/** @type {Error} */ (err).message}`);
|
|
117
|
-
sse.broadcast('walker-error', {
|
|
118
|
-
errors: [{ kind: 'ioFailure', message: /** @type {Error} */ (err).message }],
|
|
119
|
-
});
|
|
120
|
-
} finally {
|
|
121
|
-
rewalkInFlight = null;
|
|
122
|
-
}
|
|
123
|
-
})();
|
|
173
|
+
rewalkInFlight = runWalk();
|
|
124
174
|
return rewalkInFlight;
|
|
125
175
|
}
|
|
126
176
|
|
|
@@ -149,6 +199,16 @@ export async function startServer(args) {
|
|
|
149
199
|
const router = createRouter({
|
|
150
200
|
currentState: () => state,
|
|
151
201
|
sse,
|
|
202
|
+
// FBS-207 (TAC-4136): the three JSON query routes share one page-
|
|
203
|
+
// lifetime memo held on the server closure; the router reads and
|
|
204
|
+
// writes it through this handle. Replaced wholesale on each
|
|
205
|
+
// rewalk, above.
|
|
206
|
+
queryCache: {
|
|
207
|
+
get: (key) => queryCache.get(key),
|
|
208
|
+
set: (key, value) => { queryCache.set(key, value); },
|
|
209
|
+
has: (key) => queryCache.has(key),
|
|
210
|
+
size: () => queryCache.size,
|
|
211
|
+
},
|
|
152
212
|
// Snapshotted assets take precedence; the legacy path props are
|
|
153
213
|
// still passed so tests that mount the router directly with a
|
|
154
214
|
// path (no asset load) keep working.
|
|
@@ -175,7 +235,7 @@ export async function startServer(args) {
|
|
|
175
235
|
paths: [watchDir],
|
|
176
236
|
onChange: () => { rewalk().catch(() => {}); },
|
|
177
237
|
debounceMs,
|
|
178
|
-
onError: (err) => {
|
|
238
|
+
onError: (err) => { logError(`[watch] ${err.message}`); },
|
|
179
239
|
});
|
|
180
240
|
|
|
181
241
|
// Bind. EADDRINUSE surfaces as a rejected Promise so the caller can
|
|
@@ -236,5 +296,10 @@ export async function startServer(args) {
|
|
|
236
296
|
currentState: () => state,
|
|
237
297
|
sse,
|
|
238
298
|
rewalk,
|
|
299
|
+
// FBS-207 (TAC-4136, AC-208-6 test seam): read-only probe on the
|
|
300
|
+
// page-lifetime memo. Lets the AC-208-6 integration test prove the
|
|
301
|
+
// rewalk cache SWAP (not just a version-prefixed key change) by
|
|
302
|
+
// observing size before and after an explicit `rewalk()`.
|
|
303
|
+
queryCacheSize: () => queryCache.size,
|
|
239
304
|
};
|
|
240
305
|
}
|