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
@@ -4,6 +4,56 @@
4
4
  export { computeCoverage, classifyCoverageScope } from './coverage.js';
5
5
  export { computeTrace, kindOf } from './trace.js';
6
6
  export { computeImpact, labelFor } from './impact.js';
7
+ // Detection model (REQ-172, proposal 2026-09-22 §2.2 v3). Pure hashes
8
+ // over the parsed tree; every downstream DEFINE gate reads this delta.
9
+ export {
10
+ canonicaliseJson,
11
+ hashDocument,
12
+ computeTreeHash,
13
+ computeDelta,
14
+ } from './delta.js';
15
+ // DEFINE stage gates (REQ-174; proposal §2.4, §3.2 v3). One check
16
+ // function per stage; foldState + parseAcClass helpers. Persona /
17
+ // question / draft-marker helpers added for ADR-4126 (0.29.0).
18
+ export {
19
+ STAGE_ORDER,
20
+ STAGE_GATES,
21
+ STAGE_SHORT_NAMES,
22
+ STAGE_ALIASES,
23
+ INTERFACE_KINDS,
24
+ CHECK_PERSONA,
25
+ CHECK_QUESTION,
26
+ STAGE_FALLBACK_PERSONA,
27
+ stagePolicy,
28
+ parseAcClass,
29
+ parseInterfaceDraft,
30
+ parseTacPurposeDraft,
31
+ parseCoreEntityDraft,
32
+ hasDraftMarker,
33
+ checkPersona,
34
+ checkQuestion,
35
+ foldState,
36
+ runStage,
37
+ checkD1Brief,
38
+ checkD2Skeleton,
39
+ checkD3Shapes,
40
+ checkD4Stories,
41
+ checkD5Crosscut,
42
+ checkD6Consistency,
43
+ checkD7Decisions,
44
+ checkD8Freeze,
45
+ } from './gates.js';
46
+ // Readiness composer (REQ-175; proposal §2.4, §6 v3). Levels /
47
+ // personas / verdict-line helpers added for ADR-4126 (0.29.0).
48
+ export {
49
+ computeReadiness,
50
+ deriveNextAction,
51
+ deriveLevels,
52
+ derivePersonas,
53
+ formatVerdictLines,
54
+ shortHash,
55
+ formatTreeLine,
56
+ } from './readiness.js';
7
57
  export { formatTable } from './formatters/table.js';
8
58
  export { formatJson } from './formatters/json.js';
9
59
  export { formatMermaid } from './formatters/mermaid.js';
@@ -0,0 +1,438 @@
1
+ // Questions compute (REQ-186; spec 2026-10-01 §1.1, §1.2, §1.4, §1.5).
2
+ //
3
+ // Pure composer over the readiness object: folds each failing PO (or
4
+ // engineer) check into one plain question per failing item, each
5
+ // carrying the exact write-back(s) that answer it. Ordering: D1, D2,
6
+ // D4, D7 then by source span (so the harness asks about one passage
7
+ // at a time). Ids are deterministic from stage, check and item id.
8
+ // Writes NOTHING under rcf/ (same guard readiness honours).
9
+ //
10
+ // Shape of the returned object matches spec §1.2 verbatim.
11
+
12
+ /** @typedef {'productOwner' | 'engineer'} Persona */
13
+
14
+ /** The accepted personas, named everywhere the usage error mentions them. */
15
+ export const PERSONAS = /** @type {const} */ (['productOwner', 'engineer']);
16
+
17
+ /** Ordering spec §1.2: PO stages in this order; later stages fall through. */
18
+ const PO_STAGE_ORDER = ['D1', 'D2', 'D4', 'D7'];
19
+ const ENG_STAGE_ORDER = ['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8'];
20
+
21
+ /**
22
+ * Compute the persona question set from a readiness result, the tree,
23
+ * the four ledgers and the profile text.
24
+ *
25
+ * @param {import('./readiness.js').ReadinessResult} readiness
26
+ * @param {object} args
27
+ * @param {import('#core/store/walker.js').TreeModel} args.tree
28
+ * @param {import('./delta.js').LedgerBundle | undefined} [args.ledgers]
29
+ * @param {string | null | undefined} [args.profileText]
30
+ * @param {Persona} [args.persona]
31
+ * @returns {{
32
+ * treeHash: string | null,
33
+ * persona: Persona,
34
+ * level: 'intent' | 'build',
35
+ * ok: boolean,
36
+ * remaining: number,
37
+ * groups: Array<{ key: string, label: string, questionIds: string[] }>,
38
+ * questions: Array<ReturnType<typeof makeQuestion>>,
39
+ * optional: Array<{ id: string, ask: string, blocks: 'build' }>,
40
+ * engineer?: { blockers: number, nextAction: object | null },
41
+ * }}
42
+ */
43
+ export function computeQuestions(readiness, args = {}) {
44
+ const persona = args.persona ?? 'productOwner';
45
+ if (!PERSONAS.includes(persona)) {
46
+ throw new Error(
47
+ `questions: unknown persona '${persona}' (expected one of ${PERSONAS.join(' | ')})`,
48
+ );
49
+ }
50
+
51
+ const { tree, ledgers = {} } = args;
52
+ const brief = /** @type {any} */ (ledgers?.brief);
53
+ const briefStatements = Array.isArray(brief?.statements) ? brief.statements : [];
54
+ const decisions = /** @type {any} */ (ledgers?.decisions);
55
+ const decisionEntries = Array.isArray(decisions?.decisions) ? decisions.decisions : [];
56
+
57
+ const level = persona === 'engineer' ? 'build' : 'intent';
58
+ const blocks = persona === 'engineer' ? 'build' : 'intent';
59
+ const blockers = persona === 'engineer'
60
+ ? readiness?.personas?.engineer?.blockers ?? []
61
+ : readiness?.levels?.intentComplete?.blockedBy ?? [];
62
+ const ok = persona === 'engineer'
63
+ ? (readiness?.levels?.readyToBuild?.ok ?? true)
64
+ : (readiness?.levels?.intentComplete?.ok ?? true);
65
+
66
+ const stageOrder = persona === 'engineer' ? ENG_STAGE_ORDER : PO_STAGE_ORDER;
67
+ // Stable sort by stage index only; inside a stage the blocker order
68
+ // comes from readiness.stages[].checks[] insertion order (spec §1.2:
69
+ // "stage order D1, D2, D4, D7, then check order inside a stage").
70
+ const indexed = blockers.map((b, i) => ({ b, i }));
71
+ indexed.sort((x, y) => {
72
+ const si = stageOrder.indexOf(x.b.stage) - stageOrder.indexOf(y.b.stage);
73
+ if (si !== 0) return si;
74
+ return x.i - y.i;
75
+ });
76
+ const ordered = indexed.map((x) => x.b);
77
+
78
+ /** @type {ReturnType<typeof makeQuestion>[]} */
79
+ const questions = [];
80
+ for (const blocker of ordered) {
81
+ const items = extractItems(blocker, briefStatements, tree, readiness);
82
+ for (const item of items) {
83
+ const q = buildQuestionForItem(blocker, item, {
84
+ briefStatements, tree, decisionEntries, blocks, persona,
85
+ });
86
+ if (q) questions.push(q);
87
+ }
88
+ }
89
+
90
+ // Spec §1.2: ordering is stage order first, then check order, then
91
+ // grouped by source span so one passage is asked about at a time.
92
+ // Stable-sort questions[] by (stageIndex, sourceSpan) to make the
93
+ // source-span secondary deterministic even when readiness delivers
94
+ // items in a different order per run.
95
+ const spanKey = (q) => q.context?.statement?.source ?? q.context?.sourceSpan ?? '~';
96
+ const stageKey = (q) => stageOrder.indexOf(q.stage);
97
+ const withIndex = questions.map((q, i) => ({ q, i }));
98
+ withIndex.sort((a, b) => {
99
+ const sa = stageKey(a.q); const sb = stageKey(b.q);
100
+ if (sa !== sb) return sa - sb;
101
+ const spa = spanKey(a.q); const spb = spanKey(b.q);
102
+ if (spa !== spb) return spa < spb ? -1 : 1;
103
+ return a.i - b.i;
104
+ });
105
+ const sortedQuestions = withIndex.map((x) => x.q);
106
+
107
+ // Group by source span. Preserve stage order from the questions list.
108
+ /** @type {Map<string, { key: string, label: string, questionIds: string[] }>} */
109
+ const groupMap = new Map();
110
+ for (const q of sortedQuestions) {
111
+ const key = q.context?.statement?.source ?? q.context?.sourceSpan ?? 'tree';
112
+ const label = sourceLabel(key);
113
+ const g = groupMap.get(key) ?? { key, label, questionIds: [] };
114
+ g.questionIds.push(q.id);
115
+ groupMap.set(key, g);
116
+ }
117
+ const groups = [...groupMap.values()];
118
+
119
+ // optional[]: enumerated open decisions (two or more options and a
120
+ // default). These are non-blocking at L1 per decision 7; they ride
121
+ // the loop as an "offer once at the end" set.
122
+ const optional = [];
123
+ if (persona === 'productOwner') {
124
+ for (const d of decisionEntries) {
125
+ if (d?.status !== 'open') continue;
126
+ if (!Array.isArray(d?.options) || d.options.length < 2) continue;
127
+ if (typeof d?.default !== 'string' || d.default.length === 0) continue;
128
+ const letters = new Set(d.options.map((o) => (o && typeof o === 'object') ? o.letter : undefined));
129
+ if (!letters.has(d.default)) continue;
130
+ optional.push({
131
+ id: `Q-D7-open-decision:${d.id}`,
132
+ ask: `Decision ${d.id} ${d.question ?? ''} The default is (${d.default}). Answer now or let it stand.`.trim(),
133
+ blocks: 'build',
134
+ });
135
+ }
136
+ }
137
+
138
+ const engineerCount = readiness?.personas?.engineer?.blockers?.length ?? 0;
139
+ const engineerNext = readiness?.personas?.engineer?.nextAction ?? null;
140
+
141
+ return {
142
+ treeHash: readiness?.tree?.currentTreeHash ?? null,
143
+ persona,
144
+ level,
145
+ ok,
146
+ remaining: sortedQuestions.length,
147
+ groups,
148
+ questions: sortedQuestions,
149
+ optional,
150
+ ...(persona === 'productOwner'
151
+ ? { engineer: { blockers: engineerCount, nextAction: engineerNext } }
152
+ : {}),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Pull the per-item failing records out of a blocker. Returns objects
158
+ * carrying `{ id, why }` plus (when available) `statement`, `req`,
159
+ * `decision` or `profileField` so the question builder can quote the
160
+ * right passage.
161
+ *
162
+ * @param {object} blocker
163
+ * @param {Array<any>} briefStatements
164
+ * @param {object} tree
165
+ */
166
+ function extractItems(blocker, briefStatements, tree, readiness) {
167
+ // Spec §1.2: one question per failing item. `blocker.ids` is the
168
+ // readiness summary and is capped at 20 (readiness.js:373/405), so a
169
+ // check with more than 20 failing items would silently lose
170
+ // questions. Look up the full `check.failing[]` on the stage first
171
+ // and only fall back to `blocker.ids` when the stage is missing.
172
+ let ids = Array.isArray(blocker.ids) ? blocker.ids : [];
173
+ const stages = Array.isArray(readiness?.stages) ? readiness.stages : [];
174
+ const stage = stages.find((st) => st?.stage === blocker.stage);
175
+ const check = stage?.checks?.find((c) => c?.name === blocker.check);
176
+ const failing = Array.isArray(check?.failing) ? check.failing : null;
177
+ if (failing) ids = [...new Set(failing.map((f) => f?.id).filter((id) => id != null))];
178
+ /** @type {Array<{ id: string, why?: string, statement?: any, req?: any, decision?: any, profileField?: string }>} */
179
+ const out = [];
180
+ for (const id of ids) {
181
+ const item = { id, why: undefined };
182
+ if (typeof id === 'string' && id.startsWith('brief:')) {
183
+ const rest = id.slice('brief:'.length);
184
+ if (rest === 'ledger') {
185
+ out.push(item);
186
+ continue;
187
+ }
188
+ const stmtId = Number(rest);
189
+ const stmt = briefStatements.find((s) => Number(s?.id) === stmtId);
190
+ if (stmt) item.statement = stmt;
191
+ } else if (typeof id === 'string' && id.startsWith('profile:')) {
192
+ item.profileField = id.slice('profile:'.length);
193
+ } else if (typeof id === 'string' && id.startsWith('decision:')) {
194
+ const decId = id.slice('decision:'.length);
195
+ item.decision = { id: decId };
196
+ } else if (typeof id === 'string' && /^REQ-/.test(id)) {
197
+ item.req = (tree?.requirements ?? []).find((r) => r?.reqId === id) ?? { reqId: id };
198
+ } else {
199
+ // Fallback: unknown id surface, kept so the question still fires.
200
+ }
201
+ out.push(item);
202
+ }
203
+ return out;
204
+ }
205
+
206
+ function makeQuestion(/** @type {object} */ q) { return q; }
207
+
208
+ /**
209
+ * The per-check template table (spec §1.2). Each check gets:
210
+ * - a heading (short label; same as the step 2 `check.question`)
211
+ * - a per-item `ask` (plain sentence quoting the passage)
212
+ * - `answerKinds` (closed enumeration)
213
+ * - `writeBack` (one entry per `when`)
214
+ */
215
+ function buildQuestionForItem(blocker, item, {
216
+ briefStatements, tree, decisionEntries, blocks, persona,
217
+ }) {
218
+ const stage = blocker.stage;
219
+ const check = blocker.check;
220
+ const itemId = item.id;
221
+ const qid = `Q-${stage}-${check.replace(/[^A-Za-z0-9:]/g, '-')}-${itemId}`;
222
+
223
+ // ---- D1 brief:sinceFreeze -------------------------------------------
224
+ if (check === 'brief:sinceFreeze') {
225
+ return makeQuestion({
226
+ id: qid,
227
+ stage, check, itemId,
228
+ heading: 'What has changed, or what is this project for?',
229
+ ask: 'The brief is empty. Hand me your document, or tell me in a few sentences what this is for.',
230
+ context: { sourceSpan: 'tree' },
231
+ answerKinds: ['document', 'text'],
232
+ writeBack: [
233
+ { when: 'document', command: 'rcf define ledger brief add --from <path>' },
234
+ { when: 'text', command: 'rcf define ledger brief add --text "<text>"' },
235
+ ],
236
+ blocks,
237
+ });
238
+ }
239
+
240
+ // ---- D1 brief:kinds -------------------------------------------------
241
+ if (check === 'brief:kinds' && item.statement) {
242
+ const stmt = item.statement;
243
+ return makeQuestion({
244
+ id: qid,
245
+ stage, check, itemId,
246
+ heading: 'Is this a capability, a constraint, an actor, or something else?',
247
+ ask: `Statement ${stmt.id} says ${JSON.stringify(stmt.text ?? '')}. Is this something the product must do, a rule it must obey, a person or role, a thing it keeps track of, another system, a screen or channel, out of scope, or a question still to answer?`,
248
+ context: {
249
+ statement: { id: stmt.id, kind: stmt.kind, text: stmt.text, source: stmt.source },
250
+ },
251
+ answerKinds: ['rekind'],
252
+ writeBack: [
253
+ { when: 'rekind', command: `rcf define ledger brief update ${stmt.id} --kind <kind>` },
254
+ ],
255
+ blocks,
256
+ });
257
+ }
258
+
259
+ // ---- D1 brief:openQuestions ---------------------------------------
260
+ if (check === 'brief:openQuestions' && item.statement) {
261
+ const stmt = item.statement;
262
+ return makeQuestion({
263
+ id: qid,
264
+ stage, check, itemId,
265
+ heading: 'Any open questions in the brief, resolved or promoted to a decision?',
266
+ ask: `Your document asks: ${JSON.stringify(stmt.text ?? '')}. Can you answer it now, or should I write it down as a decision with options and a default?`,
267
+ context: {
268
+ statement: { id: stmt.id, kind: stmt.kind, text: stmt.text, source: stmt.source },
269
+ },
270
+ answerKinds: ['answer', 'promote'],
271
+ writeBack: [
272
+ { when: 'answer', command: `rcf define ledger brief resolve ${stmt.id} --answer "<text>"` },
273
+ { when: 'promote', command: `rcf define ledger decisions add --question "${(stmt.text ?? '').replace(/"/g, '\\"')} (brief ${stmt.id})" --option a:<optA> --option b:<optB> --default a --blocks "brief:${stmt.id}"; rcf define ledger brief resolve ${stmt.id} --answer "decision <id>"` },
274
+ ],
275
+ blocks,
276
+ });
277
+ }
278
+
279
+ // ---- D1 brief:profile ---------------------------------------------
280
+ if (check === 'brief:profile' && item.profileField) {
281
+ const field = item.profileField;
282
+ if (field === 'surface') {
283
+ return makeQuestion({
284
+ id: qid,
285
+ stage, check, itemId,
286
+ heading: 'Where will the owner look at what we produce?',
287
+ ask: 'Where will you look at what we produce: a web page, the running app, or a code review diff?',
288
+ context: { profileField: 'surface' },
289
+ answerKinds: ['choose'],
290
+ writeBack: [
291
+ { when: 'choose', command: 'edit rcf/.identity/profile.md: replace the Surface choice list with one of viewer | runningApp | prDiff' },
292
+ ],
293
+ blocks,
294
+ });
295
+ }
296
+ if (field === 'register') {
297
+ return makeQuestion({
298
+ id: qid,
299
+ stage, check, itemId,
300
+ heading: 'Who is driving: product owner or engineer?',
301
+ ask: 'Are you the product owner or an engineer?',
302
+ context: { profileField: 'register' },
303
+ answerKinds: ['choose'],
304
+ writeBack: [
305
+ { when: 'choose', command: 'edit rcf/.identity/profile.md: replace the Register choice list with one of productOwner | engineer' },
306
+ ],
307
+ blocks,
308
+ });
309
+ }
310
+ }
311
+
312
+ // ---- D2 skeleton:resolvedBy ----------------------------------------
313
+ if (check === 'skeleton:resolvedBy' && item.statement) {
314
+ const stmt = item.statement;
315
+ const suggestions = suggestReqTitles(stmt.text, tree);
316
+ return makeQuestion({
317
+ id: qid,
318
+ stage, check, itemId,
319
+ heading: 'Does this statement become a requirement, an entity, or an omission?',
320
+ ask: `Your document says ${JSON.stringify(stmt.text ?? '')}. Is this its own requirement, part of an existing one, or something else (a person, a record, another system, a screen, or out of scope)?`,
321
+ context: {
322
+ statement: { id: stmt.id, kind: stmt.kind, text: stmt.text, source: stmt.source },
323
+ suggestions,
324
+ },
325
+ answerKinds: ['existingReq', 'newReq', 'rekind', 'omit'],
326
+ writeBack: [
327
+ { when: 'existingReq', command: `rcf define ledger brief update ${stmt.id} --resolved-by <REQ-id>` },
328
+ { when: 'newReq', command: `rcf define create req --parent PRD-001 --title "<title>"; rcf define update <REQ-id> --set description="<sentence>" --set domain="<area>"; rcf define ledger brief update ${stmt.id} --resolved-by <REQ-id>` },
329
+ { when: 'rekind', command: `rcf define ledger brief update ${stmt.id} --kind <kind> --resolved-by <pointer>` },
330
+ { when: 'omit', command: `rcf define ledger brief update ${stmt.id} --resolved-by "omitted:<reason>"` },
331
+ ],
332
+ blocks,
333
+ });
334
+ }
335
+
336
+ // ---- D2 skeleton:reqIntent ----------------------------------------
337
+ if (check === 'skeleton:reqIntent' && item.req) {
338
+ const req = item.req;
339
+ return makeQuestion({
340
+ id: qid,
341
+ stage, check, itemId,
342
+ heading: 'What does this requirement mean and where does it sit?',
343
+ ask: `Requirement ${JSON.stringify(req.title ?? req.reqId ?? '')} has no plain description (or no area). In a sentence, what must the product do here, and which part of the product does it belong to?`,
344
+ context: { reqId: req.reqId, title: req.title },
345
+ answerKinds: ['text'],
346
+ writeBack: [
347
+ { when: 'text', command: `rcf define update ${req.reqId} --set description="<sentence>" --set domain="<area>"` },
348
+ ],
349
+ blocks,
350
+ });
351
+ }
352
+
353
+ // ---- D4 stories:reqHasUs -------------------------------------------
354
+ if (check === 'stories:reqHasUs' && item.req) {
355
+ const req = item.req;
356
+ return makeQuestion({
357
+ id: qid,
358
+ stage, check, itemId,
359
+ heading: 'What does someone do with this requirement?',
360
+ ask: `Who uses ${JSON.stringify(req.title ?? req.reqId ?? '')}, and what do they do with it? Say it as: as a <who>, I want <what>, so that <why>.`,
361
+ context: { reqId: req.reqId, title: req.title },
362
+ answerKinds: ['story'],
363
+ writeBack: [
364
+ { when: 'story', command: `rcf define create us --parent ${req.reqId} --title "as a <who>, I want <what>, so that <why>"; rcf define update <US-id> --set asA="<who>" --set iWant="<what>" --set soThat="<why>"; rcf define create ac --parent <US-id> --description "[happy] <criterion>"` },
365
+ ],
366
+ blocks,
367
+ });
368
+ }
369
+
370
+ // ---- D7 decisions:wellFormed --------------------------------------
371
+ if (check === 'decisions:wellFormed' && item.decision) {
372
+ const dec = decisionEntries.find((d) => String(d?.id) === String(item.decision.id));
373
+ const q = dec?.question ?? '(no question)';
374
+ return makeQuestion({
375
+ id: qid,
376
+ stage, check, itemId,
377
+ heading: 'Every decision is enumerated.',
378
+ ask: `Decision ${item.decision.id} asks ${JSON.stringify(q)} but has no options (or no default). What are the choices, and which would you pick if nobody objected?`,
379
+ context: { decision: { id: item.decision.id, question: dec?.question } },
380
+ answerKinds: ['enumerate'],
381
+ writeBack: [
382
+ { when: 'enumerate', command: `rcf define ledger decisions update ${item.decision.id} --option a:<optA> --option b:<optB> --default a` },
383
+ ],
384
+ blocks,
385
+ });
386
+ }
387
+
388
+ // Fallback: a blocker without a template still returns one question
389
+ // carrying the check's step-2 question as the ask. This keeps the
390
+ // loop monotone (an unexpected engineer check does not drop silent).
391
+ return makeQuestion({
392
+ id: qid,
393
+ stage, check, itemId,
394
+ heading: blocker.question ?? check,
395
+ ask: blocker.question ?? check,
396
+ context: { sourceSpan: 'tree' },
397
+ answerKinds: [],
398
+ writeBack: [],
399
+ blocks,
400
+ });
401
+ }
402
+
403
+ /**
404
+ * Up to three REQ titles sharing two or more words of 4+ chars with
405
+ * the statement text. Deterministic (sort by reqId).
406
+ */
407
+ function suggestReqTitles(statementText, tree) {
408
+ if (typeof statementText !== 'string' || !tree) return [];
409
+ const words = new Set(
410
+ String(statementText).toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 4),
411
+ );
412
+ if (words.size === 0) return [];
413
+ const out = [];
414
+ for (const req of tree.requirements ?? []) {
415
+ const t = typeof req?.title === 'string' ? req.title : '';
416
+ const titleWords = new Set(t.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 4));
417
+ let hits = 0;
418
+ for (const w of titleWords) if (words.has(w)) hits++;
419
+ if (hits >= 2) out.push({ reqId: req.reqId, title: t, hits });
420
+ }
421
+ out.sort((a, b) => (b.hits - a.hits) || a.reqId.localeCompare(b.reqId));
422
+ return out.slice(0, 3).map((e) => `${e.reqId} ${e.title}`);
423
+ }
424
+
425
+ /**
426
+ * Human label for a source-span key. Spec §1.2 example: a `source:
427
+ * briefs/x.md:12-14` key renders as "your document, lines 12 to 14".
428
+ */
429
+ function sourceLabel(key) {
430
+ if (!key || key === 'tree') return 'the tree';
431
+ const m = /^(?:source:)?(.+?):(\d+)(?:-(\d+))?$/.exec(key);
432
+ if (!m) return key;
433
+ const path = m[1];
434
+ const start = m[2];
435
+ const end = m[3];
436
+ if (end && start !== end) return `${path}, lines ${start} to ${end}`;
437
+ return `${path}, line ${start}`;
438
+ }