rcf-lite 0.32.4 → 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 +42 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/discovery/README.md +22 -0
- 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 +18 -1
- package/src/cli/finalise.js +141 -2
- package/src/cli/view.js +13 -1
- package/src/define/ledgers.js +9 -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
package/src/server/routes.js
CHANGED
|
@@ -35,6 +35,15 @@ import { readFile, stat } from 'node:fs/promises';
|
|
|
35
35
|
|
|
36
36
|
import { renderComponentsFixturePage } from '../view/components-fixture.js';
|
|
37
37
|
import { renderTestHostPage } from '../view/test-host/fixture.js';
|
|
38
|
+
// FBS-207 (TAC-4136, ADR-4140): the viewer's three JSON query routes
|
|
39
|
+
// call the same pure composers the CLI calls, from the walked tree
|
|
40
|
+
// kept on `state`. Admissibility bypass: these routes do NOT call
|
|
41
|
+
// `runWithAdmissibilityGate` (the CLI coverage, impact and trace
|
|
42
|
+
// verbs do not either; issues 316 and 317). Only readiness and
|
|
43
|
+
// freeze consult that gate.
|
|
44
|
+
import { classifyCoverageScope, computeCoverage } from '../query/coverage.js';
|
|
45
|
+
import { computeImpact } from '../query/impact.js';
|
|
46
|
+
import { computeTrace, kindOf } from '../query/trace.js';
|
|
38
47
|
|
|
39
48
|
// Issue #248 (0.28.3): the shipped static assets (style.css,
|
|
40
49
|
// mermaid.min.js, live-client.js) MUST be resolved once at server
|
|
@@ -180,6 +189,18 @@ export function createRouter(deps) {
|
|
|
180
189
|
res.end(body);
|
|
181
190
|
return;
|
|
182
191
|
}
|
|
192
|
+
if (path === '/trace.json') {
|
|
193
|
+
handleQueryRoute(req, res, deps, url, 'trace');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (path === '/impact.json') {
|
|
197
|
+
handleQueryRoute(req, res, deps, url, 'impact');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (path === '/coverage.json') {
|
|
201
|
+
handleQueryRoute(req, res, deps, url, 'coverage');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
183
204
|
if (path === '/scope.json') {
|
|
184
205
|
if (typeof deps.scope !== 'function') {
|
|
185
206
|
res.writeHead(404, { 'content-type': MIME.txt });
|
|
@@ -251,3 +272,151 @@ function fail(res, err) {
|
|
|
251
272
|
res.end(`internal error: ${err && err.message ? err.message : 'unknown'}\n`);
|
|
252
273
|
} catch { /* swallow */ }
|
|
253
274
|
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* FBS-207 (TAC-4136 interfaces[GET /trace.json], [GET /impact.json],
|
|
278
|
+
* [GET /coverage.json], [QueryRouteError]): one handler per route that
|
|
279
|
+
* reads deps.currentState(), returns 503 before the first walk, 404 or
|
|
280
|
+
* 400 QueryRouteError on known failures, and application/json with
|
|
281
|
+
* cache-control: no-store on success. The three routes NEVER call
|
|
282
|
+
* runWithAdmissibilityGate (admissibility bypass stated in TAC-4136:
|
|
283
|
+
* the CLI coverage, impact and trace verbs do not either, so a tree
|
|
284
|
+
* that fails admissibility still answers these three). Nothing is
|
|
285
|
+
* written under rcf/.
|
|
286
|
+
*
|
|
287
|
+
* Memoised per (version, route, pivot, direction|scope) in deps.query
|
|
288
|
+
* Cache, which the server replaces wholesale on every rewalk. The
|
|
289
|
+
* cached entry is the response body string so a repeated request
|
|
290
|
+
* writes bytes without recomputing or re-stringifying.
|
|
291
|
+
*
|
|
292
|
+
* @param {import('node:http').IncomingMessage} req
|
|
293
|
+
* @param {import('node:http').ServerResponse} res
|
|
294
|
+
* @param {RouterDeps & { queryCache?: {
|
|
295
|
+
* get(key: string): string | undefined,
|
|
296
|
+
* set(key: string, value: string): void,
|
|
297
|
+
* has(key: string): boolean,
|
|
298
|
+
* size(): number,
|
|
299
|
+
* }}} deps
|
|
300
|
+
* @param {URL} url
|
|
301
|
+
* @param {'trace' | 'impact' | 'coverage'} route
|
|
302
|
+
*/
|
|
303
|
+
function handleQueryRoute(req, res, deps, url, route) {
|
|
304
|
+
const state = deps.currentState();
|
|
305
|
+
// AC-208-7: 503 "view server initialising" before the first walk
|
|
306
|
+
// completes, exactly as /index.json answers today.
|
|
307
|
+
if (!state || !state.tree) {
|
|
308
|
+
res.writeHead(503, { 'content-type': MIME.txt });
|
|
309
|
+
res.end('view server initialising\n');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// AC-208-1: for /trace.json the query id and direction come from
|
|
314
|
+
// the URL. Default direction is forward (matches the CLI default).
|
|
315
|
+
const id = url.searchParams.get('id');
|
|
316
|
+
const direction = url.searchParams.get('direction') ?? 'forward';
|
|
317
|
+
const scope = url.searchParams.get('scope');
|
|
318
|
+
// FBS-208: opt-in CN layer for the Trace matrix - without it, the
|
|
319
|
+
// trace response excludes codeNode cross-links (back-compat with
|
|
320
|
+
// every pre-FBS-208 caller).
|
|
321
|
+
const includeCode = url.searchParams.get('includeCode') === '1';
|
|
322
|
+
|
|
323
|
+
if (route === 'trace' || route === 'impact') {
|
|
324
|
+
// AC-208-4 unknown-id: an id missing or not found in the tree is
|
|
325
|
+
// 404 with QueryRouteError.error "unknown-id" and the request id
|
|
326
|
+
// echoed back (missing is echoed as null).
|
|
327
|
+
if (!id) {
|
|
328
|
+
sendQueryError(res, 404, { error: 'unknown-id', id: null });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (!kindOf(state.tree, id)) {
|
|
332
|
+
sendQueryError(res, 404, { error: 'unknown-id', id });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (route === 'trace') {
|
|
337
|
+
// AC-208-4 bad-direction: anything outside forward | back | both
|
|
338
|
+
// is 400 with QueryRouteError.error "bad-direction".
|
|
339
|
+
if (direction !== 'forward' && direction !== 'back' && direction !== 'both') {
|
|
340
|
+
sendQueryError(res, 400, { error: 'bad-direction', id, direction });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (route === 'coverage' && scope !== null) {
|
|
345
|
+
// AC-208-4 bad-scope: a scope positional that is below AC or
|
|
346
|
+
// unknown is 400 with QueryRouteError.error "bad-scope". Reuses
|
|
347
|
+
// the exact classifier the CLI uses (src/cli/coverage.js).
|
|
348
|
+
const classification = classifyCoverageScope(state.tree, scope);
|
|
349
|
+
if (classification !== 'valid') {
|
|
350
|
+
sendQueryError(res, 400, { error: 'bad-scope', scope });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// AC-208-6: memo key covers the state version and the route-shaped
|
|
356
|
+
// request params so each (version, pivot, direction|scope) computes
|
|
357
|
+
// once and serves from the memo thereafter. The cache is replaced
|
|
358
|
+
// wholesale on each rewalk, so a new version never serves a prior
|
|
359
|
+
// version's bytes.
|
|
360
|
+
const key = route === 'trace'
|
|
361
|
+
? `${state.version}:trace:${id}:${direction}:${includeCode ? 'c1' : 'c0'}`
|
|
362
|
+
: route === 'impact'
|
|
363
|
+
? `${state.version}:impact:${id}:-`
|
|
364
|
+
: `${state.version}:coverage:-:${scope ?? ''}`;
|
|
365
|
+
|
|
366
|
+
const cache = deps.queryCache;
|
|
367
|
+
let body = cache && cache.has(key) ? cache.get(key) : null;
|
|
368
|
+
if (body === null || body === undefined) {
|
|
369
|
+
let result;
|
|
370
|
+
try {
|
|
371
|
+
if (route === 'trace') {
|
|
372
|
+
result = computeTrace(state.tree, { id, direction, includeCode });
|
|
373
|
+
} else if (route === 'impact') {
|
|
374
|
+
result = computeImpact(state.tree, { id });
|
|
375
|
+
} else {
|
|
376
|
+
// AC-208-3: strict per-AC coverage, with the CLI's optional
|
|
377
|
+
// scope narrowing. testPointers are the resolved map the
|
|
378
|
+
// walker produced (fail-closed when missing per
|
|
379
|
+
// computeCoverage's doc).
|
|
380
|
+
result = computeCoverage(state.tree, {
|
|
381
|
+
strict: true,
|
|
382
|
+
scopeId: scope,
|
|
383
|
+
testPointers: state.testPointers,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
} catch (err) {
|
|
387
|
+
fail(res, err);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
body = JSON.stringify(result);
|
|
391
|
+
if (cache) cache.set(key, body);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// AC-208-1 / -2 / -3: application/json with cache-control: no-store,
|
|
395
|
+
// computed from the current state and served relative to the mount.
|
|
396
|
+
res.writeHead(200, {
|
|
397
|
+
'content-type': MIME.json,
|
|
398
|
+
'content-length': String(Buffer.byteLength(body)),
|
|
399
|
+
'cache-control': 'no-store',
|
|
400
|
+
});
|
|
401
|
+
res.end(body);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* FBS-207 (TAC-4136 QueryRouteError): the three routes share one JSON
|
|
406
|
+
* error body `{error, id?, direction?, scope?}`, with request echo for
|
|
407
|
+
* only the fields that were present. 404 for unknown-id, 400 for the
|
|
408
|
+
* two usage errors.
|
|
409
|
+
*
|
|
410
|
+
* @param {import('node:http').ServerResponse} res
|
|
411
|
+
* @param {404 | 400} status
|
|
412
|
+
* @param {{error: 'unknown-id' | 'bad-direction' | 'bad-scope', id?: string | null, direction?: string, scope?: string}} payload
|
|
413
|
+
*/
|
|
414
|
+
function sendQueryError(res, status, payload) {
|
|
415
|
+
const body = JSON.stringify(payload);
|
|
416
|
+
res.writeHead(status, {
|
|
417
|
+
'content-type': MIME.json,
|
|
418
|
+
'content-length': String(Buffer.byteLength(body)),
|
|
419
|
+
'cache-control': 'no-store',
|
|
420
|
+
});
|
|
421
|
+
res.end(body);
|
|
422
|
+
}
|
package/src/verify/cli/run.js
CHANGED
|
@@ -12,6 +12,11 @@ import { parseArgs } from 'node:util';
|
|
|
12
12
|
import { formatError, isRcfError } from '#core/errors';
|
|
13
13
|
|
|
14
14
|
import { runVerification } from '../engine/index.js';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_VERIFY_MODE,
|
|
17
|
+
VERIFY_MODES,
|
|
18
|
+
isVerifyMode,
|
|
19
|
+
} from '../engine/deterministic.js';
|
|
15
20
|
import { isSemverString, PLAYWRIGHT_MCP_VERSION } from '../engine/launcher.js';
|
|
16
21
|
import { serialiseReport } from '../report/index.js';
|
|
17
22
|
import { gateTripped, FINDING_SEVERITIES } from '../verdict/index.js';
|
|
@@ -28,6 +33,10 @@ const OPTION_SPEC = {
|
|
|
28
33
|
'provision-mode': { type: 'string' },
|
|
29
34
|
persona: { type: 'string' },
|
|
30
35
|
'playwright-mcp-version': { type: 'string' },
|
|
36
|
+
// Issue 330 (2026-10-08): the verify mode. agentScreenshotCritique is
|
|
37
|
+
// the parent-session default; deterministic is the model-free, in-
|
|
38
|
+
// process mode a dispatched worker can run (never spawns `claude`).
|
|
39
|
+
mode: { type: 'string' },
|
|
31
40
|
help: { type: 'boolean' },
|
|
32
41
|
};
|
|
33
42
|
|
|
@@ -59,6 +68,22 @@ Optional:
|
|
|
59
68
|
fires and the report records the overridden pin
|
|
60
69
|
as runStats.playwrightMcpVersion). Must be an
|
|
61
70
|
exact semver X.Y.Z.
|
|
71
|
+
--mode <mode> Verify mode (default: agentScreenshotCritique):
|
|
72
|
+
agentScreenshotCritique parent-session mode;
|
|
73
|
+
spawns the agent CLI as a subagent with
|
|
74
|
+
Playwright MCP, drives the live app with a
|
|
75
|
+
model, and emits per-AC findings. Only run
|
|
76
|
+
from a parent session that may spawn the
|
|
77
|
+
agent CLI.
|
|
78
|
+
deterministic model-free, in-process mode a
|
|
79
|
+
dispatched worker can run; records a per-AC
|
|
80
|
+
skip ledger as blockedAcs[] with reason
|
|
81
|
+
'critique-only' and stamps
|
|
82
|
+
runStats.counts = { verified, failed,
|
|
83
|
+
skipped, total }. The finalise gate refuses
|
|
84
|
+
promotion on a deterministic run that
|
|
85
|
+
skipped any AC (ADR-4112 verified
|
|
86
|
+
invariant).
|
|
62
87
|
--help Print this help
|
|
63
88
|
|
|
64
89
|
Exit codes:
|
|
@@ -146,6 +171,20 @@ export async function main(argv, deps = {}) {
|
|
|
146
171
|
stderr.write(`Playwright MCP: pinned to @playwright/mcp@${PLAYWRIGHT_MCP_VERSION}\n`);
|
|
147
172
|
}
|
|
148
173
|
|
|
174
|
+
// Issue 330: resolve --mode. Unknown value refuses exit 2 so a typo
|
|
175
|
+
// does not silently fall through to the default agent mode (a
|
|
176
|
+
// dispatched worker that typed --mode=determinstic would otherwise
|
|
177
|
+
// spawn `claude` and be killed by the harness).
|
|
178
|
+
const modeFlag = flags.mode;
|
|
179
|
+
if (modeFlag !== undefined && !isVerifyMode(modeFlag)) {
|
|
180
|
+
stderr.write(`[error] usage --mode must be one of ${VERIFY_MODES.join(' | ')} (got "${modeFlag}")\n`);
|
|
181
|
+
return 2;
|
|
182
|
+
}
|
|
183
|
+
const mode = modeFlag ?? DEFAULT_VERIFY_MODE;
|
|
184
|
+
if (mode === 'deterministic') {
|
|
185
|
+
stderr.write('Verify mode: deterministic (model-free, in-process; a dispatched worker can run this)\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
149
188
|
const result = await runVerification({
|
|
150
189
|
repo: flags.repo,
|
|
151
190
|
chainRef: flags.chain,
|
|
@@ -157,6 +196,7 @@ export async function main(argv, deps = {}) {
|
|
|
157
196
|
persona: flags.persona,
|
|
158
197
|
severityGate: gate,
|
|
159
198
|
playwrightMcpVersion: overrideRaw,
|
|
199
|
+
mode,
|
|
160
200
|
}, deps);
|
|
161
201
|
|
|
162
202
|
if (isRcfError(result)) {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Deterministic verify mode (issue 330, 2026-10-08). A dispatched worker
|
|
2
|
+
// cannot spawn `claude` as a subagent (an estate rule the surrounding
|
|
3
|
+
// harness enforces by killing any such child), so the default
|
|
4
|
+
// `agentScreenshotCritique` mode is a parent-session mode. This module
|
|
5
|
+
// is its sister: a model-free verify path that a dispatched worker can
|
|
6
|
+
// run end-to-end from inside a tool call.
|
|
7
|
+
//
|
|
8
|
+
// Scope today (minimal, honest, additive):
|
|
9
|
+
// - HTTP reachability probe against `--url` recorded on `runStats`
|
|
10
|
+
// as diagnostic context (NOT a per-AC verification);
|
|
11
|
+
// - every AC the chain carries is recorded on `blockedAcs[]` with
|
|
12
|
+
// reason `critique-only` so the aggregate verdict is BLOCKED and
|
|
13
|
+
// the finalise gate refuses promotion by construction (ADR-4112
|
|
14
|
+
// verified invariant). The report also carries
|
|
15
|
+
// `runStats.counts = { verified, failed, skipped, total }` so a
|
|
16
|
+
// report reader sees the mode did not actively verify anything.
|
|
17
|
+
//
|
|
18
|
+
// The catalog-driven per-AC probe seam is a later train car: when the
|
|
19
|
+
// chain's AC subschema gains structured probe fields (status, path,
|
|
20
|
+
// header, body substring, selector) OR a project ships
|
|
21
|
+
// `docs/runtime-verify-catalog.md` keyed by acId, this module will
|
|
22
|
+
// resolve those probes and surface pass/fail/skipped per AC with
|
|
23
|
+
// evidence. The partial fix today keeps that door open: `runStats.mode`
|
|
24
|
+
// and `runStats.counts` are stable across future extensions.
|
|
25
|
+
//
|
|
26
|
+
// Shared-parent-state note: this module uses `globalThis.fetch`
|
|
27
|
+
// (injectable via `deps.fetchImpl`) and never spawns a child
|
|
28
|
+
// process, so it cannot leak the parent's Playwright or MCP session.
|
|
29
|
+
// A DOM-probe extension (Playwright headless, in-process) can land in
|
|
30
|
+
// a later train car; it would use the already-declared optional peer
|
|
31
|
+
// dependency and never the MCP.
|
|
32
|
+
|
|
33
|
+
const DEFAULT_REACHABLE_TIMEOUT_MS = 10000;
|
|
34
|
+
const CRITIQUE_ONLY_REASON = 'critique-only: subjective visual judgement needed; deterministic mode cannot judge this AC (parent-session agentScreenshotCritique mode is required).';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One-shot HTTP probe. Returns status/ok/elapsedMs + a short bodyHead
|
|
38
|
+
* sample so a report reader can see what the deployed URL served, or
|
|
39
|
+
* an `error.kind` on a transport failure. Diagnostic context on
|
|
40
|
+
* `runStats` only, not a verification claim against any AC.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} args
|
|
43
|
+
* @param {string} args.url
|
|
44
|
+
* @param {typeof fetch} [args.fetchImpl]
|
|
45
|
+
* @param {number} [args.timeoutMs]
|
|
46
|
+
* @returns {Promise<{ ok: boolean, status: number|null, elapsedMs: number, bodyHead?: string, error?: { kind: string, message: string } }>}
|
|
47
|
+
*/
|
|
48
|
+
export async function probeHttpReachability({ url, fetchImpl, timeoutMs = DEFAULT_REACHABLE_TIMEOUT_MS }) {
|
|
49
|
+
const fetchFn = fetchImpl ?? globalThis.fetch;
|
|
50
|
+
if (typeof fetchFn !== 'function') {
|
|
51
|
+
return { ok: false, status: null, elapsedMs: 0, error: { kind: 'noFetch', message: 'no fetch implementation available (node >=24 ships globalThis.fetch; pass deps.fetchImpl otherwise)' } };
|
|
52
|
+
}
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
55
|
+
const startedAt = Date.now();
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetchFn(url, { method: 'GET', signal: controller.signal, redirect: 'follow' });
|
|
58
|
+
const elapsedMs = Date.now() - startedAt;
|
|
59
|
+
let bodyHead;
|
|
60
|
+
try {
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
bodyHead = typeof text === 'string' ? text.slice(0, 256) : undefined;
|
|
63
|
+
} catch {
|
|
64
|
+
bodyHead = undefined;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
ok: typeof res.status === 'number' && res.status > 0 && res.status < 500,
|
|
68
|
+
status: typeof res.status === 'number' ? res.status : null,
|
|
69
|
+
elapsedMs,
|
|
70
|
+
...(bodyHead !== undefined ? { bodyHead } : {}),
|
|
71
|
+
};
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const elapsedMs = Date.now() - startedAt;
|
|
74
|
+
const kind = err?.name === 'AbortError' ? 'timeout' : 'network';
|
|
75
|
+
return { ok: false, status: null, elapsedMs, error: { kind, message: err?.message ?? String(err) } };
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run the deterministic verify pass. Pure in-process; no agent CLI,
|
|
83
|
+
* no MCP, no child processes. Returns the per-AC skip ledger and the
|
|
84
|
+
* diagnostic HTTP probe on `runStats`.
|
|
85
|
+
*
|
|
86
|
+
* Honest minimum (no chain-level probe field exists today): every
|
|
87
|
+
* testable AC lands on `blockedAcs[]` as critique-only, so the
|
|
88
|
+
* aggregate verdict is BLOCKED and the finalise gate refuses by
|
|
89
|
+
* construction. The caller (engine) MERGES these entries with the
|
|
90
|
+
* provisioning-side blockedAcs; the two are the same shape.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} args
|
|
93
|
+
* @param {string} args.url
|
|
94
|
+
* @param {Array<object>} args.acs - flattened acceptance criteria from the chain
|
|
95
|
+
* @param {object} [args.deps]
|
|
96
|
+
* @param {typeof fetch} [args.deps.fetchImpl]
|
|
97
|
+
* @returns {Promise<{ findings: object[], blockedAcs: Array<{acId: string, reason: string, mode: string}>, runStats: object }>}
|
|
98
|
+
*/
|
|
99
|
+
export async function runDeterministic({ url, acs = [], deps = {} }) {
|
|
100
|
+
const testable = Array.isArray(acs) ? acs.filter((a) => a && a.testable !== false) : [];
|
|
101
|
+
const startedAt = Date.now();
|
|
102
|
+
const probe = await probeHttpReachability({ url, fetchImpl: deps.fetchImpl });
|
|
103
|
+
probe.url = url;
|
|
104
|
+
const findings = [];
|
|
105
|
+
const blockedAcs = testable.map((ac) => ({
|
|
106
|
+
acId: ac.acId,
|
|
107
|
+
reason: CRITIQUE_ONLY_REASON,
|
|
108
|
+
mode: 'deterministic',
|
|
109
|
+
}));
|
|
110
|
+
const finishedAt = Date.now();
|
|
111
|
+
const counts = {
|
|
112
|
+
total: testable.length,
|
|
113
|
+
verified: 0,
|
|
114
|
+
failed: 0,
|
|
115
|
+
skipped: blockedAcs.length,
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
findings,
|
|
119
|
+
blockedAcs,
|
|
120
|
+
runStats: {
|
|
121
|
+
mode: 'deterministic',
|
|
122
|
+
httpProbe: {
|
|
123
|
+
url,
|
|
124
|
+
ok: probe.ok,
|
|
125
|
+
status: probe.status,
|
|
126
|
+
elapsedMs: probe.elapsedMs,
|
|
127
|
+
...(probe.bodyHead !== undefined ? { bodyHead: probe.bodyHead } : {}),
|
|
128
|
+
...(probe.error !== undefined ? { error: probe.error } : {}),
|
|
129
|
+
},
|
|
130
|
+
counts,
|
|
131
|
+
startedAtMs: startedAt,
|
|
132
|
+
finishedAtMs: finishedAt,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Supported verify modes (public enum for CLI + engine). */
|
|
138
|
+
export const VERIFY_MODES = Object.freeze(['agentScreenshotCritique', 'deterministic']);
|
|
139
|
+
export const DEFAULT_VERIFY_MODE = 'agentScreenshotCritique';
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* True iff value is a known verify mode. The CLI uses this as its
|
|
143
|
+
* sole gate so a new mode added here is accepted everywhere.
|
|
144
|
+
*
|
|
145
|
+
* @param {unknown} value
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
export function isVerifyMode(value) {
|
|
149
|
+
return typeof value === 'string' && VERIFY_MODES.includes(value);
|
|
150
|
+
}
|
|
@@ -19,6 +19,11 @@ import { readChain as defaultReadChain } from '../chain/index.js';
|
|
|
19
19
|
import { runProvisioning, cleanup as defaultCleanup } from '../provision/index.js';
|
|
20
20
|
import { composeBrief } from './brief.js';
|
|
21
21
|
import { PLAYWRIGHT_MCP_VERSION, playwrightMcpConfig, resolveLauncher } from './launcher.js';
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_VERIFY_MODE,
|
|
24
|
+
isVerifyMode,
|
|
25
|
+
runDeterministic,
|
|
26
|
+
} from './deterministic.js';
|
|
22
27
|
import { aggregateVerdict, derivePerAcVerdicts, validateFinding } from '../verdict/index.js';
|
|
23
28
|
import { buildReport } from '../report/index.js';
|
|
24
29
|
|
|
@@ -63,6 +68,10 @@ export async function runVerification(opts = {}, deps = {}) {
|
|
|
63
68
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
64
69
|
const readChain = deps.readChain ?? defaultReadChain;
|
|
65
70
|
const startedAt = now();
|
|
71
|
+
// Issue 330: verify mode selects the launcher below (step 6). Resolved
|
|
72
|
+
// early so every report (NOT-DEPLOYED, LAUNCH-FAILURE, normal) stamps
|
|
73
|
+
// runStats.mode and finalise can key its refusal line off it.
|
|
74
|
+
const mode = isVerifyMode(opts.mode) ? opts.mode : DEFAULT_VERIFY_MODE;
|
|
66
75
|
|
|
67
76
|
// 1. Resolve + validate the runtime declaration (§4).
|
|
68
77
|
const resolved = resolveProfile({ profile: opts.profile, url: opts.url, parityEnv: opts.parityEnv });
|
|
@@ -100,6 +109,7 @@ export async function runVerification(opts = {}, deps = {}) {
|
|
|
100
109
|
verdict: 'NOT-DEPLOYED', verdictAuthority,
|
|
101
110
|
findings: [], blockedAcs: [], provisioning: null,
|
|
102
111
|
perAcVerdicts,
|
|
112
|
+
runStats: { mode },
|
|
103
113
|
});
|
|
104
114
|
return { report };
|
|
105
115
|
}
|
|
@@ -139,11 +149,23 @@ export async function runVerification(opts = {}, deps = {}) {
|
|
|
139
149
|
? playwrightMcpConfig(playwrightMcpVersion)
|
|
140
150
|
: undefined;
|
|
141
151
|
|
|
142
|
-
// 6. Launch the isolated verifier agent (§7.3 isolation env, §9 fresh session)
|
|
152
|
+
// 6. Launch the isolated verifier agent (§7.3 isolation env, §9 fresh session),
|
|
153
|
+
// OR, in deterministic mode (issue 330), run the in-process model-free
|
|
154
|
+
// pass so a dispatched worker can finalise without spawning `claude`.
|
|
155
|
+
// Deterministic mode's blockedAcs merge into the provisioning-side list
|
|
156
|
+
// so the aggregate verdict is BLOCKED by construction while the honest
|
|
157
|
+
// counts (verified/failed/skipped) are stamped on `runStats` for the
|
|
158
|
+
// finalise refusal line (ADR-4112 verified invariant).
|
|
143
159
|
let launchResult;
|
|
144
160
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
161
|
+
if (mode === 'deterministic') {
|
|
162
|
+
const detResult = await runDeterministic({ url, acs: chain.acs, deps });
|
|
163
|
+
launchResult = { findings: detResult.findings, runStats: detResult.runStats };
|
|
164
|
+
for (const b of detResult.blockedAcs ?? []) blockedAcs.push(b);
|
|
165
|
+
} else {
|
|
166
|
+
const launchAgent = await resolveLauncher(deps);
|
|
167
|
+
launchResult = await launchAgent({ brief, url, profile, mcpConfig: mcpConfigForPin });
|
|
168
|
+
}
|
|
147
169
|
} catch (err) {
|
|
148
170
|
// A verifier agent that could not run — or whose output could not be
|
|
149
171
|
// ingested — is NEVER a fabricated PASS (§9). But the report is still
|
|
@@ -159,7 +181,7 @@ export async function runVerification(opts = {}, deps = {}) {
|
|
|
159
181
|
findings: [], blockedAcs, provisioning,
|
|
160
182
|
launchFailure: { message: err.message, rawOutputPath: err.rawOutputPath ?? null },
|
|
161
183
|
perAcVerdicts,
|
|
162
|
-
runStats: { playwrightMcpVersion },
|
|
184
|
+
runStats: { playwrightMcpVersion, mode },
|
|
163
185
|
});
|
|
164
186
|
return { report };
|
|
165
187
|
}
|
|
@@ -190,6 +212,7 @@ export async function runVerification(opts = {}, deps = {}) {
|
|
|
190
212
|
};
|
|
191
213
|
const runStatsForReport = {
|
|
192
214
|
...(launchResult?.runStats ?? {}),
|
|
215
|
+
mode,
|
|
193
216
|
playwrightMcpVersion,
|
|
194
217
|
evalCoverage,
|
|
195
218
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// REQ renderer.
|
|
2
|
-
// the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// REQ renderer. FBS-208 (AC-209-5): the per-REQ Mermaid slice is retired on
|
|
2
|
+
// the user-facing surface; this renderer no longer emits a slice block even
|
|
3
|
+
// when a caller still passes `ctx.subdiagram`. Child user stories are NOT
|
|
4
|
+
// rendered as inline links because the Phase 3.2 layout nests them as
|
|
5
|
+
// `<details>` under the REQ's own `<details>` wrapper.
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
anchorIdFor,
|
|
@@ -17,7 +18,9 @@ import {
|
|
|
17
18
|
* @param {object} ctx
|
|
18
19
|
* @param {string|undefined} ctx.raw
|
|
19
20
|
* @param {import('#core/errors').RcfError[]} [ctx.errors]
|
|
20
|
-
* @param {string|undefined} [ctx.subdiagram]
|
|
21
|
+
* @param {string|undefined} [ctx.subdiagram] - accepted for back-compat;
|
|
22
|
+
* FBS-208 retires rendering of the per-REQ Mermaid slice, so this value
|
|
23
|
+
* is ignored.
|
|
21
24
|
* @param {string|undefined} [ctx.idPrefix] - prefix prepended to every emitted
|
|
22
25
|
* id/anchor so the same REQ can be rendered under multiple Product Map
|
|
23
26
|
* buckets without duplicating ids. Empty when rendered from the
|
|
@@ -32,9 +35,9 @@ export function renderReq(req, ctx) {
|
|
|
32
35
|
const prefix = ctx.idPrefix ?? '';
|
|
33
36
|
const anchor = `${prefix}${anchorIdFor(req.reqId ?? 'REQ')}`;
|
|
34
37
|
const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// FBS-208 (AC-209-5): the Mermaid slice block is not emitted even when
|
|
39
|
+
// `ctx.subdiagram` is passed. The ctx key stays accepted so Product Map
|
|
40
|
+
// callers continue to compile.
|
|
38
41
|
return `
|
|
39
42
|
<article id="${anchor}" class="doc doc-req">
|
|
40
43
|
<h3>${escapeHtml(req.reqId ?? 'REQ')} - ${escapeHtml(req.title ?? '')}</h3>
|
|
@@ -45,7 +48,6 @@ export function renderReq(req, ctx) {
|
|
|
45
48
|
${fieldPara('Priority', req.priority)}
|
|
46
49
|
${fieldPara('Rationale', req.rationale)}
|
|
47
50
|
${fieldList('Tags', req.tags)}
|
|
48
|
-
${subdiagram}
|
|
49
51
|
${ctx.suppressRawJson ? '' : rawJsonDisclosure(ctx.raw, req, req.reqId, prefix)}
|
|
50
52
|
</article>`.trim();
|
|
51
53
|
}
|
package/src/view/html-page.js
CHANGED
|
@@ -46,7 +46,10 @@ import {
|
|
|
46
46
|
} from './doc-renderers/index.js';
|
|
47
47
|
import { detailsWrap, escapeHtml } from './doc-renderers/helpers.js';
|
|
48
48
|
import { computeQueue } from '../build/queue.js';
|
|
49
|
-
|
|
49
|
+
// FBS-208 (AC-209-5): the per-REQ Mermaid slice is retired on the
|
|
50
|
+
// user-facing surfaces. The generator in `./mermaid-diagram.js` is
|
|
51
|
+
// retained while AC-201-1 still binds its unit tests; it is no longer
|
|
52
|
+
// invoked from any renderer here.
|
|
50
53
|
import { renderProductMapPanel } from './product-map.js';
|
|
51
54
|
import { renderReadinessPanel } from './readiness.js';
|
|
52
55
|
import { renderToastContainer } from './components/toast.js';
|
|
@@ -177,7 +180,7 @@ export function renderPage(model, opts) {
|
|
|
177
180
|
* @returns {string}
|
|
178
181
|
*/
|
|
179
182
|
export function renderContent(model) {
|
|
180
|
-
|
|
183
|
+
// FBS-208 (AC-209-5): per-REQ Mermaid slice retired on the surface.
|
|
181
184
|
const needsWorkIds = computeReqNeedsWorkIds(model.readiness);
|
|
182
185
|
|
|
183
186
|
const prdSection = model.prd
|
|
@@ -191,13 +194,14 @@ export function renderContent(model) {
|
|
|
191
194
|
: '<p><em>No PRD on disk.</em></p>';
|
|
192
195
|
|
|
193
196
|
const prdSelector = renderPrdRequirementSelector(model);
|
|
194
|
-
const requirementsPanel = renderRequirementsPanel(model,
|
|
197
|
+
const requirementsPanel = renderRequirementsPanel(model, needsWorkIds);
|
|
195
198
|
const architecturePanel = renderArchitecturePanel(model);
|
|
196
199
|
const buildPanel = renderBuildPanel(model);
|
|
197
200
|
const productMapPanel = renderProductMapPanel(model);
|
|
198
201
|
const readinessPanel = renderReadinessPanel(model.readiness ?? null, {
|
|
199
202
|
profile: model.profileText ?? null,
|
|
200
203
|
freezeRecord: model.freezeRecord ?? null,
|
|
204
|
+
tree: model,
|
|
201
205
|
});
|
|
202
206
|
|
|
203
207
|
const errorBanner = renderErrorBanner(model.errors ?? []);
|
|
@@ -248,10 +252,10 @@ export function renderContent(model) {
|
|
|
248
252
|
* is the one the current router already walks (decision 11, PR 1).
|
|
249
253
|
*
|
|
250
254
|
* @param {import('./tree-model.js').BuiltTreeModel} model
|
|
251
|
-
*
|
|
255
|
+
* (FBS-208: the per-REQ Mermaid slice is retired; this param slot is gone)
|
|
252
256
|
* @param {Set<string>} needsWorkIds - REQ ids still needing PO work
|
|
253
257
|
*/
|
|
254
|
-
function renderRequirementsPanel(model,
|
|
258
|
+
function renderRequirementsPanel(model, needsWorkIds) {
|
|
255
259
|
if (model.requirements.length === 0 && model.userStories.length === 0) {
|
|
256
260
|
return renderEmptyState({
|
|
257
261
|
title: 'No requirements on disk',
|
|
@@ -289,7 +293,7 @@ function renderRequirementsPanel(model, subdiagrams, needsWorkIds) {
|
|
|
289
293
|
showExpandAll: true,
|
|
290
294
|
});
|
|
291
295
|
|
|
292
|
-
const reqRows = model.requirements.map((r) => renderRequirementRow(r, model,
|
|
296
|
+
const reqRows = model.requirements.map((r) => renderRequirementRow(r, model, needsWorkIds)).join('\n');
|
|
293
297
|
|
|
294
298
|
const orphanUs = model.userStories.filter((u) => !u.reqId || !model.requirements.some((r) => r.reqId === u.reqId));
|
|
295
299
|
const orphanBlock = orphanUs.length > 0
|
|
@@ -313,7 +317,7 @@ ${reqRows}
|
|
|
313
317
|
${orphanBlock}`;
|
|
314
318
|
}
|
|
315
319
|
|
|
316
|
-
function renderRequirementRow(req, model,
|
|
320
|
+
function renderRequirementRow(req, model, needsWorkIds) {
|
|
317
321
|
const stories = model.storiesByReqId.get(req.reqId) ?? [];
|
|
318
322
|
const acCount = stories.reduce((n, u) => n + (u.acceptanceCriteria?.length ?? 0), 0);
|
|
319
323
|
const area = normaliseFacet(req.domain);
|
|
@@ -348,10 +352,10 @@ function renderRequirementRow(req, model, subdiagrams, needsWorkIds) {
|
|
|
348
352
|
? `<p class="rcf-req-needswork"><strong>Needs work:</strong> ${escapeHtml(needsWorkReason)}</p>`
|
|
349
353
|
: '';
|
|
350
354
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
+
// FBS-208 (AC-209-5): per-REQ Mermaid slice retired. The chain's own
|
|
356
|
+
// `requirementSubdiagram` generator stays in `./mermaid-diagram.js`
|
|
357
|
+
// for AC-201-1, but no user-facing surface renders it any more.
|
|
358
|
+
const sliceDetails = '';
|
|
355
359
|
|
|
356
360
|
const rawJson = model.rawById.get(req.reqId) ?? JSON.stringify(req, null, 2);
|
|
357
361
|
const rawDetails = `<details class="rcf-req-raw raw-json" data-doc-id="${escapeHtml(req.reqId)}::raw"><summary class="muted small">Raw JSON</summary><pre>${escapeHtml(rawJson)}</pre></details>`;
|
package/src/view/index.js
CHANGED
|
@@ -163,7 +163,13 @@ export async function renderModelToPage({ projectRoot }) {
|
|
|
163
163
|
// shapes:pathsResolve agrees with the CLI without parsing HTML. The
|
|
164
164
|
// viewer HTML is still the operator-facing surface; this field is
|
|
165
165
|
// additive and consumed by test code only.
|
|
166
|
-
|
|
166
|
+
// FBS-207 (TAC-4136, AC-208-6): the walked `tree` and the resolved
|
|
167
|
+
// `testPointers` map are kept on the returned state so the server's
|
|
168
|
+
// query routes (/trace.json, /impact.json, /coverage.json) compute
|
|
169
|
+
// per request from the same tree the page was rendered from, and
|
|
170
|
+
// the server's queryCache invalidates on `state.version`. Additive;
|
|
171
|
+
// nothing else reads these fields on the view-layer path.
|
|
172
|
+
return { fullPageHtml, contentHtml, errors, tree, testPointers, pmPartials, indexJson, readiness };
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
/**
|
package/src/view/live-client.js
CHANGED
|
@@ -439,6 +439,10 @@
|
|
|
439
439
|
host.innerHTML = payload.contentHtml;
|
|
440
440
|
}
|
|
441
441
|
currentVersion = payload.version;
|
|
442
|
+
// FBS-208 (AC-209-4): publish the latest tree version so the
|
|
443
|
+
// Trace matrix wiring can cache per state.version and detect a
|
|
444
|
+
// swap. Previous caches stay for the stale-banner readback.
|
|
445
|
+
try { win.__rcfTreeVersion = payload.version; } catch (e) { /* soft */ }
|
|
442
446
|
// The Product Map panel keeps a client-side partial cache
|
|
443
447
|
// (window.__rcfPmPartialCache is opaque to us). A tree-update
|
|
444
448
|
// invalidates every partial: the next activatePmGroup on a lazy
|