rcf-lite 0.28.2 → 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 (327) hide show
  1. package/CHANGELOG.md +317 -0
  2. package/bin/rcf.js +8 -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-4120-canonical-json-hashing.json +30 -0
  15. package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
  16. package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
  17. package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
  18. package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
  19. package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
  20. package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
  21. package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
  22. package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
  23. package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
  24. package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
  25. package/rcf/adrs/adr-4134-index-json-route.json +35 -0
  26. package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
  27. package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
  28. package/rcf/code-nodes/cn-700.json +23 -0
  29. package/rcf/code-nodes/cn-701.json +14 -0
  30. package/rcf/code-nodes/cn-702.json +14 -0
  31. package/rcf/code-nodes/cn-703.json +16 -0
  32. package/rcf/code-nodes/cn-720.json +15 -0
  33. package/rcf/code-nodes/cn-721.json +15 -0
  34. package/rcf/code-nodes/cn-722.json +15 -0
  35. package/rcf/code-nodes/cn-723.json +18 -0
  36. package/rcf/code-nodes/cn-724.json +14 -0
  37. package/rcf/code-nodes/cn-725.json +37 -0
  38. package/rcf/code-nodes/cn-726.json +25 -0
  39. package/rcf/code-nodes/cn-727.json +23 -0
  40. package/rcf/code-nodes/cn-728.json +37 -0
  41. package/rcf/code-nodes/cn-729.json +26 -0
  42. package/rcf/code-nodes/cn-730.json +24 -0
  43. package/rcf/code-nodes/cn-731.json +20 -0
  44. package/rcf/code-nodes/cn-732.json +14 -0
  45. package/rcf/code-nodes/cn-733.json +14 -0
  46. package/rcf/code-nodes/cn-734.json +14 -0
  47. package/rcf/code-nodes/cn-735.json +20 -0
  48. package/rcf/code-nodes/cn-736.json +14 -0
  49. package/rcf/code-nodes/cn-737.json +14 -0
  50. package/rcf/code-nodes/cn-738.json +14 -0
  51. package/rcf/code-nodes/cn-739.json +18 -0
  52. package/rcf/code-nodes/cn-740.json +19 -0
  53. package/rcf/code-nodes/cn-741.json +16 -0
  54. package/rcf/code-nodes/cn-742.json +16 -0
  55. package/rcf/code-nodes/cn-743.json +15 -0
  56. package/rcf/code-nodes/cn-744.json +22 -0
  57. package/rcf/code-nodes/cn-745.json +16 -0
  58. package/rcf/code-nodes/cn-746.json +14 -0
  59. package/rcf/code-nodes/cn-747.json +18 -0
  60. package/rcf/code-nodes/cn-748.json +16 -0
  61. package/rcf/code-nodes/cn-749.json +16 -0
  62. package/rcf/code-nodes/cn-750.json +15 -0
  63. package/rcf/code-nodes/cn-751.json +18 -0
  64. package/rcf/code-nodes/cn-752.json +14 -0
  65. package/rcf/fbs/fbs-190.json +37 -0
  66. package/rcf/fbs/fbs-191.json +34 -0
  67. package/rcf/fbs/fbs-192.json +17 -0
  68. package/rcf/fbs/fbs-193.json +37 -0
  69. package/rcf/fbs/fbs-194.json +35 -0
  70. package/rcf/fbs/fbs-195.json +37 -0
  71. package/rcf/fbs/fbs-196.json +45 -0
  72. package/rcf/fbs/fbs-197.json +41 -0
  73. package/rcf/fbs/fbs-198.json +42 -0
  74. package/rcf/fbs/fbs-199.json +59 -0
  75. package/rcf/fbs/fbs-200.json +46 -0
  76. package/rcf/fbs/fbs-201.json +49 -0
  77. package/rcf/fbs/fbs-202.json +110 -0
  78. package/rcf/fbs/fbs-203.json +100 -0
  79. package/rcf/requirements/req-001.json +2 -1
  80. package/rcf/requirements/req-002.json +3 -2
  81. package/rcf/requirements/req-003.json +2 -1
  82. package/rcf/requirements/req-004.json +2 -1
  83. package/rcf/requirements/req-005.json +2 -1
  84. package/rcf/requirements/req-006.json +2 -1
  85. package/rcf/requirements/req-007.json +2 -1
  86. package/rcf/requirements/req-008.json +2 -1
  87. package/rcf/requirements/req-009.json +2 -1
  88. package/rcf/requirements/req-010.json +5 -2
  89. package/rcf/requirements/req-011.json +2 -1
  90. package/rcf/requirements/req-012.json +2 -1
  91. package/rcf/requirements/req-013.json +2 -1
  92. package/rcf/requirements/req-014.json +2 -1
  93. package/rcf/requirements/req-015.json +2 -1
  94. package/rcf/requirements/req-016.json +5 -2
  95. package/rcf/requirements/req-017.json +6 -2
  96. package/rcf/requirements/req-018.json +5 -2
  97. package/rcf/requirements/req-019.json +5 -2
  98. package/rcf/requirements/req-020.json +5 -2
  99. package/rcf/requirements/req-021.json +5 -2
  100. package/rcf/requirements/req-022.json +5 -2
  101. package/rcf/requirements/req-023.json +6 -4
  102. package/rcf/requirements/req-024.json +5 -2
  103. package/rcf/requirements/req-025.json +5 -2
  104. package/rcf/requirements/req-026.json +5 -2
  105. package/rcf/requirements/req-027.json +5 -2
  106. package/rcf/requirements/req-028.json +5 -2
  107. package/rcf/requirements/req-029.json +5 -2
  108. package/rcf/requirements/req-030.json +5 -2
  109. package/rcf/requirements/req-040.json +5 -2
  110. package/rcf/requirements/req-041.json +5 -2
  111. package/rcf/requirements/req-042.json +5 -2
  112. package/rcf/requirements/req-043.json +5 -2
  113. package/rcf/requirements/req-050.json +5 -2
  114. package/rcf/requirements/req-051.json +5 -2
  115. package/rcf/requirements/req-052.json +5 -2
  116. package/rcf/requirements/req-053.json +5 -2
  117. package/rcf/requirements/req-054.json +5 -2
  118. package/rcf/requirements/req-060.json +5 -2
  119. package/rcf/requirements/req-061.json +5 -2
  120. package/rcf/requirements/req-062.json +5 -2
  121. package/rcf/requirements/req-063.json +5 -2
  122. package/rcf/requirements/req-070.json +5 -2
  123. package/rcf/requirements/req-071.json +5 -2
  124. package/rcf/requirements/req-072.json +5 -2
  125. package/rcf/requirements/req-073.json +5 -2
  126. package/rcf/requirements/req-074.json +5 -2
  127. package/rcf/requirements/req-075.json +5 -2
  128. package/rcf/requirements/req-076.json +5 -2
  129. package/rcf/requirements/req-077.json +5 -2
  130. package/rcf/requirements/req-080.json +5 -2
  131. package/rcf/requirements/req-081.json +5 -2
  132. package/rcf/requirements/req-082.json +5 -2
  133. package/rcf/requirements/req-083.json +5 -2
  134. package/rcf/requirements/req-084.json +5 -2
  135. package/rcf/requirements/req-085.json +5 -2
  136. package/rcf/requirements/req-086.json +5 -2
  137. package/rcf/requirements/req-090.json +5 -2
  138. package/rcf/requirements/req-091.json +5 -2
  139. package/rcf/requirements/req-092.json +5 -2
  140. package/rcf/requirements/req-093.json +5 -2
  141. package/rcf/requirements/req-094.json +5 -2
  142. package/rcf/requirements/req-100.json +5 -2
  143. package/rcf/requirements/req-101.json +5 -2
  144. package/rcf/requirements/req-102.json +5 -2
  145. package/rcf/requirements/req-103.json +5 -2
  146. package/rcf/requirements/req-104.json +5 -2
  147. package/rcf/requirements/req-110.json +5 -2
  148. package/rcf/requirements/req-111.json +5 -2
  149. package/rcf/requirements/req-112.json +5 -2
  150. package/rcf/requirements/req-113.json +5 -2
  151. package/rcf/requirements/req-114.json +5 -2
  152. package/rcf/requirements/req-115.json +5 -2
  153. package/rcf/requirements/req-120.json +5 -2
  154. package/rcf/requirements/req-121.json +5 -2
  155. package/rcf/requirements/req-122.json +5 -2
  156. package/rcf/requirements/req-123.json +5 -2
  157. package/rcf/requirements/req-124.json +5 -2
  158. package/rcf/requirements/req-125.json +5 -2
  159. package/rcf/requirements/req-130.json +6 -2
  160. package/rcf/requirements/req-131.json +6 -2
  161. package/rcf/requirements/req-132.json +6 -2
  162. package/rcf/requirements/req-133.json +6 -2
  163. package/rcf/requirements/req-134.json +6 -2
  164. package/rcf/requirements/req-140.json +5 -2
  165. package/rcf/requirements/req-145.json +5 -2
  166. package/rcf/requirements/req-150.json +6 -2
  167. package/rcf/requirements/req-151.json +6 -2
  168. package/rcf/requirements/req-152.json +6 -2
  169. package/rcf/requirements/req-153.json +6 -2
  170. package/rcf/requirements/req-154.json +5 -2
  171. package/rcf/requirements/req-155.json +4 -1
  172. package/rcf/requirements/req-156.json +4 -1
  173. package/rcf/requirements/req-157.json +4 -1
  174. package/rcf/requirements/req-158.json +4 -1
  175. package/rcf/requirements/req-159.json +4 -1
  176. package/rcf/requirements/req-160.json +4 -1
  177. package/rcf/requirements/req-161.json +4 -1
  178. package/rcf/requirements/req-162.json +4 -1
  179. package/rcf/requirements/req-163.json +5 -2
  180. package/rcf/requirements/req-164.json +5 -2
  181. package/rcf/requirements/req-165.json +5 -2
  182. package/rcf/requirements/req-170.json +33 -0
  183. package/rcf/requirements/req-171.json +44 -0
  184. package/rcf/requirements/req-172.json +25 -0
  185. package/rcf/requirements/req-173.json +25 -0
  186. package/rcf/requirements/req-174.json +31 -0
  187. package/rcf/requirements/req-175.json +31 -0
  188. package/rcf/requirements/req-176.json +25 -0
  189. package/rcf/requirements/req-180.json +31 -0
  190. package/rcf/requirements/req-181.json +31 -0
  191. package/rcf/requirements/req-182.json +25 -0
  192. package/rcf/requirements/req-186.json +25 -0
  193. package/rcf/requirements/req-187.json +31 -0
  194. package/rcf/requirements/req-188.json +25 -0
  195. package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
  196. package/rcf/tacs/tac-4121-define-ledgers.json +35 -0
  197. package/rcf/tacs/tac-4122-define-gates.json +61 -0
  198. package/rcf/tacs/tac-4123-define-readiness.json +61 -0
  199. package/rcf/tacs/tac-4124-define-freeze.json +28 -0
  200. package/rcf/tacs/tac-4127-readiness-view.json +49 -0
  201. package/rcf/tacs/tac-4128-define-guidance.json +53 -0
  202. package/rcf/tacs/tac-4129-define-questions.json +45 -0
  203. package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
  204. package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
  205. package/rcf/tacs/tac-4132-id-lookup.json +42 -0
  206. package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
  207. package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
  208. package/rcf/test-suites/ts-016.json +3 -3
  209. package/rcf/test-suites/ts-204.json +30 -0
  210. package/rcf/test-suites/ts-205.json +22 -0
  211. package/rcf/test-suites/ts-206.json +22 -0
  212. package/rcf/test-suites/ts-207.json +22 -0
  213. package/rcf/test-suites/ts-208.json +22 -0
  214. package/rcf/test-suites/ts-209.json +22 -0
  215. package/rcf/test-suites/ts-210.json +22 -0
  216. package/rcf/test-suites/ts-211.json +22 -0
  217. package/rcf/test-suites/ts-212.json +22 -0
  218. package/rcf/test-suites/ts-213.json +50 -0
  219. package/rcf/test-suites/ts-214.json +74 -0
  220. package/rcf/test-suites/ts-215.json +211 -0
  221. package/rcf/test-suites/ts-216.json +94 -0
  222. package/rcf/test-suites/ts-217.json +62 -0
  223. package/rcf/test-suites/ts-218.json +78 -0
  224. package/rcf/test-suites/ts-219.json +70 -0
  225. package/rcf/test-suites/ts-220.json +78 -0
  226. package/rcf/test-suites/ts-221.json +78 -0
  227. package/rcf/test-suites/ts-222.json +100 -0
  228. package/rcf/test-suites/ts-223.json +94 -0
  229. package/rcf/test-suites/ts-224.json +70 -0
  230. package/rcf/test-suites/ts-225.json +78 -0
  231. package/rcf/test-suites/ts-226.json +158 -0
  232. package/rcf/test-suites/ts-227.json +38 -0
  233. package/rcf/test-suites/ts-228.json +86 -0
  234. package/rcf/test-suites/ts-229.json +83 -0
  235. package/rcf/user-stories/us-15601.json +4 -4
  236. package/rcf/user-stories/us-17001.json +27 -0
  237. package/rcf/user-stories/us-17002.json +22 -0
  238. package/rcf/user-stories/us-17003.json +22 -0
  239. package/rcf/user-stories/us-17004.json +22 -0
  240. package/rcf/user-stories/us-17005.json +22 -0
  241. package/rcf/user-stories/us-17006.json +21 -0
  242. package/rcf/user-stories/us-17007.json +21 -0
  243. package/rcf/user-stories/us-17008.json +22 -0
  244. package/rcf/user-stories/us-17101.json +22 -0
  245. package/rcf/user-stories/us-17102.json +22 -0
  246. package/rcf/user-stories/us-17201.json +38 -0
  247. package/rcf/user-stories/us-17301.json +38 -0
  248. package/rcf/user-stories/us-17302.json +50 -0
  249. package/rcf/user-stories/us-17401.json +70 -0
  250. package/rcf/user-stories/us-17402.json +55 -0
  251. package/rcf/user-stories/us-17403.json +60 -0
  252. package/rcf/user-stories/us-17501.json +48 -0
  253. package/rcf/user-stories/us-17502.json +60 -0
  254. package/rcf/user-stories/us-17503.json +65 -0
  255. package/rcf/user-stories/us-17601.json +58 -0
  256. package/rcf/user-stories/us-18001.json +55 -0
  257. package/rcf/user-stories/us-18002.json +35 -0
  258. package/rcf/user-stories/us-18101.json +35 -0
  259. package/rcf/user-stories/us-18202.json +40 -0
  260. package/rcf/user-stories/us-18601.json +55 -0
  261. package/rcf/user-stories/us-18602.json +50 -0
  262. package/rcf/user-stories/us-18603.json +45 -0
  263. package/rcf/user-stories/us-18701.json +50 -0
  264. package/rcf/user-stories/us-18702.json +50 -0
  265. package/rcf/user-stories/us-18801.json +45 -0
  266. package/rcf/user-stories/us-204.json +59 -0
  267. package/rcf/user-stories/us-205.json +64 -0
  268. package/rcf/user-stories/us-206.json +54 -0
  269. package/releases/releases.yaml +31 -1
  270. package/src/cli/feedback.js +126 -10
  271. package/src/cli/freeze.js +730 -0
  272. package/src/cli/help.js +23 -0
  273. package/src/cli/ledger.js +615 -0
  274. package/src/cli/questions.js +288 -0
  275. package/src/cli/readiness.js +498 -0
  276. package/src/cli/view.js +29 -0
  277. package/src/core/store/writer.js +86 -6
  278. package/src/define/freeze-record.js +247 -0
  279. package/src/define/ledgers.js +712 -0
  280. package/src/feedback/redact.js +105 -19
  281. package/src/intake/index.js +1 -1
  282. package/src/intake/orchestrator.js +117 -0
  283. package/src/mcp/tools.js +354 -0
  284. package/src/query/delta.js +282 -0
  285. package/src/query/gates.js +1107 -0
  286. package/src/query/index.js +50 -0
  287. package/src/query/questions.js +438 -0
  288. package/src/query/readiness.js +574 -0
  289. package/src/server/index.js +60 -1
  290. package/src/server/routes.js +133 -4
  291. package/src/setup/identity-seed.js +17 -0
  292. package/src/view/build-dag.js +563 -0
  293. package/src/view/components/badge.js +41 -0
  294. package/src/view/components/diff.js +72 -0
  295. package/src/view/components/doc-row.js +62 -0
  296. package/src/view/components/empty-state.js +35 -0
  297. package/src/view/components/entity-selector.js +137 -0
  298. package/src/view/components/filter-bar.js +97 -0
  299. package/src/view/components/findings-list.js +49 -0
  300. package/src/view/components/index.js +12 -0
  301. package/src/view/components/pill.js +46 -0
  302. package/src/view/components/sub-tab-strip.js +43 -0
  303. package/src/view/components/toast.js +25 -0
  304. package/src/view/components-fixture.js +265 -0
  305. package/src/view/doc-renderers/architecture.js +304 -0
  306. package/src/view/doc-renderers/build.js +160 -0
  307. package/src/view/doc-renderers/helpers.js +2 -2
  308. package/src/view/doc-renderers/index.js +2 -0
  309. package/src/view/doc-renderers/prd.js +35 -6
  310. package/src/view/doc-renderers/req.js +10 -2
  311. package/src/view/doc-renderers/user-story.js +10 -5
  312. package/src/view/html-page.js +717 -300
  313. package/src/view/index.js +109 -3
  314. package/src/view/live-client.js +61 -8
  315. package/src/view/needs-work.js +79 -0
  316. package/src/view/page-init.js +2193 -0
  317. package/src/view/product-map.js +751 -0
  318. package/src/view/readiness/phrasebook.js +115 -0
  319. package/src/view/readiness/po-layer.js +288 -0
  320. package/src/view/readiness/question-adapter.js +329 -0
  321. package/src/view/readiness/stage-legend.js +71 -0
  322. package/src/view/readiness.js +505 -0
  323. package/src/view/style.css +2279 -266
  324. package/src/view/test-host/embed-client.js +121 -0
  325. package/src/view/test-host/fixture.js +183 -0
  326. package/src/view/test-host/lints.js +220 -0
  327. package/src/view/view-index.js +323 -0
package/src/cli/help.js CHANGED
@@ -45,6 +45,10 @@ import { HELP as REQ_BASELINE_HELP } from './req-baseline.js';
45
45
  import { HELP as INTAKE_HELP } from './intake.js';
46
46
  import { HELP as BLUEPRINT_HELP } from './blueprint.js';
47
47
  import { HELP as STANDARDS_HELP } from './standards.js';
48
+ import { HELP as LEDGER_HELP } from './ledger.js';
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';
48
52
 
49
53
  // Verify group members (from the verify-suite subtree).
50
54
  import { HELP as VERIFY_RUN_HELP } from '../verify/cli/run.js';
@@ -93,6 +97,9 @@ define Author the RCF tree.
93
97
  design <fbs-id> <verb> Design substage for a UI-bearing FBS.
94
98
  blueprint <verb> Compose blueprints onto the project.
95
99
  standards <verb> Register standards packs against the project.
100
+ ledger <name> <verb> Sidecar-ledger CRUD under rcf/define/.
101
+ readiness DEFINE stage-gate readiness view.
102
+ questions Persona question set for the intent-complete loop.
96
103
 
97
104
  build Drive the FBS queue and the five-stage build cycle.
98
105
  queue Show the FBS build queue.
@@ -189,6 +196,18 @@ Verbs:
189
196
  design <fbs-id> <verb> Design substage for a UI-bearing FBS.
190
197
  blueprint <verb> Compose blueprints onto the project.
191
198
  standards <verb> Register standards packs against the project.
199
+ ledger <name> <verb> Sidecar-ledger CRUD under rcf/define/
200
+ (brief | decisions | concerns | probes;
201
+ verbs: add | resolve | list).
202
+ readiness DEFINE stage-gate readiness view; twin of
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.
192
211
 
193
212
  Run 'rcf help define <verb>' for per-verb help.
194
213
  `;
@@ -284,6 +303,10 @@ export const HELP_MAP = {
284
303
  design: DESIGN_HELP,
285
304
  blueprint: BLUEPRINT_HELP,
286
305
  standards: STANDARDS_HELP,
306
+ ledger: LEDGER_HELP,
307
+ readiness: READINESS_HELP,
308
+ questions: QUESTIONS_HELP,
309
+ freeze: FREEZE_HELP,
287
310
  },
288
311
  build: {
289
312
  queue: BUILD_HELP,
@@ -0,0 +1,615 @@
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
+ isResolvedByGrammar,
20
+ loadLedger,
21
+ parseBriefFromFile,
22
+ resolveEntry,
23
+ saveLedger,
24
+ updateEntry,
25
+ } from '../define/ledgers.js';
26
+ import { runIntakeScansOnDelta } from '../intake/orchestrator.js';
27
+ import { findProjectRoot } from '../view/index.js';
28
+
29
+ const OPTION_SPEC = {
30
+ kind: { type: 'string' },
31
+ text: { type: 'string' },
32
+ from: { type: 'string' },
33
+ source: { type: 'string' },
34
+ reason: { type: 'string' },
35
+ answer: { type: 'string' },
36
+ question: { type: 'string' },
37
+ option: { type: 'string', multiple: true },
38
+ default: { type: 'string' },
39
+ blocks: { type: 'string' },
40
+ req: { type: 'string' },
41
+ concern: { type: 'string' },
42
+ disposition: { type: 'string' },
43
+ finding: { type: 'string' },
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' },
50
+ json: { type: 'boolean' },
51
+ help: { type: 'boolean' },
52
+ };
53
+
54
+ export const HELP = `Usage: rcf define ledger <name> <verb> [options]
55
+
56
+ Ledgers (proposal 2026-09-22 §2.5, §3.2 v3) are sidecars under
57
+ rcf/define/. Four ledgers, each one JSON file, each with one numbered
58
+ sequence for the life of the project.
59
+
60
+ Ledger names:
61
+ brief D1 brief statements (kinded from a closed set)
62
+ decisions D7 open / answered decisions (#241 format)
63
+ concerns D5 (concern, REQ) pairs (applied | waived)
64
+ probes D6 probe findings (severity: low | medium | high)
65
+
66
+ Verbs:
67
+ add Append a new entry, next id
68
+ update <id> Patch named fields on an existing entry
69
+ resolve <id> Flip status to resolved, stamp resolvedAt
70
+ list Print every entry (JSON with --json)
71
+
72
+ Common flags:
73
+ --json Emit machine-readable envelope on list
74
+ --help Print this help
75
+
76
+ brief add flags:
77
+ --kind <${BRIEF_KINDS.join('|')}>
78
+ Statement kind (default: capability)
79
+ --text <text> The statement text (mutually exclusive with --from)
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
92
+
93
+ decisions add flags:
94
+ --question <text> The decision question (required)
95
+ --option <letter:text> Repeatable option, format 'a:First option'
96
+ --default <letter> Default option letter
97
+ --blocks <text> What this decision blocks (a gate id, doc id)
98
+
99
+ concerns add flags:
100
+ --req <req-id> REQ the (concern, REQ) pair belongs to (required)
101
+ --concern <slug> Concern key (auth, retention, ...) (required)
102
+ --disposition applied|waived
103
+ Disposition (required)
104
+ --reason <text> Reason (mandatory when disposition=waived)
105
+
106
+ probes add flags:
107
+ --req <req-id> REQ the probe finding sits on (required)
108
+ --finding <text> The finding text (required)
109
+ --severity low|medium|high
110
+
111
+ resolve flags:
112
+ --answer <text> On decisions: the answer chosen (a letter, or free text)
113
+ --reason <text> On probes / concerns: the resolution note
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
+
124
+ Notes:
125
+ - Every ledger file lives under rcf/define/. The loader treats missing
126
+ files as empty ledgers; a malformed file is a hard error.
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.
134
+ - 'decisions list' prints the numbered format from #241: one decision
135
+ per item; options with letters; the default; what it blocks.
136
+ `;
137
+
138
+ /**
139
+ * @param {string[]} argv - argv slice after `ledger`
140
+ * @param {object} [deps]
141
+ * @returns {Promise<number>}
142
+ */
143
+ export async function main(argv, deps = {}) {
144
+ const stdout = deps.stdout ?? process.stdout;
145
+ const stderr = deps.stderr ?? process.stderr;
146
+ const cwd = deps.cwd ?? process.cwd();
147
+
148
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
149
+ stdout.write(HELP);
150
+ return argv.length === 0 ? 2 : 0;
151
+ }
152
+
153
+ const name = argv[0];
154
+ const verb = argv[1];
155
+ if (!LEDGER_NAMES.includes(/** @type {any} */ (name))) {
156
+ stderr.write(`[error] usage ledger: unknown name '${name}' (expected: ${LEDGER_NAMES.join(', ')})\n`);
157
+ return 2;
158
+ }
159
+ if (!verb || !['add', 'update', 'resolve', 'list'].includes(verb)) {
160
+ stderr.write("[error] usage ledger: expected verb 'add', 'update', 'resolve' or 'list'\n");
161
+ return 2;
162
+ }
163
+
164
+ let parsed;
165
+ try {
166
+ parsed = parseArgs({
167
+ args: argv.slice(2), options: OPTION_SPEC, allowPositionals: true, strict: true,
168
+ });
169
+ } catch (err) {
170
+ stderr.write(`[error] usage ${/** @type {Error} */ (err).message}\n`);
171
+ return 2;
172
+ }
173
+ const flags = parsed.values;
174
+ const positionals = parsed.positionals;
175
+ if (flags.help) { stdout.write(HELP); return 0; }
176
+
177
+ const projectRoot = await findProjectRoot(cwd);
178
+ if (!projectRoot) {
179
+ 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');
180
+ return 2;
181
+ }
182
+
183
+ try {
184
+ if (verb === 'list') {
185
+ return await runList({ projectRoot, name, flags, stdout });
186
+ }
187
+ if (verb === 'add') {
188
+ return await runAdd({ projectRoot, name, flags, stdout, stderr });
189
+ }
190
+ if (verb === 'update') {
191
+ return await runUpdate({ projectRoot, name, positionals, flags, stdout, stderr });
192
+ }
193
+ return await runResolve({ projectRoot, name, positionals, flags, stdout, stderr });
194
+ } catch (err) {
195
+ if (err instanceof LedgerError) {
196
+ stderr.write(`[error] ${err.code === 'usage' ? 'usage ' : ''}ledger: ${err.message}\n`);
197
+ return err.code === 'usage' ? 2 : 1;
198
+ }
199
+ stderr.write(`[error] ledger: ${/** @type {Error} */ (err).message}\n`);
200
+ return 1;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * @param {object} args
206
+ * @param {string} args.projectRoot
207
+ * @param {import('../define/ledgers.js').LedgerName} args.name
208
+ * @param {Record<string, unknown>} args.flags
209
+ * @param {NodeJS.WriteStream} args.stdout
210
+ * @returns {Promise<number>}
211
+ */
212
+ async function runList({ projectRoot, name, flags, stdout }) {
213
+ const body = await loadLedger({ projectRoot, name });
214
+ if (flags.json) {
215
+ stdout.write(`${JSON.stringify({ ledger: name, ...body }, null, 2)}\n`);
216
+ return 0;
217
+ }
218
+ if (name === 'decisions') {
219
+ stdout.write(renderDecisionsList(body));
220
+ return 0;
221
+ }
222
+ stdout.write(renderGenericList(name, body));
223
+ return 0;
224
+ }
225
+
226
+ /**
227
+ * Render decisions in the numbered #241 format:
228
+ * 1. <question> [status]
229
+ * a) <option a>
230
+ * b) <option b>
231
+ * default: <letter>
232
+ * blocks: <text>
233
+ * answer: <text> (when resolved)
234
+ *
235
+ * @param {Record<string, any>} body
236
+ * @returns {string}
237
+ */
238
+ export function renderDecisionsList(body) {
239
+ const list = /** @type {any[]} */ (body.decisions ?? []);
240
+ if (list.length === 0) return '(no decisions)\n';
241
+ const lines = [];
242
+ for (const d of list) {
243
+ const status = d.status === 'resolved' ? 'resolved' : 'open';
244
+ lines.push(`${d.id}. ${d.question} [${status}]`);
245
+ for (const opt of d.options ?? []) {
246
+ lines.push(` ${opt.letter}) ${opt.text}`);
247
+ }
248
+ if (d.default != null) lines.push(` default: ${d.default}`);
249
+ if (d.blocks) lines.push(` blocks: ${d.blocks}`);
250
+ if (status === 'resolved' && d.answer != null) lines.push(` answer: ${d.answer}`);
251
+ lines.push('');
252
+ }
253
+ return `${lines.join('\n').trimEnd()}\n`;
254
+ }
255
+
256
+ /**
257
+ * @param {import('../define/ledgers.js').LedgerName} name
258
+ * @param {Record<string, any>} body
259
+ * @returns {string}
260
+ */
261
+ function renderGenericList(name, body) {
262
+ const key = name === 'brief' ? 'statements' : name;
263
+ const list = /** @type {any[]} */ (body[key] ?? []);
264
+ if (list.length === 0) return `(no ${name} entries)\n`;
265
+ const lines = [];
266
+ for (const e of list) {
267
+ const status = e.status === 'resolved' ? 'resolved' : 'open';
268
+ if (name === 'brief') {
269
+ lines.push(`${e.id}. [${e.kind}] ${e.text} (${status})`);
270
+ if (e.source) lines.push(` source: ${e.source}`);
271
+ } else if (name === 'concerns') {
272
+ lines.push(`${e.id}. ${e.concern} on ${e.reqId}: ${e.disposition} (${status})`);
273
+ if (e.reason) lines.push(` reason: ${e.reason}`);
274
+ } else if (name === 'probes') {
275
+ lines.push(`${e.id}. [${e.severity}] ${e.reqId}: ${e.finding} (${status})`);
276
+ }
277
+ }
278
+ return `${lines.join('\n')}\n`;
279
+ }
280
+
281
+ /**
282
+ * @param {object} args
283
+ */
284
+ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
285
+ const body = await loadLedger({ projectRoot, name });
286
+ const now = new Date().toISOString();
287
+
288
+ if (name === 'brief') {
289
+ const kind = /** @type {string} */ (flags.kind ?? 'capability');
290
+ if (!BRIEF_KINDS.includes(/** @type {any} */ (kind))) {
291
+ stderr.write(`[error] usage ledger: brief --kind must be one of: ${BRIEF_KINDS.join(', ')}\n`);
292
+ return 2;
293
+ }
294
+ if (flags.from && flags.text) {
295
+ stderr.write('[error] usage ledger: brief add takes --text OR --from, not both\n');
296
+ return 2;
297
+ }
298
+ if (!flags.from && !flags.text) {
299
+ stderr.write('[error] usage ledger: brief add requires --text <text> or --from <path>\n');
300
+ return 2;
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
+
311
+ let drafts;
312
+ if (flags.from) {
313
+ const raw = await readFile(String(flags.from), 'utf8');
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
+ }
326
+ if (drafts.length === 0) {
327
+ stderr.write(`[error] usage ledger: brief --from ${flags.from} produced no non-empty lines\n`);
328
+ return 2;
329
+ }
330
+ } else {
331
+ const entry = { text: String(flags.text), kind };
332
+ if (flags.source) entry.source = String(flags.source);
333
+ drafts = [entry];
334
+ }
335
+
336
+ // Compute what WOULD be minted (for both the real path and dry-run).
337
+ let cur = body;
338
+ const added = [];
339
+ for (const d of drafts) {
340
+ const step = addEntry({ name: 'brief', body: cur, entry: d, now });
341
+ cur = step.body;
342
+ added.push(step.entry);
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
+
402
+ await saveLedger({ projectRoot, name: 'brief', body: cur });
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
+ }
412
+ return 0;
413
+ }
414
+
415
+ if (name === 'decisions') {
416
+ if (!flags.question) {
417
+ stderr.write('[error] usage ledger: decisions add requires --question <text>\n');
418
+ return 2;
419
+ }
420
+ const options = parseOptionFlags(flags.option);
421
+ if (options.error) {
422
+ stderr.write(`[error] usage ledger: ${options.error}\n`);
423
+ return 2;
424
+ }
425
+ const entry = {
426
+ question: String(flags.question),
427
+ options: options.value,
428
+ default: flags.default ? String(flags.default) : null,
429
+ };
430
+ if (flags.blocks) entry.blocks = String(flags.blocks);
431
+ const step = addEntry({ name: 'decisions', body, entry, now });
432
+ await saveLedger({ projectRoot, name: 'decisions', body: step.body });
433
+ stdout.write(`decisions-ledger: added decision ${step.entry.id}\n`);
434
+ return 0;
435
+ }
436
+
437
+ if (name === 'concerns') {
438
+ if (!flags.req || !flags.concern || !flags.disposition) {
439
+ stderr.write('[error] usage ledger: concerns add requires --req <req-id> --concern <slug> --disposition applied|waived\n');
440
+ return 2;
441
+ }
442
+ if (flags.disposition === 'waived' && !flags.reason) {
443
+ stderr.write('[error] usage ledger: concerns add --disposition waived requires --reason <text>\n');
444
+ return 2;
445
+ }
446
+ const entry = {
447
+ concern: String(flags.concern),
448
+ reqId: String(flags.req),
449
+ disposition: String(flags.disposition),
450
+ };
451
+ if (flags.reason) entry.reason = String(flags.reason);
452
+ const step = addEntry({ name: 'concerns', body, entry, now });
453
+ await saveLedger({ projectRoot, name: 'concerns', body: step.body });
454
+ stdout.write(`concern-ledger: added concern ${step.entry.id}\n`);
455
+ return 0;
456
+ }
457
+
458
+ // probes
459
+ if (!flags.req || !flags.finding) {
460
+ stderr.write('[error] usage ledger: probes add requires --req <req-id> --finding <text>\n');
461
+ return 2;
462
+ }
463
+ const entry = {
464
+ reqId: String(flags.req),
465
+ finding: String(flags.finding),
466
+ severity: flags.severity ? String(flags.severity) : 'medium',
467
+ };
468
+ const step = addEntry({ name: 'probes', body, entry, now });
469
+ await saveLedger({ projectRoot, name: 'probes', body: step.body });
470
+ stdout.write(`probe-ledger: added probe ${step.entry.id}\n`);
471
+ return 0;
472
+ }
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
+
570
+ async function runResolve({ projectRoot, name, positionals, flags, stdout, stderr }) {
571
+ if (positionals.length !== 1) {
572
+ stderr.write('[error] usage ledger: resolve expects exactly one <id>\n');
573
+ return 2;
574
+ }
575
+ const id = Number(positionals[0]);
576
+ if (!Number.isInteger(id) || id < 1) {
577
+ stderr.write(`[error] usage ledger: resolve id must be a positive integer, got '${positionals[0]}'\n`);
578
+ return 2;
579
+ }
580
+ const body = await loadLedger({ projectRoot, name });
581
+ const now = new Date().toISOString();
582
+ /** @type {Record<string, unknown>} */
583
+ const patch = {};
584
+ if (flags.answer) patch.answer = String(flags.answer);
585
+ if (flags.reason) patch.reason = String(flags.reason);
586
+ if (flags.answer) patch.answeredAt = now;
587
+ const step = resolveEntry({ name, body, id, patch, now });
588
+ await saveLedger({ projectRoot, name, body: step.body });
589
+ stdout.write(`${name}-ledger: resolved ${id}\n`);
590
+ return 0;
591
+ }
592
+
593
+ /**
594
+ * Parse repeated `--option letter:text` into option records. Returns
595
+ * `{ value }` on success or `{ error }` on any malformed input.
596
+ *
597
+ * @param {unknown} raw
598
+ * @returns {{ value: Array<{ letter: string, text: string }> } | { error: string }}
599
+ */
600
+ export function parseOptionFlags(raw) {
601
+ if (!raw || !Array.isArray(raw) || raw.length === 0) {
602
+ return { error: "decisions add requires at least one --option 'a:First option'" };
603
+ }
604
+ const value = [];
605
+ for (const s of raw) {
606
+ if (typeof s !== 'string') return { error: `--option value must be a string, got ${typeof s}` };
607
+ const sep = s.indexOf(':');
608
+ if (sep < 1) return { error: `--option must be 'letter:text', got '${s}'` };
609
+ const letter = s.slice(0, sep).trim();
610
+ const text = s.slice(sep + 1).trim();
611
+ if (!letter || !text) return { error: `--option must be 'letter:text', got '${s}'` };
612
+ value.push({ letter, text });
613
+ }
614
+ return { value };
615
+ }