rcf-lite 0.7.1 → 0.9.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 +97 -0
- package/bin/rcf.js +6 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/harness-template.md +11 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +11 -0
- package/package.json +5 -3
- package/rcf/adrs/adr-010.json +30 -0
- package/rcf/code-nodes/cn-058.json +18 -0
- package/rcf/code-nodes/cn-059.json +14 -0
- package/rcf/code-nodes/cn-060.json +14 -0
- package/rcf/code-nodes/cn-061.json +14 -0
- package/rcf/code-nodes/cn-062.json +14 -0
- package/rcf/code-nodes/cn-063.json +15 -0
- package/rcf/code-nodes/cn-064.json +15 -0
- package/rcf/code-nodes/cn-065.json +16 -0
- package/rcf/code-nodes/cn-066.json +14 -0
- package/rcf/code-nodes/cn-067.json +15 -0
- package/rcf/code-nodes/cn-068.json +15 -0
- package/rcf/code-nodes/cn-069.json +16 -0
- package/rcf/fbs/fbs-016.json +39 -0
- package/rcf/fbs/fbs-017.json +40 -0
- package/rcf/fbs/fbs-018.json +34 -0
- package/rcf/fbs/fbs-019.json +33 -0
- package/rcf/requirements/req-010.json +20 -0
- package/rcf/test-suites/ts-026.json +54 -0
- package/rcf/test-suites/ts-027.json +115 -0
- package/rcf/test-suites/ts-028.json +46 -0
- package/rcf/test-suites/ts-029.json +46 -0
- package/rcf/user-stories/us-1001.json +56 -0
- package/rcf/user-stories/us-1002.json +96 -0
- package/rcf/user-stories/us-1003.json +48 -0
- package/rcf/user-stories/us-1004.json +48 -0
- 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/blueprint/apply.js +464 -0
- package/src/blueprint/conflicts.js +351 -0
- package/src/blueprint/diff.js +82 -0
- package/src/blueprint/index.js +12 -0
- package/src/blueprint/list.js +21 -0
- package/src/blueprint/loader.js +163 -0
- package/src/blueprint/manifest-writer.js +49 -0
- package/src/blueprint/namespace.js +145 -0
- package/src/blueprint/remove.js +105 -0
- package/src/blueprint/resolutions.js +83 -0
- package/src/blueprint/standards.js +148 -0
- package/src/blueprint/supersede.js +318 -0
- package/src/browser-verify/invariants.js +33 -6
- package/src/build/bundle.js +34 -11
- package/src/build/standards-selector.js +52 -0
- package/src/cli/blueprint.js +325 -0
- package/src/cli/create.js +49 -1
- package/src/cli/help.js +8 -0
- package/src/cli/init.js +20 -5
- package/src/cli/read.js +7 -1
- package/src/cli/standards.js +127 -0
- package/src/cli/test-suite.js +7 -2
- package/src/core/store/ids.js +168 -18
- package/src/core/store/loader.js +31 -17
- package/src/core/store/walker.js +62 -4
- package/src/core/store/writer.js +41 -11
- package/src/deployment/index.js +13 -0
- package/src/deployment/placeholder-detector.js +113 -0
- 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/formatters/table.js +7 -10
- package/src/query/index.js +4 -0
- package/src/query/refuse-on-admissibility.js +73 -0
- package/src/query/trace.js +45 -4
- 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
|
@@ -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
|
+
}
|