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.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bin/rcf.js +5 -0
  3. package/fixtures/canary-manifest.json +6 -6
  4. package/package.json +2 -2
  5. package/rcf/code-nodes/cn-098.json +13 -0
  6. package/rcf/code-nodes/cn-099.json +13 -0
  7. package/rcf/code-nodes/cn-100.json +13 -0
  8. package/rcf/code-nodes/cn-101.json +13 -0
  9. package/rcf/code-nodes/cn-102.json +13 -0
  10. package/rcf/code-nodes/cn-103.json +13 -0
  11. package/rcf/code-nodes/cn-104.json +13 -0
  12. package/rcf/code-nodes/cn-105.json +13 -0
  13. package/rcf/evals/eval-001.json +55 -0
  14. package/rcf/fbs/fbs-035.json +18 -0
  15. package/rcf/requirements/req-016.json +37 -0
  16. package/rcf/test-suites/ts-045.json +46 -0
  17. package/rcf/user-stories/us-1601.json +37 -0
  18. package/releases/releases.yaml +10 -1
  19. package/src/cli/create.js +14 -0
  20. package/src/cli/eval-coverage.js +221 -0
  21. package/src/cli/eval.js +43 -0
  22. package/src/cli/finalise.js +64 -0
  23. package/src/cli/help.js +4 -0
  24. package/src/core/store/ids.js +5 -1
  25. package/src/core/store/init.js +4 -0
  26. package/src/core/store/loader.js +4 -0
  27. package/src/core/store/validator.js +8 -1
  28. package/src/core/store/walker.js +71 -2
  29. package/src/core/store/writer.js +4 -0
  30. package/src/eval/judge.js +338 -0
  31. package/src/finalise/index.js +8 -0
  32. package/src/finalise/ingest.js +27 -0
  33. package/src/finalise/ship-without-eval.js +123 -0
  34. package/src/query/eval-coverage.js +162 -0
  35. package/src/verify/chain/index.js +67 -0
  36. package/src/verify/cli/run.js +15 -0
  37. package/src/verify/engine/index.js +10 -0
  38. 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),
@@ -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
+ }