rcf-lite 0.28.4 → 0.29.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 (168) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/bin/rcf.js +4 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/guidance/define-draft-shapes.md +89 -0
  5. package/guidance/define-intake.md +97 -0
  6. package/guidance/define-intent.md +107 -0
  7. package/guidance/discover-prototype.md +67 -0
  8. package/guidance/discovery-prototypes.md +69 -0
  9. package/guidance/harness-template.md +34 -0
  10. package/guidance/managed/agent-instructions-block.hash +1 -1
  11. package/guidance/managed/agent-instructions-block.md +34 -0
  12. package/guidance/manifest.json +9 -2
  13. package/package.json +1 -1
  14. package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
  15. package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
  16. package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
  17. package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
  18. package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
  19. package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
  20. package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
  21. package/rcf/adrs/adr-4134-index-json-route.json +35 -0
  22. package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
  23. package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
  24. package/rcf/code-nodes/cn-725.json +18 -2
  25. package/rcf/code-nodes/cn-726.json +10 -2
  26. package/rcf/code-nodes/cn-727.json +13 -4
  27. package/rcf/code-nodes/cn-728.json +18 -2
  28. package/rcf/code-nodes/cn-729.json +10 -2
  29. package/rcf/code-nodes/cn-730.json +13 -3
  30. package/rcf/code-nodes/cn-731.json +20 -0
  31. package/rcf/code-nodes/cn-732.json +14 -0
  32. package/rcf/code-nodes/cn-733.json +14 -0
  33. package/rcf/code-nodes/cn-734.json +14 -0
  34. package/rcf/code-nodes/cn-735.json +20 -0
  35. package/rcf/code-nodes/cn-736.json +14 -0
  36. package/rcf/code-nodes/cn-737.json +14 -0
  37. package/rcf/code-nodes/cn-738.json +14 -0
  38. package/rcf/code-nodes/cn-739.json +18 -0
  39. package/rcf/code-nodes/cn-740.json +19 -0
  40. package/rcf/code-nodes/cn-741.json +16 -0
  41. package/rcf/code-nodes/cn-742.json +16 -0
  42. package/rcf/code-nodes/cn-743.json +15 -0
  43. package/rcf/code-nodes/cn-744.json +22 -0
  44. package/rcf/code-nodes/cn-745.json +16 -0
  45. package/rcf/code-nodes/cn-746.json +14 -0
  46. package/rcf/code-nodes/cn-747.json +18 -0
  47. package/rcf/code-nodes/cn-748.json +16 -0
  48. package/rcf/code-nodes/cn-749.json +16 -0
  49. package/rcf/code-nodes/cn-750.json +15 -0
  50. package/rcf/code-nodes/cn-751.json +18 -0
  51. package/rcf/code-nodes/cn-752.json +14 -0
  52. package/rcf/fbs/fbs-196.json +13 -5
  53. package/rcf/fbs/fbs-198.json +42 -0
  54. package/rcf/fbs/fbs-199.json +59 -0
  55. package/rcf/fbs/fbs-200.json +46 -0
  56. package/rcf/fbs/fbs-201.json +49 -0
  57. package/rcf/fbs/fbs-202.json +110 -0
  58. package/rcf/fbs/fbs-203.json +100 -0
  59. package/rcf/requirements/req-002.json +2 -2
  60. package/rcf/requirements/req-173.json +2 -2
  61. package/rcf/requirements/req-174.json +10 -4
  62. package/rcf/requirements/req-175.json +10 -4
  63. package/rcf/requirements/req-176.json +25 -0
  64. package/rcf/requirements/req-180.json +31 -0
  65. package/rcf/requirements/req-181.json +31 -0
  66. package/rcf/requirements/req-182.json +25 -0
  67. package/rcf/requirements/req-186.json +25 -0
  68. package/rcf/requirements/req-187.json +31 -0
  69. package/rcf/requirements/req-188.json +25 -0
  70. package/rcf/tacs/tac-4121-define-ledgers.json +15 -3
  71. package/rcf/tacs/tac-4122-define-gates.json +40 -4
  72. package/rcf/tacs/tac-4123-define-readiness.json +39 -3
  73. package/rcf/tacs/tac-4124-define-freeze.json +28 -0
  74. package/rcf/tacs/tac-4127-readiness-view.json +49 -0
  75. package/rcf/tacs/tac-4128-define-guidance.json +53 -0
  76. package/rcf/tacs/tac-4129-define-questions.json +45 -0
  77. package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
  78. package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
  79. package/rcf/tacs/tac-4132-id-lookup.json +42 -0
  80. package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
  81. package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
  82. package/rcf/test-suites/ts-016.json +3 -3
  83. package/rcf/test-suites/ts-215.json +148 -4
  84. package/rcf/test-suites/ts-216.json +19 -3
  85. package/rcf/test-suites/ts-218.json +78 -0
  86. package/rcf/test-suites/ts-219.json +70 -0
  87. package/rcf/test-suites/ts-220.json +78 -0
  88. package/rcf/test-suites/ts-221.json +78 -0
  89. package/rcf/test-suites/ts-222.json +100 -0
  90. package/rcf/test-suites/ts-223.json +94 -0
  91. package/rcf/test-suites/ts-224.json +70 -0
  92. package/rcf/test-suites/ts-225.json +78 -0
  93. package/rcf/test-suites/ts-226.json +158 -0
  94. package/rcf/test-suites/ts-227.json +38 -0
  95. package/rcf/test-suites/ts-228.json +86 -0
  96. package/rcf/test-suites/ts-229.json +83 -0
  97. package/rcf/user-stories/us-17302.json +50 -0
  98. package/rcf/user-stories/us-17401.json +14 -2
  99. package/rcf/user-stories/us-17402.json +55 -0
  100. package/rcf/user-stories/us-17403.json +60 -0
  101. package/rcf/user-stories/us-17502.json +60 -0
  102. package/rcf/user-stories/us-17503.json +65 -0
  103. package/rcf/user-stories/us-17601.json +58 -0
  104. package/rcf/user-stories/us-18001.json +55 -0
  105. package/rcf/user-stories/us-18002.json +35 -0
  106. package/rcf/user-stories/us-18101.json +35 -0
  107. package/rcf/user-stories/us-18202.json +40 -0
  108. package/rcf/user-stories/us-18601.json +55 -0
  109. package/rcf/user-stories/us-18602.json +50 -0
  110. package/rcf/user-stories/us-18603.json +45 -0
  111. package/rcf/user-stories/us-18701.json +50 -0
  112. package/rcf/user-stories/us-18702.json +50 -0
  113. package/rcf/user-stories/us-18801.json +45 -0
  114. package/rcf/user-stories/us-204.json +59 -0
  115. package/rcf/user-stories/us-205.json +64 -0
  116. package/rcf/user-stories/us-206.json +54 -0
  117. package/releases/releases.yaml +11 -1
  118. package/src/cli/freeze.js +730 -0
  119. package/src/cli/help.js +12 -0
  120. package/src/cli/ledger.js +228 -12
  121. package/src/cli/questions.js +288 -0
  122. package/src/cli/readiness.js +267 -49
  123. package/src/cli/view.js +29 -0
  124. package/src/define/freeze-record.js +30 -15
  125. package/src/define/ledgers.js +210 -11
  126. package/src/intake/index.js +1 -1
  127. package/src/intake/orchestrator.js +117 -0
  128. package/src/mcp/tools.js +354 -0
  129. package/src/query/delta.js +6 -0
  130. package/src/query/gates.js +314 -39
  131. package/src/query/index.js +16 -2
  132. package/src/query/questions.js +438 -0
  133. package/src/query/readiness.js +248 -7
  134. package/src/server/index.js +24 -2
  135. package/src/server/routes.js +68 -1
  136. package/src/setup/identity-seed.js +17 -0
  137. package/src/view/build-dag.js +563 -0
  138. package/src/view/components/badge.js +41 -0
  139. package/src/view/components/diff.js +72 -0
  140. package/src/view/components/doc-row.js +62 -0
  141. package/src/view/components/empty-state.js +35 -0
  142. package/src/view/components/entity-selector.js +137 -0
  143. package/src/view/components/filter-bar.js +97 -0
  144. package/src/view/components/findings-list.js +49 -0
  145. package/src/view/components/index.js +12 -0
  146. package/src/view/components/pill.js +46 -0
  147. package/src/view/components/sub-tab-strip.js +43 -0
  148. package/src/view/components/toast.js +25 -0
  149. package/src/view/components-fixture.js +265 -0
  150. package/src/view/doc-renderers/architecture.js +304 -0
  151. package/src/view/doc-renderers/build.js +160 -0
  152. package/src/view/doc-renderers/index.js +2 -0
  153. package/src/view/doc-renderers/prd.js +35 -6
  154. package/src/view/html-page.js +700 -602
  155. package/src/view/index.js +96 -3
  156. package/src/view/live-client.js +39 -6
  157. package/src/view/needs-work.js +79 -0
  158. package/src/view/page-init.js +2193 -0
  159. package/src/view/readiness/phrasebook.js +115 -0
  160. package/src/view/readiness/po-layer.js +288 -0
  161. package/src/view/readiness/question-adapter.js +329 -0
  162. package/src/view/readiness/stage-legend.js +71 -0
  163. package/src/view/readiness.js +505 -0
  164. package/src/view/style.css +2017 -382
  165. package/src/view/test-host/embed-client.js +121 -0
  166. package/src/view/test-host/fixture.js +183 -0
  167. package/src/view/test-host/lints.js +220 -0
  168. package/src/view/view-index.js +323 -0
package/src/cli/help.js CHANGED
@@ -47,6 +47,8 @@ import { HELP as BLUEPRINT_HELP } from './blueprint.js';
47
47
  import { HELP as STANDARDS_HELP } from './standards.js';
48
48
  import { HELP as LEDGER_HELP } from './ledger.js';
49
49
  import { HELP as READINESS_HELP } from './readiness.js';
50
+ import { HELP as QUESTIONS_HELP } from './questions.js';
51
+ import { HELP as FREEZE_HELP } from './freeze.js';
50
52
 
51
53
  // Verify group members (from the verify-suite subtree).
52
54
  import { HELP as VERIFY_RUN_HELP } from '../verify/cli/run.js';
@@ -97,6 +99,7 @@ define Author the RCF tree.
97
99
  standards <verb> Register standards packs against the project.
98
100
  ledger <name> <verb> Sidecar-ledger CRUD under rcf/define/.
99
101
  readiness DEFINE stage-gate readiness view.
102
+ questions Persona question set for the intent-complete loop.
100
103
 
101
104
  build Drive the FBS queue and the five-stage build cycle.
102
105
  queue Show the FBS build queue.
@@ -198,6 +201,13 @@ Verbs:
198
201
  verbs: add | resolve | list).
199
202
  readiness DEFINE stage-gate readiness view; twin of
200
203
  the viewer's Readiness tab (slice 5).
204
+ questions Persona question set for the intent-complete
205
+ loop: one plain question per failing
206
+ product-owner item carrying its write-back.
207
+ freeze Close a change: run readiness, refuse on any
208
+ failing stage, write rcf/define/freeze.json
209
+ and print the section 2.5 summary. --status
210
+ reports the current record without writing.
201
211
 
202
212
  Run 'rcf help define <verb>' for per-verb help.
203
213
  `;
@@ -295,6 +305,8 @@ export const HELP_MAP = {
295
305
  standards: STANDARDS_HELP,
296
306
  ledger: LEDGER_HELP,
297
307
  readiness: READINESS_HELP,
308
+ questions: QUESTIONS_HELP,
309
+ freeze: FREEZE_HELP,
298
310
  },
299
311
  build: {
300
312
  queue: BUILD_HELP,
package/src/cli/ledger.js CHANGED
@@ -16,11 +16,14 @@ import {
16
16
  LEDGER_NAMES,
17
17
  LedgerError,
18
18
  addEntry,
19
+ isResolvedByGrammar,
19
20
  loadLedger,
20
21
  parseBriefFromFile,
21
22
  resolveEntry,
22
23
  saveLedger,
24
+ updateEntry,
23
25
  } from '../define/ledgers.js';
26
+ import { runIntakeScansOnDelta } from '../intake/orchestrator.js';
24
27
  import { findProjectRoot } from '../view/index.js';
25
28
 
26
29
  const OPTION_SPEC = {
@@ -39,6 +42,11 @@ const OPTION_SPEC = {
39
42
  disposition: { type: 'string' },
40
43
  finding: { type: 'string' },
41
44
  severity: { type: 'string' },
45
+ scan: { type: 'boolean' },
46
+ 'no-scan': { type: 'boolean' },
47
+ findings: { type: 'string' },
48
+ 'dry-run': { type: 'boolean' },
49
+ 'resolved-by': { type: 'string' },
42
50
  json: { type: 'boolean' },
43
51
  help: { type: 'boolean' },
44
52
  };
@@ -57,6 +65,7 @@ Ledger names:
57
65
 
58
66
  Verbs:
59
67
  add Append a new entry, next id
68
+ update <id> Patch named fields on an existing entry
60
69
  resolve <id> Flip status to resolved, stamp resolvedAt
61
70
  list Print every entry (JSON with --json)
62
71
 
@@ -68,8 +77,18 @@ brief add flags:
68
77
  --kind <${BRIEF_KINDS.join('|')}>
69
78
  Statement kind (default: capability)
70
79
  --text <text> The statement text (mutually exclusive with --from)
71
- --from <path> Read one statement per non-empty line from a file
72
- --source <span> Optional source span label (file:line, URL, note)
80
+ --from <path> Read one statement per non-empty line from a file;
81
+ each line may carry [kind] and (source: ...) markers
82
+ --source <span> Default source span when a line has no marker
83
+ --scan Run the intake scans over new statements against
84
+ existing ones (default on with --from, off with --text)
85
+ --no-scan Skip scans
86
+ --findings <path> Harness-supplied findings JSON (same shape
87
+ rcf discover intake --input accepts). Each finding
88
+ becomes an openQuestion statement
89
+ --dry-run Print the statements and findings that would be
90
+ minted; write nothing
91
+ --json Emit { added[], findings[] } for scripting
73
92
 
74
93
  decisions add flags:
75
94
  --question <text> The decision question (required)
@@ -93,12 +112,25 @@ resolve flags:
93
112
  --answer <text> On decisions: the answer chosen (a letter, or free text)
94
113
  --reason <text> On probes / concerns: the resolution note
95
114
 
115
+ update flags (any subset; validation runs as on add):
116
+ brief: --kind --text --source --resolved-by --blocks
117
+ decisions: --question --option --default --blocks
118
+ concerns: --concern --req --disposition --reason
119
+ probes: --req --finding --severity --reason
120
+ --resolved-by must match the closed grammar:
121
+ REQ-nnn | TAD.entity:<name> | PRD.user:<name>
122
+ | TAD.system:<name> | TAC-nnnn | omitted:<reason>
123
+
96
124
  Notes:
97
125
  - Every ledger file lives under rcf/define/. The loader treats missing
98
126
  files as empty ledgers; a malformed file is a hard error.
99
- - The 'brief add --from <path>' seam for running rcf discover intake
100
- against the frozen statements is wired in a later slice; this slice
101
- only splits by non-empty lines and mints ids.
127
+ - 'brief add --from <path>' parses one statement per line, supports
128
+ the leading '[kind]' and trailing '(source: ...)' markers, mints
129
+ statements, and (unless --no-scan) runs the mechanical intake scans
130
+ over the new statements against existing ones, appending one
131
+ openQuestion statement per finding with source 'scan:<kind>:<ids>'.
132
+ See '--findings' for attested findings and '--dry-run' to preview
133
+ without writing.
102
134
  - 'decisions list' prints the numbered format from #241: one decision
103
135
  per item; options with letters; the default; what it blocks.
104
136
  `;
@@ -124,8 +156,8 @@ export async function main(argv, deps = {}) {
124
156
  stderr.write(`[error] usage ledger: unknown name '${name}' (expected: ${LEDGER_NAMES.join(', ')})\n`);
125
157
  return 2;
126
158
  }
127
- if (!verb || !['add', 'resolve', 'list'].includes(verb)) {
128
- stderr.write("[error] usage ledger: expected verb 'add', 'resolve' or 'list'\n");
159
+ if (!verb || !['add', 'update', 'resolve', 'list'].includes(verb)) {
160
+ stderr.write("[error] usage ledger: expected verb 'add', 'update', 'resolve' or 'list'\n");
129
161
  return 2;
130
162
  }
131
163
 
@@ -155,6 +187,9 @@ export async function main(argv, deps = {}) {
155
187
  if (verb === 'add') {
156
188
  return await runAdd({ projectRoot, name, flags, stdout, stderr });
157
189
  }
190
+ if (verb === 'update') {
191
+ return await runUpdate({ projectRoot, name, positionals, flags, stdout, stderr });
192
+ }
158
193
  return await runResolve({ projectRoot, name, positionals, flags, stdout, stderr });
159
194
  } catch (err) {
160
195
  if (err instanceof LedgerError) {
@@ -264,10 +299,30 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
264
299
  stderr.write('[error] usage ledger: brief add requires --text <text> or --from <path>\n');
265
300
  return 2;
266
301
  }
302
+ if (flags['no-scan'] && flags.findings) {
303
+ stderr.write('[error] usage ledger: brief add --no-scan and --findings cannot be combined\n');
304
+ return 2;
305
+ }
306
+ // Scans default on with --from and off with --text (spec 2.2).
307
+ const scansOn = flags['no-scan']
308
+ ? false
309
+ : (flags.scan ? true : Boolean(flags.from));
310
+
267
311
  let drafts;
268
312
  if (flags.from) {
269
313
  const raw = await readFile(String(flags.from), 'utf8');
270
- drafts = parseBriefFromFile(raw, { kind, source: flags.source ? String(flags.source) : String(flags.from) });
314
+ try {
315
+ drafts = parseBriefFromFile(raw, {
316
+ kind,
317
+ source: flags.source ? String(flags.source) : String(flags.from),
318
+ });
319
+ } catch (err) {
320
+ if (err instanceof LedgerError && err.code === 'usage') {
321
+ stderr.write(`[error] usage ledger: ${err.message}\n`);
322
+ return 2;
323
+ }
324
+ throw err;
325
+ }
271
326
  if (drafts.length === 0) {
272
327
  stderr.write(`[error] usage ledger: brief --from ${flags.from} produced no non-empty lines\n`);
273
328
  return 2;
@@ -277,9 +332,8 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
277
332
  if (flags.source) entry.source = String(flags.source);
278
333
  drafts = [entry];
279
334
  }
280
- // The intake-scan-against-frozen-statements seam: a later slice
281
- // calls the intake scans over drafts + the currently-frozen ledger
282
- // here. This slice mints ids and writes.
335
+
336
+ // Compute what WOULD be minted (for both the real path and dry-run).
283
337
  let cur = body;
284
338
  const added = [];
285
339
  for (const d of drafts) {
@@ -287,8 +341,74 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
287
341
  cur = step.body;
288
342
  added.push(step.entry);
289
343
  }
344
+
345
+ // Convert operator-supplied --findings into openQuestion statements.
346
+ /** @type {Array<{ kind: string, text: string, source: string }>} */
347
+ const findingStatements = [];
348
+ if (flags.findings) {
349
+ let findingsFile;
350
+ try {
351
+ findingsFile = JSON.parse(await readFile(String(flags.findings), 'utf8'));
352
+ } catch (err) {
353
+ stderr.write(`[error] usage ledger: brief add --findings: cannot read file: ${/** @type {Error} */ (err).message}\n`);
354
+ return 2;
355
+ }
356
+ const findings = Array.isArray(findingsFile?.validationFindings)
357
+ ? findingsFile.validationFindings
358
+ : [];
359
+ for (const f of findings) {
360
+ if (typeof f?.kind !== 'string' || typeof f?.detail !== 'string') continue;
361
+ findingStatements.push({
362
+ kind: 'openQuestion',
363
+ text: f.detail,
364
+ source: `scan:${f.kind}`,
365
+ });
366
+ }
367
+ }
368
+
369
+ // Run mechanical scans (new against existing).
370
+ if (scansOn) {
371
+ const existing = /** @type {any[]} */ (body.statements ?? []);
372
+ const scanResults = runIntakeScansOnDelta(added, existing);
373
+ for (const r of scanResults) {
374
+ findingStatements.push({
375
+ kind: 'openQuestion',
376
+ text: r.detail,
377
+ source: `scan:${r.scanName}:${r.ids.join(',')}`,
378
+ });
379
+ }
380
+ }
381
+
382
+ // Mint the finding statements onto `cur` so they share the id space.
383
+ const findingEntries = [];
384
+ for (const fs of findingStatements) {
385
+ const step = addEntry({ name: 'brief', body: cur, entry: fs, now });
386
+ cur = step.body;
387
+ findingEntries.push(step.entry);
388
+ }
389
+
390
+ if (flags['dry-run']) {
391
+ if (flags.json) {
392
+ stdout.write(`${JSON.stringify({ added, findings: findingEntries }, null, 2)}\n`);
393
+ } else {
394
+ stdout.write(`[dry-run] brief-ledger: would add ${added.length} statement(s), ids ${added.map((e) => e.id).join(', ') || '(none)'}\n`);
395
+ if (findingEntries.length > 0) {
396
+ stdout.write(`[dry-run] brief-ledger: would add ${findingEntries.length} openQuestion from findings, ids ${findingEntries.map((e) => e.id).join(', ')}\n`);
397
+ }
398
+ }
399
+ return 0;
400
+ }
401
+
290
402
  await saveLedger({ projectRoot, name: 'brief', body: cur });
291
- stdout.write(`brief-ledger: added ${added.length} statement(s), ids ${added.map((e) => e.id).join(', ')}\n`);
403
+ if (flags.json) {
404
+ stdout.write(`${JSON.stringify({ added, findings: findingEntries }, null, 2)}\n`);
405
+ } else {
406
+ stdout.write(`brief-ledger: added ${added.length} statement(s), ids ${added.map((e) => e.id).join(', ')}`);
407
+ if (findingEntries.length > 0) {
408
+ stdout.write(`; added ${findingEntries.length} openQuestion from findings, ids ${findingEntries.map((e) => e.id).join(', ')}`);
409
+ }
410
+ stdout.write('\n');
411
+ }
292
412
  return 0;
293
413
  }
294
414
 
@@ -351,6 +471,102 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
351
471
  return 0;
352
472
  }
353
473
 
474
+ /**
475
+ * `rcf define ledger <name> update <id>` subverb (REQ-173 amended,
476
+ * US-17302). Field patches honour the same `add`-side validation.
477
+ * `--resolved-by` on brief must match the closed pointer grammar; a
478
+ * pointer outside the grammar refuses the write (exit 2).
479
+ *
480
+ * @param {object} args
481
+ * @param {string} args.projectRoot
482
+ * @param {import('../define/ledgers.js').LedgerName} args.name
483
+ * @param {string[]} args.positionals
484
+ * @param {Record<string, unknown>} args.flags
485
+ * @param {NodeJS.WriteStream} args.stdout
486
+ * @param {NodeJS.WriteStream} args.stderr
487
+ * @returns {Promise<number>}
488
+ */
489
+ async function runUpdate({ projectRoot, name, positionals, flags, stdout, stderr }) {
490
+ if (positionals.length !== 1) {
491
+ stderr.write('[error] usage ledger: update expects exactly one <id>\n');
492
+ return 2;
493
+ }
494
+ const id = Number(positionals[0]);
495
+ if (!Number.isInteger(id) || id < 1) {
496
+ stderr.write(`[error] usage ledger: update id must be a positive integer, got '${positionals[0]}'\n`);
497
+ return 2;
498
+ }
499
+
500
+ // --resolved-by only lands on brief statements; the pointer grammar
501
+ // is validated here (shape-only) BEFORE loading the ledger, so a
502
+ // bad pointer never touches the file.
503
+ if (flags['resolved-by'] !== undefined) {
504
+ if (name !== 'brief') {
505
+ stderr.write(`[error] usage ledger: --resolved-by applies to brief statements only\n`);
506
+ return 2;
507
+ }
508
+ if (!isResolvedByGrammar(String(flags['resolved-by']))) {
509
+ stderr.write(
510
+ '[error] usage ledger: --resolved-by outside grammar (REQ-nnn | TAD.entity:<name> | PRD.user:<name> | TAD.system:<name> | TAC-nnnn | omitted:<reason>)\n',
511
+ );
512
+ return 2;
513
+ }
514
+ }
515
+
516
+ const body = await loadLedger({ projectRoot, name });
517
+
518
+ /** @type {Record<string, unknown>} */
519
+ const patch = {};
520
+
521
+ if (name === 'brief') {
522
+ if (flags.kind !== undefined) {
523
+ const kind = String(flags.kind);
524
+ if (!BRIEF_KINDS.includes(/** @type {any} */ (kind))) {
525
+ stderr.write(`[error] usage ledger: brief --kind must be one of: ${BRIEF_KINDS.join(', ')}\n`);
526
+ return 2;
527
+ }
528
+ patch.kind = kind;
529
+ }
530
+ if (flags.text !== undefined) patch.text = String(flags.text);
531
+ if (flags.source !== undefined) patch.source = String(flags.source);
532
+ if (flags['resolved-by'] !== undefined) patch.resolvedBy = String(flags['resolved-by']);
533
+ if (flags.blocks !== undefined) patch.blocks = String(flags.blocks);
534
+ } else if (name === 'decisions') {
535
+ if (flags.question !== undefined) patch.question = String(flags.question);
536
+ if (flags.option !== undefined) {
537
+ const parsedOpts = parseOptionFlags(flags.option);
538
+ if (parsedOpts.error) {
539
+ stderr.write(`[error] usage ledger: ${parsedOpts.error}\n`);
540
+ return 2;
541
+ }
542
+ patch.options = parsedOpts.value;
543
+ }
544
+ if (flags.default !== undefined) patch.default = String(flags.default);
545
+ if (flags.blocks !== undefined) patch.blocks = String(flags.blocks);
546
+ } else if (name === 'concerns') {
547
+ if (flags.concern !== undefined) patch.concern = String(flags.concern);
548
+ if (flags.req !== undefined) patch.reqId = String(flags.req);
549
+ if (flags.disposition !== undefined) patch.disposition = String(flags.disposition);
550
+ if (flags.reason !== undefined) patch.reason = String(flags.reason);
551
+ } else {
552
+ // probes
553
+ if (flags.req !== undefined) patch.reqId = String(flags.req);
554
+ if (flags.finding !== undefined) patch.finding = String(flags.finding);
555
+ if (flags.severity !== undefined) patch.severity = String(flags.severity);
556
+ if (flags.reason !== undefined) patch.reason = String(flags.reason);
557
+ }
558
+
559
+ if (Object.keys(patch).length === 0) {
560
+ stderr.write('[error] usage ledger: update needs at least one field flag to patch\n');
561
+ return 2;
562
+ }
563
+
564
+ const step = updateEntry({ name, body, id, patch });
565
+ await saveLedger({ projectRoot, name, body: step.body });
566
+ stdout.write(`${name}-ledger: updated ${id}\n`);
567
+ return 0;
568
+ }
569
+
354
570
  async function runResolve({ projectRoot, name, positionals, flags, stdout, stderr }) {
355
571
  if (positionals.length !== 1) {
356
572
  stderr.write('[error] usage ledger: resolve expects exactly one <id>\n');
@@ -0,0 +1,288 @@
1
+ // `rcf define questions` subcommand handler (REQ-186; spec 2026-10-01
2
+ // §1.2, §6).
3
+ //
4
+ // Computes and prints the persona question set from readiness and the
5
+ // four ledgers. Pure read: writes NOTHING under rcf/.
6
+ //
7
+ // rcf define questions [--json] [--persona productOwner|engineer]
8
+ // [--stage <D|short>] [--limit <n>]
9
+ //
10
+ // Persona defaults from the profile register (viewer / register
11
+ // marker scan in rcf/.identity/profile.md); when the profile is
12
+ // absent or unstated, the default is productOwner. Unknown persona
13
+ // exits 2 with the usage line naming productOwner and engineer.
14
+ // The verdict exit code lives on `rcf define readiness`; this verb
15
+ // is a diagnostic that always exits 0.
16
+
17
+ import { parseArgs } from 'node:util';
18
+ import { readFile } from 'node:fs/promises';
19
+ import { join } from 'node:path';
20
+
21
+ import { resolveTestPointers, walkTree } from '#core/store';
22
+ import { checkCodeNodeResolution } from '#core/store';
23
+ import { findProjectRoot } from '../view/index.js';
24
+ import { loadFreezeRecord } from '../define/freeze-record.js';
25
+ import { loadAllLedgers } from '../define/ledgers.js';
26
+ import { STAGE_ALIASES } from '../query/gates.js';
27
+ import { computeReadiness } from '../query/readiness.js';
28
+ import { computeQuestions, PERSONAS } from '../query/questions.js';
29
+
30
+ const OPTION_SPEC = {
31
+ json: { type: 'boolean' },
32
+ persona: { type: 'string' },
33
+ stage: { type: 'string' },
34
+ limit: { type: 'string' },
35
+ help: { type: 'boolean' },
36
+ };
37
+
38
+ export const HELP = `Usage: rcf define questions [options]
39
+
40
+ Print the persona question set (REQ-186). Each failing product-owner
41
+ check in readiness becomes one plain question per failing item, each
42
+ carrying the exact write-back that answers it.
43
+
44
+ The compute is pure: it never creates or modifies a file under rcf/.
45
+ Answers land through the write-back the question carries; the loop
46
+ ends when 'rcf define readiness --level intent' reports ok.
47
+
48
+ Options:
49
+ --json Emit the full questions object as JSON.
50
+ --persona <who> One of productOwner | engineer. Defaults
51
+ to the register marker in
52
+ rcf/.identity/profile.md, else
53
+ productOwner.
54
+ --stage <stage> Narrow the output to one stage. Accepts
55
+ a stage id (D1..D8) or its short name
56
+ (brief | skeleton | shapes | stories |
57
+ crosscut | consistency | decisions |
58
+ freeze).
59
+ --limit <n> Cap the questions[] array to the first n
60
+ entries. 'remaining' still counts every
61
+ matching item.
62
+ --help Print this help.
63
+ `;
64
+
65
+ /**
66
+ * Read `rcf/.identity/profile.md` when it exists; return null on
67
+ * ENOENT or on any read error (the question compute treats absence
68
+ * as "productOwner default" rather than a runtime error).
69
+ *
70
+ * @param {string} projectRoot
71
+ * @returns {Promise<string|null>}
72
+ */
73
+ async function readProfileText(projectRoot) {
74
+ try {
75
+ return await readFile(join(projectRoot, 'rcf', '.identity', 'profile.md'), 'utf8');
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Pick the profile register from the text, returning one of
83
+ * `productOwner` | `engineer` | `unstated`.
84
+ */
85
+ function pickRegister(text) {
86
+ if (typeof text !== 'string' || text.length === 0) return 'unstated';
87
+ // The canonical register template (src/setup/identity-seed.js) names
88
+ // all three markers in its explanation ("- `productOwner`: ..."),
89
+ // then places the chosen value as a bare word on a line of its own
90
+ // ("engineer\n", "unstated\n", etc.). Ad-hoc profiles also use
91
+ // "Register: engineer" on one line. In both shapes the ACTIVE value
92
+ // sits AFTER the explanation, so pick the LAST marker occurrence
93
+ // in the text; a line matching nothing but a marker word takes
94
+ // precedence over an in-prose occurrence at the same position.
95
+ const markers = ['productOwner', 'engineer', 'unstated'];
96
+ // Pass 1: prefer a bare-line marker (canonical template shape).
97
+ const lines = text.split(/\r?\n/);
98
+ let bareLine = null;
99
+ for (const raw of lines) {
100
+ const trimmed = raw.trim();
101
+ if (markers.includes(trimmed)) bareLine = trimmed;
102
+ }
103
+ if (bareLine) return /** @type {any} */ (bareLine);
104
+ // Pass 2: fallback - last occurrence anywhere (handles
105
+ // "Register: engineer" and other operator-prose shapes).
106
+ let bestIdx = -1;
107
+ let best = null;
108
+ for (const m of markers) {
109
+ const idx = text.lastIndexOf(m);
110
+ if (idx > bestIdx) { bestIdx = idx; best = m; }
111
+ }
112
+ return /** @type {any} */ (best) ?? 'unstated';
113
+ }
114
+
115
+ /**
116
+ * @param {string[]} argv
117
+ * @param {object} [deps]
118
+ * @returns {Promise<number>}
119
+ */
120
+ export async function main(argv, deps = {}) {
121
+ const stdout = deps.stdout ?? process.stdout;
122
+ const stderr = deps.stderr ?? process.stderr;
123
+ const cwd = deps.cwd ?? process.cwd();
124
+
125
+ let parsed;
126
+ try {
127
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: false, strict: true });
128
+ } catch (err) {
129
+ stderr.write(`[error] usage ${err.message}\n`);
130
+ stderr.write(HELP);
131
+ return 2;
132
+ }
133
+ const flags = parsed.values;
134
+ if (flags.help) {
135
+ stdout.write(HELP);
136
+ return 0;
137
+ }
138
+
139
+ // Validate --persona before any filesystem work.
140
+ let persona = flags.persona;
141
+ if (persona !== undefined && !PERSONAS.includes(persona)) {
142
+ stderr.write(`[error] usage questions: unknown persona '${persona}' (expected one of ${PERSONAS.join(' | ')})\n`);
143
+ return 2;
144
+ }
145
+
146
+ // Validate --stage against the readiness aliases (same vocabulary).
147
+ let stageFilter = null;
148
+ if (typeof flags.stage === 'string') {
149
+ const canonical = STAGE_ALIASES[flags.stage];
150
+ if (!canonical) {
151
+ stderr.write(`[error] usage questions: unknown --stage '${flags.stage}' (expected D1..D8 or a short name)\n`);
152
+ return 2;
153
+ }
154
+ stageFilter = canonical;
155
+ }
156
+
157
+ let limit = null;
158
+ if (flags.limit !== undefined) {
159
+ const n = Number(flags.limit);
160
+ if (!Number.isInteger(n) || n < 0) {
161
+ stderr.write(`[error] usage questions: --limit expects a non-negative integer, got '${flags.limit}'\n`);
162
+ return 2;
163
+ }
164
+ limit = n;
165
+ }
166
+
167
+ const projectRoot = await findProjectRoot(cwd);
168
+ if (!projectRoot) {
169
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
170
+ return 2;
171
+ }
172
+
173
+ const { tree, errors } = await walkTree({ projectRoot });
174
+ const staleErrors = await checkCodeNodeResolution({ projectRoot, tree });
175
+ const validateErrors = [...errors, ...staleErrors];
176
+
177
+ const [freeze, ledgers, testPointers, profileText] = await Promise.all([
178
+ loadFreezeRecord({ projectRoot }),
179
+ loadAllLedgers({ projectRoot }),
180
+ resolveTestPointers({ projectRoot, tree }),
181
+ readProfileText(projectRoot),
182
+ ]);
183
+
184
+ if (persona === undefined) {
185
+ const register = pickRegister(profileText);
186
+ persona = register === 'engineer' ? 'engineer' : 'productOwner';
187
+ }
188
+
189
+ const readiness = computeReadiness(tree, {
190
+ freeze, ledgers, profile: undefined, profileText, testPointers, validateErrors,
191
+ });
192
+
193
+ const result = computeQuestions(readiness, { tree, ledgers, profileText, persona });
194
+
195
+ // Apply --stage and --limit AFTER compute so `remaining` counts
196
+ // every matching item (spec §6).
197
+ let filtered = result.questions;
198
+ if (stageFilter) filtered = filtered.filter((q) => q.stage === stageFilter);
199
+ const remaining = filtered.length;
200
+ if (limit !== null) filtered = filtered.slice(0, limit);
201
+
202
+ // Rebuild groups[] from the filtered list, preserving source-span keys.
203
+ const groupMap = new Map();
204
+ for (const q of filtered) {
205
+ const key = q.context?.statement?.source ?? q.context?.sourceSpan ?? 'tree';
206
+ const existing = groupMap.get(key) ?? { key, label: result.groups.find((g) => g.key === key)?.label ?? key, questionIds: [] };
207
+ existing.questionIds.push(q.id);
208
+ groupMap.set(key, existing);
209
+ }
210
+ const envelope = {
211
+ ...result,
212
+ questions: filtered,
213
+ groups: [...groupMap.values()],
214
+ remaining,
215
+ };
216
+
217
+ if (flags.json) {
218
+ stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
219
+ return 0;
220
+ }
221
+
222
+ renderText(stdout, envelope);
223
+ return 0;
224
+ }
225
+
226
+ /**
227
+ * Print the text summary (spec §1.5): one progress line, then each
228
+ * group with its asks in plain words. Ids, verbs and stage names are
229
+ * not shown unless the owner asks.
230
+ *
231
+ * @param {NodeJS.WritableStream} stdout
232
+ * @param {ReturnType<typeof computeQuestions>} result
233
+ */
234
+ function renderText(stdout, result) {
235
+ const persona = result.persona;
236
+ // Termination is `result.ok` alone (spec §1.4): --stage filtering
237
+ // can leave remaining=0 for a stage while the whole tree is still
238
+ // failing, which must not print the complete verdict.
239
+ if (result.ok) {
240
+ stdout.write(persona === 'productOwner'
241
+ ? 'Intent-complete: yes. The question set is empty.\n'
242
+ : 'Ready-to-build: no engineer blockers in this stage.\n');
243
+ return;
244
+ }
245
+
246
+ // Progress line: "Intent-complete: not yet; N questions left, ..."
247
+ const n = result.remaining;
248
+ const partition = partitionByGroupLabel(result);
249
+ const parts = Object.entries(partition)
250
+ .filter(([, c]) => c > 0)
251
+ .map(([label, c]) => `${c} about ${label}`);
252
+ const progressLine = persona === 'productOwner'
253
+ ? `Intent-complete: not yet; ${n} ${n === 1 ? 'question' : 'questions'} left${parts.length ? `, ${parts.join(', ')}` : ''}.`
254
+ : `Ready-to-build: not yet; ${n} ${n === 1 ? 'check' : 'checks'} blocking.`;
255
+ stdout.write(`${progressLine}\n`);
256
+
257
+ for (const group of result.groups) {
258
+ stdout.write(`\n(${group.label})\n`);
259
+ for (const qid of group.questionIds) {
260
+ const q = result.questions.find((x) => x.id === qid);
261
+ if (!q) continue;
262
+ stdout.write(` - ${q.ask}\n`);
263
+ }
264
+ }
265
+
266
+ if (persona === 'productOwner' && result.optional?.length) {
267
+ stdout.write('\nOpen decisions (optional, not blocking):\n');
268
+ for (const o of result.optional) {
269
+ stdout.write(` - ${o.ask}\n`);
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Count questions by group-label bucket for the progress line. For
276
+ * the PO register the two buckets are "your document" (brief:... items)
277
+ * and "requirements" (REQ-... items); anything else goes under "other".
278
+ */
279
+ function partitionByGroupLabel(result) {
280
+ const buckets = { 'your document': 0, 'requirements': 0, 'other': 0 };
281
+ for (const q of result.questions) {
282
+ const src = q.context?.statement?.source ?? q.context?.sourceSpan ?? 'tree';
283
+ if (typeof src === 'string' && src !== 'tree' && !src.startsWith('scan:')) buckets['your document']++;
284
+ else if (q.context?.reqId) buckets['requirements']++;
285
+ else buckets['other']++;
286
+ }
287
+ return buckets;
288
+ }