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
package/src/finalise/detect.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
// rcf-verify install detection (spec §8.3, amendment 5 - install-together
|
|
2
|
-
// posture).
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// posture). The finalise gate MUST detect whether `rcf-verify` is resolvable
|
|
3
|
+
// and, when it is absent, prompt to install it - NEVER silently skip the ship
|
|
4
|
+
// gate (the one behaviour §8.3 explicitly forbids).
|
|
5
5
|
//
|
|
6
|
-
// Two detection routes, in order, matching how the
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// global bins (`npm i -g @stravica-ai/rcf-build-lite @stravica-ai/rcf-verify-lite`).
|
|
6
|
+
// Two detection routes, in order, matching how the CLI is actually installed:
|
|
7
|
+
// 1. The `rcf-verify` bin on PATH - the umbrella install exposes both `rcf`
|
|
8
|
+
// and `rcf-verify` as global bins (`npm i -g rcf-lite`).
|
|
10
9
|
// 2. Package resolution from the project dir - the local-project install
|
|
11
|
-
// (`npm i
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
10
|
+
// (`npm i rcf-lite` in a repo's node_modules). Two package specifiers
|
|
11
|
+
// are tried in order: the current umbrella `rcf-lite`, then the legacy
|
|
12
|
+
// `@stravica-ai/rcf-verify-lite` name (deprecated at 0.7.1 but still
|
|
13
|
+
// resolvable in lockfiles that pinned it). Either resolves to the same
|
|
14
|
+
// `bin/rcf-verify.js` entry point.
|
|
15
|
+
// Either route yields a concrete invocation the finalise spawn (spawn.js)
|
|
16
|
+
// uses verbatim. A miss returns { installed:false } and the caller enters
|
|
17
|
+
// the prompt-or-explicit-flag path.
|
|
15
18
|
|
|
16
19
|
import { access, constants } from 'node:fs/promises';
|
|
17
20
|
import { createRequire } from 'node:module';
|
|
18
21
|
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
// VERIFY_PACKAGE is the current install target and the name shown to
|
|
24
|
+
// operators in absent-verify messaging. VERIFY_PACKAGE_LEGACY is only used
|
|
25
|
+
// as a resolution fallback for lockfiles still pinning the pre-0.7.1 scoped
|
|
26
|
+
// name; it is never displayed as an install target.
|
|
27
|
+
const VERIFY_PACKAGE = 'rcf-lite';
|
|
28
|
+
const VERIFY_PACKAGE_LEGACY = '@stravica-ai/rcf-verify-lite';
|
|
29
|
+
const VERIFY_PACKAGE_CANDIDATES = [VERIFY_PACKAGE, VERIFY_PACKAGE_LEGACY];
|
|
21
30
|
const VERIFY_BIN = 'rcf-verify';
|
|
22
31
|
|
|
23
32
|
/**
|
|
@@ -69,31 +78,44 @@ export async function findOnPath(name, { env = process.env } = {}) {
|
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/**
|
|
72
|
-
* Resolve the rcf-verify
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
81
|
+
* Resolve the rcf-verify bin entry point from a starting directory, following
|
|
82
|
+
* the normal node_modules resolution the caller's project sees. Returns the
|
|
83
|
+
* absolute path to `bin/rcf-verify.js`, or null if no candidate package is
|
|
84
|
+
* installed / resolvable from there.
|
|
85
|
+
*
|
|
86
|
+
* Candidates are tried in the order defined by VERIFY_PACKAGE_CANDIDATES: the
|
|
87
|
+
* current umbrella (`rcf-lite`) first, then the deprecated scoped name
|
|
88
|
+
* (`@stravica-ai/rcf-verify-lite`) as a fallback for lockfiles pinned to it.
|
|
89
|
+
* The first candidate whose bin resolves and exists on disk wins.
|
|
76
90
|
*
|
|
77
91
|
* @param {string} fromDir - directory to resolve from (the project root / cwd)
|
|
78
92
|
* @returns {Promise<string|null>}
|
|
79
93
|
*/
|
|
80
94
|
export async function resolvePackageBin(fromDir) {
|
|
95
|
+
// Resolve from a synthetic module living in fromDir so node walks that
|
|
96
|
+
// project's node_modules chain, not this package's own.
|
|
97
|
+
let req;
|
|
81
98
|
try {
|
|
82
|
-
|
|
83
|
-
// project's node_modules chain, not build-lite's own.
|
|
84
|
-
const req = createRequire(join(fromDir, 'noop.js'));
|
|
85
|
-
const pkgJsonPath = req.resolve(`${VERIFY_PACKAGE}/package.json`);
|
|
86
|
-
const req2 = createRequire(pkgJsonPath);
|
|
87
|
-
const pkg = req2(`${VERIFY_PACKAGE}/package.json`);
|
|
88
|
-
const binField = pkg.bin;
|
|
89
|
-
const rel = typeof binField === 'string' ? binField : binField?.[VERIFY_BIN];
|
|
90
|
-
if (!rel) return null;
|
|
91
|
-
const abs = resolve(dirname(pkgJsonPath), rel);
|
|
92
|
-
await access(abs, constants.F_OK);
|
|
93
|
-
return abs;
|
|
99
|
+
req = createRequire(join(fromDir, 'noop.js'));
|
|
94
100
|
} catch {
|
|
95
101
|
return null;
|
|
96
102
|
}
|
|
103
|
+
for (const candidate of VERIFY_PACKAGE_CANDIDATES) {
|
|
104
|
+
try {
|
|
105
|
+
const pkgJsonPath = req.resolve(`${candidate}/package.json`);
|
|
106
|
+
const req2 = createRequire(pkgJsonPath);
|
|
107
|
+
const pkg = req2(`${candidate}/package.json`);
|
|
108
|
+
const binField = pkg.bin;
|
|
109
|
+
const rel = typeof binField === 'string' ? binField : binField?.[VERIFY_BIN];
|
|
110
|
+
if (!rel) continue;
|
|
111
|
+
const abs = resolve(dirname(pkgJsonPath), rel);
|
|
112
|
+
await access(abs, constants.F_OK);
|
|
113
|
+
return abs;
|
|
114
|
+
} catch {
|
|
115
|
+
// Candidate not resolvable from here; try the next one.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
/**
|
|
@@ -126,4 +148,4 @@ export async function detectVerify(deps = {}) {
|
|
|
126
148
|
return { installed: false, invocation: null };
|
|
127
149
|
}
|
|
128
150
|
|
|
129
|
-
export { VERIFY_PACKAGE, VERIFY_BIN };
|
|
151
|
+
export { VERIFY_PACKAGE, VERIFY_PACKAGE_LEGACY, VERIFY_PACKAGE_CANDIDATES, VERIFY_BIN };
|
package/src/finalise/index.js
CHANGED
|
@@ -6,10 +6,24 @@
|
|
|
6
6
|
// absent - prompts to install rather than silently skipping the gate (detect.js
|
|
7
7
|
// + install.js).
|
|
8
8
|
|
|
9
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
detectVerify,
|
|
11
|
+
findOnPath,
|
|
12
|
+
resolvePackageBin,
|
|
13
|
+
VERIFY_PACKAGE,
|
|
14
|
+
VERIFY_PACKAGE_LEGACY,
|
|
15
|
+
VERIFY_PACKAGE_CANDIDATES,
|
|
16
|
+
VERIFY_BIN,
|
|
17
|
+
} from './detect.js';
|
|
10
18
|
export { buildVerifyArgs, spawnVerify } from './spawn.js';
|
|
11
19
|
export { promptYesNo, installVerify, resolveAbsentVerify } from './install.js';
|
|
12
|
-
export {
|
|
20
|
+
export {
|
|
21
|
+
loadReport, summariseReport,
|
|
22
|
+
findMockOnlyDeclaredAcs, reportHasMockOnlyDeclared,
|
|
23
|
+
// 0.8.0 slug-train car 4: NV-BL-GATE-01 pull-in of the profile-vs-AC
|
|
24
|
+
// scope-mismatch check into REVIEW.
|
|
25
|
+
findScopeMismatchAcs, reportHasScopeMismatch,
|
|
26
|
+
} from './ingest.js';
|
|
13
27
|
export {
|
|
14
28
|
composeShipWithoutVerifiedRecord,
|
|
15
29
|
nextShipWithoutVerifiedId,
|
package/src/finalise/ingest.js
CHANGED
|
@@ -82,6 +82,17 @@ export function summariseReport(report) {
|
|
|
82
82
|
lines.push(` - ${d.acId ?? '?'} (${d.verdict}): ${d.reason ?? 'declaredMockOnly at pre-flight; verify emitted the honest verdict rather than a false PASS.'}`);
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
// 0.8.0 slug-train car 4: NV-BL-GATE-01 pulls verify's profile-vs-AC
|
|
86
|
+
// scope-mismatch check into REVIEW. When verify emits SCOPE-MISMATCH
|
|
87
|
+
// on perAcVerdicts[] the summary surfaces it so REVIEW / finalise see
|
|
88
|
+
// it. Zero-mismatch reports render nothing (no false-flag noise).
|
|
89
|
+
const scopeMismatches = findScopeMismatchAcs(report);
|
|
90
|
+
if (scopeMismatches.length > 0) {
|
|
91
|
+
lines.push(`scope mismatches (${scopeMismatches.length}):`);
|
|
92
|
+
for (const s of scopeMismatches) {
|
|
93
|
+
lines.push(` - ${s.acId ?? '?'} (${s.verdict}): ${s.reason ?? 'a bound TC is narrower than the AC scope; NV-BL-ADM-03 / NV-BL-GATE-01.'}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
85
96
|
if (report.launchFailure?.message) {
|
|
86
97
|
lines.push(`launch failure: ${report.launchFailure.message}`);
|
|
87
98
|
}
|
|
@@ -117,3 +128,33 @@ export function findMockOnlyDeclaredAcs(report) {
|
|
|
117
128
|
export function reportHasMockOnlyDeclared(report) {
|
|
118
129
|
return findMockOnlyDeclaredAcs(report).length > 0;
|
|
119
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract SCOPE-MISMATCH per-AC verdicts from a verify report. Introduced
|
|
134
|
+
* in the 0.8.0 slug-train (car 4) so REVIEW consumes the same shape via
|
|
135
|
+
* NV-BL-GATE-01. Earlier reports carry no such entries; this returns an
|
|
136
|
+
* empty array on those.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} report
|
|
139
|
+
* @returns {Array<{ acId: string, verdict: string, reason?: string }>}
|
|
140
|
+
*/
|
|
141
|
+
export function findScopeMismatchAcs(report) {
|
|
142
|
+
const perAc = Array.isArray(report?.perAcVerdicts) ? report.perAcVerdicts : [];
|
|
143
|
+
return perAc
|
|
144
|
+
.filter((e) => e && e.verdict === 'SCOPE-MISMATCH')
|
|
145
|
+
.map((e) => ({ acId: e.acId, verdict: e.verdict, reason: e.reason }));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* True when a verify report carries at least one SCOPE-MISMATCH per-AC
|
|
150
|
+
* verdict. NV-BL-GATE-01 (0.8.0 slug-train car 4): the REVIEW gate
|
|
151
|
+
* consumes this so a scope mismatch caught at REVIEW-time fails the
|
|
152
|
+
* gate and returns the FBS to BUILD; the finalise gate reads the same
|
|
153
|
+
* shape as a last-mile refusal.
|
|
154
|
+
*
|
|
155
|
+
* @param {object} report
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
export function reportHasScopeMismatch(report) {
|
|
159
|
+
return findScopeMismatchAcs(report).length > 0;
|
|
160
|
+
}
|
package/src/mcp/tools.js
CHANGED
|
@@ -762,7 +762,12 @@ function resolveTarget(tree, id) {
|
|
|
762
762
|
const entry = (us.acceptanceCriteria ?? []).find((ac) => ac.id === id);
|
|
763
763
|
return entry ? { doc: entry } : null;
|
|
764
764
|
}
|
|
765
|
-
|
|
765
|
+
// 0.8.0 slug-train (w-2026-07-28-012 landmine 3, consumer-path
|
|
766
|
+
// straggler): widened `\d{3}` -> `\d{3,}` in lockstep with rcf-schemas
|
|
767
|
+
// 0.4.3's TC pattern. Under the previous shape a `read TC-1000-x` MCP
|
|
768
|
+
// call silently returned null even when the TC existed -- the same
|
|
769
|
+
// silent-skip class as the CLI `rcf read` path (src/cli/read.js).
|
|
770
|
+
if (/^TC-\d{3,}-[a-z0-9-]+$/.test(id)) {
|
|
766
771
|
const parentId = tree.parentByChild.get(id);
|
|
767
772
|
if (!parentId) return null;
|
|
768
773
|
const ts = tree.byId.get(parentId);
|
|
@@ -1005,7 +1010,10 @@ export function createToolRegistry({ projectRoot, log }) {
|
|
|
1005
1010
|
if (kind === 'tc') {
|
|
1006
1011
|
if (!args.acId) return usageErrorResult('create tc: acId is required');
|
|
1007
1012
|
body.acId = args.acId;
|
|
1008
|
-
|
|
1013
|
+
// 0.8.0 slug-train (w-2026-07-28-012 landmine 4): deriveSlug returns
|
|
1014
|
+
// '' on empty derivation; TC keeps its historical 'tc' fallback
|
|
1015
|
+
// locally rather than letting deriveSlug bake it in.
|
|
1016
|
+
options.slug = args.slug ?? (deriveSlug(body.description) || 'tc');
|
|
1009
1017
|
if (args.testPointer !== undefined) options.testPointer = args.testPointer;
|
|
1010
1018
|
}
|
|
1011
1019
|
|
|
@@ -122,7 +122,7 @@ function formatTraceTable(result) {
|
|
|
122
122
|
lines.push(`${directionLabel}: (none)`);
|
|
123
123
|
} else {
|
|
124
124
|
for (const n of showList) {
|
|
125
|
-
rows.push([String(n.depth), n.id, n.kind,
|
|
125
|
+
rows.push([String(n.depth), n.id, n.kind, n.title ?? '']);
|
|
126
126
|
}
|
|
127
127
|
lines.push(renderTable(rows));
|
|
128
128
|
}
|
|
@@ -139,7 +139,7 @@ function formatBothTraceTable(result) {
|
|
|
139
139
|
lines.push(' (none)');
|
|
140
140
|
} else {
|
|
141
141
|
const rows = [['Depth', 'Id', 'Kind', 'Title']];
|
|
142
|
-
for (const n of ancestors) rows.push([String(n.depth), n.id, n.kind, '']);
|
|
142
|
+
for (const n of ancestors) rows.push([String(n.depth), n.id, n.kind, n.title ?? '']);
|
|
143
143
|
lines.push(renderTable(rows));
|
|
144
144
|
}
|
|
145
145
|
lines.push('');
|
|
@@ -151,7 +151,7 @@ function formatBothTraceTable(result) {
|
|
|
151
151
|
lines.push(' (none)');
|
|
152
152
|
} else {
|
|
153
153
|
const rows = [['Depth', 'Id', 'Kind', 'Title']];
|
|
154
|
-
for (const n of descendants) rows.push([String(n.depth), n.id, n.kind, '']);
|
|
154
|
+
for (const n of descendants) rows.push([String(n.depth), n.id, n.kind, n.title ?? '']);
|
|
155
155
|
lines.push(renderTable(rows));
|
|
156
156
|
}
|
|
157
157
|
return `${lines.join('\n')}\n`;
|
|
@@ -194,10 +194,7 @@ function renderTable(rows) {
|
|
|
194
194
|
return [out[0], sep, ...out.slice(1)].join('\n');
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
function cellTitle(_id) {
|
|
202
|
-
return '';
|
|
203
|
-
}
|
|
197
|
+
// Trace nodes carry `title` as of the paper-cut batch (previously the
|
|
198
|
+
// Title column was always empty). Doc-kind title source: PRD →
|
|
199
|
+
// productName, REQ/US/TAC/ADR/TAD/TS/FBS/CN → title, inline AC/TC →
|
|
200
|
+
// description. Nodes lacking a title render blank.
|
package/src/query/index.js
CHANGED
|
@@ -7,3 +7,7 @@ export { computeImpact, labelFor } from './impact.js';
|
|
|
7
7
|
export { formatTable } from './formatters/table.js';
|
|
8
8
|
export { formatJson } from './formatters/json.js';
|
|
9
9
|
export { formatMermaid } from './formatters/mermaid.js';
|
|
10
|
+
// 0.8.0 slug-train car 3: NV-BL-SR-03 addendum (ruling-sheet item 1)
|
|
11
|
+
// -- traceability / query tools share the refuse-first posture that
|
|
12
|
+
// gates rcf build. Callers wrap their query producer with this.
|
|
13
|
+
export { runWithAdmissibilityGate } from './refuse-on-admissibility.js';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Traceability / query tool refuse-first wrapper (NV-BL-SR-03
|
|
2
|
+
// addendum on ruling-sheet item 1, ratified 2026-08-11).
|
|
3
|
+
//
|
|
4
|
+
// The ruleset's `toolScope` block declares:
|
|
5
|
+
// { chainAdmissibility: true, traceabilityAndQueryTools: true }
|
|
6
|
+
//
|
|
7
|
+
// meaning the same refusal semantics that gate `rcf build` also gate
|
|
8
|
+
// the traceability and query tools. A tool that hides an admissibility
|
|
9
|
+
// failure is the same class of defect as a build that hides one.
|
|
10
|
+
//
|
|
11
|
+
// This module wraps a query result so a REFUSE verdict from
|
|
12
|
+
// `enforceAdmissibility` short-circuits the tool's output. Callers
|
|
13
|
+
// pass the walker tree and the chain's declared ruleset version; on
|
|
14
|
+
// REFUSE the wrapper returns a refusal envelope naming the unresolved
|
|
15
|
+
// findings. On PASS or PASS-WITH-OVERRIDES the query's own result flows
|
|
16
|
+
// through unchanged.
|
|
17
|
+
|
|
18
|
+
import { enforceAdmissibility, getRulesetToolScope } from '#admissibility';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {import('../admissibility/enforce.js').AdmissibilityVerdict} AdmissibilityVerdict
|
|
22
|
+
* @typedef {import('../admissibility/enforce.js').AdmissibilityOverride} AdmissibilityOverride
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} QueryResult
|
|
27
|
+
* @property {'ok' | 'refused-admissibility'} status
|
|
28
|
+
* @property {AdmissibilityVerdict} [admissibility] - always present so callers can log.
|
|
29
|
+
* @property {*} [payload] - the underlying query result on status 'ok'.
|
|
30
|
+
* @property {string} [refusal] - human-readable summary on 'refused-admissibility'.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wrap a query producer with the refuse-first posture. The producer is
|
|
35
|
+
* only called when admissibility passes (or passes-with-overrides);
|
|
36
|
+
* on refusal, its produce function does NOT run and the wrapper
|
|
37
|
+
* returns a refusal envelope naming the unresolved rules.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} args
|
|
40
|
+
* @param {object} args.tree
|
|
41
|
+
* @param {string|null} [args.chainRulesetVersion]
|
|
42
|
+
* @param {AdmissibilityOverride[]} [args.overrides]
|
|
43
|
+
* @param {() => (Promise<*> | *)} args.produce - the underlying query
|
|
44
|
+
* @param {object} [args.opts] - passed through to enforceAdmissibility
|
|
45
|
+
* @returns {Promise<QueryResult>}
|
|
46
|
+
*/
|
|
47
|
+
export async function runWithAdmissibilityGate({
|
|
48
|
+
tree,
|
|
49
|
+
chainRulesetVersion = null,
|
|
50
|
+
overrides = [],
|
|
51
|
+
produce,
|
|
52
|
+
opts = {},
|
|
53
|
+
} = {}) {
|
|
54
|
+
const toolScope = await getRulesetToolScope();
|
|
55
|
+
if (!toolScope.traceabilityAndQueryTools) {
|
|
56
|
+
// Ruleset opted out of tool-scope gating (currently the artefact
|
|
57
|
+
// ships with this on -- item 1 addendum -- but the switch is
|
|
58
|
+
// read at runtime so a future ruleset revision can amend it).
|
|
59
|
+
const payload = await Promise.resolve(produce());
|
|
60
|
+
return { status: 'ok', payload };
|
|
61
|
+
}
|
|
62
|
+
const verdict = await enforceAdmissibility({ tree, chainRulesetVersion, overrides, opts });
|
|
63
|
+
if (verdict.verdict === 'refuse') {
|
|
64
|
+
const ruleIds = [...new Set(verdict.unresolved.map((f) => f.rule).filter(Boolean))].sort();
|
|
65
|
+
return {
|
|
66
|
+
status: 'refused-admissibility',
|
|
67
|
+
admissibility: verdict,
|
|
68
|
+
refusal: `traceability/query tool refused (NV-BL-SR-03 addendum): unresolved admissibility rules [${ruleIds.join(', ')}]. Fix or record a NV-BL-ADM-05 override before re-querying.`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const payload = await Promise.resolve(produce());
|
|
72
|
+
return { status: 'ok', admissibility: verdict, payload };
|
|
73
|
+
}
|
package/src/query/trace.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* @property {string} id
|
|
33
33
|
* @property {string} kind
|
|
34
34
|
* @property {number} depth - 0 for pivot; positive for descendants; negative for ancestors
|
|
35
|
+
* @property {string} title - display title, or '' when the node has none
|
|
35
36
|
*/
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -71,6 +72,41 @@ export function kindOf(tree, id) {
|
|
|
71
72
|
return null;
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Return the display title for an id, or '' when the node carries none.
|
|
77
|
+
* Different doc kinds carry the title on different fields (PRD →
|
|
78
|
+
* productName; inline AC/TC → description; everything else → title). A
|
|
79
|
+
* standalone doc that lacks its title field renders blank rather than
|
|
80
|
+
* throwing; that keeps trace output readable on a partially-authored tree.
|
|
81
|
+
*
|
|
82
|
+
* Inline lookup: AC / TC nodes live inside their parent's inline array
|
|
83
|
+
* (userStory.acceptanceCriteria[] / testSuite.testCases[]), not as
|
|
84
|
+
* standalone entries in `tree.byId`. Resolve via parentByChild.
|
|
85
|
+
*
|
|
86
|
+
* @param {object} tree - walker TreeModel
|
|
87
|
+
* @param {string} id
|
|
88
|
+
* @param {string} kind - as returned by `kindOf`
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
export function titleOf(tree, id, kind) {
|
|
92
|
+
if (kind === 'ac') {
|
|
93
|
+
const usId = tree.parentByChild.get(id);
|
|
94
|
+
const us = usId ? tree.byId.get(usId) : null;
|
|
95
|
+
const ac = us?.acceptanceCriteria?.find((a) => a?.id === id);
|
|
96
|
+
return ac?.description ?? '';
|
|
97
|
+
}
|
|
98
|
+
if (kind === 'tc') {
|
|
99
|
+
const tsId = tree.parentByChild.get(id);
|
|
100
|
+
const ts = tsId ? tree.byId.get(tsId) : null;
|
|
101
|
+
const tc = ts?.testCases?.find((t) => t?.id === id);
|
|
102
|
+
return tc?.description ?? '';
|
|
103
|
+
}
|
|
104
|
+
const doc = tree.byId.get(id);
|
|
105
|
+
if (!doc) return '';
|
|
106
|
+
if (kind === 'prd') return doc.productName ?? '';
|
|
107
|
+
return doc.title ?? '';
|
|
108
|
+
}
|
|
109
|
+
|
|
74
110
|
/**
|
|
75
111
|
* Compute a trace from `id` in the requested direction. Unknown pivot
|
|
76
112
|
* returns `{found: false}`; the handler layer converts this to exit 2.
|
|
@@ -134,7 +170,7 @@ export function computeTrace(tree, {
|
|
|
134
170
|
*/
|
|
135
171
|
function walkForward(tree, pivot, pivotKind, includeCode = false, expandFbsDependents = false) {
|
|
136
172
|
/** @type {TraceNode[]} */
|
|
137
|
-
const nodes = [{ id: pivot, kind: pivotKind, depth: 0 }];
|
|
173
|
+
const nodes = [{ id: pivot, kind: pivotKind, depth: 0, title: titleOf(tree, pivot, pivotKind) }];
|
|
138
174
|
/** @type {TraceEdge[]} */
|
|
139
175
|
const edges = [];
|
|
140
176
|
const seen = new Set([pivot]);
|
|
@@ -158,7 +194,12 @@ function walkForward(tree, pivot, pivotKind, includeCode = false, expandFbsDepen
|
|
|
158
194
|
if (!childKind) continue;
|
|
159
195
|
const nextDepth = curDepth + 1;
|
|
160
196
|
depthById.set(child.id, nextDepth);
|
|
161
|
-
nodes.push({
|
|
197
|
+
nodes.push({
|
|
198
|
+
id: child.id,
|
|
199
|
+
kind: childKind,
|
|
200
|
+
depth: nextDepth,
|
|
201
|
+
title: titleOf(tree, child.id, childKind),
|
|
202
|
+
});
|
|
162
203
|
queue.push(child.id);
|
|
163
204
|
}
|
|
164
205
|
}
|
|
@@ -178,7 +219,7 @@ function walkForward(tree, pivot, pivotKind, includeCode = false, expandFbsDepen
|
|
|
178
219
|
*/
|
|
179
220
|
function walkBack(tree, pivot, pivotKind) {
|
|
180
221
|
/** @type {TraceNode[]} */
|
|
181
|
-
const nodes = [{ id: pivot, kind: pivotKind, depth: 0 }];
|
|
222
|
+
const nodes = [{ id: pivot, kind: pivotKind, depth: 0, title: titleOf(tree, pivot, pivotKind) }];
|
|
182
223
|
/** @type {TraceEdge[]} */
|
|
183
224
|
const edges = [];
|
|
184
225
|
const seen = new Set([pivot]);
|
|
@@ -188,7 +229,7 @@ function walkBack(tree, pivot, pivotKind) {
|
|
|
188
229
|
const k = kindOf(tree, toId);
|
|
189
230
|
if (!k) return;
|
|
190
231
|
seen.add(toId);
|
|
191
|
-
nodes.push({ id: toId, kind: k, depth });
|
|
232
|
+
nodes.push({ id: toId, kind: k, depth, title: titleOf(tree, toId, k) });
|
|
192
233
|
edges.push({ from: fromId, to: toId, kind: edgeKind });
|
|
193
234
|
};
|
|
194
235
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Shared standards ruleset loader (NV-BL-SR-01, NV-BL-SR-02, NV-BL-SR-03).
|
|
2
|
+
//
|
|
3
|
+
// The ruleset is a single machine-readable artefact bundled inside the
|
|
4
|
+
// rcf-lite umbrella package (JSON, camelCase per estate convention). Both
|
|
5
|
+
// build-lite (as a gate) and rcf-define-lite (as an elicitation checklist,
|
|
6
|
+
// from the umbrella release that adds the define payload) consume the
|
|
7
|
+
// identical ruleset from the identical umbrella version.
|
|
8
|
+
//
|
|
9
|
+
// Per NV-BL-SR-02 (ratified 2026-08-11, ruling-sheet items 2 and 6): the
|
|
10
|
+
// ruleset carries no separate semver. Its version IS the rcf-lite umbrella
|
|
11
|
+
// package version. This loader stamps `rulesetVersion` at read time from
|
|
12
|
+
// the umbrella's package.json so a redeploy of the same JSON on a bumped
|
|
13
|
+
// umbrella version reports the new version without a data edit.
|
|
14
|
+
//
|
|
15
|
+
// Ruleset scope covers chain admissibility AND the estate's traceability
|
|
16
|
+
// and query tooling (ratified 2026-08-11, ruling-sheet item 1 addendum).
|
|
17
|
+
// See toolScope on the artefact.
|
|
18
|
+
|
|
19
|
+
import { readFile } from 'node:fs/promises';
|
|
20
|
+
import { dirname, join, resolve } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const rulesetPath = join(here, 'ruleset.json');
|
|
25
|
+
const packageJsonPath = resolve(here, '..', '..', 'package.json');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} RulesetRule
|
|
29
|
+
* @property {string} id
|
|
30
|
+
* @property {string} title
|
|
31
|
+
* @property {boolean} [refuseByDefault]
|
|
32
|
+
* @property {string} [overrideChannel]
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {object} Ruleset
|
|
37
|
+
* @property {string} id
|
|
38
|
+
* @property {string} rulesetVersion - the rcf-lite umbrella package version at load time.
|
|
39
|
+
* @property {RulesetRule[]} admissibilityRules
|
|
40
|
+
* @property {RulesetRule[]} gateRules
|
|
41
|
+
* @property {object} scopeTagVocabulary
|
|
42
|
+
* @property {Array<{ marker: string, caseInsensitive: boolean }>} sourceCommentMarkers
|
|
43
|
+
* @property {Array<{ id: string, title: string, surface: string }>} tcTemplateFamily
|
|
44
|
+
* @property {Array<{ id: string, title: string, kind: string }>} rulingConsistencyChecks
|
|
45
|
+
* @property {{ chainAdmissibility: boolean, traceabilityAndQueryTools: boolean }} toolScope
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
let cachedRuleset = null;
|
|
49
|
+
let cachedUmbrellaVersion = null;
|
|
50
|
+
|
|
51
|
+
async function readJson(path) {
|
|
52
|
+
const raw = await readFile(path, 'utf8');
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The rcf-lite umbrella package version at load time. Used by
|
|
58
|
+
* `getRuleset()` to stamp `rulesetVersion` per NV-BL-SR-02; also exported
|
|
59
|
+
* so consumers can read the umbrella version without opening
|
|
60
|
+
* `package.json` themselves.
|
|
61
|
+
*
|
|
62
|
+
* @returns {Promise<string>}
|
|
63
|
+
*/
|
|
64
|
+
export async function getUmbrellaVersion() {
|
|
65
|
+
if (cachedUmbrellaVersion) return cachedUmbrellaVersion;
|
|
66
|
+
const pkg = await readJson(packageJsonPath);
|
|
67
|
+
if (typeof pkg?.version !== 'string' || pkg.version.length === 0) {
|
|
68
|
+
throw new Error('rcf-lite umbrella package.json is missing a version string');
|
|
69
|
+
}
|
|
70
|
+
cachedUmbrellaVersion = pkg.version;
|
|
71
|
+
return cachedUmbrellaVersion;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load the shared standards ruleset artefact and stamp its `rulesetVersion`
|
|
76
|
+
* from the umbrella package.json. The artefact itself carries no version
|
|
77
|
+
* field (NV-BL-SR-02); read-time stamping is the single source of truth.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} [opts]
|
|
80
|
+
* @param {boolean} [opts.fresh] - bypass the module-scope cache and re-read
|
|
81
|
+
* @returns {Promise<Ruleset>}
|
|
82
|
+
*/
|
|
83
|
+
export async function getRuleset({ fresh = false } = {}) {
|
|
84
|
+
if (!fresh && cachedRuleset) return cachedRuleset;
|
|
85
|
+
const [artefact, umbrellaVersion] = await Promise.all([
|
|
86
|
+
readJson(rulesetPath),
|
|
87
|
+
getUmbrellaVersion(),
|
|
88
|
+
]);
|
|
89
|
+
// Defensive: strip any accidental rulesetVersion baked into the JSON so
|
|
90
|
+
// the umbrella version is authoritative. NV-BL-SR-02 is emphatic about
|
|
91
|
+
// this: a divergence here is a spec drift, not a data field.
|
|
92
|
+
delete artefact.rulesetVersion;
|
|
93
|
+
cachedRuleset = Object.freeze({ ...artefact, rulesetVersion: umbrellaVersion });
|
|
94
|
+
return cachedRuleset;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Detect whether the ruleset version on a chain differs from the shipping
|
|
99
|
+
* ruleset version, and classify the drift for NV-BL-ADM-06 (build-stage
|
|
100
|
+
* refusal) and DL-REQ-VALIDATE-03 (define-stage warning).
|
|
101
|
+
*
|
|
102
|
+
* Additive-only drift (only new rule ids appear on the shipping side) is
|
|
103
|
+
* classified `additive` and warns rather than refuses. Any other version
|
|
104
|
+
* mismatch is classified `behavioural` and refuses at build stage.
|
|
105
|
+
*
|
|
106
|
+
* Same-version comparisons return `{ drift: 'none' }`.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} args
|
|
109
|
+
* @param {string|null|undefined} args.chainRulesetVersion - version the chain declared it was authored against.
|
|
110
|
+
* @param {Ruleset} [args.ruleset] - shipping ruleset; defaults to the loaded artefact.
|
|
111
|
+
* @returns {Promise<{ drift: 'none' | 'additive' | 'behavioural' | 'missing', shippingVersion: string, chainVersion: string | null }>}
|
|
112
|
+
*/
|
|
113
|
+
export async function detectRulesetDrift({ chainRulesetVersion, ruleset } = {}) {
|
|
114
|
+
const shipping = ruleset ?? (await getRuleset());
|
|
115
|
+
const shippingVersion = shipping.rulesetVersion;
|
|
116
|
+
const chainVersion = typeof chainRulesetVersion === 'string' && chainRulesetVersion.length > 0
|
|
117
|
+
? chainRulesetVersion
|
|
118
|
+
: null;
|
|
119
|
+
if (chainVersion === null) {
|
|
120
|
+
return { drift: 'missing', shippingVersion, chainVersion };
|
|
121
|
+
}
|
|
122
|
+
if (chainVersion === shippingVersion) {
|
|
123
|
+
return { drift: 'none', shippingVersion, chainVersion };
|
|
124
|
+
}
|
|
125
|
+
// v1 policy: any version mismatch is treated as behavioural drift for
|
|
126
|
+
// the build stage refusal path (NV-BL-ADM-06). Additive-only drift
|
|
127
|
+
// becomes distinguishable once the umbrella starts landing patch bumps
|
|
128
|
+
// that only add rules; the classifier lives here so define-stage
|
|
129
|
+
// warning (DL-REQ-VALIDATE-03) can consume it without duplicating logic.
|
|
130
|
+
return { drift: 'behavioural', shippingVersion, chainVersion };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Reset the module-scope cache. For tests that mutate the on-disk artefact
|
|
135
|
+
* or the package version.
|
|
136
|
+
*/
|
|
137
|
+
export function resetRulesetCache() {
|
|
138
|
+
cachedRuleset = null;
|
|
139
|
+
cachedUmbrellaVersion = null;
|
|
140
|
+
}
|