rcf-lite 0.19.0 → 0.20.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 +15 -0
- package/bin/rcf.js +5 -0
- package/fixtures/canary-manifest.json +6 -6
- package/package.json +2 -2
- package/rcf/code-nodes/cn-098.json +13 -0
- package/rcf/code-nodes/cn-099.json +13 -0
- package/rcf/code-nodes/cn-100.json +13 -0
- package/rcf/code-nodes/cn-101.json +13 -0
- package/rcf/code-nodes/cn-102.json +13 -0
- package/rcf/code-nodes/cn-103.json +13 -0
- package/rcf/code-nodes/cn-104.json +13 -0
- package/rcf/code-nodes/cn-105.json +13 -0
- package/rcf/evals/eval-001.json +55 -0
- package/rcf/fbs/fbs-035.json +18 -0
- package/rcf/requirements/req-016.json +37 -0
- package/rcf/test-suites/ts-045.json +46 -0
- package/rcf/user-stories/us-1601.json +37 -0
- package/releases/releases.yaml +10 -1
- package/src/cli/create.js +14 -0
- package/src/cli/eval-coverage.js +221 -0
- package/src/cli/eval.js +43 -0
- package/src/cli/finalise.js +64 -0
- package/src/cli/help.js +4 -0
- package/src/core/store/ids.js +5 -1
- package/src/core/store/init.js +4 -0
- package/src/core/store/loader.js +4 -0
- package/src/core/store/validator.js +8 -1
- package/src/core/store/walker.js +71 -2
- package/src/core/store/writer.js +4 -0
- package/src/eval/judge.js +338 -0
- package/src/finalise/index.js +8 -0
- package/src/finalise/ingest.js +27 -0
- package/src/finalise/ship-without-eval.js +123 -0
- package/src/query/eval-coverage.js +162 -0
- package/src/verify/chain/index.js +67 -0
- package/src/verify/cli/run.js +15 -0
- package/src/verify/engine/index.js +10 -0
- package/src/verify/verdict/index.js +46 -0
|
@@ -142,6 +142,63 @@ function boundTcsFor(testSuites, acId) {
|
|
|
142
142
|
return out;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* rcf-eval-node spec 2026-09-04 sections 3 + 5. Aggregate the EVAL
|
|
147
|
+
* binding state for one AC. Reads:
|
|
148
|
+
* - `evalIds`: every EVAL under the parent US whose acIds[] names
|
|
149
|
+
* this AC.
|
|
150
|
+
* - `evalStatus`: `resolving` | `pending` | `superseded` | `absent`.
|
|
151
|
+
* `resolving` = at least one bound EVAL is not superseded AND has
|
|
152
|
+
* at least one runRecord[] entry whose verdict is not `pending`.
|
|
153
|
+
* - `evalRunVerdict`: the freshest runRecord[] verdict on the
|
|
154
|
+
* resolving EVAL: `pass` | `fail` | `pending` | null.
|
|
155
|
+
*
|
|
156
|
+
* The consumer (verdict derivation) uses these to decide whether
|
|
157
|
+
* EVAL-MISSING or EVAL-BELOW-THRESHOLD applies.
|
|
158
|
+
*
|
|
159
|
+
* @param {object[]} evals - walker's tree.evals[]
|
|
160
|
+
* @param {string} usId
|
|
161
|
+
* @param {string} acId
|
|
162
|
+
* @returns {{ evalIds: string[], evalStatus: 'resolving'|'pending'|'superseded'|'absent', evalRunVerdict: string|null }}
|
|
163
|
+
*/
|
|
164
|
+
function evalBindingFor(evals, usId, acId) {
|
|
165
|
+
const bound = (evals ?? []).filter(
|
|
166
|
+
(e) => e?.usId === usId && Array.isArray(e?.acIds) && e.acIds.includes(acId),
|
|
167
|
+
);
|
|
168
|
+
const evalIds = bound.map((e) => e.id);
|
|
169
|
+
if (bound.length === 0) {
|
|
170
|
+
return { evalIds: [], evalStatus: 'absent', evalRunVerdict: null };
|
|
171
|
+
}
|
|
172
|
+
// Pick the best status: resolving beats pending beats superseded.
|
|
173
|
+
let bestRank = -1;
|
|
174
|
+
let bestStatus = 'absent';
|
|
175
|
+
let bestRunVerdict = null;
|
|
176
|
+
for (const evalDoc of bound) {
|
|
177
|
+
let status;
|
|
178
|
+
let runVerdict = null;
|
|
179
|
+
if (evalDoc.status === 'superseded') {
|
|
180
|
+
status = 'superseded';
|
|
181
|
+
} else {
|
|
182
|
+
const records = Array.isArray(evalDoc.runRecord) ? evalDoc.runRecord : [];
|
|
183
|
+
if (records.length === 0) {
|
|
184
|
+
status = 'pending';
|
|
185
|
+
} else {
|
|
186
|
+
const sorted = [...records].sort((a, b) => (a?.runAt ?? '').localeCompare(b?.runAt ?? ''));
|
|
187
|
+
const latest = sorted[sorted.length - 1];
|
|
188
|
+
runVerdict = latest?.verdict ?? null;
|
|
189
|
+
status = runVerdict === 'pending' ? 'pending' : 'resolving';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const rank = status === 'resolving' ? 3 : status === 'pending' ? 2 : 1;
|
|
193
|
+
if (rank > bestRank) {
|
|
194
|
+
bestRank = rank;
|
|
195
|
+
bestStatus = status;
|
|
196
|
+
bestRunVerdict = runVerdict;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { evalIds, evalStatus: bestStatus, evalRunVerdict: bestRunVerdict };
|
|
200
|
+
}
|
|
201
|
+
|
|
145
202
|
/**
|
|
146
203
|
* Read the acceptance contract from the chain. Returns the flattened list of
|
|
147
204
|
* acceptance criteria (each mapped back to its user story + requirement — the
|
|
@@ -181,6 +238,7 @@ export async function readChain({ repo, chainRef } = {}) {
|
|
|
181
238
|
const resolvedRef = chainRef ?? tree.prd?.prdId ?? 'PRD-UNKNOWN';
|
|
182
239
|
const fbsItems = tree.fbsItems ?? [];
|
|
183
240
|
const testSuites = tree.testSuites ?? [];
|
|
241
|
+
const evals = tree.evals ?? [];
|
|
184
242
|
const acs = [];
|
|
185
243
|
for (const us of tree.userStories ?? []) {
|
|
186
244
|
for (const ac of us.acceptanceCriteria ?? []) {
|
|
@@ -194,6 +252,15 @@ export async function readChain({ repo, chainRef } = {}) {
|
|
|
194
252
|
when: ac.when ?? '',
|
|
195
253
|
then: ac.then ?? '',
|
|
196
254
|
testable: ac.testable !== false,
|
|
255
|
+
// rcf-eval-node spec section 2: `determinism` classification on
|
|
256
|
+
// each AC (schema-optional; absence resolves to 'deterministic').
|
|
257
|
+
// Consumed by the eval-coverage derivation below.
|
|
258
|
+
determinism: ac.determinism ?? 'deterministic',
|
|
259
|
+
// rcf-eval-node spec sections 5.1 / 5.2: per-AC EVAL binding
|
|
260
|
+
// aggregate. `evalStatus` is 'resolving' | 'pending' |
|
|
261
|
+
// 'superseded' | 'absent'; `evalRunVerdict` is the most recent
|
|
262
|
+
// runRecord verdict on the resolving EVAL, or null.
|
|
263
|
+
...evalBindingFor(evals, us.usId, ac.id),
|
|
197
264
|
// 0.7.0 derived fields — verify does the aggregation here per Track A
|
|
198
265
|
// changelog 2026-07-31 and Track B §18 N2 fold; core does not.
|
|
199
266
|
serviceAttestations: serviceAttestationsFor(fbsItems, ac.id),
|
package/src/verify/cli/run.js
CHANGED
|
@@ -176,6 +176,21 @@ export async function main(argv, deps = {}) {
|
|
|
176
176
|
return 1;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// rcf-eval-node spec section 5.3: preflight-style print for EVAL
|
|
180
|
+
// coverage. Runs on the report because the engine has already read
|
|
181
|
+
// the chain; a report re-render carries runStats.evalCoverage so the
|
|
182
|
+
// same line is reconstructable from the report artefact.
|
|
183
|
+
const ec = report?.run?.runStats?.evalCoverage ?? report?.runStats?.evalCoverage ?? null;
|
|
184
|
+
if (ec) {
|
|
185
|
+
if ((ec.nonDeterministic ?? 0) === 0) {
|
|
186
|
+
stderr.write('EVAL coverage: no nonDeterministic ACs on this chain\n');
|
|
187
|
+
} else {
|
|
188
|
+
stderr.write(
|
|
189
|
+
`EVAL coverage: nonDeterministic=${ec.nonDeterministic}, covered=${ec.covered}, missing=${ec.missing}\n`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
179
194
|
stderr.write(`[rcf-verify] verdict ${report.verdict} [${report.verdictAuthority}] -> ${flags.out}\n`);
|
|
180
195
|
|
|
181
196
|
// Exit code is the gate (§8.2). NOT-DEPLOYED / BLOCKED always trip.
|
|
@@ -179,9 +179,19 @@ export async function runVerification(opts = {}, deps = {}) {
|
|
|
179
179
|
// (default or overridden) always lands on the report as
|
|
180
180
|
// runStats.playwrightMcpVersion so a report re-render tells the operator
|
|
181
181
|
// which browser tooling this pass ran against (spec 2026-09-03, Q1).
|
|
182
|
+
// rcf-eval-node spec section 5.3: eval-coverage rollup lands on
|
|
183
|
+
// runStats so a report re-render surfaces the pass state. Counts
|
|
184
|
+
// are read off the chain's per-AC evalStatus derivation.
|
|
185
|
+
const nonDeterministic = chain.acs.filter((a) => a.determinism === 'nonDeterministic');
|
|
186
|
+
const evalCoverage = {
|
|
187
|
+
nonDeterministic: nonDeterministic.length,
|
|
188
|
+
covered: nonDeterministic.filter((a) => a.evalStatus === 'resolving').length,
|
|
189
|
+
missing: nonDeterministic.filter((a) => a.evalStatus !== 'resolving').length,
|
|
190
|
+
};
|
|
182
191
|
const runStatsForReport = {
|
|
183
192
|
...(launchResult?.runStats ?? {}),
|
|
184
193
|
playwrightMcpVersion,
|
|
194
|
+
evalCoverage,
|
|
185
195
|
};
|
|
186
196
|
const report = buildReport({
|
|
187
197
|
profile, url, parityEnv, reachability, chainRef: chain.chainRef, repo: opts.repo,
|
|
@@ -57,12 +57,27 @@ export const VERDICTS = Object.freeze([...FINDING_SEVERITIES, 'NOT-DEPLOYED', 'B
|
|
|
57
57
|
* from the finalise-time profile-vs-AC check fires per FBS rather
|
|
58
58
|
* than only at finalise.
|
|
59
59
|
*/
|
|
60
|
+
/*
|
|
61
|
+
* rcf-eval-node spec 2026-09-04 sections 5.1 + 5.2: two new per-AC
|
|
62
|
+
* verdicts joining the family without adding to the top-level set:
|
|
63
|
+
* - EVAL-MISSING: an AC whose determinism is nonDeterministic and
|
|
64
|
+
* which carries no resolving EVAL in the chain at verify time.
|
|
65
|
+
* Semantically equivalent to BROWSER-VERIFICATION-MISSING on a
|
|
66
|
+
* UI-bearing AC.
|
|
67
|
+
* - EVAL-BELOW-THRESHOLD: an AC whose bound EVAL's most recent run
|
|
68
|
+
* had verdict `fail` (aggregate below threshold or a critical
|
|
69
|
+
* failure). Semantically equivalent to UI-BASELINE-UNMET.
|
|
70
|
+
* Both refuse `rcf finalise` promotion to `verified` unless the
|
|
71
|
+
* operator opts out via `--ship-without-eval "<reason>"`.
|
|
72
|
+
*/
|
|
60
73
|
export const PER_AC_VERDICTS = Object.freeze([
|
|
61
74
|
'MOCK-ONLY-DECLARED',
|
|
62
75
|
'BLOCKED-BY-DECLARATION',
|
|
63
76
|
'UI-BASELINE-UNMET',
|
|
64
77
|
'BROWSER-VERIFICATION-MISSING',
|
|
65
78
|
'SCOPE-MISMATCH',
|
|
79
|
+
'EVAL-MISSING',
|
|
80
|
+
'EVAL-BELOW-THRESHOLD',
|
|
66
81
|
]);
|
|
67
82
|
|
|
68
83
|
/**
|
|
@@ -308,6 +323,37 @@ export function derivePerAcVerdicts({ acs = [], browserVerification = [] } = {})
|
|
|
308
323
|
if (ui) out.push({ acId: ac.acId, verdict: ui.verdict, reason: ui.reason });
|
|
309
324
|
const scopeMismatch = scopePerAcVerdict(ac);
|
|
310
325
|
if (scopeMismatch) out.push({ acId: ac.acId, verdict: scopeMismatch.verdict, reason: scopeMismatch.reason });
|
|
326
|
+
// rcf-eval-node spec sections 5.1 + 5.4: EVAL-MISSING and
|
|
327
|
+
// EVAL-BELOW-THRESHOLD fire only for nonDeterministic ACs. A
|
|
328
|
+
// deterministic AC ships when TS/TC coverage is honest (unchanged);
|
|
329
|
+
// a nonDeterministic AC additionally requires a resolving EVAL with
|
|
330
|
+
// a passing latest run.
|
|
331
|
+
const evalVerdict = evalPerAcVerdict(ac);
|
|
332
|
+
if (evalVerdict) out.push({ acId: ac.acId, verdict: evalVerdict.verdict, reason: evalVerdict.reason });
|
|
311
333
|
}
|
|
312
334
|
return out;
|
|
313
335
|
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* rcf-eval-node spec sections 5.1 + 5.4. Resolve the per-AC EVAL
|
|
339
|
+
* verdict for one AC. Deterministic ACs never trigger this verdict.
|
|
340
|
+
*
|
|
341
|
+
* @param {object} ac - a flattened AC with `determinism`, `evalStatus`, `evalRunVerdict`
|
|
342
|
+
* @returns {{ verdict: 'EVAL-MISSING'|'EVAL-BELOW-THRESHOLD', reason: string } | null}
|
|
343
|
+
*/
|
|
344
|
+
export function evalPerAcVerdict(ac) {
|
|
345
|
+
if (!ac || ac.determinism !== 'nonDeterministic') return null;
|
|
346
|
+
if (ac.evalStatus !== 'resolving') {
|
|
347
|
+
return {
|
|
348
|
+
verdict: 'EVAL-MISSING',
|
|
349
|
+
reason: `AC ${ac.acId} is nonDeterministic and carries no resolving EVAL (status=${ac.evalStatus ?? 'absent'}); author an EVAL or reclassify the AC as deterministic.`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (ac.evalRunVerdict === 'fail') {
|
|
353
|
+
return {
|
|
354
|
+
verdict: 'EVAL-BELOW-THRESHOLD',
|
|
355
|
+
reason: `AC ${ac.acId} is nonDeterministic and its bound EVAL's most recent run failed (aggregate below threshold or a critical criterion failed); investigate the runRecord or ship with --ship-without-eval.`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|