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,325 @@
|
|
|
1
|
+
// `rcf blueprint <verb>` CLI landing.
|
|
2
|
+
//
|
|
3
|
+
// Verbs:
|
|
4
|
+
// add apply a blueprint (with optional --resolve)
|
|
5
|
+
// list projection over manifest.blueprints[]
|
|
6
|
+
// remove remove an applied blueprint
|
|
7
|
+
// supersede scaffold a project ADR + record a resolutions[] entry
|
|
8
|
+
// diff side-by-side view of applied blueprints' scope:global
|
|
9
|
+
// ADRs on a topic
|
|
10
|
+
|
|
11
|
+
import { parseArgs } from 'node:util';
|
|
12
|
+
|
|
13
|
+
import { isRcfError } from '#core/errors';
|
|
14
|
+
import { walkTree } from '#core/store';
|
|
15
|
+
import { findProjectRoot } from '../view/index.js';
|
|
16
|
+
import {
|
|
17
|
+
applyBlueprint,
|
|
18
|
+
diffBlueprintTopic,
|
|
19
|
+
listBlueprints,
|
|
20
|
+
removeBlueprint,
|
|
21
|
+
renderDiff,
|
|
22
|
+
supersedeBlueprintTopic,
|
|
23
|
+
} from '../blueprint/index.js';
|
|
24
|
+
import { conflictReportJson, renderConflictReport } from '../blueprint/conflicts.js';
|
|
25
|
+
|
|
26
|
+
export const HELP = `Usage: rcf blueprint <verb> [options]
|
|
27
|
+
|
|
28
|
+
Verbs:
|
|
29
|
+
add <source> Apply a blueprint from a source directory
|
|
30
|
+
(Phase 1: local path; the registry / git-ref
|
|
31
|
+
resolver is a Phase 2 concern). Writes an entry
|
|
32
|
+
to manifest.blueprints[] and copies namespaced
|
|
33
|
+
contributions into the tree.
|
|
34
|
+
list List every applied blueprint (slug, version,
|
|
35
|
+
appliedAt, contributionCount).
|
|
36
|
+
remove <slug> Remove an applied blueprint. Refuses when any
|
|
37
|
+
project-authored doc references a contribution
|
|
38
|
+
id; prints the referring docs and exits 3.
|
|
39
|
+
supersede <topic> [--incoming <source>]
|
|
40
|
+
Author a project-level ADR that supersedes the
|
|
41
|
+
conflict pair on <topic> (one applied blueprint
|
|
42
|
+
ADR + one incoming blueprint ADR named via
|
|
43
|
+
--incoming <source>), and record a
|
|
44
|
+
manifest.resolutions[] entry so the conflict
|
|
45
|
+
detector honours the resolution when the
|
|
46
|
+
operator re-runs \`rcf blueprint add <source>\`.
|
|
47
|
+
--incoming is required when the topic has fewer
|
|
48
|
+
than two applied scope:global ADRs (the
|
|
49
|
+
refused-add state) and is silently accepted
|
|
50
|
+
when already >= 2 are applied. Both blueprint
|
|
51
|
+
ADRs co-reside on disk as superseded history.
|
|
52
|
+
diff <topic> Side-by-side view of every applied blueprint's
|
|
53
|
+
scope:global ADR on <topic>: id, path, title,
|
|
54
|
+
status, decision. Read-only.
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--namespace <slug> Override the blueprint's default namespace
|
|
58
|
+
(defaults to the blueprint's slug).
|
|
59
|
+
--resolve <t=project:ADR-id>
|
|
60
|
+
(add only) Declare a resolution on this add.
|
|
61
|
+
Repeatable per conflicted topic. Records a
|
|
62
|
+
manifest.resolutions[] entry before conflict
|
|
63
|
+
detection runs, so a would-be conflict on the
|
|
64
|
+
topic is honoured. resolvedByAdrId must be a
|
|
65
|
+
well-formed ADR id; the referenced ADR should
|
|
66
|
+
already exist on the project (this verb does
|
|
67
|
+
not scaffold one -- use \`supersede\` for that).
|
|
68
|
+
--reason <text> (supersede, add --resolve) Optional operator
|
|
69
|
+
note attached to the manifest.resolutions[]
|
|
70
|
+
record.
|
|
71
|
+
--json (add only) Emit the result (or conflict
|
|
72
|
+
report) as a machine-readable JSON object.
|
|
73
|
+
Exit code is unchanged (0 on apply, 3 on
|
|
74
|
+
conflict).
|
|
75
|
+
--dry-run Print intended writes without executing.
|
|
76
|
+
--quiet Suppress non-error stdout.
|
|
77
|
+
--help Print this help.
|
|
78
|
+
|
|
79
|
+
Composition and namespacing:
|
|
80
|
+
|
|
81
|
+
Blueprint-contributed doc ids are namespaced by the blueprint's slug.
|
|
82
|
+
REQ / US / PRD / BS / TAD / TS: slug PREFIX (spa-REQ-001).
|
|
83
|
+
ADR / TAC / FBS / CN: slug SUFFIX (ADR-005-spa).
|
|
84
|
+
Two blueprints both contributing a scope:global ADR on the same topic
|
|
85
|
+
is a genuine conflict: rcf blueprint add refuses and prints both
|
|
86
|
+
sides, plus four resolution paths (adopt incoming, keep existing,
|
|
87
|
+
supersede via project ADR, or declare on the add itself via
|
|
88
|
+
--resolve).
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const OPTION_SPEC = {
|
|
92
|
+
namespace: { type: 'string' },
|
|
93
|
+
resolve: { type: 'string', multiple: true },
|
|
94
|
+
reason: { type: 'string' },
|
|
95
|
+
incoming: { type: 'string' },
|
|
96
|
+
json: { type: 'boolean' },
|
|
97
|
+
'dry-run': { type: 'boolean' },
|
|
98
|
+
quiet: { type: 'boolean' },
|
|
99
|
+
help: { type: 'boolean' },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string[]} argv - argv slice after `blueprint`
|
|
104
|
+
* @param {object} [deps]
|
|
105
|
+
* @returns {Promise<number>}
|
|
106
|
+
*/
|
|
107
|
+
export async function main(argv, deps = {}) {
|
|
108
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
109
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
110
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
111
|
+
const now = deps.now ?? new Date();
|
|
112
|
+
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
stderr.write(`[error] ${err.message}\n`);
|
|
118
|
+
stderr.write(HELP);
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
if (parsed.values.help || parsed.positionals.length === 0) {
|
|
122
|
+
stdout.write(HELP);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const verb = parsed.positionals[0];
|
|
126
|
+
const rest = parsed.positionals.slice(1);
|
|
127
|
+
|
|
128
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
129
|
+
if (!projectRoot) {
|
|
130
|
+
stderr.write('[error] no rcf/ tree found in this directory or any ancestor.\n');
|
|
131
|
+
return 2;
|
|
132
|
+
}
|
|
133
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
134
|
+
if (errors.length > 0 && verb !== 'list') {
|
|
135
|
+
// Tree errors are non-fatal for list; every other verb needs a clean tree.
|
|
136
|
+
for (const e of errors) stderr.write(`[tree] ${e.kind}: ${e.message}\n`);
|
|
137
|
+
return 2;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (verb === 'add') {
|
|
141
|
+
if (rest.length === 0) {
|
|
142
|
+
stderr.write('[error] blueprint add: missing <source>\n');
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
const source = rest[0];
|
|
146
|
+
const resolveDeclarations = parseResolveOptions(parsed.values.resolve, parsed.values.reason);
|
|
147
|
+
if (resolveDeclarations.error) {
|
|
148
|
+
stderr.write(`[error] blueprint add: ${resolveDeclarations.error}\n`);
|
|
149
|
+
return 2;
|
|
150
|
+
}
|
|
151
|
+
const result = await applyBlueprint({
|
|
152
|
+
projectRoot, tree, source,
|
|
153
|
+
namespaceOverride: parsed.values.namespace,
|
|
154
|
+
resolveDeclarations: resolveDeclarations.value,
|
|
155
|
+
now,
|
|
156
|
+
dryRun: parsed.values['dry-run'] === true,
|
|
157
|
+
});
|
|
158
|
+
// Surface any writer-side warnings (currently only
|
|
159
|
+
// duplicate-topic --resolve dedupe). Warnings do not change the
|
|
160
|
+
// exit code; they land on stderr so the human sees them alongside
|
|
161
|
+
// the applied line on stdout.
|
|
162
|
+
if (result && !isRcfError(result) && Array.isArray(result.warnings)) {
|
|
163
|
+
for (const w of result.warnings) {
|
|
164
|
+
if (w.kind === 'duplicateResolveTopic' && Array.isArray(w.topics)) {
|
|
165
|
+
for (const t of w.topics) {
|
|
166
|
+
stderr.write(`[warn] blueprint add: duplicate --resolve for topic '${t}'; keeping the first declaration only.\n`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (isRcfError(result)) {
|
|
172
|
+
if (parsed.values.json) {
|
|
173
|
+
stderr.write(`${JSON.stringify({ refused: true, error: { kind: result.kind, message: result.message } })}\n`);
|
|
174
|
+
} else {
|
|
175
|
+
stderr.write(`[error] blueprint add: ${result.message}\n`);
|
|
176
|
+
}
|
|
177
|
+
return 2;
|
|
178
|
+
}
|
|
179
|
+
if (result.conflicts && result.conflicts.length > 0) {
|
|
180
|
+
if (parsed.values.json) {
|
|
181
|
+
stdout.write(`${JSON.stringify(conflictReportJson(result.conflicts), null, 2)}\n`);
|
|
182
|
+
} else {
|
|
183
|
+
stderr.write(renderConflictReport(result.conflicts));
|
|
184
|
+
}
|
|
185
|
+
return 3;
|
|
186
|
+
}
|
|
187
|
+
if (parsed.values.json) {
|
|
188
|
+
stdout.write(`${JSON.stringify({
|
|
189
|
+
refused: false,
|
|
190
|
+
applied: result.applied === true,
|
|
191
|
+
alreadyApplied: result.alreadyApplied === true,
|
|
192
|
+
slug: result.slug,
|
|
193
|
+
version: result.version,
|
|
194
|
+
contributionCount: Array.isArray(result.contributions) ? result.contributions.length : 0,
|
|
195
|
+
})}\n`);
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
if (result.alreadyApplied) {
|
|
199
|
+
if (!parsed.values.quiet) stdout.write(`[blueprint] '${result.slug}' already applied at ${result.version}; no changes.\n`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
if (!parsed.values.quiet) {
|
|
203
|
+
stdout.write(`[blueprint] applied '${result.slug}' at ${result.version} (${result.contributions.length} contribution(s)).\n`);
|
|
204
|
+
}
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (verb === 'list') {
|
|
209
|
+
const rows = listBlueprints(tree);
|
|
210
|
+
if (rows.length === 0) {
|
|
211
|
+
if (!parsed.values.quiet) stdout.write('[blueprint] no blueprints applied on this project.\n');
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
for (const row of rows) {
|
|
215
|
+
stdout.write(`${row.slug}\t${row.version}\t${row.appliedAt}\t${row.contributionCount} contribution(s)\n`);
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (verb === 'remove') {
|
|
221
|
+
if (rest.length === 0) {
|
|
222
|
+
stderr.write('[error] blueprint remove: missing <slug>\n');
|
|
223
|
+
return 2;
|
|
224
|
+
}
|
|
225
|
+
const slug = rest[0];
|
|
226
|
+
const result = await removeBlueprint({
|
|
227
|
+
projectRoot, tree, slug, dryRun: parsed.values['dry-run'] === true,
|
|
228
|
+
});
|
|
229
|
+
if (isRcfError(result)) {
|
|
230
|
+
stderr.write(`[error] blueprint remove: ${result.message}\n`);
|
|
231
|
+
return 2;
|
|
232
|
+
}
|
|
233
|
+
if (!result.removed) {
|
|
234
|
+
stderr.write(`[blueprint] remove refused: ${result.referringDocs.length} referring doc(s):\n`);
|
|
235
|
+
for (const r of result.referringDocs) {
|
|
236
|
+
stderr.write(` ${r.docId} references ${r.matchedId}\n`);
|
|
237
|
+
}
|
|
238
|
+
stderr.write('resolve by unbinding the references, then re-run.\n');
|
|
239
|
+
return 3;
|
|
240
|
+
}
|
|
241
|
+
if (!parsed.values.quiet) stdout.write(`[blueprint] removed '${result.slug}' (${result.deletedPaths.length} file(s) deleted).\n`);
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (verb === 'supersede') {
|
|
246
|
+
if (rest.length === 0) {
|
|
247
|
+
stderr.write('[error] blueprint supersede: missing <topic>\n');
|
|
248
|
+
return 2;
|
|
249
|
+
}
|
|
250
|
+
const topic = rest[0];
|
|
251
|
+
const result = await supersedeBlueprintTopic({
|
|
252
|
+
projectRoot, tree, topic,
|
|
253
|
+
incomingSource: parsed.values.incoming,
|
|
254
|
+
now,
|
|
255
|
+
dryRun: parsed.values['dry-run'] === true,
|
|
256
|
+
reason: parsed.values.reason,
|
|
257
|
+
});
|
|
258
|
+
if (isRcfError(result)) {
|
|
259
|
+
stderr.write(`[error] blueprint supersede: ${result.message}\n`);
|
|
260
|
+
return 2;
|
|
261
|
+
}
|
|
262
|
+
if (!parsed.values.quiet) {
|
|
263
|
+
stdout.write(`[blueprint] superseded topic '${result.topic}' via ${result.resolvedByAdrId} at ${result.resolvedByAdrPath}.\n`);
|
|
264
|
+
stdout.write(`[blueprint] resolution recorded as ${result.resolutionId}; superseded: ${result.supersedes.map((s) => `${s.adrId} (blueprint ${s.slug})`).join(', ')}.\n`);
|
|
265
|
+
stdout.write(`[blueprint] edit ${result.resolvedByAdrPath} to fill out the operator's ruling context / decision / consequences.\n`);
|
|
266
|
+
}
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (verb === 'diff') {
|
|
271
|
+
if (rest.length === 0) {
|
|
272
|
+
stderr.write('[error] blueprint diff: missing <topic>\n');
|
|
273
|
+
return 2;
|
|
274
|
+
}
|
|
275
|
+
const topic = rest[0];
|
|
276
|
+
const result = diffBlueprintTopic({ tree, topic });
|
|
277
|
+
stdout.write(renderDiff(result));
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
stderr.write(`[error] blueprint: unknown verb '${verb}'\n`);
|
|
282
|
+
stderr.write(HELP);
|
|
283
|
+
return 2;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Parse `--resolve <topic>=project:<ADR-id>` occurrences into a list
|
|
288
|
+
* of declarations, attaching an optional shared `reason` (from the
|
|
289
|
+
* single `--reason` flag) to every declaration on this add. Returns
|
|
290
|
+
* `{ value: Array }` on success or `{ error: string }` on any
|
|
291
|
+
* mis-shaped input.
|
|
292
|
+
*
|
|
293
|
+
* The `--reason` flag is singular per invocation because a single
|
|
294
|
+
* add's resolutions typically share one operator justification
|
|
295
|
+
* (`--reason "Project auth model stands over both blueprint
|
|
296
|
+
* defaults."`); if per-topic reasons are needed later, the flag can
|
|
297
|
+
* grow a `--reason <topic>=<text>` shape without breaking this call
|
|
298
|
+
* shape.
|
|
299
|
+
*/
|
|
300
|
+
function parseResolveOptions(rawList, reason) {
|
|
301
|
+
if (!Array.isArray(rawList) || rawList.length === 0) return { value: [] };
|
|
302
|
+
const trimmedReason = typeof reason === 'string' ? reason : undefined;
|
|
303
|
+
const out = [];
|
|
304
|
+
for (const raw of rawList) {
|
|
305
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
306
|
+
return { error: `--resolve expects <topic>=project:<ADR-id>, got '${raw}'` };
|
|
307
|
+
}
|
|
308
|
+
const eq = raw.indexOf('=');
|
|
309
|
+
if (eq === -1) {
|
|
310
|
+
return { error: `--resolve expects <topic>=project:<ADR-id>, got '${raw}' (missing '=')` };
|
|
311
|
+
}
|
|
312
|
+
const topic = raw.slice(0, eq);
|
|
313
|
+
const rhs = raw.slice(eq + 1);
|
|
314
|
+
if (topic.length === 0) return { error: `--resolve expects a topic before '=', got '${raw}'` };
|
|
315
|
+
if (!rhs.startsWith('project:')) {
|
|
316
|
+
return { error: `--resolve rhs must start with 'project:' (that is the currently supported resolution target), got '${rhs}'` };
|
|
317
|
+
}
|
|
318
|
+
const adrId = rhs.slice('project:'.length);
|
|
319
|
+
if (adrId.length === 0) return { error: `--resolve resolvedByAdrId is empty for topic '${topic}'` };
|
|
320
|
+
const decl = { topic, resolvedByAdrId: adrId };
|
|
321
|
+
if (trimmedReason !== undefined) decl.reason = trimmedReason;
|
|
322
|
+
out.push(decl);
|
|
323
|
+
}
|
|
324
|
+
return { value: out };
|
|
325
|
+
}
|
package/src/cli/create.js
CHANGED
|
@@ -207,6 +207,15 @@ export async function main(argv, deps = {}) {
|
|
|
207
207
|
stderr.write('[error] usage create cn: --path is required\n');
|
|
208
208
|
return 2;
|
|
209
209
|
}
|
|
210
|
+
// Pre-validate the #symbol portion so the refusal names the rule and
|
|
211
|
+
// the fix (paper-cut batch): the CN symbol regex admits identifier
|
|
212
|
+
// characters only, so a dotted symbol like `#Store.put` fails the
|
|
213
|
+
// schema pattern with an opaque message. Catch it here and teach.
|
|
214
|
+
const symbolCheck = checkCnSymbolPath(body.path);
|
|
215
|
+
if (!symbolCheck.ok) {
|
|
216
|
+
stderr.write(`[error] usage create cn: ${symbolCheck.message}\n`);
|
|
217
|
+
return 2;
|
|
218
|
+
}
|
|
210
219
|
// Phase 10 D5: --derive-deps assist. Optional, dev-time only, never a
|
|
211
220
|
// runtime dependency - errors helpfully (exit 2) when the tool cannot
|
|
212
221
|
// be resolved rather than silently degrading or reaching for the
|
|
@@ -261,7 +270,10 @@ export async function main(argv, deps = {}) {
|
|
|
261
270
|
return 2;
|
|
262
271
|
}
|
|
263
272
|
body.acId = flags.ac;
|
|
264
|
-
|
|
273
|
+
// 0.8.0 slug-train (w-2026-07-28-012 landmine 4): deriveSlug returns ''
|
|
274
|
+
// on empty derivation; TC keeps its historical 'tc' fallback locally
|
|
275
|
+
// rather than letting deriveSlug bake it in.
|
|
276
|
+
options.slug = flags.slug ?? (deriveSlug(body.description) || 'tc');
|
|
265
277
|
options.testPointer = flags['test-pointer'];
|
|
266
278
|
}
|
|
267
279
|
|
|
@@ -339,6 +351,42 @@ function isRcfError(value) {
|
|
|
339
351
|
&& ERROR_KINDS.has(value.kind) && typeof value.message === 'string';
|
|
340
352
|
}
|
|
341
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Pre-flight the CN --path against the same identity rule the schema
|
|
356
|
+
* enforces (see cn.schema.json: `^[^#]+(#[A-Za-z_$][A-Za-z0-9_$]*)?$`).
|
|
357
|
+
* When the symbol part carries a dot (or any other non-identifier
|
|
358
|
+
* character), the schema-driven message reads as a bare pattern-mismatch
|
|
359
|
+
* and buries the fix. This surfaces it: name the rule, name the fix,
|
|
360
|
+
* point at the TC pointer as the right seat for method-level precision.
|
|
361
|
+
*
|
|
362
|
+
* @param {string} path - the --path argument
|
|
363
|
+
* @returns {{ ok: true } | { ok: false, message: string }}
|
|
364
|
+
*/
|
|
365
|
+
function checkCnSymbolPath(path) {
|
|
366
|
+
const hash = path.indexOf('#');
|
|
367
|
+
if (hash < 0) return { ok: true };
|
|
368
|
+
const symbol = path.slice(hash + 1);
|
|
369
|
+
if (symbol.length === 0) {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
message: `--path '${path}' has an empty #symbol suffix; drop the '#' to name the file only, or add an identifier after it (e.g. '#ClassName').`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(symbol)) return { ok: true };
|
|
376
|
+
// The pattern rejects anything that isn't a bare identifier. The dot
|
|
377
|
+
// case is by far the most common (people reach for `Class.method`), so
|
|
378
|
+
// call it out explicitly; other rejections fall through to the same
|
|
379
|
+
// teaching message.
|
|
380
|
+
const dotted = symbol.includes('.');
|
|
381
|
+
const hint = dotted
|
|
382
|
+
? `dotted symbols (like '${symbol}') are not admitted: name the class or function alone (e.g. '#${symbol.split('.')[0]}') and record method-level precision on the TC test-pointer instead`
|
|
383
|
+
: `the symbol suffix admits identifier characters only (letters, digits, '_', '$'; a leading digit is not allowed)`;
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
message: `--path '${path}' has an invalid #symbol '${symbol}': ${hint}.`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
342
390
|
function handleWriterError(err, stderr) {
|
|
343
391
|
const kind = err.kind;
|
|
344
392
|
// BUG-007 fix: spec §D15 mandates exit-1 emit
|
package/src/cli/help.js
CHANGED
|
@@ -41,6 +41,9 @@ import { HELP as INTAKE_HELP } from './intake.js';
|
|
|
41
41
|
// 0.7.1 packaging consolidation: `rcf verify <verb>` routes to the same
|
|
42
42
|
// help block the transition-grace `rcf-verify` alias bin uses.
|
|
43
43
|
import { TOP_LEVEL_HELP as VERIFY_HELP } from '../verify/cli/help.js';
|
|
44
|
+
// Blueprint mechanism (Phase 1, w-2026-08-18-016).
|
|
45
|
+
import { HELP as BLUEPRINT_HELP } from './blueprint.js';
|
|
46
|
+
import { HELP as STANDARDS_HELP } from './standards.js';
|
|
44
47
|
|
|
45
48
|
const TOP_LEVEL = `Usage: rcf <command> [options]
|
|
46
49
|
|
|
@@ -74,6 +77,8 @@ Commands:
|
|
|
74
77
|
req-baseline <verb> Baseline-AC sweep and opt-out ledger for classified REQs
|
|
75
78
|
intake Variable-fidelity intake stage (classify supplied artefacts)
|
|
76
79
|
verify <verb> Adversarial ship gate (run|report|provision|cleanup|mcp); the legacy rcf-verify bin is a transition alias
|
|
80
|
+
blueprint <verb> Compose blueprints onto the project (add | list | remove)
|
|
81
|
+
standards <verb> Register standards packs against the project (add | list)
|
|
77
82
|
help [command] Print help for a command
|
|
78
83
|
|
|
79
84
|
Options:
|
|
@@ -124,6 +129,9 @@ const HELP_MAP = {
|
|
|
124
129
|
intake: INTAKE_HELP,
|
|
125
130
|
// 0.7.1 packaging consolidation: verify subcommand tree.
|
|
126
131
|
verify: VERIFY_HELP,
|
|
132
|
+
// Blueprint mechanism (Phase 1, w-2026-08-18-016).
|
|
133
|
+
blueprint: BLUEPRINT_HELP,
|
|
134
|
+
standards: STANDARDS_HELP,
|
|
127
135
|
};
|
|
128
136
|
|
|
129
137
|
/**
|
package/src/cli/init.js
CHANGED
|
@@ -22,7 +22,7 @@ import { parseArgs } from 'node:util';
|
|
|
22
22
|
import { createInterface } from 'node:readline/promises';
|
|
23
23
|
|
|
24
24
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
25
|
-
import { join } from 'node:path';
|
|
25
|
+
import { basename, join } from 'node:path';
|
|
26
26
|
|
|
27
27
|
import { initProject } from '#core/store/init.js';
|
|
28
28
|
import {
|
|
@@ -61,7 +61,11 @@ to elicit. Re-running on an existing project leaves the tree alone and
|
|
|
61
61
|
refreshes the wiring.
|
|
62
62
|
|
|
63
63
|
Options:
|
|
64
|
-
--project-name <name> Project name
|
|
64
|
+
--project-name <name> Project name. In non-interactive mode this
|
|
65
|
+
defaults to the working directory's basename
|
|
66
|
+
(pass explicitly to override); an unusable
|
|
67
|
+
basename (empty, '.', or '/') still requires
|
|
68
|
+
the flag.
|
|
65
69
|
--non-interactive Skip prompts; use seed values (default when
|
|
66
70
|
not on a TTY or when piped)
|
|
67
71
|
--no-agent-setup Scaffold the tree only; print the manual
|
|
@@ -115,10 +119,21 @@ export async function main(argv, deps = {}) {
|
|
|
115
119
|
rl.close();
|
|
116
120
|
}
|
|
117
121
|
} else {
|
|
122
|
+
// Non-interactive: --project-name defaults to the working
|
|
123
|
+
// directory's basename. An unusable basename (empty, '.', or '/')
|
|
124
|
+
// still requires the flag - no silent fallback to a generic name.
|
|
118
125
|
if (!projectName) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
const candidate = basename(cwd).trim();
|
|
127
|
+
if (candidate && candidate !== '.' && candidate !== '/') {
|
|
128
|
+
projectName = candidate;
|
|
129
|
+
} else {
|
|
130
|
+
stderr.write(
|
|
131
|
+
`[error] usage --project-name is required in non-interactive mode `
|
|
132
|
+
+ `(cwd basename '${candidate}' is unusable as a default)\n`,
|
|
133
|
+
);
|
|
134
|
+
stderr.write(HELP);
|
|
135
|
+
return 2;
|
|
136
|
+
}
|
|
122
137
|
}
|
|
123
138
|
}
|
|
124
139
|
|
package/src/cli/read.js
CHANGED
|
@@ -119,7 +119,13 @@ function resolveTarget(tree, id) {
|
|
|
119
119
|
const entry = (us.acceptanceCriteria ?? []).find((ac) => ac.id === id);
|
|
120
120
|
return entry ? { doc: entry, containerId: parentId, invalid: false } : null;
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
// 0.8.0 slug-train (w-2026-07-28-012 landmine 3, consumer-path
|
|
123
|
+
// straggler): widened `\d{3}` -> `\d{3,}` in lockstep with rcf-schemas
|
|
124
|
+
// 0.4.3's TC pattern. Under the previous shape `rcf read TC-1000-x`
|
|
125
|
+
// fell through to the `return null` below (silent skip) even when the
|
|
126
|
+
// TC existed under a widened TS -- exactly the class the landmine
|
|
127
|
+
// charter names.
|
|
128
|
+
if (/^TC-\d{3,}-[a-z0-9-]+$/.test(id)) {
|
|
123
129
|
const parentId = tree.parentByChild.get(id);
|
|
124
130
|
if (!parentId) return null;
|
|
125
131
|
const ts = tree.byId.get(parentId);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// `rcf standards <verb>` — Phase 1 landing (add | list).
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
|
|
5
|
+
import { isRcfError } from '#core/errors';
|
|
6
|
+
import { walkTree } from '#core/store';
|
|
7
|
+
import { findProjectRoot } from '../view/index.js';
|
|
8
|
+
import { listStandards, registerStandardsPack } from '../blueprint/index.js';
|
|
9
|
+
|
|
10
|
+
export const HELP = `Usage: rcf standards <verb> [options]
|
|
11
|
+
|
|
12
|
+
Verbs:
|
|
13
|
+
add <source> Register a standards pack against the project.
|
|
14
|
+
Reference-by-default: if <source> lives inside
|
|
15
|
+
the project root, the pack is referenced in place
|
|
16
|
+
and no copy is written. If <source> lives OUTSIDE
|
|
17
|
+
the project root, the pack is copied into
|
|
18
|
+
rcf/standards/<slug>/ so the tree stays portable.
|
|
19
|
+
list List every registered standards pack.
|
|
20
|
+
|
|
21
|
+
Options (for add):
|
|
22
|
+
--slug <slug> Required. Kebab slug for this pack.
|
|
23
|
+
--tags <t1,t2,...> Required. Comma-separated tag vocabulary.
|
|
24
|
+
--summary <string> Optional short summary read by the selective-
|
|
25
|
+
retrieval step alongside the tags.
|
|
26
|
+
--tests-provided-by <val> Required. One of: standard | agent | none.
|
|
27
|
+
--provenance <val> Required. One of: personal | corporate.
|
|
28
|
+
--dry-run Print intended writes without executing.
|
|
29
|
+
--quiet Suppress non-error stdout.
|
|
30
|
+
--help Print this help.
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const OPTION_SPEC = {
|
|
34
|
+
slug: { type: 'string' },
|
|
35
|
+
tags: { type: 'string' },
|
|
36
|
+
summary: { type: 'string' },
|
|
37
|
+
'tests-provided-by': { type: 'string' },
|
|
38
|
+
provenance: { type: 'string' },
|
|
39
|
+
'dry-run': { type: 'boolean' },
|
|
40
|
+
quiet: { type: 'boolean' },
|
|
41
|
+
help: { type: 'boolean' },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string[]} argv
|
|
46
|
+
* @param {object} [deps]
|
|
47
|
+
* @returns {Promise<number>}
|
|
48
|
+
*/
|
|
49
|
+
export async function main(argv, deps = {}) {
|
|
50
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
51
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
52
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
53
|
+
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
|
|
57
|
+
} catch (err) {
|
|
58
|
+
stderr.write(`[error] ${err.message}\n`);
|
|
59
|
+
stderr.write(HELP);
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
if (parsed.values.help || parsed.positionals.length === 0) {
|
|
63
|
+
stdout.write(HELP);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
const verb = parsed.positionals[0];
|
|
67
|
+
const rest = parsed.positionals.slice(1);
|
|
68
|
+
|
|
69
|
+
const projectRoot = await findProjectRoot(cwd);
|
|
70
|
+
if (!projectRoot) {
|
|
71
|
+
stderr.write('[error] no rcf/ tree found in this directory or any ancestor.\n');
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
75
|
+
if (errors.length > 0 && verb !== 'list') {
|
|
76
|
+
for (const e of errors) stderr.write(`[tree] ${e.kind}: ${e.message}\n`);
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (verb === 'add') {
|
|
81
|
+
if (rest.length === 0) {
|
|
82
|
+
stderr.write('[error] standards add: missing <source>\n');
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
if (!parsed.values.slug || !parsed.values.tags || !parsed.values['tests-provided-by'] || !parsed.values.provenance) {
|
|
86
|
+
stderr.write('[error] standards add: --slug, --tags, --tests-provided-by and --provenance are required\n');
|
|
87
|
+
return 2;
|
|
88
|
+
}
|
|
89
|
+
const result = await registerStandardsPack({
|
|
90
|
+
projectRoot, tree,
|
|
91
|
+
sourcePath: rest[0],
|
|
92
|
+
slug: parsed.values.slug,
|
|
93
|
+
tags: parsed.values.tags.split(',').map((t) => t.trim()).filter(Boolean),
|
|
94
|
+
summary: parsed.values.summary,
|
|
95
|
+
testsProvidedBy: parsed.values['tests-provided-by'],
|
|
96
|
+
provenance: parsed.values.provenance,
|
|
97
|
+
dryRun: parsed.values['dry-run'] === true,
|
|
98
|
+
});
|
|
99
|
+
if (isRcfError(result)) {
|
|
100
|
+
stderr.write(`[error] standards add: ${result.message}\n`);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
if (!parsed.values.quiet) {
|
|
104
|
+
const shape = result.copyPath ? `copied to ${result.copyPath}` : `referenced in place at ${result.entry.sourcePath}`;
|
|
105
|
+
const state = result.alreadyRegistered ? 'already registered (no change)' : 'registered';
|
|
106
|
+
stdout.write(`[standards] '${result.entry.slug}' ${state} (${shape}).\n`);
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (verb === 'list') {
|
|
112
|
+
const rows = listStandards(tree);
|
|
113
|
+
if (rows.length === 0) {
|
|
114
|
+
if (!parsed.values.quiet) stdout.write('[standards] no standards packs registered on this project.\n');
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
const shape = row.copyPath ? 'copied' : 'referenced';
|
|
119
|
+
stdout.write(`${row.slug}\t${row.testsProvidedBy}\t${row.provenance}\t${shape}\t${(row.tags ?? []).join(',')}\n`);
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
stderr.write(`[error] standards: unknown verb '${verb}'\n`);
|
|
125
|
+
stderr.write(HELP);
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
package/src/cli/test-suite.js
CHANGED
|
@@ -82,8 +82,13 @@ export async function main(argv, deps = {}) {
|
|
|
82
82
|
stderr.write('[error] usage test-suite: verb required (provenance | approve)\n');
|
|
83
83
|
return 2;
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// 0.8.0 slug-train (w-2026-07-28-012 landmine 3, consumer-path
|
|
86
|
+
// straggler): widened `\d{3}` -> `\d{3,}` in lockstep with rcf-schemas
|
|
87
|
+
// 0.4.3's TS pattern. The previous shape hard-refused any TS >= 1000 --
|
|
88
|
+
// silent from the operator's perspective (the tsId matched the schema
|
|
89
|
+
// but the CLI verb refused with a usage error citing "TS-015").
|
|
90
|
+
if (!/^TS-\d{3,}$/.test(tsId)) {
|
|
91
|
+
stderr.write(`[error] usage test-suite: expected a TS id like TS-015 or TS-1000, got '${tsId}'\n`);
|
|
87
92
|
return 2;
|
|
88
93
|
}
|
|
89
94
|
|