rcf-lite 0.7.1 → 0.8.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.
@@ -0,0 +1,146 @@
1
+ {
2
+ "$schemaComment": "rcf-lite shared standards ruleset v1. Machine-readable contract consumed by build-lite's chain-admissibility lint, verify's per-AC scope check, and (from the umbrella release that adds it) rcf-define-lite's elicitation checklist. camelCase per estate convention. The rulesetVersion is stamped at load time from the umbrella package.json (see src/ruleset/index.js) per NV-BL-SR-02: there is no separate ruleset semver, its version is the rcf-lite umbrella version.",
3
+ "id": "rcf-lite-ruleset",
4
+ "provenance": {
5
+ "requirementsDoc": "projects/rcf-build-lite/docs/2026-08-06_build-lite-nextver-requirements.md",
6
+ "rulingSheet": "projects/rcf-build-lite/docs/2026-08-06_nextver-ruling-sheet.md",
7
+ "designWorkItem": "w-2026-07-28-012",
8
+ "ratifiedItems": [1, 2, 3, 4, 6, 7, 8, 11, 14, 15, 16, 17, 19]
9
+ },
10
+ "admissibilityRules": [
11
+ {
12
+ "id": "NV-BL-ADM-01",
13
+ "title": "Every functional REQ terminates in at least one AC observable as behaviour of the running product",
14
+ "requiresScopeTagVocabulary": true,
15
+ "refuseByDefault": true,
16
+ "overrideChannel": "recordedInChain",
17
+ "notes": "For every REQ classified functional, the chain must contain at least one AC that is observable in the running product, not only in a library or a fixture. AC-shape classifier tags govern which REQ-shapes must terminate in a runtime- or deployed-scope AC."
18
+ },
19
+ {
20
+ "id": "NV-BL-ADM-02",
21
+ "title": "AC-shape classifier assigns each AC a scope tag (library / runtime / deployed)",
22
+ "requiresScopeTagVocabulary": true,
23
+ "refuseByDefault": true,
24
+ "overrideChannel": "migrationStateUnclassified",
25
+ "notes": "The scope-tag vocabulary and the AC schema field that carries the tag live in rcf-schemas. See scopeTagVocabulary below."
26
+ },
27
+ {
28
+ "id": "NV-BL-ADM-03",
29
+ "title": "Bound TCs must exercise their AC's declared scope",
30
+ "requiresScopeTagVocabulary": true,
31
+ "refuseByDefault": true,
32
+ "overrideChannel": "migrationStateUnclassified",
33
+ "notes": "For each AC, the bound TC's scope must be equal to or wider than the AC's scope tag. Bootstrap window: chains authored before scope tags land carry scope 'unclassified' and are tolerated for one release cycle."
34
+ },
35
+ {
36
+ "id": "NV-BL-ADM-04",
37
+ "title": "Source-comment honesty gate",
38
+ "refuseByDefault": true,
39
+ "overrideChannel": "adrExternalBoundaryOnly",
40
+ "markerVocabularyRef": "sourceCommentMarkers",
41
+ "notes": "Source files bound to any FBS at status complete are scanned for admission markers. Any match refuses FBS completion. Per ratified ruling-sheet item 16, the only permitted override channel is an ADR recording the deferral for a genuine external-boundary blocker; the ADR names the marker text, the blocker, and the reason. NV-BL-ADM-05's generic recorded-override channel no longer covers source markers."
42
+ },
43
+ {
44
+ "id": "NV-BL-ADM-05",
45
+ "title": "Chain-admissibility gate is refuse-first, override-recorded",
46
+ "refuseByDefault": true,
47
+ "overrideChannel": "recordedInChain",
48
+ "notes": "A chain failing any admissibility check is refused before build starts. Explicit overrides are permitted for non-source-marker checks, recorded in the chain with rule id, reason, and authoring identity, and surfaced in the finalise summary. Source-comment markers (NV-BL-ADM-04) fall outside this generic override channel."
49
+ },
50
+ {
51
+ "id": "NV-BL-ADM-06",
52
+ "title": "Build-stage refusal on ruleset-version drift",
53
+ "refuseByDefault": true,
54
+ "overrideChannel": "recordedInChain",
55
+ "notes": "At build start, build-lite compares the ruleset version the chain declares it was authored against with the version the shipping lint enforces. Behaviour-changing drift refuses; additive-only drift warns. Ratified 2026-08-11 ruling-sheet item 7: define-stage drift is a warning-with-acknowledgement; build-stage drift is a refusal."
56
+ }
57
+ ],
58
+ "gateRules": [
59
+ {
60
+ "id": "NV-BL-GATE-01",
61
+ "title": "Pull rcf-verify profile-vs-AC check into the REVIEW stage",
62
+ "notes": "The profile-vs-AC scope-mismatch check runs at REVIEW time for every FBS, not only at finalise. Verify carries the check; the review-stage gate consumes the same helper."
63
+ },
64
+ {
65
+ "id": "NV-BL-GATE-02",
66
+ "title": "Deploy-smoke FBS is a chain-mandatory element for any REQ with deployed-scope ACs",
67
+ "requiresScopeTagVocabulary": true,
68
+ "notes": "Any chain whose REQ set contains at least one deployed-scope AC must contain at least one FBS whose build sequence boots the built artefact against a real config and asserts operator-visible behaviour."
69
+ },
70
+ {
71
+ "id": "NV-BL-GATE-03",
72
+ "title": "Boot-integration TC template family for runtime-scope ACs",
73
+ "templateFamilyRef": "tcTemplateFamily",
74
+ "notes": "Runtime-scope ACs whose bound TC is not a boot-integration TC or a wider-scope TC fail admissibility (via NV-BL-ADM-03) and REVIEW (via NV-BL-GATE-01)."
75
+ },
76
+ {
77
+ "id": "NV-BL-GATE-04",
78
+ "title": "Ruling-set consistency light-check at ruling-application time",
79
+ "checkFamilyRef": "rulingConsistencyChecks",
80
+ "notes": "Light mechanical checks only per ratified ruling-sheet item 4: no two rulings name the same external resource with contradictory constraints; no ruling implies a capability the ruled-in service does not have on the ruled-in tier. Probabilistic and suitability work belongs in rcf-define-lite under DL-REQ-VALIDATE-04."
81
+ }
82
+ ],
83
+ "scopeTagVocabulary": {
84
+ "sourcePackage": "@stravica-ai/rcf-schemas",
85
+ "schemaRef": "https://schemas.stravica.io/rcf/v0.4.0/common.schema.json#/$defs/scopeTag",
86
+ "values": [
87
+ { "value": "library", "description": "Observable in an isolated library or fixture." },
88
+ { "value": "runtime", "description": "Observable in the running product against local integration." },
89
+ { "value": "deployed", "description": "Observable on a booted artefact against real config and real network." },
90
+ { "value": "unclassified", "description": "Migration state for chains authored before the scope-tag vocabulary shipped; ruleset-enforcing consumers decide whether unclassified is currently accepted." }
91
+ ],
92
+ "unclassifiedMigrationTolerance": {
93
+ "toleratedFor": "one release cycle from the umbrella release that first ships this ruleset",
94
+ "expiresWhen": "rcf-define-lite ships and can take over elicitation"
95
+ }
96
+ },
97
+ "sourceCommentMarkers": [
98
+ { "marker": "TODO", "caseInsensitive": true },
99
+ { "marker": "FIXME", "caseInsensitive": true },
100
+ { "marker": "XXX", "caseInsensitive": true },
101
+ { "marker": "HACK", "caseInsensitive": true },
102
+ { "marker": "placeholder", "caseInsensitive": true },
103
+ { "marker": "v1 refinement", "caseInsensitive": true },
104
+ { "marker": "deferred", "caseInsensitive": true },
105
+ { "marker": "stub", "caseInsensitive": true }
106
+ ],
107
+ "tcTemplateFamily": [
108
+ {
109
+ "id": "TCT-SERVER-BOOT",
110
+ "title": "Server-boot integration TC",
111
+ "surface": "server",
112
+ "notes": "Boots the built server against a real config and asserts an operator-visible observable (probe fires, page returns 200 with expected content, and so on)."
113
+ },
114
+ {
115
+ "id": "TCT-CLI-INVOKE",
116
+ "title": "CLI-invoke integration TC",
117
+ "surface": "cli",
118
+ "notes": "Invokes the packaged CLI as an installed artefact (not via the workspace source) against a real config and asserts an operator-visible observable."
119
+ },
120
+ {
121
+ "id": "TCT-CONTAINER-RUN",
122
+ "title": "Container-run integration TC",
123
+ "surface": "container",
124
+ "notes": "Runs the built container against a real config and asserts an operator-visible observable."
125
+ }
126
+ ],
127
+ "rulingConsistencyChecks": [
128
+ {
129
+ "id": "RCC-EXTERNAL-RESOURCE-CONTRADICTION",
130
+ "title": "No two rulings name the same external resource with contradictory constraints",
131
+ "kind": "lightMechanical",
132
+ "notes": "Detects the Resend-collision class: Ruling A names service.tier=free and Ruling B names service.domainCount=N with N > tier-limit(free)."
133
+ },
134
+ {
135
+ "id": "RCC-TIER-CAPABILITY-MISMATCH",
136
+ "title": "No ruling implies a capability the ruled-in service does not have on the ruled-in tier",
137
+ "kind": "lightMechanical",
138
+ "notes": "Detects the ruling-in of a capability against a tier that cannot deliver it. Deep suitability checks live in rcf-define-lite under DL-REQ-VALIDATE-04."
139
+ }
140
+ ],
141
+ "toolScope": {
142
+ "chainAdmissibility": true,
143
+ "traceabilityAndQueryTools": true,
144
+ "notes": "Per ratified ruling-sheet item 1 addendum: the same rule set governs what a traceability or query tool may return as much as it governs what build may consume. A query tool that hides an admissibility failure is the same class of defect as a build that hides one."
145
+ }
146
+ }
@@ -119,6 +119,29 @@ function fbsIdsFor(fbsItems, acId) {
119
119
  return out;
120
120
  }
121
121
 
122
+ /**
123
+ * The bound TCs for `acId`, each carrying its own scope tag (if the TC
124
+ * declares one; unspecified surfaces as `undefined`). 0.8.0 slug-train
125
+ * car 4 (NV-BL-GATE-01: pull rcf-verify profile-vs-AC check into the
126
+ * REVIEW stage). Verify uses this to detect SCOPE-MISMATCH per-AC
127
+ * verdicts: a bound TC whose scope is NARROWER than the AC's scope tag
128
+ * cannot legitimately cover that AC (per NV-BL-ADM-03).
129
+ *
130
+ * @param {object[]} testSuites
131
+ * @param {string} acId
132
+ * @returns {Array<{ tsId: string, tcId: string, scope: string|undefined }>}
133
+ */
134
+ function boundTcsFor(testSuites, acId) {
135
+ const out = [];
136
+ for (const ts of testSuites ?? []) {
137
+ for (const tc of ts.testCases ?? []) {
138
+ if (tc?.acId !== acId) continue;
139
+ out.push({ tsId: ts.id, tcId: tc.id, scope: tc.scope });
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+
122
145
  /**
123
146
  * Read the acceptance contract from the chain. Returns the flattened list of
124
147
  * acceptance criteria (each mapped back to its user story + requirement — the
@@ -157,6 +180,7 @@ export async function readChain({ repo, chainRef } = {}) {
157
180
 
158
181
  const resolvedRef = chainRef ?? tree.prd?.prdId ?? 'PRD-UNKNOWN';
159
182
  const fbsItems = tree.fbsItems ?? [];
183
+ const testSuites = tree.testSuites ?? [];
160
184
  const acs = [];
161
185
  for (const us of tree.userStories ?? []) {
162
186
  for (const ac of us.acceptanceCriteria ?? []) {
@@ -175,6 +199,13 @@ export async function readChain({ repo, chainRef } = {}) {
175
199
  serviceAttestations: serviceAttestationsFor(fbsItems, ac.id),
176
200
  fbsUiBearing: fbsUiBearingFor(fbsItems, ac.id),
177
201
  fbsIds: fbsIdsFor(fbsItems, ac.id),
202
+ // 0.8.0 slug-train car 4 (NV-BL-GATE-01, NV-BL-ADM-03): scope
203
+ // tags read straight off rcf-schemas 0.4.3's AC/TC subschemas;
204
+ // the verdict layer runs the scope check per-AC in REVIEW,
205
+ // matching what NV-BL-GATE-01 pulls in from the finalise-time
206
+ // profile check.
207
+ scope: ac.scope,
208
+ boundTcs: boundTcsFor(testSuites, ac.id),
178
209
  });
179
210
  }
180
211
  }
@@ -50,12 +50,19 @@ export const VERDICTS = Object.freeze([...FINDING_SEVERITIES, 'NOT-DEPLOYED', 'B
50
50
  * `report.perAcVerdicts[]`. Consumed by `rcf finalise` to refuse promotion
51
51
  * to `verified` on any of these AC-level verdicts (see
52
52
  * `packages/rcf-lite/src/finalise/ingest.js:findMockOnlyDeclaredAcs`).
53
+ *
54
+ * 0.8.0 slug-train car 4 addition (NV-BL-GATE-01 + NV-BL-ADM-03):
55
+ * - SCOPE-MISMATCH: an AC whose declared scope tag exceeds the scope
56
+ * of every bound TC. Verify emits this at REVIEW time so the pull-in
57
+ * from the finalise-time profile-vs-AC check fires per FBS rather
58
+ * than only at finalise.
53
59
  */
54
60
  export const PER_AC_VERDICTS = Object.freeze([
55
61
  'MOCK-ONLY-DECLARED',
56
62
  'BLOCKED-BY-DECLARATION',
57
63
  'UI-BASELINE-UNMET',
58
64
  'BROWSER-VERIFICATION-MISSING',
65
+ 'SCOPE-MISMATCH',
59
66
  ]);
60
67
 
61
68
  /**
@@ -220,6 +227,61 @@ export function uiPerAcVerdict(ac, browserVerification = []) {
220
227
  return null;
221
228
  }
222
229
 
230
+ /**
231
+ * Rank each scope tag so we can compare "TC scope >= AC scope"
232
+ * numerically. Mirrors `packages/rcf-lite/src/admissibility/scope-lint.js`
233
+ * (single source of truth on rcf-schemas 0.4.3's scopeTag values;
234
+ * this copy stays deliberately local so the verdict layer does not
235
+ * depend on the admissibility module -- both consume the same schemas
236
+ * vocabulary and the same values, and duplicating the ranking here
237
+ * keeps the verdict layer standalone).
238
+ */
239
+ const SCOPE_RANK = Object.freeze({
240
+ library: 1,
241
+ runtime: 2,
242
+ deployed: 3,
243
+ });
244
+
245
+ /**
246
+ * 0.8.0 slug-train car 4 (NV-BL-GATE-01 + NV-BL-ADM-03): the profile-vs-AC
247
+ * scope-mismatch check, pulled from finalise into REVIEW so it fires
248
+ * per FBS. An AC declared runtime-scope bound only to library-scope TCs
249
+ * emits SCOPE-MISMATCH; a wider TC (deployed covering runtime) is fine;
250
+ * an AC with no scope declaration is silent here (NV-BL-ADM-02 catches
251
+ * it at admissibility); a bound TC with no declared scope is silent
252
+ * here (bootstrap window; NV-BL-ADM-03 in the admissibility lint
253
+ * catches it there).
254
+ *
255
+ * @param {object} ac - flattened AC with `scope` and `boundTcs`
256
+ * @returns {{ verdict: 'SCOPE-MISMATCH', reason: string } | null}
257
+ */
258
+ export function scopePerAcVerdict(ac) {
259
+ if (!ac || typeof ac.scope !== 'string') return null;
260
+ if (!Object.prototype.hasOwnProperty.call(SCOPE_RANK, ac.scope)) return null;
261
+ const boundTcs = Array.isArray(ac.boundTcs) ? ac.boundTcs : [];
262
+ if (boundTcs.length === 0) return null; // coverage rule handles "no TC at all".
263
+ const acRank = SCOPE_RANK[ac.scope];
264
+ const narrower = [];
265
+ let anyAtLeastAsWide = false;
266
+ for (const tc of boundTcs) {
267
+ if (typeof tc?.scope !== 'string') continue;
268
+ if (!Object.prototype.hasOwnProperty.call(SCOPE_RANK, tc.scope)) continue;
269
+ const tcRank = SCOPE_RANK[tc.scope];
270
+ if (tcRank >= acRank) {
271
+ anyAtLeastAsWide = true;
272
+ break;
273
+ }
274
+ narrower.push({ tsId: tc.tsId, tcId: tc.tcId, scope: tc.scope });
275
+ }
276
+ if (anyAtLeastAsWide) return null;
277
+ if (narrower.length === 0) return null;
278
+ const detail = narrower.map((n) => `${n.tcId} on ${n.tsId} (scope=${n.scope})`).join(', ');
279
+ return {
280
+ verdict: 'SCOPE-MISMATCH',
281
+ reason: `AC ${ac.acId} is scope=${ac.scope} but every bound TC is narrower: ${detail}. NV-BL-GATE-01 pulls this check into REVIEW; NV-BL-ADM-03 refuses at admissibility.`,
282
+ };
283
+ }
284
+
223
285
  /**
224
286
  * Emit per-AC verdicts across the whole chain. Combines Track A's service
225
287
  * attestation verdicts with Track B's UI-baseline verdicts. An AC can carry
@@ -229,6 +291,9 @@ export function uiPerAcVerdict(ac, browserVerification = []) {
229
291
  * `packages/rcf-lite/src/finalise/ingest.js:findMockOnlyDeclaredAcs`, which
230
292
  * filters on verdict class and does not deduplicate by acId.
231
293
  *
294
+ * 0.8.0 slug-train car 4: SCOPE-MISMATCH runs here too, alongside the
295
+ * attestation and UI verdicts.
296
+ *
232
297
  * @param {object} opts
233
298
  * @param {Array<object>} opts.acs - flattened ACs from `readChain`
234
299
  * @param {object[]} [opts.browserVerification] - manifest.browserVerification[]
@@ -241,6 +306,8 @@ export function derivePerAcVerdicts({ acs = [], browserVerification = [] } = {})
241
306
  if (attest) out.push({ acId: ac.acId, verdict: attest.verdict, reason: attest.reason });
242
307
  const ui = uiPerAcVerdict(ac, browserVerification);
243
308
  if (ui) out.push({ acId: ac.acId, verdict: ui.verdict, reason: ui.reason });
309
+ const scopeMismatch = scopePerAcVerdict(ac);
310
+ if (scopeMismatch) out.push({ acId: ac.acId, verdict: scopeMismatch.verdict, reason: scopeMismatch.reason });
244
311
  }
245
312
  return out;
246
313
  }