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.
- package/CHANGELOG.md +56 -0
- package/fixtures/canary-manifest.json +6 -6
- package/package.json +5 -3
- package/src/admissibility/enforce.js +142 -0
- package/src/admissibility/index.js +8 -0
- package/src/admissibility/markers.js +104 -0
- package/src/admissibility/scope-lint.js +163 -0
- package/src/cli/create.js +4 -1
- package/src/cli/read.js +7 -1
- package/src/cli/test-suite.js +7 -2
- package/src/core/store/loader.js +4 -1
- package/src/core/store/walker.js +49 -3
- package/src/core/store/writer.js +41 -11
- package/src/finalise/detect.js +51 -29
- package/src/finalise/index.js +16 -2
- package/src/finalise/ingest.js +41 -0
- package/src/mcp/tools.js +10 -2
- package/src/query/index.js +4 -0
- package/src/query/refuse-on-admissibility.js +73 -0
- package/src/ruleset/index.js +140 -0
- package/src/ruleset/ruleset.json +146 -0
- package/src/verify/chain/index.js +31 -0
- package/src/verify/verdict/index.js +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,62 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Pre-1.0, breaking changes are signalled by a minor version bump.
|
|
6
6
|
|
|
7
|
+
## [0.8.0] - 2026-08-12 (on `slug-train/0.8.0`, PR pending)
|
|
8
|
+
|
|
9
|
+
### Added (Car 4, verify updates per requirements doc)
|
|
10
|
+
|
|
11
|
+
- **`src/verify/chain/index.js` (`readChain`) surfaces AC `scope` and per-TC `boundTcs`.** Every flattened AC now carries the schema-declared scope tag (or `undefined` for bootstrap-era ACs) plus a `boundTcs[]` list of `{ tsId, tcId, scope }` for every bound TC. Verify's downstream verdict layer runs the scope check off this shape, matching the shape the admissibility lint's `scanTcScopeVsAc` consumes at build stage.
|
|
12
|
+
- **`src/verify/verdict/index.js` (`scopePerAcVerdict`, extended `derivePerAcVerdicts`).** New `SCOPE-MISMATCH` per-AC verdict (NV-BL-GATE-01 + NV-BL-ADM-03). Fires when an AC declares a scope tag AND every bound TC's scope is narrower than the AC's scope AND at least one bound TC declares a scope tag (bootstrap-era TCs without a tag are silent -- the admissibility lint handles that class). At least one TC at or wider than the AC scope clears the mismatch (a wider TC is always admissible). Emitted alongside the four 0.7.0 per-AC verdict classes on `report.perAcVerdicts[]`. Existing report validator (`src/verify/report/index.js`) admits the new class via the widened `PER_AC_VERDICTS` enum.
|
|
13
|
+
- **`src/finalise/ingest.js` (`findScopeMismatchAcs`, `reportHasScopeMismatch`; extended `summariseReport`).** NV-BL-GATE-01 REVIEW-stage consumers (`rcf review <fbs-id>`, `rcf finalise`) read the same shape via new predicates. `summariseReport` renders a dedicated `scope mismatches (N)` section when the report carries any SCOPE-MISMATCH entries (silent when none, so pre-0.8.0 reports remain byte-identical in the summary output).
|
|
14
|
+
- **`src/finalise/index.js` re-exports** `findScopeMismatchAcs` and `reportHasScopeMismatch` alongside the pre-existing mock-only-declared predicates.
|
|
15
|
+
- **`test/verify/chain/scope-derivation-0-8-0.test.js` (2 tests):** chain reader hands through AC.scope + per-TC scope on boundTcs; leaves AC.scope undefined when the AC has no tag.
|
|
16
|
+
- **`test/verify/verdict/scope-mismatch-0-8-0.test.js` (7 tests):** narrower TC surfaces SCOPE-MISMATCH; wider or equal TC clears; deployed TC covering runtime AC is fine; AC without scope is silent; TC without scope is silent; AC with no bound TCs is silent; `derivePerAcVerdicts` emits SCOPE-MISMATCH alongside MOCK-ONLY-DECLARED on the same AC when both apply.
|
|
17
|
+
- **`test/finalise/scope-mismatch-0-8-0.test.js` (5 tests):** `findScopeMismatchAcs` extracts SCOPE-MISMATCH only; graceful with absent perAcVerdicts; `reportHasScopeMismatch` truthiness; `summariseReport` renders a dedicated section; silent when no mismatches present.
|
|
18
|
+
- **`test/verify/verdict/per-ac-verdicts-0-7-0.test.js` PER_AC_VERDICTS assertion updated** to include SCOPE-MISMATCH (0.8.0 slug-train car 4).
|
|
19
|
+
|
|
20
|
+
### Version bump
|
|
21
|
+
|
|
22
|
+
- **`package.json` version 0.7.1 -> 0.8.0.** Consumes `@stravica-ai/rcf-schemas@0.4.3` (exact pin per the umbrella exact-pin doctrine ratified alongside item 18; the 0.4.3 diff is additive-only and preserved by the existing back-compat fixtures on the schemas side). The `rcf-lite@0.8.0` tag + publish is out of this train's scope and Dave will cut it post-merge.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
The `0.8.0` admissibility-and-ruleset release, cut against the ratified next-version plan (`projects/rcf-build-lite/docs/2026-08-06_next-version-plan-amendment.md`) and requirements (`projects/rcf-build-lite/docs/2026-08-06_build-lite-nextver-requirements.md`). Ships the shared standards ruleset bundled inside this umbrella package, wires build-lite's fitness tracks around it (admissibility lint refuse-by-default, build-stage drift refusal, gate tightenings), and pulls verify's per-AC scope check into REVIEW. Car 1 of the slug-train, `@stravica-ai/rcf-schemas@0.4.3`, published to npm on 2026-08-12 and is EXACT-PINNED here (no caret, no range) per the umbrella exact-pin doctrine ratified alongside item 18. The 0.4.3 diff is additive-only: widened id patterns to accept an optional kebab-case slug tail on FBS / CN / ADR / TAC ids, widened TS/TC to drop the 999 cap, added the shared `scopeTag` vocabulary in `common.schema.json`, and added optional `scope` fields on AC and TC. Every existing chain continues to validate; no retro-slugging is performed.
|
|
27
|
+
|
|
28
|
+
### Added (Car 3, admissibility lint + drift refusal + refuse-by-default in query)
|
|
29
|
+
|
|
30
|
+
- **`src/admissibility/` module (`#admissibility`).** Chain-admissibility gate wiring per NV-BL-ADM-02, -03, -04, -05, -06. Refuse-first per ratified ruling-sheet item 1.
|
|
31
|
+
- `scanAcScopeCoverage(tree, { tolerateUnclassified })` (NV-BL-ADM-02): every AC must carry a known scope tag from the shared vocabulary. `unclassified` is tolerated during the migration window (default; `tolerateUnclassified: false` fails those too after the window closes).
|
|
32
|
+
- `scanTcScopeVsAc(tree, opts)` (NV-BL-ADM-03): every bound TC's scope must be equal to or wider than the covered AC's scope. `library` < `runtime` < `deployed`; a wider TC on a narrower AC is admissible (opposite trips a finding).
|
|
33
|
+
- `scanSourceStringForMarkers(source, { filePath, markers })` and `scanFilesForMarkers(filePaths)` (NV-BL-ADM-04): scan source for the ratified marker vocabulary (`TODO`, `FIXME`, `XXX`, `HACK`, `placeholder`, `v1 refinement`, `deferred`, `stub`; case-insensitive). Line + column offsets on each finding. Vocabulary is read from the ruleset at call time so a future ruleset revision picks up without recompiling this module.
|
|
34
|
+
- `enforceAdmissibility({ tree, chainRulesetVersion, overrides, opts })` (NV-BL-ADM-05 + -06): the top-level refuse-first gate. Combines the scope scans with `detectRulesetDrift`; applies recorded overrides per rule id + optional documentId; emits `{ verdict: 'pass' | 'refuse' | 'passWithOverrides', findings, unresolved, appliedOverrides, drift }`. Guardrail: NV-BL-ADM-04 findings can NEVER be masked through the generic override channel (ruling-sheet item 16 narrowed override to ADR-only for source markers).
|
|
35
|
+
- `getRulesetToolScope()`: exposes the ruleset's `toolScope` block so consumers decide whether to apply the traceability-and-query-tools refusal per NV-BL-SR-03 addendum.
|
|
36
|
+
- **`src/query/refuse-on-admissibility.js` (`runWithAdmissibilityGate`).** NV-BL-SR-03 addendum wiring (ruling-sheet item 1): traceability and query tools share the refuse-first posture. Wraps a query producer such that a REFUSE verdict short-circuits the query and returns a refusal envelope naming the unresolved rule ids; PASS lets the producer run and returns its payload. Re-exported from `src/query/index.js`.
|
|
37
|
+
- **`#admissibility` package.json import specifier.** New subpath alias mapping to `./src/admissibility/index.js`.
|
|
38
|
+
- **`test/admissibility/scope-lint.test.js` (5 tests)** covering NV-BL-ADM-02 absent / unclassified / tolerance, NV-BL-ADM-03 narrower-fails / wider-passes.
|
|
39
|
+
- **`test/admissibility/markers.test.js` (4 tests)** covering the case-insensitive vocabulary scan, line/column offsets, empty input, and multi-file scan + ioFailure surfacing.
|
|
40
|
+
- **`test/admissibility/enforce.test.js` (7 tests)** covering NV-BL-ADM-06 missing-version + behavioural drift refusal, NV-BL-ADM-05 override application, the NV-BL-ADM-04 override guardrail, `getRulesetToolScope`, and `runWithAdmissibilityGate` short-circuit + pass paths.
|
|
41
|
+
|
|
42
|
+
### Changed (Car 3, landmines + schemas pin)
|
|
43
|
+
|
|
44
|
+
- **`@stravica-ai/rcf-schemas` bumped to EXACT `0.4.3`** (no caret, no range) per the ratified umbrella exact-pin doctrine (item 18 alongside the 0.4.3 release). The 0.4.3 diff is additive-only: widened `fbsId` / `cnId` / `adrId` / `tacId` to accept an optional kebab-case slug tail, widened `tsId` / `tcId` from `\d{3}` to `\d{3,}` (drops the 999 cap), added `common.$defs.scopeTag` (`library | runtime | deployed | unclassified`), added OPTIONAL `scope` on the AC subschema (`user-story.schema.json`) and TC subschema (`test-suite.schema.json`). Every pre-0.4.3 chain continues to validate; no retro-slugging is performed (the design is preferred-with-numeric-fallback per w-2026-07-28-012). Pin review: the additive-only nature is asserted by the schemas repo's `manifest-004-pre-040-backcompat.json` fixture round-tripping cleanly under 0.4.3, and by the existing writer / walker tests continuing to pass with the bumped pin.
|
|
45
|
+
|
|
46
|
+
### Fixed (Car 3, w-2026-07-28-012 landmines)
|
|
47
|
+
|
|
48
|
+
Four landmine fixes lifted from `w-2026-07-28-012` (design notes + d-2026-07-28-011 recon), each landing WITH its regression test. Every fix is a precondition for the slug-consuming changes that follow it in the same car; the walker prefix-only-uppercase fix lands first, per the ratified sequencing.
|
|
49
|
+
|
|
50
|
+
- **Landmine 1: `packages/rcf-lite/src/core/store/walker.js` (loadChildKind).** The walker used to derive tree ids by upper-casing the whole filename stem (`stem.toUpperCase()`). Lossless while every id was `<PREFIX>-<digits>`; the moment a slug lands (`fbs-004-user-login.json` -> body `fbsId: "FBS-004-user-login"`), the whole-stem fold produced `FBS-004-USER-LOGIN` in `tree.byId` / `kindById` / `parentByChild` while every inbound reference used the lower-case form. The graph silently detached. The fix (`idFromFilenameStem`) upper-cases the prefix segment only and leaves the slug tail verbatim. Regression: `test/store/walker.test.js` `walkTree preserves case on slug tails when deriving id from filename`; also asserts a valid inbound `dependsOnFbsIds` reference resolves to the byId entry.
|
|
51
|
+
- **Landmine 2: `packages/rcf-lite/src/core/store/writer.js` (nextFlatId).** The allocator's high-water mark used a slug-blind regex `^${prefix}-(\d+)$`; a slugged id was invisible to the max and the allocator reset to 001, re-issuing taken numbers. The fix replaces the local regex with the shared `idNumber(id, prefix)` helper (`ids.js:72-78`, pattern `^${prefix}-(\d+)(?:-|$)`) which parses both numeric-only and slugged shapes into the same numeric key. Regression: `test/core/store/writer.test.js` `nextIdForKind sees slugged ids in the occupancy set` and `nextIdForKind never re-issues a slugged FBS number even without a companion numeric-only FBS at the same slot`.
|
|
52
|
+
- **Landmine 3: hardcoded three-digit id patterns across seven walker/writer sites AND three consumer-path stragglers.** The moment a TS crosses 999 (rcf-schemas 0.4.3 admits `TS-1000`), the walker's `idPrefixMatchesParent` rule silently skipped every inline TC under that TS, and the writer's `nextIdForKind tc` refused with "unrecognised TS id" even though the schema admitted the parent. Widened all sites to `\d{3,}` in lockstep with the schema. Walker + writer sites (7 total): `packages/rcf-lite/src/core/store/walker.js:851/854` and `writer.js:252/908/1163` (four regex sites), plus the corresponding comments and the resolveInlineId TC branch on writer.js widened for consistency. Consumer-path stragglers (3 sites, folded in as a rework commit after the initial car 3.1 landed): `packages/rcf-lite/src/cli/test-suite.js:85` (hard-refused any TS >= 1000 in `rcf test-suite <id> {provenance|approve}` with an error text citing "TS-015"; widened + error text updated to cite both "TS-015" and "TS-1000"), `packages/rcf-lite/src/cli/read.js:122` (`rcf read TC-1000-x` silently fell through to null even when the TC existed under a widened TS; widened the inline-TC guard), and `packages/rcf-lite/src/mcp/tools.js:765` (same silent-null pattern via the MCP read tool). Regressions: `test/core/store/writer.test.js` `nextIdForKind tc admits a TS beyond 999` + `walker inline-TC prefix-match rule fires for TS beyond 999`; `test/cli/test-suite-cli.test.js` `rcf test-suite TS-1000 provenance accepts a four-digit TS id` + `rcf test-suite TS-1000 approve accepts a four-digit TS id` + `rcf test-suite refuses a non-TS positional and cites both three-digit and four-digit shapes`; `test/cli/read.test.js` `rcf read TC-1000-first-case resolves the inline TC on a widened (four-digit) TS`; `test/mcp/tools.test.js` `rcf_read: resolves an inline TC on a widened (four-digit) TS`.
|
|
53
|
+
- **Landmine 4: `packages/rcf-lite/src/core/store/writer.js` (`deriveSlug`).** Returned the literal `'tc'` on empty derivation. Every non-TC caller (FBS, CN, ADR, TAC create paths) would silently produce an id ending in `-tc` when the source description derived to empty -- wrong kind label baked in as a slug. The fix returns `''` at the source and moves the `|| 'tc'` fallback to the TC-specific callers (`writer.js` `createInlineTc`, `mcp/tools.js`, `cli/create.js`). Regression: `deriveSlug lowercases and squashes non-alphanumeric runs` updated (`!!!` now maps to `''`, not `'tc'`) and a new `0.8.0 slug-train (landmine 4): deriveSlug returns empty string, not the "tc" literal` test.
|
|
54
|
+
|
|
55
|
+
### Added (Car 2, shared standards ruleset)
|
|
56
|
+
|
|
57
|
+
- **`src/ruleset/ruleset.json` (NV-BL-SR-01).** Single machine-readable artefact bundled inside the umbrella package, camelCase per estate convention. Names the admissibility rule identifiers (`NV-BL-ADM-01`..`NV-BL-ADM-06`), the gate rule identifiers (`NV-BL-GATE-01`..`NV-BL-GATE-04`), the scope-tag vocabulary reference (points at `@stravica-ai/rcf-schemas` `common.schema.json#/$defs/scopeTag` rather than owning the enum, ratified 2026-08-11 item 11), the source-comment marker vocabulary (`TODO`, `FIXME`, `XXX`, `HACK`, `placeholder`, `v1 refinement`, `deferred`, `stub` — all case-insensitive per NV-BL-ADM-04), the canonical TC template family identifiers (`TCT-SERVER-BOOT`, `TCT-CLI-INVOKE`, `TCT-CONTAINER-RUN` — NV-BL-GATE-03), and the light-mechanical ruling-consistency check family (`RCC-EXTERNAL-RESOURCE-CONTRADICTION`, `RCC-TIER-CAPABILITY-MISMATCH` — NV-BL-GATE-04; probabilistic/suitability checks live in rcf-define-lite under DL-REQ-VALIDATE-04). `toolScope` covers chain admissibility AND traceability/query tools per the ratified addendum on item 1.
|
|
58
|
+
- **`src/ruleset/index.js` (`#ruleset`).** Loader for the artefact. `getRuleset()` returns the frozen artefact with `rulesetVersion` stamped at read time from the umbrella `package.json` (NV-BL-SR-02: the ruleset has no separate semver; its version IS the umbrella version). `detectRulesetDrift({ chainRulesetVersion })` classifies chain-vs-shipping version mismatch as `none | additive | behavioural | missing`, feeding `NV-BL-ADM-06` (build-stage refusal on behavioural drift) and `DL-REQ-VALIDATE-03` (define-stage warning). `getUmbrellaVersion()` exposes the same version so callers do not re-parse `package.json`. `resetRulesetCache()` for tests.
|
|
59
|
+
- **`#ruleset` package.json import specifier.** New subpath alias mapping to `./src/ruleset/index.js` so consumers reach the loader without hard-coding a relative path.
|
|
60
|
+
- **`docs/ruleset.md`.** Ruleset reference doc — artefact location, version policy (NV-BL-SR-02), content at v1 (NV-BL-SR-03), consumers, migration state.
|
|
61
|
+
- **`test/ruleset/ruleset.test.js`.** 14 tests covering NV-BL-SR-01 shape, NV-BL-SR-02 version stamping (umbrella-authoritative; baked-in `rulesetVersion` on the JSON refused), NV-BL-SR-03 content (every ADM/GATE id present, every ADM rule `refuseByDefault: true`, scope-tag vocabulary points at schemas, marker list matches NV-BL-ADM-04 vocabulary, TC template family matches NV-BL-GATE-03 surfaces, ruling-consistency checks are light-mechanical), and `detectRulesetDrift` classes.
|
|
62
|
+
|
|
7
63
|
## [0.7.0] - 2026-07-31
|
|
8
64
|
|
|
9
65
|
The cold-run response train. Four tracks land together in one release, each closing a systemic gap the 0.5.2 watchpost cold run exposed. Ships alongside `@stravica-ai/rcf-lite-core@0.3.0` (shared pattern sets, baseline catalog, canary fixtures) and `@stravica-ai/rcf-verify-lite@0.2.0` (per-AC verdict classes, chain-reader derivation); `@stravica-ai/rcf-schemas@0.4.2` already carries the full 0.7.0 surface additively so every new chain field is optional at the schema layer. Track A (verification integrity) ships the primitives that keep a mocked integration test from ever again looking identical to a live one: the chain gains an honest record of what the tests actually verified against. Track B (UI design gate + quality baseline) ships the Design substage (Stage 1.5), the ruled UI baseline, and the browser-verification gate for UI-bearing FBSes. Tracks C and D (elicitation upstream + playbook hardening) ship the REQ-shape classifier and baseline-AC catalog with silence-refuses-build enforcement (Track C), the variable-fidelity intake stage (Track C), and the release-time register canary, never-skip-RCF invariant, review-surface persistence and persona tail-interview template (Track D). Every field is optional at schema; the ENFORCEMENT (elicitation, `coverage --strict`, the review-stage audit, the finalise gate, `--mark complete` refusals, the Stage-1 sweep gate) is what makes them de-facto required for new chains from 0.7.0 onward.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"registerCanary": [
|
|
3
3
|
{
|
|
4
4
|
"id": "rc-2026-08-12-001",
|
|
5
|
-
"createdAt": "2026-08-
|
|
6
|
-
"buildVersion": "0.
|
|
5
|
+
"createdAt": "2026-08-12T16:39:41.040Z",
|
|
6
|
+
"buildVersion": "0.8.0-mockdriver",
|
|
7
7
|
"fixturePromptId": "canary-prompt-01",
|
|
8
8
|
"responseWordCount": 55,
|
|
9
9
|
"grades": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"id": "rc-2026-08-12-002",
|
|
38
|
-
"createdAt": "2026-08-
|
|
39
|
-
"buildVersion": "0.
|
|
38
|
+
"createdAt": "2026-08-12T16:39:41.041Z",
|
|
39
|
+
"buildVersion": "0.8.0-mockdriver",
|
|
40
40
|
"fixturePromptId": "canary-prompt-02",
|
|
41
41
|
"responseWordCount": 55,
|
|
42
42
|
"grades": {
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"id": "rc-2026-08-12-003",
|
|
71
|
-
"createdAt": "2026-08-
|
|
72
|
-
"buildVersion": "0.
|
|
71
|
+
"createdAt": "2026-08-12T16:39:41.041Z",
|
|
72
|
+
"buildVersion": "0.8.0-mockdriver",
|
|
73
73
|
"fixturePromptId": "canary-prompt-03",
|
|
74
74
|
"responseWordCount": 55,
|
|
75
75
|
"grades": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rcf-lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One-install tooling for the Requirements Confidence Framework (RCF): the unified `rcf` CLI (init, view, validate, build, verify, finalise and 25+ more verbs), an MCP server, the live tree viewer and the fresh-context adversarial ship-gate verifier. Consumes @stravica-ai/rcf-schemas.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -53,10 +53,12 @@
|
|
|
53
53
|
"#core/patterns/services": "./src/core/patterns/services.js",
|
|
54
54
|
"#core/patterns/register-canary": "./src/core/patterns/register-canary.js",
|
|
55
55
|
"#core/baseline-catalog": "./src/core/baseline-catalog/index.js",
|
|
56
|
-
"#core/fixtures/register-canary/*": "./src/core/fixtures/register-canary/*"
|
|
56
|
+
"#core/fixtures/register-canary/*": "./src/core/fixtures/register-canary/*",
|
|
57
|
+
"#ruleset": "./src/ruleset/index.js",
|
|
58
|
+
"#admissibility": "./src/admissibility/index.js"
|
|
57
59
|
},
|
|
58
60
|
"dependencies": {
|
|
59
|
-
"@stravica-ai/rcf-schemas": "0.4.
|
|
61
|
+
"@stravica-ai/rcf-schemas": "0.4.3",
|
|
60
62
|
"ajv": "^8.20.0",
|
|
61
63
|
"ajv-formats": "^3.0.1"
|
|
62
64
|
},
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Chain-admissibility gate (NV-BL-ADM-05, NV-BL-ADM-06).
|
|
2
|
+
//
|
|
3
|
+
// Refuse-first per ratified ruling-sheet item 1 (2026-08-11): a chain
|
|
4
|
+
// that fails any admissibility check is refused before build starts.
|
|
5
|
+
// Explicit overrides are permitted for non-source-marker checks,
|
|
6
|
+
// recorded in the chain with rule id, reason, and authoring identity,
|
|
7
|
+
// and surfaced in the finalise summary. Source-comment markers
|
|
8
|
+
// (NV-BL-ADM-04) fall outside this generic override channel and are
|
|
9
|
+
// governed by the narrower ADR-only mechanism per ruling-sheet
|
|
10
|
+
// item 16 -- callers apply that check separately.
|
|
11
|
+
//
|
|
12
|
+
// NV-BL-ADM-06 (build-stage refusal on ruleset-version drift):
|
|
13
|
+
// composes with the scope-tag scans below so a build-time entry point
|
|
14
|
+
// gets one call that returns a single verdict.
|
|
15
|
+
|
|
16
|
+
import { detectRulesetDrift, getRuleset } from '#ruleset';
|
|
17
|
+
|
|
18
|
+
import { scanAcScopeCoverage, scanTcScopeVsAc } from './scope-lint.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} AdmissibilityOverride
|
|
22
|
+
* @property {string} rule - rule id being overridden (e.g. "NV-BL-ADM-02").
|
|
23
|
+
* @property {string} reason - operator-provided reason recorded on the chain.
|
|
24
|
+
* @property {string} [authoredBy] - author identity string.
|
|
25
|
+
* @property {string} [documentId] - optional pin to a specific doc id.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} AdmissibilityVerdict
|
|
30
|
+
* @property {'pass' | 'refuse' | 'passWithOverrides'} verdict
|
|
31
|
+
* @property {import('#core/errors').RcfError[]} findings - all findings before override application.
|
|
32
|
+
* @property {import('#core/errors').RcfError[]} unresolved - findings not covered by a supplied override.
|
|
33
|
+
* @property {AdmissibilityOverride[]} appliedOverrides
|
|
34
|
+
* @property {object} drift - shape from detectRulesetDrift.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Should this finding be masked by the given override? Match on rule id
|
|
39
|
+
* and, when the override pins a documentId, on documentId too.
|
|
40
|
+
*/
|
|
41
|
+
function overrideCovers(finding, override) {
|
|
42
|
+
if (finding.rule !== override.rule) return false;
|
|
43
|
+
if (override.documentId && finding.documentId !== override.documentId) return false;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Enforce admissibility across the whole chain (NV-BL-ADM-05 gate) plus
|
|
49
|
+
* ruleset-version drift (NV-BL-ADM-06). Callers pass the walker tree,
|
|
50
|
+
* the chain's declared ruleset version (per DL-REQ-VALIDATE-03), and an
|
|
51
|
+
* optional list of recorded overrides.
|
|
52
|
+
*
|
|
53
|
+
* A pure function on top of the ruleset + tree. Callers decide what to
|
|
54
|
+
* do with a `refuse` verdict (`rcf build` refuses with exit 4; a query
|
|
55
|
+
* or traceability tool refuses to surface the chain per the item 1
|
|
56
|
+
* addendum -- see `#query/refuse-on-admissibility`).
|
|
57
|
+
*
|
|
58
|
+
* @param {object} args
|
|
59
|
+
* @param {object} args.tree - walker output
|
|
60
|
+
* @param {string|null} [args.chainRulesetVersion]
|
|
61
|
+
* @param {AdmissibilityOverride[]} [args.overrides]
|
|
62
|
+
* @param {object} [args.opts]
|
|
63
|
+
* @param {boolean} [args.opts.tolerateUnclassified] - default true.
|
|
64
|
+
* @returns {Promise<AdmissibilityVerdict>}
|
|
65
|
+
*/
|
|
66
|
+
export async function enforceAdmissibility({
|
|
67
|
+
tree,
|
|
68
|
+
chainRulesetVersion = null,
|
|
69
|
+
overrides = [],
|
|
70
|
+
opts = {},
|
|
71
|
+
} = {}) {
|
|
72
|
+
const ruleset = await getRuleset();
|
|
73
|
+
const drift = await detectRulesetDrift({ chainRulesetVersion, ruleset });
|
|
74
|
+
|
|
75
|
+
const findings = [];
|
|
76
|
+
|
|
77
|
+
// NV-BL-ADM-06: build-stage refusal on behavioural drift. Missing
|
|
78
|
+
// chain-ruleset-version is a separate class -- the chain never
|
|
79
|
+
// declared a version, so we cannot classify drift; the finding
|
|
80
|
+
// asks the operator to run the define-stage warning path
|
|
81
|
+
// (DL-REQ-VALIDATE-03) or acknowledge the omission via override.
|
|
82
|
+
if (drift.drift === 'behavioural') {
|
|
83
|
+
findings.push({
|
|
84
|
+
kind: 'validation',
|
|
85
|
+
message: `NV-BL-ADM-06: chain was authored against ruleset version ${drift.chainVersion}; shipping version is ${drift.shippingVersion}. Behaviour-changing drift refuses at build stage.`,
|
|
86
|
+
rule: 'NV-BL-ADM-06',
|
|
87
|
+
});
|
|
88
|
+
} else if (drift.drift === 'missing') {
|
|
89
|
+
findings.push({
|
|
90
|
+
kind: 'validation',
|
|
91
|
+
message: `NV-BL-ADM-06: chain does not declare a ruleset version. Run the define-stage adequacy check against ruleset ${drift.shippingVersion} (DL-REQ-VALIDATE-03) or record an override.`,
|
|
92
|
+
rule: 'NV-BL-ADM-06',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// NV-BL-ADM-02 / -03: scope-tag coverage + TC-scope-vs-AC-scope.
|
|
97
|
+
findings.push(...scanAcScopeCoverage(tree, opts));
|
|
98
|
+
findings.push(...scanTcScopeVsAc(tree, opts));
|
|
99
|
+
|
|
100
|
+
// NV-BL-ADM-05: refuse-first, override-recorded. Overrides are
|
|
101
|
+
// recorded on the chain; we apply them here to produce the
|
|
102
|
+
// unresolved-findings set. Source-comment markers (NV-BL-ADM-04) do
|
|
103
|
+
// NOT flow through this override channel; callers running that scan
|
|
104
|
+
// filter its findings only through the ADR-only channel per
|
|
105
|
+
// ruling-sheet item 16.
|
|
106
|
+
const appliedOverrides = [];
|
|
107
|
+
const unresolved = [];
|
|
108
|
+
for (const finding of findings) {
|
|
109
|
+
if (finding.rule === 'NV-BL-ADM-04') {
|
|
110
|
+
// Guardrail: source-marker findings must never be masked through
|
|
111
|
+
// the generic override channel. If a caller mistakenly threaded
|
|
112
|
+
// them into this function, leave them unresolved.
|
|
113
|
+
unresolved.push(finding);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const hit = overrides.find((o) => overrideCovers(finding, o));
|
|
117
|
+
if (hit) {
|
|
118
|
+
appliedOverrides.push(hit);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
unresolved.push(finding);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let verdict;
|
|
125
|
+
if (unresolved.length === 0 && appliedOverrides.length === 0) verdict = 'pass';
|
|
126
|
+
else if (unresolved.length === 0) verdict = 'passWithOverrides';
|
|
127
|
+
else verdict = 'refuse';
|
|
128
|
+
|
|
129
|
+
return { verdict, findings, unresolved, appliedOverrides, drift };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Convenience: the ruleset's toolScope block. Query / traceability tools
|
|
134
|
+
* read this to decide whether to apply the refuse-first posture when
|
|
135
|
+
* surfacing chain data (NV-BL-SR-03 addendum on ruling-sheet item 1).
|
|
136
|
+
*
|
|
137
|
+
* @returns {Promise<{ chainAdmissibility: boolean, traceabilityAndQueryTools: boolean }>}
|
|
138
|
+
*/
|
|
139
|
+
export async function getRulesetToolScope() {
|
|
140
|
+
const ruleset = await getRuleset();
|
|
141
|
+
return ruleset.toolScope;
|
|
142
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Public surface for the chain-admissibility gate (0.8.0 slug-train,
|
|
2
|
+
// car 3). Rules consumed from the shared standards ruleset bundled
|
|
3
|
+
// inside this umbrella package (`#ruleset`) per the ratified
|
|
4
|
+
// requirements doc (2026-08-06 canonical since 2026-08-12).
|
|
5
|
+
|
|
6
|
+
export { enforceAdmissibility, getRulesetToolScope } from './enforce.js';
|
|
7
|
+
export { scanAcScopeCoverage, scanTcScopeVsAc } from './scope-lint.js';
|
|
8
|
+
export { scanFilesForMarkers, scanSourceStringForMarkers } from './markers.js';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Source-comment marker scanner (NV-BL-ADM-04).
|
|
2
|
+
//
|
|
3
|
+
// Scans a set of source files for admission markers from the ratified
|
|
4
|
+
// vocabulary carried on the shared standards ruleset
|
|
5
|
+
// (`sourceCommentMarkers[]`). Any match is a finding. Per ratified
|
|
6
|
+
// ruling-sheet item 16, the only permitted override channel is an ADR
|
|
7
|
+
// recording the deferral for a genuine external-boundary blocker; the
|
|
8
|
+
// generic recorded-override channel (NV-BL-ADM-05) does not cover
|
|
9
|
+
// source markers.
|
|
10
|
+
//
|
|
11
|
+
// This module produces findings only. Wiring the gate into
|
|
12
|
+
// `rcf build --mark complete` is a build-lite verb change that reads
|
|
13
|
+
// the FBS's build-sequence files and calls `scanSourceForMarkers` for
|
|
14
|
+
// each; the CLI change belongs in that verb's PR, not here.
|
|
15
|
+
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
|
|
18
|
+
import { rcfError } from '#core/errors';
|
|
19
|
+
import { getRuleset } from '#ruleset';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compile the ruleset's marker vocabulary into a single case-insensitive
|
|
23
|
+
* pattern. Markers are ratified as case-insensitive per NV-BL-ADM-04, so
|
|
24
|
+
* we build the pattern with the `i` flag. Longest markers first so
|
|
25
|
+
* "v1 refinement" wins over "v1" if a shorter marker is ever added to
|
|
26
|
+
* the vocabulary in future releases.
|
|
27
|
+
*
|
|
28
|
+
* @param {Array<{ marker: string, caseInsensitive: boolean }>} markers
|
|
29
|
+
* @returns {RegExp}
|
|
30
|
+
*/
|
|
31
|
+
function compileMarkerPattern(markers) {
|
|
32
|
+
const sorted = [...markers].sort((a, b) => b.marker.length - a.marker.length);
|
|
33
|
+
const alternation = sorted
|
|
34
|
+
.map((m) => m.marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
35
|
+
.join('|');
|
|
36
|
+
// Boundary-free by design: matches a marker anywhere in a comment
|
|
37
|
+
// ("PLACEHOLDER function" matches; "// placeholder" matches; a random
|
|
38
|
+
// word like "todos" also matches, which is the trade-off named in the
|
|
39
|
+
// ruleset's NV-BL-ADM-04 notes -- false positives on unrelated tokens
|
|
40
|
+
// are cheaper than false negatives on real deferrals.
|
|
41
|
+
return new RegExp(`(${alternation})`, 'ig');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Scan a single source string for the ratified marker vocabulary.
|
|
46
|
+
* Returns one finding per match, with line + column offsets. Empty
|
|
47
|
+
* input returns an empty findings array.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} source
|
|
50
|
+
* @param {object} [opts]
|
|
51
|
+
* @param {string} [opts.filePath] - decorates each finding for callers
|
|
52
|
+
* @param {Array<{ marker: string, caseInsensitive: boolean }>} [opts.markers]
|
|
53
|
+
* @returns {Promise<import('#core/errors').RcfError[]>}
|
|
54
|
+
*/
|
|
55
|
+
export async function scanSourceStringForMarkers(source, { filePath, markers } = {}) {
|
|
56
|
+
if (typeof source !== 'string' || source.length === 0) return [];
|
|
57
|
+
const vocab = markers ?? (await getRuleset()).sourceCommentMarkers;
|
|
58
|
+
const pattern = compileMarkerPattern(vocab);
|
|
59
|
+
const findings = [];
|
|
60
|
+
let match;
|
|
61
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
62
|
+
const before = source.slice(0, match.index);
|
|
63
|
+
const line = before.split('\n').length;
|
|
64
|
+
const lastNewline = before.lastIndexOf('\n');
|
|
65
|
+
const column = match.index - (lastNewline === -1 ? -1 : lastNewline);
|
|
66
|
+
findings.push(rcfError({
|
|
67
|
+
kind: 'validation',
|
|
68
|
+
message: `NV-BL-ADM-04: source-comment admission marker "${match[1]}" at ${filePath ?? '<source>'}:${line}:${column}`,
|
|
69
|
+
filePath: filePath ?? null,
|
|
70
|
+
rule: 'NV-BL-ADM-04',
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
return findings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Scan a set of files by path for the ratified marker vocabulary. IO
|
|
78
|
+
* failures on a single file are captured as `ioFailure` findings; other
|
|
79
|
+
* files continue to scan. Order is deterministic in the input file list.
|
|
80
|
+
*
|
|
81
|
+
* @param {string[]} filePaths - absolute paths to scan
|
|
82
|
+
* @returns {Promise<import('#core/errors').RcfError[]>}
|
|
83
|
+
*/
|
|
84
|
+
export async function scanFilesForMarkers(filePaths) {
|
|
85
|
+
if (!Array.isArray(filePaths) || filePaths.length === 0) return [];
|
|
86
|
+
const ruleset = await getRuleset();
|
|
87
|
+
const vocab = ruleset.sourceCommentMarkers;
|
|
88
|
+
const findings = [];
|
|
89
|
+
for (const filePath of filePaths) {
|
|
90
|
+
try {
|
|
91
|
+
const source = await readFile(filePath, 'utf8');
|
|
92
|
+
const perFile = await scanSourceStringForMarkers(source, { filePath, markers: vocab });
|
|
93
|
+
findings.push(...perFile);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
findings.push(rcfError({
|
|
96
|
+
kind: 'ioFailure',
|
|
97
|
+
message: `NV-BL-ADM-04: failed to read source for marker scan: ${err.message}`,
|
|
98
|
+
filePath,
|
|
99
|
+
rule: 'NV-BL-ADM-04',
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return findings;
|
|
104
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Chain-admissibility scope checks (NV-BL-ADM-02, NV-BL-ADM-03).
|
|
2
|
+
//
|
|
3
|
+
// The scope-tag vocabulary itself lives on rcf-schemas 0.4.3
|
|
4
|
+
// (`common.$defs.scopeTag`). This module consumes the tags off ACs
|
|
5
|
+
// and TCs and enforces the admissibility rules the shared standards
|
|
6
|
+
// ruleset references (`NV-BL-ADM-02` for AC scope classification and
|
|
7
|
+
// `NV-BL-ADM-03` for TC scope >= AC scope).
|
|
8
|
+
//
|
|
9
|
+
// Refuse-first, override-recorded per NV-BL-ADM-05; source-comment
|
|
10
|
+
// markers are governed separately by NV-BL-ADM-04 and its ADR-only
|
|
11
|
+
// override channel.
|
|
12
|
+
|
|
13
|
+
import { rcfError } from '#core/errors';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Rank each scope so we can compare "TC scope >= AC scope" numerically.
|
|
17
|
+
* `unclassified` is the migration state; ranked -1 so a TC-scoped
|
|
18
|
+
* unclassified against a runtime-scope AC surfaces as a mismatch.
|
|
19
|
+
* @type {Record<string, number>}
|
|
20
|
+
*/
|
|
21
|
+
const SCOPE_RANK = Object.freeze({
|
|
22
|
+
library: 1,
|
|
23
|
+
runtime: 2,
|
|
24
|
+
deployed: 3,
|
|
25
|
+
unclassified: -1,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True when the tag is a known scope value per the shared vocabulary.
|
|
30
|
+
* @param {unknown} tag
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function isKnownScope(tag) {
|
|
34
|
+
return typeof tag === 'string' && Object.prototype.hasOwnProperty.call(SCOPE_RANK, tag);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* NV-BL-ADM-02: every AC must carry a known scope tag. An AC without a
|
|
39
|
+
* scope, or with a non-vocabulary value, produces an admissibility
|
|
40
|
+
* finding. Per the ruleset's `unclassifiedMigrationTolerance` block,
|
|
41
|
+
* an AC scoped `unclassified` is currently tolerated (findings not
|
|
42
|
+
* emitted); callers who want to enforce full migration pass
|
|
43
|
+
* `tolerateUnclassified: false`.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} tree - walkTree output
|
|
46
|
+
* @param {object} [opts]
|
|
47
|
+
* @param {boolean} [opts.tolerateUnclassified] - default true (migration state)
|
|
48
|
+
* @returns {import('#core/errors').RcfError[]}
|
|
49
|
+
*/
|
|
50
|
+
export function scanAcScopeCoverage(tree, { tolerateUnclassified = true } = {}) {
|
|
51
|
+
const findings = [];
|
|
52
|
+
for (const us of tree.userStories ?? []) {
|
|
53
|
+
for (const ac of us.acceptanceCriteria ?? []) {
|
|
54
|
+
const scope = ac?.scope;
|
|
55
|
+
if (scope === undefined) {
|
|
56
|
+
findings.push(rcfError({
|
|
57
|
+
kind: 'validation',
|
|
58
|
+
message: `NV-BL-ADM-02: AC ${ac.id} on US ${us.usId} carries no scope tag`,
|
|
59
|
+
documentId: ac.id,
|
|
60
|
+
field: 'scope',
|
|
61
|
+
rule: 'NV-BL-ADM-02',
|
|
62
|
+
}));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!isKnownScope(scope)) {
|
|
66
|
+
findings.push(rcfError({
|
|
67
|
+
kind: 'validation',
|
|
68
|
+
message: `NV-BL-ADM-02: AC ${ac.id} on US ${us.usId} carries an unknown scope tag "${scope}"`,
|
|
69
|
+
documentId: ac.id,
|
|
70
|
+
field: 'scope',
|
|
71
|
+
rule: 'NV-BL-ADM-02',
|
|
72
|
+
}));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (scope === 'unclassified' && !tolerateUnclassified) {
|
|
76
|
+
findings.push(rcfError({
|
|
77
|
+
kind: 'validation',
|
|
78
|
+
message: `NV-BL-ADM-02: AC ${ac.id} on US ${us.usId} still scoped "unclassified" after the migration window`,
|
|
79
|
+
documentId: ac.id,
|
|
80
|
+
field: 'scope',
|
|
81
|
+
rule: 'NV-BL-ADM-02',
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return findings;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* NV-BL-ADM-03: for each AC, every bound TC's scope must be equal to
|
|
91
|
+
* or wider than the AC's scope. A library-scope TC bound to a
|
|
92
|
+
* runtime-scope AC surfaces as a mismatch. TCs with no scope tag are
|
|
93
|
+
* flagged the same way ACs are in NV-BL-ADM-02: absent = finding
|
|
94
|
+
* (bootstrap: unclassified tolerated).
|
|
95
|
+
*
|
|
96
|
+
* @param {object} tree - walkTree output
|
|
97
|
+
* @param {object} [opts]
|
|
98
|
+
* @param {boolean} [opts.tolerateUnclassified] - default true
|
|
99
|
+
* @returns {import('#core/errors').RcfError[]}
|
|
100
|
+
*/
|
|
101
|
+
export function scanTcScopeVsAc(tree, { tolerateUnclassified = true } = {}) {
|
|
102
|
+
const findings = [];
|
|
103
|
+
const acScope = new Map();
|
|
104
|
+
for (const us of tree.userStories ?? []) {
|
|
105
|
+
for (const ac of us.acceptanceCriteria ?? []) {
|
|
106
|
+
if (ac?.id) acScope.set(ac.id, ac.scope);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const ts of tree.testSuites ?? []) {
|
|
110
|
+
for (const tc of ts.testCases ?? []) {
|
|
111
|
+
if (!tc?.id || !tc?.acId) continue;
|
|
112
|
+
const tcScope = tc?.scope;
|
|
113
|
+
const acTag = acScope.get(tc.acId);
|
|
114
|
+
if (acTag === undefined || !isKnownScope(acTag) || acTag === 'unclassified') {
|
|
115
|
+
// The AC's own scope problem surfaces via scanAcScopeCoverage;
|
|
116
|
+
// this scan is silent for ACs the classifier could not read.
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (tcScope === undefined) {
|
|
120
|
+
findings.push(rcfError({
|
|
121
|
+
kind: 'validation',
|
|
122
|
+
message: `NV-BL-ADM-03: TC ${tc.id} on TS ${ts.id} carries no scope tag (bound AC ${tc.acId} is scope=${acTag})`,
|
|
123
|
+
documentId: tc.id,
|
|
124
|
+
field: 'scope',
|
|
125
|
+
rule: 'NV-BL-ADM-03',
|
|
126
|
+
}));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!isKnownScope(tcScope)) {
|
|
130
|
+
findings.push(rcfError({
|
|
131
|
+
kind: 'validation',
|
|
132
|
+
message: `NV-BL-ADM-03: TC ${tc.id} on TS ${ts.id} carries an unknown scope tag "${tcScope}"`,
|
|
133
|
+
documentId: tc.id,
|
|
134
|
+
field: 'scope',
|
|
135
|
+
rule: 'NV-BL-ADM-03',
|
|
136
|
+
}));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (tcScope === 'unclassified') {
|
|
140
|
+
if (!tolerateUnclassified) {
|
|
141
|
+
findings.push(rcfError({
|
|
142
|
+
kind: 'validation',
|
|
143
|
+
message: `NV-BL-ADM-03: TC ${tc.id} on TS ${ts.id} still scoped "unclassified" after the migration window`,
|
|
144
|
+
documentId: tc.id,
|
|
145
|
+
field: 'scope',
|
|
146
|
+
rule: 'NV-BL-ADM-03',
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (SCOPE_RANK[tcScope] < SCOPE_RANK[acTag]) {
|
|
152
|
+
findings.push(rcfError({
|
|
153
|
+
kind: 'validation',
|
|
154
|
+
message: `NV-BL-ADM-03: TC ${tc.id} scope "${tcScope}" is narrower than the AC ${tc.acId} scope "${acTag}"; a bound TC must be equal to or wider than the AC scope`,
|
|
155
|
+
documentId: tc.id,
|
|
156
|
+
field: 'scope',
|
|
157
|
+
rule: 'NV-BL-ADM-03',
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return findings;
|
|
163
|
+
}
|
package/src/cli/create.js
CHANGED
|
@@ -261,7 +261,10 @@ export async function main(argv, deps = {}) {
|
|
|
261
261
|
return 2;
|
|
262
262
|
}
|
|
263
263
|
body.acId = flags.ac;
|
|
264
|
-
|
|
264
|
+
// 0.8.0 slug-train (w-2026-07-28-012 landmine 4): deriveSlug returns ''
|
|
265
|
+
// on empty derivation; TC keeps its historical 'tc' fallback locally
|
|
266
|
+
// rather than letting deriveSlug bake it in.
|
|
267
|
+
options.slug = flags.slug ?? (deriveSlug(body.description) || 'tc');
|
|
265
268
|
options.testPointer = flags['test-pointer'];
|
|
266
269
|
}
|
|
267
270
|
|
package/src/cli/read.js
CHANGED
|
@@ -119,7 +119,13 @@ function resolveTarget(tree, id) {
|
|
|
119
119
|
const entry = (us.acceptanceCriteria ?? []).find((ac) => ac.id === id);
|
|
120
120
|
return entry ? { doc: entry, containerId: parentId, invalid: false } : null;
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
// 0.8.0 slug-train (w-2026-07-28-012 landmine 3, consumer-path
|
|
123
|
+
// straggler): widened `\d{3}` -> `\d{3,}` in lockstep with rcf-schemas
|
|
124
|
+
// 0.4.3's TC pattern. Under the previous shape `rcf read TC-1000-x`
|
|
125
|
+
// fell through to the `return null` below (silent skip) even when the
|
|
126
|
+
// TC existed under a widened TS -- exactly the class the landmine
|
|
127
|
+
// charter names.
|
|
128
|
+
if (/^TC-\d{3,}-[a-z0-9-]+$/.test(id)) {
|
|
123
129
|
const parentId = tree.parentByChild.get(id);
|
|
124
130
|
if (!parentId) return null;
|
|
125
131
|
const ts = tree.byId.get(parentId);
|
package/src/cli/test-suite.js
CHANGED
|
@@ -82,8 +82,13 @@ export async function main(argv, deps = {}) {
|
|
|
82
82
|
stderr.write('[error] usage test-suite: verb required (provenance | approve)\n');
|
|
83
83
|
return 2;
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// 0.8.0 slug-train (w-2026-07-28-012 landmine 3, consumer-path
|
|
86
|
+
// straggler): widened `\d{3}` -> `\d{3,}` in lockstep with rcf-schemas
|
|
87
|
+
// 0.4.3's TS pattern. The previous shape hard-refused any TS >= 1000 --
|
|
88
|
+
// silent from the operator's perspective (the tsId matched the schema
|
|
89
|
+
// but the CLI verb refused with a usage error citing "TS-015").
|
|
90
|
+
if (!/^TS-\d{3,}$/.test(tsId)) {
|
|
91
|
+
stderr.write(`[error] usage test-suite: expected a TS id like TS-015 or TS-1000, got '${tsId}'\n`);
|
|
87
92
|
return 2;
|
|
88
93
|
}
|
|
89
94
|
|
package/src/core/store/loader.js
CHANGED
|
@@ -154,7 +154,10 @@ export async function loadDocument({ projectRoot, id }) {
|
|
|
154
154
|
* discovery mechanism for tree topology (topology comes from parent-id
|
|
155
155
|
* fields); this is just the load-time enumeration required to bring every
|
|
156
156
|
* on-disk file into memory. Callers derive the document id from the
|
|
157
|
-
* filename stem
|
|
157
|
+
* filename stem by upper-casing the PREFIX segment only (0.8.0
|
|
158
|
+
* slug-train, w-2026-07-28-012 landmine 1); slug tails stay verbatim
|
|
159
|
+
* because rcf-schemas 0.4.3 admits lower-case kebab tails on FBS / CN /
|
|
160
|
+
* ADR / TAC and a full-stem fold would silently detach the graph.
|
|
158
161
|
*
|
|
159
162
|
* Returns `{ files: string[] }` on success. Missing subdir returns
|
|
160
163
|
* `{ files: [] }` (an empty children collection is a valid tree state).
|