rcf-lite 0.8.0 → 0.10.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 +124 -51
- package/README.md +8 -4
- package/bin/rcf.js +147 -53
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/README.md +1 -1
- package/guidance/build-cycle-playbook.md +51 -51
- package/guidance/build-cycle.md +7 -7
- package/guidance/document-model.md +1 -1
- package/guidance/elicitation-playbook.md +29 -29
- package/guidance/harness-template.md +21 -10
- package/guidance/managed/README.md +1 -1
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +20 -9
- package/guidance/manifest.json +1 -1
- package/guidance/overview.md +4 -4
- package/package.json +5 -7
- package/rcf/adrs/adr-008.json +1 -1
- package/rcf/adrs/adr-009.json +4 -4
- package/rcf/adrs/adr-010.json +30 -0
- package/rcf/code-nodes/cn-016.json +1 -1
- package/rcf/code-nodes/cn-019.json +1 -1
- package/rcf/code-nodes/cn-020.json +1 -1
- package/rcf/code-nodes/cn-021.json +1 -1
- package/rcf/code-nodes/cn-022.json +1 -1
- package/rcf/code-nodes/cn-049.json +1 -1
- package/rcf/code-nodes/cn-055.json +1 -1
- package/rcf/code-nodes/cn-057.json +5 -5
- 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-005.json +1 -1
- package/rcf/fbs/fbs-006.json +2 -2
- package/rcf/fbs/fbs-007.json +1 -1
- package/rcf/fbs/fbs-014.json +1 -1
- package/rcf/fbs/fbs-015.json +9 -8
- 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-008.json +1 -1
- package/rcf/requirements/req-009.json +2 -2
- package/rcf/requirements/req-010.json +20 -0
- package/rcf/test-suites/PENDING.md +2 -2
- package/rcf/test-suites/ts-004.json +1 -1
- package/rcf/test-suites/ts-006.json +3 -3
- package/rcf/test-suites/ts-008.json +2 -2
- package/rcf/test-suites/ts-009.json +2 -2
- package/rcf/test-suites/ts-017.json +1 -1
- package/rcf/test-suites/ts-024.json +1 -1
- package/rcf/test-suites/ts-025.json +32 -18
- 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/rcf/user-stories/us-805.json +2 -2
- package/rcf/user-stories/us-901.json +13 -13
- 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 +37 -14
- package/src/build/formatters/markdown.js +9 -9
- package/src/build/mark.js +3 -3
- package/src/build/queue.js +1 -1
- package/src/build/standards-selector.js +52 -0
- package/src/cli/blueprint.js +325 -0
- package/src/cli/browser-verify.js +1 -1
- package/src/cli/build.js +139 -69
- package/src/cli/coverage.js +1 -1
- package/src/cli/create.js +48 -3
- package/src/cli/delete.js +2 -2
- package/src/cli/design.js +10 -10
- package/src/cli/fbs.js +1 -1
- package/src/cli/finalise.js +22 -20
- package/src/cli/help.js +269 -89
- package/src/cli/impact.js +1 -1
- package/src/cli/init.js +20 -5
- package/src/cli/intake.js +3 -3
- package/src/cli/link.js +3 -3
- package/src/cli/preflight.js +2 -2
- package/src/cli/read.js +1 -1
- package/src/cli/req-baseline.js +2 -2
- package/src/cli/req-classify.js +3 -3
- package/src/cli/review.js +1 -1
- package/src/cli/standards.js +127 -0
- package/src/cli/test-suite.js +1 -1
- package/src/cli/trace.js +1 -1
- package/src/cli/ui-baseline.js +3 -3
- package/src/cli/ui-classify.js +4 -4
- package/src/cli/update.js +2 -2
- package/src/cli/validate.js +2 -2
- package/src/cli/view.js +12 -10
- package/src/core/store/ids.js +168 -18
- package/src/core/store/loader.js +27 -16
- package/src/core/store/walker.js +27 -15
- package/src/core/store/writer.js +1 -1
- package/src/deployment/index.js +13 -0
- package/src/deployment/placeholder-detector.js +113 -0
- package/src/design/writer.js +3 -3
- package/src/finalise/detect.js +32 -38
- package/src/finalise/index.js +0 -1
- package/src/finalise/install.js +9 -8
- package/src/finalise/spawn.js +14 -10
- package/src/mcp/tools.js +1 -1
- package/src/query/formatters/table.js +7 -10
- package/src/query/trace.js +45 -4
- package/src/req-baseline/gate.js +1 -1
- package/src/ui-baseline/manifest-writer.js +2 -2
- package/src/verify/cli/cleanup.js +1 -1
- package/src/verify/cli/mcp.js +1 -1
- package/src/verify/cli/provision.js +1 -1
- package/src/verify/cli/report.js +1 -1
- package/src/verify/cli/run.js +1 -1
- package/src/view-supervisor/manifest-writer.js +2 -2
- package/bin/rcf-verify.js +0 -122
- package/src/verify/cli/help.js +0 -56
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Canonical placeholder-credential-shape detector for the deployment-gate
|
|
2
|
+
// class cure (spa-US-1132 / TAC-210, w-2026-08-24-006).
|
|
3
|
+
//
|
|
4
|
+
// The watchpost first production review (w-2026-08-24-005) shipped with
|
|
5
|
+
// RESEND_API_KEY set to a placeholder value; the only login path (magic-link
|
|
6
|
+
// email) was inert; sign-off still said DEPLOYED. The gap was filed as a
|
|
7
|
+
// "quirk" note in status.md rather than blocked at gate time. This module
|
|
8
|
+
// owns the one canonical placeholder shape catalogue rcf-lite ships so
|
|
9
|
+
// downstream projects (via TAC-210 realisations) apply the same detector
|
|
10
|
+
// - extending the ruleset is a rcf-lite minor bump, never a per-project
|
|
11
|
+
// reinvention.
|
|
12
|
+
//
|
|
13
|
+
// The catalogue is deliberately narrow: only credential-shape strings a
|
|
14
|
+
// developer would reasonably type as a stand-in ("YOUR_API_KEY_HERE",
|
|
15
|
+
// "changeme", "xxx", empty). It does NOT try to classify all bad values;
|
|
16
|
+
// entropy checks, provider-specific prefix rules, and format regexes live
|
|
17
|
+
// in project-authored probes. This catalogue is what would have refused
|
|
18
|
+
// the watchpost RESEND_API_KEY at gate time.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} PlaceholderMatch
|
|
22
|
+
* @property {'empty'|'single-dash'|'null-token'|'you-here'|'named-stand-in'|'repeat-char'} pattern
|
|
23
|
+
* The named pattern that matched. Callers surface this on their report
|
|
24
|
+
* so a refusal message can name why.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} PlaceholderVerdict
|
|
29
|
+
* @property {boolean} isPlaceholder true when the value matches a placeholder shape
|
|
30
|
+
* @property {PlaceholderMatch['pattern']} [pattern] the matched pattern name, when isPlaceholder is true
|
|
31
|
+
* @property {string} [reason] a short refusal-message-ready reason string
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// Case-insensitive named stand-ins a developer would reasonably type. The
|
|
35
|
+
// set is intentionally small; adding to it is a rcf-lite minor bump, and
|
|
36
|
+
// the additions land in ONE place downstream projects inherit.
|
|
37
|
+
const NAMED_STAND_INS = new Set([
|
|
38
|
+
'changeme',
|
|
39
|
+
'placeholder',
|
|
40
|
+
'example',
|
|
41
|
+
'test',
|
|
42
|
+
'dummy',
|
|
43
|
+
'fake',
|
|
44
|
+
'todo',
|
|
45
|
+
'tbd',
|
|
46
|
+
'sample',
|
|
47
|
+
'default',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// `YOUR_X_HERE`, `your-key-here`, etc. Case-insensitive.
|
|
51
|
+
const YOU_HERE_RE = /^your[_-].+[_-]here$/i;
|
|
52
|
+
|
|
53
|
+
// Repeated single characters ("xxxx", "----", "0000") of length >= 3, or
|
|
54
|
+
// the literal "xxx+" family common in scaffolds.
|
|
55
|
+
const REPEAT_CHAR_RE = /^(.)\1{2,}$/;
|
|
56
|
+
|
|
57
|
+
// Literal null-token strings that leak in when a resolver hands back a
|
|
58
|
+
// nullish value stringified. `.env` files stringify unset values to the
|
|
59
|
+
// empty string, but a JS resolver that String()s an undefined lands
|
|
60
|
+
// "undefined" in the ship environment - that IS a placeholder, not a
|
|
61
|
+
// live credential.
|
|
62
|
+
const NULL_TOKENS = new Set(['null', 'undefined', 'none']);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detect whether `value` matches a placeholder-shape a credential value
|
|
66
|
+
* would never legitimately hold. Deterministic; case-insensitive against
|
|
67
|
+
* the named-stand-in catalogue and the `YOUR_X_HERE` pattern.
|
|
68
|
+
*
|
|
69
|
+
* The intent is a hard refusal filter, not a heuristic: every match is
|
|
70
|
+
* something a developer typed as a stand-in and forgot to replace. False
|
|
71
|
+
* positives are vanishingly unlikely; the shortest name-family entry is
|
|
72
|
+
* six characters ("dummy" being five is the shortest, and no real credential
|
|
73
|
+
* is a five-character English word); the repeat-char rule requires length
|
|
74
|
+
* >= 3 so a two-character hash prefix does not match.
|
|
75
|
+
*
|
|
76
|
+
* @param {unknown} value the raw credential-field value at ship time
|
|
77
|
+
* @returns {PlaceholderVerdict}
|
|
78
|
+
*/
|
|
79
|
+
export function detectPlaceholderCredentialShape(value) {
|
|
80
|
+
// Non-strings are not credentials the shipped runtime carries; skip.
|
|
81
|
+
if (typeof value !== 'string') {
|
|
82
|
+
return { isPlaceholder: false };
|
|
83
|
+
}
|
|
84
|
+
const raw = value;
|
|
85
|
+
const trimmed = raw.trim();
|
|
86
|
+
if (trimmed.length === 0) {
|
|
87
|
+
return { isPlaceholder: true, pattern: 'empty', reason: 'empty or whitespace-only credential value' };
|
|
88
|
+
}
|
|
89
|
+
if (trimmed === '-' || trimmed === '_') {
|
|
90
|
+
return { isPlaceholder: true, pattern: 'single-dash', reason: `credential value is the single-character stand-in "${trimmed}"` };
|
|
91
|
+
}
|
|
92
|
+
const lower = trimmed.toLowerCase();
|
|
93
|
+
if (NULL_TOKENS.has(lower)) {
|
|
94
|
+
return { isPlaceholder: true, pattern: 'null-token', reason: `credential value is the literal null-token string "${trimmed}"` };
|
|
95
|
+
}
|
|
96
|
+
if (YOU_HERE_RE.test(trimmed)) {
|
|
97
|
+
return { isPlaceholder: true, pattern: 'you-here', reason: `credential value matches the YOUR_X_HERE scaffold pattern: "${trimmed}"` };
|
|
98
|
+
}
|
|
99
|
+
if (NAMED_STAND_INS.has(lower)) {
|
|
100
|
+
return { isPlaceholder: true, pattern: 'named-stand-in', reason: `credential value is the named stand-in "${trimmed}"` };
|
|
101
|
+
}
|
|
102
|
+
if (REPEAT_CHAR_RE.test(trimmed)) {
|
|
103
|
+
return { isPlaceholder: true, pattern: 'repeat-char', reason: `credential value is a repeated-single-character stand-in "${trimmed}"` };
|
|
104
|
+
}
|
|
105
|
+
return { isPlaceholder: false };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The catalogue's stable identifier. Downstream probes surface this on
|
|
110
|
+
* their report so the gate-failure trail names which detector version
|
|
111
|
+
* refused the value. Extending the catalogue bumps this constant.
|
|
112
|
+
*/
|
|
113
|
+
export const PLACEHOLDER_DETECTOR_VERSION = '1.0.0';
|
package/src/design/writer.js
CHANGED
|
@@ -180,7 +180,7 @@ export async function writeMarkComplete({
|
|
|
180
180
|
return rcfError({
|
|
181
181
|
kind: 'usage',
|
|
182
182
|
message: `design --mark-complete: refused - ${fbsId} designStage is missing: ${missing.join(', ')}. `
|
|
183
|
-
+ 'Author the missing artefacts first via `rcf design ' + fbsId + ' journeys add ...`, `nav set ...`, or `theme-a11y set ...`.',
|
|
183
|
+
+ 'Author the missing artefacts first via `rcf define design ' + fbsId + ' journeys add ...`, `nav set ...`, or `theme-a11y set ...`.',
|
|
184
184
|
documentId: fbsId,
|
|
185
185
|
});
|
|
186
186
|
}
|
|
@@ -189,8 +189,8 @@ export async function writeMarkComplete({
|
|
|
189
189
|
return rcfError({
|
|
190
190
|
kind: 'usage',
|
|
191
191
|
message: `design --mark-complete: refused - ${fbsId} designStage.${disagreement.designStagePath} = ${JSON.stringify(disagreement.designValue)} conflicts with uiBaseline.defaults.${disagreement.path} = ${JSON.stringify(disagreement.baselineValue)} and there is no operatorOptOuts entry. `
|
|
192
|
-
+ 'Options: (1) change designStage to match, (2) rcf ui-baseline opt-out --field '
|
|
193
|
-
+ disagreement.path + ' --reason "..." (project override), (3) rcf update '
|
|
192
|
+
+ 'Options: (1) change designStage to match, (2) rcf discover ui-baseline opt-out --field '
|
|
193
|
+
+ disagreement.path + ' --reason "..." (project override), (3) rcf define update '
|
|
194
194
|
+ fbsId + ' --set designStage.' + disagreement.designStagePath + '=... (per-FBS override).',
|
|
195
195
|
documentId: fbsId,
|
|
196
196
|
});
|
package/src/finalise/detect.js
CHANGED
|
@@ -1,36 +1,35 @@
|
|
|
1
|
-
// rcf
|
|
2
|
-
// posture). The finalise gate MUST detect whether `rcf
|
|
3
|
-
// and, when it is absent,
|
|
4
|
-
//
|
|
1
|
+
// rcf verify install detection (spec §8.3, amendment 5 - install-together
|
|
2
|
+
// posture). The finalise gate MUST detect whether the `rcf` bin (which
|
|
3
|
+
// carries the `verify` group) is resolvable and, when it is absent,
|
|
4
|
+
// prompt to install rcf-lite - NEVER silently skip the ship gate (the
|
|
5
|
+
// one behaviour §8.3 explicitly forbids).
|
|
5
6
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// `
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
7
|
+
// The 0.10.0 CLI reorganisation folded the standalone `rcf-verify` bin
|
|
8
|
+
// into the umbrella `rcf` CLI as the `verify` group. Detection now
|
|
9
|
+
// resolves the `rcf` bin exclusively; there is no legacy fallback.
|
|
10
|
+
//
|
|
11
|
+
// Two detection routes, in order:
|
|
12
|
+
// 1. The `rcf` bin on PATH - the umbrella install exposes `rcf` as a
|
|
13
|
+
// global bin (`npm i -g rcf-lite`).
|
|
14
|
+
// 2. Package resolution from the project dir - the local-project
|
|
15
|
+
// install (`npm i rcf-lite` in a repo's node_modules) resolves the
|
|
16
|
+
// `rcf-lite` package and its `bin/rcf.js` entry point.
|
|
17
|
+
// Either route yields a concrete invocation the finalise spawn
|
|
18
|
+
// (spawn.js) uses verbatim. A miss returns { installed:false } and the
|
|
19
|
+
// caller enters the prompt-or-explicit-flag path.
|
|
18
20
|
|
|
19
21
|
import { access, constants } from 'node:fs/promises';
|
|
20
22
|
import { createRequire } from 'node:module';
|
|
21
23
|
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
22
24
|
|
|
23
|
-
// VERIFY_PACKAGE is the
|
|
24
|
-
//
|
|
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.
|
|
25
|
+
// VERIFY_PACKAGE is the install target and the name shown to operators
|
|
26
|
+
// in absent-verify messaging.
|
|
27
27
|
const VERIFY_PACKAGE = 'rcf-lite';
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const VERIFY_BIN = 'rcf-verify';
|
|
28
|
+
const VERIFY_PACKAGE_CANDIDATES = [VERIFY_PACKAGE];
|
|
29
|
+
const VERIFY_BIN = 'rcf';
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
|
-
* The concrete way to launch rcf
|
|
32
|
+
* The concrete way to launch the `rcf` bin as a fresh subprocess.
|
|
34
33
|
* - `{ command: '<abs-bin>', prefixArgs: [] }` for a PATH / shim bin
|
|
35
34
|
* (its own shebang runs it).
|
|
36
35
|
* - `{ command: process.execPath, prefixArgs: ['<abs-entry.js>'] }` for a
|
|
@@ -43,8 +42,8 @@ const VERIFY_BIN = 'rcf-verify';
|
|
|
43
42
|
*/
|
|
44
43
|
|
|
45
44
|
/**
|
|
46
|
-
* Scan PATH for an executable named `rcf
|
|
47
|
-
* `.
|
|
45
|
+
* Scan PATH for an executable named `rcf` (plus the Windows `.cmd` /
|
|
46
|
+
* `.exe` shim variants). Returns the first absolute path that exists
|
|
48
47
|
* and is executable, or null.
|
|
49
48
|
*
|
|
50
49
|
* @param {string} name
|
|
@@ -78,16 +77,11 @@ export async function findOnPath(name, { env = process.env } = {}) {
|
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
/**
|
|
81
|
-
* Resolve the rcf
|
|
82
|
-
* the normal node_modules resolution the caller's project sees. Returns
|
|
83
|
-
* absolute path to `bin/rcf
|
|
80
|
+
* Resolve the `rcf` bin entry point from a starting directory, following
|
|
81
|
+
* the normal node_modules resolution the caller's project sees. Returns
|
|
82
|
+
* the absolute path to `bin/rcf.js`, or null if the package is not
|
|
84
83
|
* installed / resolvable from there.
|
|
85
84
|
*
|
|
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.
|
|
90
|
-
*
|
|
91
85
|
* @param {string} fromDir - directory to resolve from (the project root / cwd)
|
|
92
86
|
* @returns {Promise<string|null>}
|
|
93
87
|
*/
|
|
@@ -119,9 +113,9 @@ export async function resolvePackageBin(fromDir) {
|
|
|
119
113
|
}
|
|
120
114
|
|
|
121
115
|
/**
|
|
122
|
-
* Detect whether rcf
|
|
123
|
-
*
|
|
124
|
-
*
|
|
116
|
+
* Detect whether the `rcf` bin (carrying the `verify` group) is
|
|
117
|
+
* resolvable and, if so, how to launch it. Deps are injectable so the
|
|
118
|
+
* finalise gate can be exercised without a real installed rcf-lite.
|
|
125
119
|
*
|
|
126
120
|
* @param {object} [deps]
|
|
127
121
|
* @param {string} [deps.cwd] - project dir to resolve a local install from
|
|
@@ -148,4 +142,4 @@ export async function detectVerify(deps = {}) {
|
|
|
148
142
|
return { installed: false, invocation: null };
|
|
149
143
|
}
|
|
150
144
|
|
|
151
|
-
export { VERIFY_PACKAGE,
|
|
145
|
+
export { VERIFY_PACKAGE, VERIFY_PACKAGE_CANDIDATES, VERIFY_BIN };
|
package/src/finalise/index.js
CHANGED
package/src/finalise/install.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Absent-verify handling (spec §8.3, amendment 5). When `rcf
|
|
2
|
-
//
|
|
1
|
+
// Absent-verify handling (spec §8.3, amendment 5). When the `rcf` bin
|
|
2
|
+
// (which now carries the `verify` group post-0.10.0) is not resolvable,
|
|
3
|
+
// the finalise gate MUST NOT silently skip - that recreates the
|
|
3
4
|
// false-confidence failure the whole programme exists to prevent. The
|
|
4
5
|
// sanctioned paths are exactly two, and nothing else:
|
|
5
6
|
// - interactive TTY -> prompt the operator to install now (y/N);
|
|
@@ -35,8 +36,8 @@ export function promptYesNo(question, { input = process.stdin, output = process.
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
|
-
* Install rcf-
|
|
39
|
-
*
|
|
39
|
+
* Install rcf-lite globally so the `rcf verify` group becomes
|
|
40
|
+
* resolvable. Streams npm's output to the operator. Returns the install
|
|
40
41
|
* process exit code.
|
|
41
42
|
*
|
|
42
43
|
* @param {object} [deps]
|
|
@@ -77,11 +78,11 @@ export async function resolveAbsentVerify({ installFlag, isTty }, io, deps = {})
|
|
|
77
78
|
let wantsInstall = false;
|
|
78
79
|
if (installFlag) {
|
|
79
80
|
// Explicit flag: the sanctioned non-interactive install path.
|
|
80
|
-
io.stdout.write(`[finalise] rcf
|
|
81
|
+
io.stdout.write(`[finalise] the rcf CLI (with verify) not found; installing ${VERIFY_PACKAGE} (--install-verify)...\n`);
|
|
81
82
|
wantsInstall = true;
|
|
82
83
|
} else if (isTty) {
|
|
83
84
|
// Interactive: prompt. Declining aborts (never a silent skip).
|
|
84
|
-
io.stderr.write(`[finalise] rcf-
|
|
85
|
+
io.stderr.write(`[finalise] rcf-lite is not installed. The ship gate cannot run without it.\n`);
|
|
85
86
|
wantsInstall = await prompt(`Install ${VERIFY_PACKAGE} now? [y/N]`, {
|
|
86
87
|
input: io.input, output: io.output,
|
|
87
88
|
});
|
|
@@ -89,7 +90,7 @@ export async function resolveAbsentVerify({ installFlag, isTty }, io, deps = {})
|
|
|
89
90
|
return {
|
|
90
91
|
action: 'abort',
|
|
91
92
|
code: 4,
|
|
92
|
-
reason: `finalise refused: rcf-
|
|
93
|
+
reason: `finalise refused: rcf-lite is required and install was declined. `
|
|
93
94
|
+ `Install it (npm i -g ${VERIFY_PACKAGE}) or re-run with --install-verify. `
|
|
94
95
|
+ `The ship gate is never skipped.`,
|
|
95
96
|
};
|
|
@@ -100,7 +101,7 @@ export async function resolveAbsentVerify({ installFlag, isTty }, io, deps = {})
|
|
|
100
101
|
return {
|
|
101
102
|
action: 'abort',
|
|
102
103
|
code: 4,
|
|
103
|
-
reason: `finalise refused: rcf-
|
|
104
|
+
reason: `finalise refused: rcf-lite is not installed and no TTY is available to prompt. `
|
|
104
105
|
+ `Install it (npm i -g ${VERIFY_PACKAGE}) or re-run with --install-verify. `
|
|
105
106
|
+ `The ship gate is never silently skipped.`,
|
|
106
107
|
};
|
package/src/finalise/spawn.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Fresh-subprocess invocation of `rcf
|
|
2
|
-
// load-bearing independence guarantee: build-lite's finalise step
|
|
3
|
-
// verify as a SEPARATE OS PROCESS with the isolation env - it
|
|
4
|
-
// verify's engine in-process, because the verifier
|
|
5
|
-
// cold session with zero build context (§9).
|
|
6
|
-
// gate; findings flow back via the
|
|
1
|
+
// Fresh-subprocess invocation of `rcf verify run` (spec §8.2). This is
|
|
2
|
+
// the load-bearing independence guarantee: build-lite's finalise step
|
|
3
|
+
// launches verify as a SEPARATE OS PROCESS with the isolation env - it
|
|
4
|
+
// MUST NOT import verify's engine in-process, because the verifier
|
|
5
|
+
// agent must start from a cold session with zero build context (§9).
|
|
6
|
+
// The subprocess exit code is the gate; findings flow back via the
|
|
7
|
+
// --out report file, never stdout scraping.
|
|
7
8
|
//
|
|
8
9
|
// The isolation env (core's ISOLATION_RECIPE, §7.3) is layered onto the child
|
|
9
10
|
// env here so the recipe travels with the process boundary exactly as §8.2
|
|
@@ -14,9 +15,11 @@ import { spawn } from 'node:child_process';
|
|
|
14
15
|
import { isolationEnv } from '#core/isolation';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* Assemble the `rcf
|
|
18
|
-
* Deterministic and pure so it is directly unit-testable; the exact
|
|
19
|
-
* the §8.2 invocation contract.
|
|
18
|
+
* Assemble the `rcf verify run` argument vector from finalise options.
|
|
19
|
+
* Deterministic and pure so it is directly unit-testable; the exact
|
|
20
|
+
* shape is the §8.2 invocation contract. The 0.10.0 CLI reorganisation
|
|
21
|
+
* prepends the `verify` group token; the run-verb arguments below are
|
|
22
|
+
* otherwise unchanged.
|
|
20
23
|
*
|
|
21
24
|
* @param {object} opts
|
|
22
25
|
* @param {string} opts.repo - RCF chain source (the project root)
|
|
@@ -32,6 +35,7 @@ import { isolationEnv } from '#core/isolation';
|
|
|
32
35
|
*/
|
|
33
36
|
export function buildVerifyArgs(opts) {
|
|
34
37
|
const args = [
|
|
38
|
+
'verify',
|
|
35
39
|
'run',
|
|
36
40
|
'--repo', opts.repo,
|
|
37
41
|
'--profile', opts.profile,
|
|
@@ -47,7 +51,7 @@ export function buildVerifyArgs(opts) {
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
|
-
* Spawn rcf
|
|
54
|
+
* Spawn `rcf verify run` as a fresh subprocess and resolve with its exit code.
|
|
51
55
|
* stdio is inherited by default so the operator sees verify's live progress
|
|
52
56
|
* and its verdict line; the report artifact is read separately by the caller.
|
|
53
57
|
*
|
package/src/mcp/tools.js
CHANGED
|
@@ -69,7 +69,7 @@ const ISSUE_SCHEMA = {
|
|
|
69
69
|
// success branch stays the verbatim envelope transcription (D8/D9).
|
|
70
70
|
const ERROR_PAYLOAD_SCHEMA = {
|
|
71
71
|
type: 'object',
|
|
72
|
-
description: 'Tool execution error payload (accompanies isError: true): the same issue shape rcf validate --json ships',
|
|
72
|
+
description: 'Tool execution error payload (accompanies isError: true): the same issue shape rcf define validate --json ships',
|
|
73
73
|
properties: {
|
|
74
74
|
ok: { const: false },
|
|
75
75
|
errors: { type: 'array', items: ISSUE_SCHEMA },
|
|
@@ -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/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
|
|
package/src/req-baseline/gate.js
CHANGED
|
@@ -73,7 +73,7 @@ export function formatStage1RefusalMessage({ fbsId, usId, reqId, openCandidates
|
|
|
73
73
|
const short = shortDescription(c.canonicalText);
|
|
74
74
|
lines.push(` ${c.baselineKey}: ${short}`);
|
|
75
75
|
}
|
|
76
|
-
lines.push(` Resolve: rcf req-baseline sweep --req ${reqId}`);
|
|
76
|
+
lines.push(` Resolve: rcf discover req-baseline sweep --req ${reqId}`);
|
|
77
77
|
return lines.join('\n');
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -210,14 +210,14 @@ export async function writeUiBaselineOptOut({
|
|
|
210
210
|
if (!priorBaseline) {
|
|
211
211
|
return rcfError({
|
|
212
212
|
kind: 'usage',
|
|
213
|
-
message: 'ui-baseline opt-out: no uiBaseline record exists yet; run \'rcf ui-baseline init\' first.',
|
|
213
|
+
message: 'ui-baseline opt-out: no uiBaseline record exists yet; run \'rcf discover ui-baseline init\' first.',
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
const normalisedField = String(field).startsWith('defaults.') ? String(field).slice('defaults.'.length) : String(field);
|
|
217
217
|
if (typeof isKnownField === 'function' && !isKnownField(normalisedField)) {
|
|
218
218
|
return rcfError({
|
|
219
219
|
kind: 'usage',
|
|
220
|
-
message: `ui-baseline opt-out: unknown baseline field '${normalisedField}' (see 'rcf ui-baseline show' for the field list).`,
|
|
220
|
+
message: `ui-baseline opt-out: unknown baseline field '${normalisedField}' (see 'rcf discover ui-baseline show' for the field list).`,
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
223
|
const isoNow = now.toISOString();
|
|
@@ -9,7 +9,7 @@ import { formatError, isRcfError } from '#core/errors';
|
|
|
9
9
|
|
|
10
10
|
import { readProvisionFile, cleanup } from '../provision/index.js';
|
|
11
11
|
|
|
12
|
-
export const HELP = `Usage: rcf
|
|
12
|
+
export const HELP = `Usage: rcf verify cleanup --provision <file> [--url <app-url>]
|
|
13
13
|
|
|
14
14
|
Tear down artefacts recorded in the --provision file (all prefixed
|
|
15
15
|
'zzverify-'). Reports what was removed and what could not be.
|
package/src/verify/cli/mcp.js
CHANGED
|
@@ -14,7 +14,7 @@ import { createToolRegistry } from '../mcp/tools.js';
|
|
|
14
14
|
|
|
15
15
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
|
|
17
|
-
export const HELP = `Usage: rcf
|
|
17
|
+
export const HELP = `Usage: rcf verify mcp [options]
|
|
18
18
|
|
|
19
19
|
Serve verify over the Model Context Protocol (local stdio). Exposes one tool,
|
|
20
20
|
rcf_verify_run, over the same in-process engine the CLI uses. stdout carries
|
|
@@ -11,7 +11,7 @@ import { formatError, isRcfError } from '#core/errors';
|
|
|
11
11
|
import { readChain } from '../chain/index.js';
|
|
12
12
|
import { runProvisioning } from '../provision/index.js';
|
|
13
13
|
|
|
14
|
-
export const HELP = `Usage: rcf
|
|
14
|
+
export const HELP = `Usage: rcf verify provision --repo <path> --url <app-url> --provision <file>
|
|
15
15
|
|
|
16
16
|
Stand up prerequisite accounts / sandboxes / seed data for a later run.
|
|
17
17
|
Credentials are written to the --provision FILE only, never echoed.
|
package/src/verify/cli/report.js
CHANGED
|
@@ -11,7 +11,7 @@ import { formatError, isRcfError } from '#core/errors';
|
|
|
11
11
|
import { parseReport } from '../report/index.js';
|
|
12
12
|
import { renderReport } from '../report/renderer.js';
|
|
13
13
|
|
|
14
|
-
export const HELP = `Usage: rcf
|
|
14
|
+
export const HELP = `Usage: rcf verify report <report-path> [--json]
|
|
15
15
|
|
|
16
16
|
Re-render a prior report artifact. Default output is the human-readable
|
|
17
17
|
summary; --json re-emits the parsed artifact as JSON.
|
package/src/verify/cli/run.js
CHANGED
|
@@ -29,7 +29,7 @@ const OPTION_SPEC = {
|
|
|
29
29
|
help: { type: 'boolean' },
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
export const HELP = `Usage: rcf
|
|
32
|
+
export const HELP = `Usage: rcf verify run [options]
|
|
33
33
|
|
|
34
34
|
Launch a fresh-context adversarial verifier against a running app and emit a
|
|
35
35
|
structured verdict stamped with the runtime it ran against.
|
|
@@ -19,13 +19,13 @@ export const DEFAULT_PID_PATH = '.rcf/view-server.pid';
|
|
|
19
19
|
*/
|
|
20
20
|
export const viewServerGitignoreEntry = Object.freeze({
|
|
21
21
|
path: '.rcf/view-server.pid',
|
|
22
|
-
owner: 'rcf view --detach (view-server pid file)',
|
|
22
|
+
owner: 'rcf audit view --detach (view-server pid file)',
|
|
23
23
|
since: '0.7.0',
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
export const viewServerLogGitignoreEntry = Object.freeze({
|
|
27
27
|
path: '.rcf/view-server.log',
|
|
28
|
-
owner: 'rcf view --detach (supervisor log)',
|
|
28
|
+
owner: 'rcf audit view --detach (supervisor log)',
|
|
29
29
|
since: '0.7.0',
|
|
30
30
|
});
|
|
31
31
|
|