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.
Files changed (77) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/bin/rcf.js +6 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/guidance/harness-template.md +11 -0
  5. package/guidance/managed/agent-instructions-block.hash +1 -1
  6. package/guidance/managed/agent-instructions-block.md +11 -0
  7. package/package.json +5 -3
  8. package/rcf/adrs/adr-010.json +30 -0
  9. package/rcf/code-nodes/cn-058.json +18 -0
  10. package/rcf/code-nodes/cn-059.json +14 -0
  11. package/rcf/code-nodes/cn-060.json +14 -0
  12. package/rcf/code-nodes/cn-061.json +14 -0
  13. package/rcf/code-nodes/cn-062.json +14 -0
  14. package/rcf/code-nodes/cn-063.json +15 -0
  15. package/rcf/code-nodes/cn-064.json +15 -0
  16. package/rcf/code-nodes/cn-065.json +16 -0
  17. package/rcf/code-nodes/cn-066.json +14 -0
  18. package/rcf/code-nodes/cn-067.json +15 -0
  19. package/rcf/code-nodes/cn-068.json +15 -0
  20. package/rcf/code-nodes/cn-069.json +16 -0
  21. package/rcf/fbs/fbs-016.json +39 -0
  22. package/rcf/fbs/fbs-017.json +40 -0
  23. package/rcf/fbs/fbs-018.json +34 -0
  24. package/rcf/fbs/fbs-019.json +33 -0
  25. package/rcf/requirements/req-010.json +20 -0
  26. package/rcf/test-suites/ts-026.json +54 -0
  27. package/rcf/test-suites/ts-027.json +115 -0
  28. package/rcf/test-suites/ts-028.json +46 -0
  29. package/rcf/test-suites/ts-029.json +46 -0
  30. package/rcf/user-stories/us-1001.json +56 -0
  31. package/rcf/user-stories/us-1002.json +96 -0
  32. package/rcf/user-stories/us-1003.json +48 -0
  33. package/rcf/user-stories/us-1004.json +48 -0
  34. package/src/admissibility/enforce.js +142 -0
  35. package/src/admissibility/index.js +8 -0
  36. package/src/admissibility/markers.js +104 -0
  37. package/src/admissibility/scope-lint.js +163 -0
  38. package/src/blueprint/apply.js +464 -0
  39. package/src/blueprint/conflicts.js +351 -0
  40. package/src/blueprint/diff.js +82 -0
  41. package/src/blueprint/index.js +12 -0
  42. package/src/blueprint/list.js +21 -0
  43. package/src/blueprint/loader.js +163 -0
  44. package/src/blueprint/manifest-writer.js +49 -0
  45. package/src/blueprint/namespace.js +145 -0
  46. package/src/blueprint/remove.js +105 -0
  47. package/src/blueprint/resolutions.js +83 -0
  48. package/src/blueprint/standards.js +148 -0
  49. package/src/blueprint/supersede.js +318 -0
  50. package/src/browser-verify/invariants.js +33 -6
  51. package/src/build/bundle.js +34 -11
  52. package/src/build/standards-selector.js +52 -0
  53. package/src/cli/blueprint.js +325 -0
  54. package/src/cli/create.js +49 -1
  55. package/src/cli/help.js +8 -0
  56. package/src/cli/init.js +20 -5
  57. package/src/cli/read.js +7 -1
  58. package/src/cli/standards.js +127 -0
  59. package/src/cli/test-suite.js +7 -2
  60. package/src/core/store/ids.js +168 -18
  61. package/src/core/store/loader.js +31 -17
  62. package/src/core/store/walker.js +62 -4
  63. package/src/core/store/writer.js +41 -11
  64. package/src/deployment/index.js +13 -0
  65. package/src/deployment/placeholder-detector.js +113 -0
  66. package/src/finalise/detect.js +51 -29
  67. package/src/finalise/index.js +16 -2
  68. package/src/finalise/ingest.js +41 -0
  69. package/src/mcp/tools.js +10 -2
  70. package/src/query/formatters/table.js +7 -10
  71. package/src/query/index.js +4 -0
  72. package/src/query/refuse-on-admissibility.js +73 -0
  73. package/src/query/trace.js +45 -4
  74. package/src/ruleset/index.js +140 -0
  75. package/src/ruleset/ruleset.json +146 -0
  76. package/src/verify/chain/index.js +31 -0
  77. 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
- options.slug = flags.slug ?? deriveSlug(body.description);
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 (required for --non-interactive)
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
- stderr.write('[error] usage --project-name is required in non-interactive mode\n');
120
- stderr.write(HELP);
121
- return 2;
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
- if (/^TC-\d{3}-[a-z0-9-]+$/.test(id)) {
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
+ }
@@ -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
- if (!/^TS-\d{3}$/.test(tsId)) {
86
- stderr.write(`[error] usage test-suite: expected a TS id like TS-015, got '${tsId}'\n`);
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