rcf-lite 0.28.1 → 0.28.4

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 (219) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/bin/rcf.js +4 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/package.json +1 -1
  5. package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
  6. package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
  7. package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
  8. package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
  9. package/rcf/code-nodes/cn-602.json +3 -2
  10. package/rcf/code-nodes/cn-700.json +23 -0
  11. package/rcf/code-nodes/cn-701.json +14 -0
  12. package/rcf/code-nodes/cn-702.json +14 -0
  13. package/rcf/code-nodes/cn-703.json +16 -0
  14. package/rcf/code-nodes/cn-720.json +15 -0
  15. package/rcf/code-nodes/cn-721.json +15 -0
  16. package/rcf/code-nodes/cn-722.json +15 -0
  17. package/rcf/code-nodes/cn-723.json +18 -0
  18. package/rcf/code-nodes/cn-724.json +14 -0
  19. package/rcf/code-nodes/cn-725.json +21 -0
  20. package/rcf/code-nodes/cn-726.json +17 -0
  21. package/rcf/code-nodes/cn-727.json +14 -0
  22. package/rcf/code-nodes/cn-728.json +21 -0
  23. package/rcf/code-nodes/cn-729.json +18 -0
  24. package/rcf/code-nodes/cn-730.json +14 -0
  25. package/rcf/fbs/fbs-181.json +3 -2
  26. package/rcf/fbs/fbs-190.json +37 -0
  27. package/rcf/fbs/fbs-191.json +34 -0
  28. package/rcf/fbs/fbs-192.json +17 -0
  29. package/rcf/fbs/fbs-193.json +37 -0
  30. package/rcf/fbs/fbs-194.json +35 -0
  31. package/rcf/fbs/fbs-195.json +37 -0
  32. package/rcf/fbs/fbs-196.json +37 -0
  33. package/rcf/fbs/fbs-197.json +41 -0
  34. package/rcf/requirements/req-001.json +2 -1
  35. package/rcf/requirements/req-002.json +2 -1
  36. package/rcf/requirements/req-003.json +2 -1
  37. package/rcf/requirements/req-004.json +2 -1
  38. package/rcf/requirements/req-005.json +2 -1
  39. package/rcf/requirements/req-006.json +2 -1
  40. package/rcf/requirements/req-007.json +2 -1
  41. package/rcf/requirements/req-008.json +2 -1
  42. package/rcf/requirements/req-009.json +2 -1
  43. package/rcf/requirements/req-010.json +5 -2
  44. package/rcf/requirements/req-011.json +2 -1
  45. package/rcf/requirements/req-012.json +2 -1
  46. package/rcf/requirements/req-013.json +2 -1
  47. package/rcf/requirements/req-014.json +2 -1
  48. package/rcf/requirements/req-015.json +2 -1
  49. package/rcf/requirements/req-016.json +5 -2
  50. package/rcf/requirements/req-017.json +6 -2
  51. package/rcf/requirements/req-018.json +5 -2
  52. package/rcf/requirements/req-019.json +5 -2
  53. package/rcf/requirements/req-020.json +5 -2
  54. package/rcf/requirements/req-021.json +5 -2
  55. package/rcf/requirements/req-022.json +5 -2
  56. package/rcf/requirements/req-023.json +6 -4
  57. package/rcf/requirements/req-024.json +5 -2
  58. package/rcf/requirements/req-025.json +5 -2
  59. package/rcf/requirements/req-026.json +5 -2
  60. package/rcf/requirements/req-027.json +5 -2
  61. package/rcf/requirements/req-028.json +5 -2
  62. package/rcf/requirements/req-029.json +5 -2
  63. package/rcf/requirements/req-030.json +5 -2
  64. package/rcf/requirements/req-040.json +5 -2
  65. package/rcf/requirements/req-041.json +5 -2
  66. package/rcf/requirements/req-042.json +5 -2
  67. package/rcf/requirements/req-043.json +5 -2
  68. package/rcf/requirements/req-050.json +5 -2
  69. package/rcf/requirements/req-051.json +5 -2
  70. package/rcf/requirements/req-052.json +5 -2
  71. package/rcf/requirements/req-053.json +5 -2
  72. package/rcf/requirements/req-054.json +5 -2
  73. package/rcf/requirements/req-060.json +5 -2
  74. package/rcf/requirements/req-061.json +5 -2
  75. package/rcf/requirements/req-062.json +5 -2
  76. package/rcf/requirements/req-063.json +5 -2
  77. package/rcf/requirements/req-070.json +5 -2
  78. package/rcf/requirements/req-071.json +5 -2
  79. package/rcf/requirements/req-072.json +5 -2
  80. package/rcf/requirements/req-073.json +5 -2
  81. package/rcf/requirements/req-074.json +5 -2
  82. package/rcf/requirements/req-075.json +5 -2
  83. package/rcf/requirements/req-076.json +5 -2
  84. package/rcf/requirements/req-077.json +5 -2
  85. package/rcf/requirements/req-080.json +5 -2
  86. package/rcf/requirements/req-081.json +5 -2
  87. package/rcf/requirements/req-082.json +5 -2
  88. package/rcf/requirements/req-083.json +5 -2
  89. package/rcf/requirements/req-084.json +5 -2
  90. package/rcf/requirements/req-085.json +5 -2
  91. package/rcf/requirements/req-086.json +5 -2
  92. package/rcf/requirements/req-090.json +5 -2
  93. package/rcf/requirements/req-091.json +5 -2
  94. package/rcf/requirements/req-092.json +5 -2
  95. package/rcf/requirements/req-093.json +5 -2
  96. package/rcf/requirements/req-094.json +5 -2
  97. package/rcf/requirements/req-100.json +5 -2
  98. package/rcf/requirements/req-101.json +5 -2
  99. package/rcf/requirements/req-102.json +5 -2
  100. package/rcf/requirements/req-103.json +5 -2
  101. package/rcf/requirements/req-104.json +5 -2
  102. package/rcf/requirements/req-110.json +5 -2
  103. package/rcf/requirements/req-111.json +5 -2
  104. package/rcf/requirements/req-112.json +5 -2
  105. package/rcf/requirements/req-113.json +5 -2
  106. package/rcf/requirements/req-114.json +5 -2
  107. package/rcf/requirements/req-115.json +5 -2
  108. package/rcf/requirements/req-120.json +5 -2
  109. package/rcf/requirements/req-121.json +5 -2
  110. package/rcf/requirements/req-122.json +5 -2
  111. package/rcf/requirements/req-123.json +5 -2
  112. package/rcf/requirements/req-124.json +5 -2
  113. package/rcf/requirements/req-125.json +5 -2
  114. package/rcf/requirements/req-130.json +6 -2
  115. package/rcf/requirements/req-131.json +6 -2
  116. package/rcf/requirements/req-132.json +6 -2
  117. package/rcf/requirements/req-133.json +6 -2
  118. package/rcf/requirements/req-134.json +6 -2
  119. package/rcf/requirements/req-140.json +5 -2
  120. package/rcf/requirements/req-145.json +5 -2
  121. package/rcf/requirements/req-150.json +6 -2
  122. package/rcf/requirements/req-151.json +6 -2
  123. package/rcf/requirements/req-152.json +6 -2
  124. package/rcf/requirements/req-153.json +6 -2
  125. package/rcf/requirements/req-154.json +5 -2
  126. package/rcf/requirements/req-155.json +4 -1
  127. package/rcf/requirements/req-156.json +4 -1
  128. package/rcf/requirements/req-157.json +4 -1
  129. package/rcf/requirements/req-158.json +4 -1
  130. package/rcf/requirements/req-159.json +4 -1
  131. package/rcf/requirements/req-160.json +4 -1
  132. package/rcf/requirements/req-161.json +4 -1
  133. package/rcf/requirements/req-162.json +4 -1
  134. package/rcf/requirements/req-163.json +5 -2
  135. package/rcf/requirements/req-164.json +5 -2
  136. package/rcf/requirements/req-165.json +5 -2
  137. package/rcf/requirements/req-170.json +33 -0
  138. package/rcf/requirements/req-171.json +44 -0
  139. package/rcf/requirements/req-172.json +25 -0
  140. package/rcf/requirements/req-173.json +25 -0
  141. package/rcf/requirements/req-174.json +25 -0
  142. package/rcf/requirements/req-175.json +25 -0
  143. package/rcf/tacs/tac-4103-feedback-redact.json +2 -2
  144. package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
  145. package/rcf/tacs/tac-4121-define-ledgers.json +23 -0
  146. package/rcf/tacs/tac-4122-define-gates.json +25 -0
  147. package/rcf/tacs/tac-4123-define-readiness.json +25 -0
  148. package/rcf/test-suites/ts-188.json +10 -2
  149. package/rcf/test-suites/ts-191.json +19 -5
  150. package/rcf/test-suites/ts-204.json +30 -0
  151. package/rcf/test-suites/ts-205.json +22 -0
  152. package/rcf/test-suites/ts-206.json +22 -0
  153. package/rcf/test-suites/ts-207.json +22 -0
  154. package/rcf/test-suites/ts-208.json +22 -0
  155. package/rcf/test-suites/ts-209.json +22 -0
  156. package/rcf/test-suites/ts-210.json +22 -0
  157. package/rcf/test-suites/ts-211.json +22 -0
  158. package/rcf/test-suites/ts-212.json +22 -0
  159. package/rcf/test-suites/ts-213.json +50 -0
  160. package/rcf/test-suites/ts-214.json +74 -0
  161. package/rcf/test-suites/ts-215.json +67 -0
  162. package/rcf/test-suites/ts-216.json +78 -0
  163. package/rcf/test-suites/ts-217.json +62 -0
  164. package/rcf/user-stories/us-15601.json +10 -1
  165. package/rcf/user-stories/us-15701.json +4 -4
  166. package/rcf/user-stories/us-17001.json +27 -0
  167. package/rcf/user-stories/us-17002.json +22 -0
  168. package/rcf/user-stories/us-17003.json +22 -0
  169. package/rcf/user-stories/us-17004.json +22 -0
  170. package/rcf/user-stories/us-17005.json +22 -0
  171. package/rcf/user-stories/us-17006.json +21 -0
  172. package/rcf/user-stories/us-17007.json +21 -0
  173. package/rcf/user-stories/us-17008.json +22 -0
  174. package/rcf/user-stories/us-17101.json +22 -0
  175. package/rcf/user-stories/us-17102.json +22 -0
  176. package/rcf/user-stories/us-17201.json +38 -0
  177. package/rcf/user-stories/us-17301.json +38 -0
  178. package/rcf/user-stories/us-17401.json +58 -0
  179. package/rcf/user-stories/us-17501.json +48 -0
  180. package/releases/releases.yaml +31 -1
  181. package/src/browser-verify/manifest-writer.js +9 -5
  182. package/src/cli/browser-verify.js +9 -1
  183. package/src/cli/feedback.js +316 -32
  184. package/src/cli/help.js +11 -0
  185. package/src/cli/intake.js +25 -4
  186. package/src/cli/ledger.js +399 -0
  187. package/src/cli/preflight.js +24 -3
  188. package/src/cli/readiness.js +280 -0
  189. package/src/cli/review.js +9 -1
  190. package/src/cli/ui-baseline.js +14 -1
  191. package/src/core/store/index.js +5 -0
  192. package/src/core/store/validate-composed-record.js +95 -0
  193. package/src/core/store/writer.js +86 -6
  194. package/src/define/freeze-record.js +232 -0
  195. package/src/define/ledgers.js +513 -0
  196. package/src/feedback/gh.js +27 -1
  197. package/src/feedback/redact-allowlist.json +7 -0
  198. package/src/feedback/redact.js +126 -11
  199. package/src/feedback/render.js +34 -12
  200. package/src/intake/manifest-writer.js +6 -3
  201. package/src/intake/orchestrator.js +44 -7
  202. package/src/preflight/manifest-writer.js +11 -11
  203. package/src/query/delta.js +276 -0
  204. package/src/query/gates.js +832 -0
  205. package/src/query/index.js +36 -0
  206. package/src/query/readiness.js +333 -0
  207. package/src/req-baseline/sweep.js +34 -5
  208. package/src/server/index.js +37 -0
  209. package/src/server/routes.js +65 -3
  210. package/src/ui-baseline/manifest-writer.js +11 -10
  211. package/src/view/doc-renderers/helpers.js +39 -2
  212. package/src/view/doc-renderers/req.js +10 -2
  213. package/src/view/doc-renderers/tad.js +67 -6
  214. package/src/view/doc-renderers/user-story.js +10 -5
  215. package/src/view/html-page.js +347 -28
  216. package/src/view/index.js +14 -1
  217. package/src/view/live-client.js +22 -2
  218. package/src/view/product-map.js +751 -0
  219. package/src/view/style.css +383 -0
@@ -0,0 +1,399 @@
1
+ // `rcf define ledger <brief|decisions|concerns|probes> <add|resolve|list>`
2
+ // (REQ-173; proposal §8.2 v3).
3
+ //
4
+ // Sidecar-ledger CRUD. The intake-scan-against-frozen-statements part
5
+ // of `brief add --from <file>` is a seam here (slice 1 does not wire
6
+ // it): `rcf discover intake` is the intended caller in a later slice
7
+ // and the CLI exposes `--kind` and `--source` so a caller can feed
8
+ // them in. Exit codes match the rest of the CLI: 0 success, 2 usage
9
+ // error, 1 unexpected runtime failure.
10
+
11
+ import { readFile } from 'node:fs/promises';
12
+ import { parseArgs } from 'node:util';
13
+
14
+ import {
15
+ BRIEF_KINDS,
16
+ LEDGER_NAMES,
17
+ LedgerError,
18
+ addEntry,
19
+ loadLedger,
20
+ parseBriefFromFile,
21
+ resolveEntry,
22
+ saveLedger,
23
+ } from '../define/ledgers.js';
24
+ import { findProjectRoot } from '../view/index.js';
25
+
26
+ const OPTION_SPEC = {
27
+ kind: { type: 'string' },
28
+ text: { type: 'string' },
29
+ from: { type: 'string' },
30
+ source: { type: 'string' },
31
+ reason: { type: 'string' },
32
+ answer: { type: 'string' },
33
+ question: { type: 'string' },
34
+ option: { type: 'string', multiple: true },
35
+ default: { type: 'string' },
36
+ blocks: { type: 'string' },
37
+ req: { type: 'string' },
38
+ concern: { type: 'string' },
39
+ disposition: { type: 'string' },
40
+ finding: { type: 'string' },
41
+ severity: { type: 'string' },
42
+ json: { type: 'boolean' },
43
+ help: { type: 'boolean' },
44
+ };
45
+
46
+ export const HELP = `Usage: rcf define ledger <name> <verb> [options]
47
+
48
+ Ledgers (proposal 2026-09-22 §2.5, §3.2 v3) are sidecars under
49
+ rcf/define/. Four ledgers, each one JSON file, each with one numbered
50
+ sequence for the life of the project.
51
+
52
+ Ledger names:
53
+ brief D1 brief statements (kinded from a closed set)
54
+ decisions D7 open / answered decisions (#241 format)
55
+ concerns D5 (concern, REQ) pairs (applied | waived)
56
+ probes D6 probe findings (severity: low | medium | high)
57
+
58
+ Verbs:
59
+ add Append a new entry, next id
60
+ resolve <id> Flip status to resolved, stamp resolvedAt
61
+ list Print every entry (JSON with --json)
62
+
63
+ Common flags:
64
+ --json Emit machine-readable envelope on list
65
+ --help Print this help
66
+
67
+ brief add flags:
68
+ --kind <${BRIEF_KINDS.join('|')}>
69
+ Statement kind (default: capability)
70
+ --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)
73
+
74
+ decisions add flags:
75
+ --question <text> The decision question (required)
76
+ --option <letter:text> Repeatable option, format 'a:First option'
77
+ --default <letter> Default option letter
78
+ --blocks <text> What this decision blocks (a gate id, doc id)
79
+
80
+ concerns add flags:
81
+ --req <req-id> REQ the (concern, REQ) pair belongs to (required)
82
+ --concern <slug> Concern key (auth, retention, ...) (required)
83
+ --disposition applied|waived
84
+ Disposition (required)
85
+ --reason <text> Reason (mandatory when disposition=waived)
86
+
87
+ probes add flags:
88
+ --req <req-id> REQ the probe finding sits on (required)
89
+ --finding <text> The finding text (required)
90
+ --severity low|medium|high
91
+
92
+ resolve flags:
93
+ --answer <text> On decisions: the answer chosen (a letter, or free text)
94
+ --reason <text> On probes / concerns: the resolution note
95
+
96
+ Notes:
97
+ - Every ledger file lives under rcf/define/. The loader treats missing
98
+ 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.
102
+ - 'decisions list' prints the numbered format from #241: one decision
103
+ per item; options with letters; the default; what it blocks.
104
+ `;
105
+
106
+ /**
107
+ * @param {string[]} argv - argv slice after `ledger`
108
+ * @param {object} [deps]
109
+ * @returns {Promise<number>}
110
+ */
111
+ export async function main(argv, deps = {}) {
112
+ const stdout = deps.stdout ?? process.stdout;
113
+ const stderr = deps.stderr ?? process.stderr;
114
+ const cwd = deps.cwd ?? process.cwd();
115
+
116
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
117
+ stdout.write(HELP);
118
+ return argv.length === 0 ? 2 : 0;
119
+ }
120
+
121
+ const name = argv[0];
122
+ const verb = argv[1];
123
+ if (!LEDGER_NAMES.includes(/** @type {any} */ (name))) {
124
+ stderr.write(`[error] usage ledger: unknown name '${name}' (expected: ${LEDGER_NAMES.join(', ')})\n`);
125
+ return 2;
126
+ }
127
+ if (!verb || !['add', 'resolve', 'list'].includes(verb)) {
128
+ stderr.write("[error] usage ledger: expected verb 'add', 'resolve' or 'list'\n");
129
+ return 2;
130
+ }
131
+
132
+ let parsed;
133
+ try {
134
+ parsed = parseArgs({
135
+ args: argv.slice(2), options: OPTION_SPEC, allowPositionals: true, strict: true,
136
+ });
137
+ } catch (err) {
138
+ stderr.write(`[error] usage ${/** @type {Error} */ (err).message}\n`);
139
+ return 2;
140
+ }
141
+ const flags = parsed.values;
142
+ const positionals = parsed.positionals;
143
+ if (flags.help) { stdout.write(HELP); return 0; }
144
+
145
+ const projectRoot = await findProjectRoot(cwd);
146
+ if (!projectRoot) {
147
+ 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');
148
+ return 2;
149
+ }
150
+
151
+ try {
152
+ if (verb === 'list') {
153
+ return await runList({ projectRoot, name, flags, stdout });
154
+ }
155
+ if (verb === 'add') {
156
+ return await runAdd({ projectRoot, name, flags, stdout, stderr });
157
+ }
158
+ return await runResolve({ projectRoot, name, positionals, flags, stdout, stderr });
159
+ } catch (err) {
160
+ if (err instanceof LedgerError) {
161
+ stderr.write(`[error] ${err.code === 'usage' ? 'usage ' : ''}ledger: ${err.message}\n`);
162
+ return err.code === 'usage' ? 2 : 1;
163
+ }
164
+ stderr.write(`[error] ledger: ${/** @type {Error} */ (err).message}\n`);
165
+ return 1;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * @param {object} args
171
+ * @param {string} args.projectRoot
172
+ * @param {import('../define/ledgers.js').LedgerName} args.name
173
+ * @param {Record<string, unknown>} args.flags
174
+ * @param {NodeJS.WriteStream} args.stdout
175
+ * @returns {Promise<number>}
176
+ */
177
+ async function runList({ projectRoot, name, flags, stdout }) {
178
+ const body = await loadLedger({ projectRoot, name });
179
+ if (flags.json) {
180
+ stdout.write(`${JSON.stringify({ ledger: name, ...body }, null, 2)}\n`);
181
+ return 0;
182
+ }
183
+ if (name === 'decisions') {
184
+ stdout.write(renderDecisionsList(body));
185
+ return 0;
186
+ }
187
+ stdout.write(renderGenericList(name, body));
188
+ return 0;
189
+ }
190
+
191
+ /**
192
+ * Render decisions in the numbered #241 format:
193
+ * 1. <question> [status]
194
+ * a) <option a>
195
+ * b) <option b>
196
+ * default: <letter>
197
+ * blocks: <text>
198
+ * answer: <text> (when resolved)
199
+ *
200
+ * @param {Record<string, any>} body
201
+ * @returns {string}
202
+ */
203
+ export function renderDecisionsList(body) {
204
+ const list = /** @type {any[]} */ (body.decisions ?? []);
205
+ if (list.length === 0) return '(no decisions)\n';
206
+ const lines = [];
207
+ for (const d of list) {
208
+ const status = d.status === 'resolved' ? 'resolved' : 'open';
209
+ lines.push(`${d.id}. ${d.question} [${status}]`);
210
+ for (const opt of d.options ?? []) {
211
+ lines.push(` ${opt.letter}) ${opt.text}`);
212
+ }
213
+ if (d.default != null) lines.push(` default: ${d.default}`);
214
+ if (d.blocks) lines.push(` blocks: ${d.blocks}`);
215
+ if (status === 'resolved' && d.answer != null) lines.push(` answer: ${d.answer}`);
216
+ lines.push('');
217
+ }
218
+ return `${lines.join('\n').trimEnd()}\n`;
219
+ }
220
+
221
+ /**
222
+ * @param {import('../define/ledgers.js').LedgerName} name
223
+ * @param {Record<string, any>} body
224
+ * @returns {string}
225
+ */
226
+ function renderGenericList(name, body) {
227
+ const key = name === 'brief' ? 'statements' : name;
228
+ const list = /** @type {any[]} */ (body[key] ?? []);
229
+ if (list.length === 0) return `(no ${name} entries)\n`;
230
+ const lines = [];
231
+ for (const e of list) {
232
+ const status = e.status === 'resolved' ? 'resolved' : 'open';
233
+ if (name === 'brief') {
234
+ lines.push(`${e.id}. [${e.kind}] ${e.text} (${status})`);
235
+ if (e.source) lines.push(` source: ${e.source}`);
236
+ } else if (name === 'concerns') {
237
+ lines.push(`${e.id}. ${e.concern} on ${e.reqId}: ${e.disposition} (${status})`);
238
+ if (e.reason) lines.push(` reason: ${e.reason}`);
239
+ } else if (name === 'probes') {
240
+ lines.push(`${e.id}. [${e.severity}] ${e.reqId}: ${e.finding} (${status})`);
241
+ }
242
+ }
243
+ return `${lines.join('\n')}\n`;
244
+ }
245
+
246
+ /**
247
+ * @param {object} args
248
+ */
249
+ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
250
+ const body = await loadLedger({ projectRoot, name });
251
+ const now = new Date().toISOString();
252
+
253
+ if (name === 'brief') {
254
+ const kind = /** @type {string} */ (flags.kind ?? 'capability');
255
+ if (!BRIEF_KINDS.includes(/** @type {any} */ (kind))) {
256
+ stderr.write(`[error] usage ledger: brief --kind must be one of: ${BRIEF_KINDS.join(', ')}\n`);
257
+ return 2;
258
+ }
259
+ if (flags.from && flags.text) {
260
+ stderr.write('[error] usage ledger: brief add takes --text OR --from, not both\n');
261
+ return 2;
262
+ }
263
+ if (!flags.from && !flags.text) {
264
+ stderr.write('[error] usage ledger: brief add requires --text <text> or --from <path>\n');
265
+ return 2;
266
+ }
267
+ let drafts;
268
+ if (flags.from) {
269
+ const raw = await readFile(String(flags.from), 'utf8');
270
+ drafts = parseBriefFromFile(raw, { kind, source: flags.source ? String(flags.source) : String(flags.from) });
271
+ if (drafts.length === 0) {
272
+ stderr.write(`[error] usage ledger: brief --from ${flags.from} produced no non-empty lines\n`);
273
+ return 2;
274
+ }
275
+ } else {
276
+ const entry = { text: String(flags.text), kind };
277
+ if (flags.source) entry.source = String(flags.source);
278
+ drafts = [entry];
279
+ }
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.
283
+ let cur = body;
284
+ const added = [];
285
+ for (const d of drafts) {
286
+ const step = addEntry({ name: 'brief', body: cur, entry: d, now });
287
+ cur = step.body;
288
+ added.push(step.entry);
289
+ }
290
+ 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`);
292
+ return 0;
293
+ }
294
+
295
+ if (name === 'decisions') {
296
+ if (!flags.question) {
297
+ stderr.write('[error] usage ledger: decisions add requires --question <text>\n');
298
+ return 2;
299
+ }
300
+ const options = parseOptionFlags(flags.option);
301
+ if (options.error) {
302
+ stderr.write(`[error] usage ledger: ${options.error}\n`);
303
+ return 2;
304
+ }
305
+ const entry = {
306
+ question: String(flags.question),
307
+ options: options.value,
308
+ default: flags.default ? String(flags.default) : null,
309
+ };
310
+ if (flags.blocks) entry.blocks = String(flags.blocks);
311
+ const step = addEntry({ name: 'decisions', body, entry, now });
312
+ await saveLedger({ projectRoot, name: 'decisions', body: step.body });
313
+ stdout.write(`decisions-ledger: added decision ${step.entry.id}\n`);
314
+ return 0;
315
+ }
316
+
317
+ if (name === 'concerns') {
318
+ if (!flags.req || !flags.concern || !flags.disposition) {
319
+ stderr.write('[error] usage ledger: concerns add requires --req <req-id> --concern <slug> --disposition applied|waived\n');
320
+ return 2;
321
+ }
322
+ if (flags.disposition === 'waived' && !flags.reason) {
323
+ stderr.write('[error] usage ledger: concerns add --disposition waived requires --reason <text>\n');
324
+ return 2;
325
+ }
326
+ const entry = {
327
+ concern: String(flags.concern),
328
+ reqId: String(flags.req),
329
+ disposition: String(flags.disposition),
330
+ };
331
+ if (flags.reason) entry.reason = String(flags.reason);
332
+ const step = addEntry({ name: 'concerns', body, entry, now });
333
+ await saveLedger({ projectRoot, name: 'concerns', body: step.body });
334
+ stdout.write(`concern-ledger: added concern ${step.entry.id}\n`);
335
+ return 0;
336
+ }
337
+
338
+ // probes
339
+ if (!flags.req || !flags.finding) {
340
+ stderr.write('[error] usage ledger: probes add requires --req <req-id> --finding <text>\n');
341
+ return 2;
342
+ }
343
+ const entry = {
344
+ reqId: String(flags.req),
345
+ finding: String(flags.finding),
346
+ severity: flags.severity ? String(flags.severity) : 'medium',
347
+ };
348
+ const step = addEntry({ name: 'probes', body, entry, now });
349
+ await saveLedger({ projectRoot, name: 'probes', body: step.body });
350
+ stdout.write(`probe-ledger: added probe ${step.entry.id}\n`);
351
+ return 0;
352
+ }
353
+
354
+ async function runResolve({ projectRoot, name, positionals, flags, stdout, stderr }) {
355
+ if (positionals.length !== 1) {
356
+ stderr.write('[error] usage ledger: resolve expects exactly one <id>\n');
357
+ return 2;
358
+ }
359
+ const id = Number(positionals[0]);
360
+ if (!Number.isInteger(id) || id < 1) {
361
+ stderr.write(`[error] usage ledger: resolve id must be a positive integer, got '${positionals[0]}'\n`);
362
+ return 2;
363
+ }
364
+ const body = await loadLedger({ projectRoot, name });
365
+ const now = new Date().toISOString();
366
+ /** @type {Record<string, unknown>} */
367
+ const patch = {};
368
+ if (flags.answer) patch.answer = String(flags.answer);
369
+ if (flags.reason) patch.reason = String(flags.reason);
370
+ if (flags.answer) patch.answeredAt = now;
371
+ const step = resolveEntry({ name, body, id, patch, now });
372
+ await saveLedger({ projectRoot, name, body: step.body });
373
+ stdout.write(`${name}-ledger: resolved ${id}\n`);
374
+ return 0;
375
+ }
376
+
377
+ /**
378
+ * Parse repeated `--option letter:text` into option records. Returns
379
+ * `{ value }` on success or `{ error }` on any malformed input.
380
+ *
381
+ * @param {unknown} raw
382
+ * @returns {{ value: Array<{ letter: string, text: string }> } | { error: string }}
383
+ */
384
+ export function parseOptionFlags(raw) {
385
+ if (!raw || !Array.isArray(raw) || raw.length === 0) {
386
+ return { error: "decisions add requires at least one --option 'a:First option'" };
387
+ }
388
+ const value = [];
389
+ for (const s of raw) {
390
+ if (typeof s !== 'string') return { error: `--option value must be a string, got ${typeof s}` };
391
+ const sep = s.indexOf(':');
392
+ if (sep < 1) return { error: `--option must be 'letter:text', got '${s}'` };
393
+ const letter = s.slice(0, sep).trim();
394
+ const text = s.slice(sep + 1).trim();
395
+ if (!letter || !text) return { error: `--option must be 'letter:text', got '${s}'` };
396
+ value.push({ letter, text });
397
+ }
398
+ return { value };
399
+ }
@@ -23,7 +23,7 @@ import { parseArgs } from 'node:util';
23
23
  import { createInterface } from 'node:readline/promises';
24
24
  import process from 'node:process';
25
25
 
26
- import { walkTree } from '#core/store';
26
+ import { walkTree, validateComposedRecord } from '#core/store';
27
27
  import { writeUnexpectedFailure, rcfError } from '#core/errors';
28
28
 
29
29
  import { findProjectRoot } from '../view/index.js';
@@ -63,11 +63,18 @@ Options:
63
63
  --input <path> Non-interactive: read pre-filled session
64
64
  --non-interactive Force non-interactive mode (default when
65
65
  not on a TTY or when piped)
66
- --dry-run Print the composed record; do not write
66
+ --dry-run Print the composed record; do not write.
67
+ The composed record is still run through
68
+ the same manifest schema pass the write
69
+ path runs (0.28.2 fix for #232), so a
70
+ preview fails on the same schema misses.
67
71
  --json Emit the composed record as JSON to stdout
68
72
  --quiet Suppress non-error stdout
69
73
  --help Print this help
70
74
 
75
+ Service id shape: ^[a-z][a-zA-Z0-9]*$ (camelCase, no hyphens; example:
76
+ "stripeCheckout" is valid, "stripe-checkout" is not).
77
+
71
78
  Credentials NEVER enter the chain. The session prompts for env var
72
79
  NAMES only; values are read from the shell at test / finalise time.
73
80
  The name-metadata is written to .rcf/preflight-secrets.local.json,
@@ -77,7 +84,7 @@ Exit codes:
77
84
  0 success
78
85
  1 IO / unexpected runtime failure
79
86
  2 usage error (bad flags, unresolvable PRD id)
80
- 3 schema validation on the composed record
87
+ 3 schema validation on the composed record (including --dry-run)
81
88
  4 operator cancelled the session at the confirm step
82
89
  `;
83
90
 
@@ -189,6 +196,20 @@ export async function main(argv, deps = {}) {
189
196
  });
190
197
 
191
198
  if (flags['dry-run']) {
199
+ // 0.28.2 (issue #232): dry-run now runs the same schema pass the
200
+ // writer runs. The service-id pattern `^[a-z][a-zA-Z0-9]*$` is
201
+ // enforced by the manifest schema on the write path; the preview
202
+ // is no longer allowed to lie about a kebab-case id.
203
+ const dryValidation = validateComposedRecord({
204
+ tree,
205
+ record,
206
+ verb: 'preflight',
207
+ extraRecords: optOuts,
208
+ });
209
+ if (dryValidation) {
210
+ stderr.write(`[error] ${dryValidation.kind} ${dryValidation.message}\n`);
211
+ return 3;
212
+ }
192
213
  if (flags.json) stdout.write(`${JSON.stringify({ record, optOuts }, null, 2)}\n`);
193
214
  if (!flags.quiet) {
194
215
  stdout.write(`[dry-run] preflight would write record ${record.id} (${record.servicesInScope.length} services, ${record.designShapeAnswers?.length ?? 0} design-shape answers, ${optOuts.length} opt-outs)\n`);