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
@@ -0,0 +1,329 @@
1
+ // Readiness PO question adapter (viewer UI refresh PR 8, TAC-4133,
2
+ // ADR-4135). One pure module that returns the question groups the PO
3
+ // layer renders. Today it reads `personas.productOwner.blockers` and
4
+ // fills asks from the phrasebook. When DEFINE step 3 PR 2 ships
5
+ // `readiness.questions`, `preferReadinessQuestions(readiness)` flips
6
+ // the source: the call site in the tab reads only this module's
7
+ // output so no downstream edit is needed.
8
+ //
9
+ // Grouping follows design doc section 8: one group per source span.
10
+ // Brief checks group by `brief:<n>` on the failing item's `why`; the
11
+ // resolvedBy and reqIntent checks group by the REQ ancestor named on
12
+ // the failing item (REQ-N); stories:reqHasUs groups under
13
+ // "Requirements without a story yet". A conservative default groups
14
+ // by stage when no better key is available.
15
+ //
16
+ // Decisions-with-a-default (the phrasebook `optional` flag) sit in a
17
+ // dashed optional card and never hold up the hand-over.
18
+
19
+ import { phrasebookEntry } from './phrasebook.js';
20
+
21
+ /**
22
+ * @typedef {object} AdapterItem
23
+ * @property {string} heading
24
+ * @property {string} ask
25
+ * @property {string} hint
26
+ * @property {string} detail engineer-style one-line detail for the Show detail inner block
27
+ * @property {string} checkId stage/check for the hover title
28
+ * @property {string} [itemId] the failing item id when known
29
+ * @property {boolean} [optional]
30
+ */
31
+
32
+ /**
33
+ * @typedef {object} AdapterGroup
34
+ * @property {string} label the source span heading, in plain words
35
+ * @property {AdapterItem[]} items
36
+ * @property {boolean} [optional]
37
+ */
38
+
39
+ /**
40
+ * Feature detect for DEFINE step 3 PR 2's `questions[]` shape on the
41
+ * readiness result. When the compute lands, the adapter reads from
42
+ * `readiness.questions` instead of from blockers.
43
+ *
44
+ * @param {object | null | undefined} readiness
45
+ * @returns {boolean}
46
+ */
47
+ export function preferReadinessQuestions(readiness) {
48
+ if (!readiness || typeof readiness !== 'object') return false;
49
+ const q = /** @type {{ questions?: unknown }} */ (readiness).questions;
50
+ return Array.isArray(q);
51
+ }
52
+
53
+ /**
54
+ * Pure projection of the readiness object into question groups for
55
+ * the PO layer. Does not read disk, does not render HTML.
56
+ *
57
+ * @param {object | null | undefined} readiness
58
+ * @returns {{ groups: AdapterGroup[], optional: AdapterGroup | null }}
59
+ */
60
+ export function toQuestions(readiness) {
61
+ if (!readiness || typeof readiness !== 'object') {
62
+ return { groups: [], optional: null };
63
+ }
64
+ const base = preferReadinessQuestions(readiness)
65
+ ? fromReadinessQuestions(readiness)
66
+ : fromBlockers(readiness);
67
+ // Thread `readiness.questionOptional` (the raw `computeQuestions`
68
+ // open-decisions list, carried onto readiness in view/index.js) into
69
+ // the dashed optional group so DEFINE open decisions with a default
70
+ // reach the PO layer even when they are not among `questions[]`.
71
+ const extra = buildOptionalFromRaw(readiness);
72
+ if (!extra || extra.items.length === 0) return base;
73
+ if (!base.optional) {
74
+ return { groups: base.groups, optional: extra };
75
+ }
76
+ const seen = new Set(base.optional.items.map((it) => it.itemId ?? it.heading));
77
+ const merged = base.optional.items.slice();
78
+ for (const it of extra.items) {
79
+ const key = it.itemId ?? it.heading;
80
+ if (!seen.has(key)) {
81
+ merged.push(it);
82
+ seen.add(key);
83
+ }
84
+ }
85
+ return {
86
+ groups: base.groups,
87
+ optional: { label: base.optional.label, items: merged, optional: true },
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Fold `readiness.questionOptional` (the raw `computeQuestions`
93
+ * `optional[]` list of `{ id, ask, blocks }`) into AdapterItem shape
94
+ * so the PO layer renders open decisions as dashed cards.
95
+ *
96
+ * @param {object} readiness
97
+ * @returns {AdapterGroup | null}
98
+ */
99
+ function buildOptionalFromRaw(readiness) {
100
+ const raw = /** @type {{ questionOptional?: Array<unknown> }} */ (readiness).questionOptional;
101
+ if (!Array.isArray(raw) || raw.length === 0) return null;
102
+ /** @type {AdapterItem[]} */
103
+ const items = [];
104
+ for (const row of raw) {
105
+ if (!row || typeof row !== 'object') continue;
106
+ const r = /** @type {Record<string, unknown>} */ (row);
107
+ const id = typeof r.id === 'string' ? r.id : '';
108
+ const ask = typeof r.ask === 'string' && r.ask.length > 0 ? r.ask : 'Decision has a default; answer now or let it stand.';
109
+ const heading = 'An open decision has a default';
110
+ const hint = 'Answer with the option letter, or let the default stand.';
111
+ items.push({
112
+ heading,
113
+ ask,
114
+ hint,
115
+ detail: id ? `open decision ${id}` : 'open decision',
116
+ checkId: 'D7/open-decision',
117
+ itemId: id || undefined,
118
+ optional: true,
119
+ });
120
+ }
121
+ if (items.length === 0) return null;
122
+ return { label: 'Optional: decisions with a default (they do not hold up the hand-over)', items, optional: true };
123
+ }
124
+
125
+ // ---- source A: readiness.questions[] (DEFINE step 3 PR 2) --------------
126
+ //
127
+ // Accepts both shapes:
128
+ // 1. The real `computeQuestions` output: each question carries
129
+ // `.id` (a Q-...-...), `.itemId` (brief:7 / REQ-012 / ...),
130
+ // `.stage`, `.check`, `.heading`, `.ask`, and `.context` whose
131
+ // `.sourceSpan` or nested `.statement.source` names the span.
132
+ // 2. A flattened test / adapter shape that pre-labels the group via
133
+ // `.sourceSpanLabel` and names the item on `.id` directly.
134
+ // The group label falls back to a conservative prose sentence built
135
+ // from the source span key when no `sourceSpanLabel` is present.
136
+
137
+ function fromReadinessQuestions(readiness) {
138
+ const q = /** @type {Array<unknown>} */ (readiness.questions);
139
+ /** @type {Map<string, AdapterGroup>} */
140
+ const byLabel = new Map();
141
+ /** @type {AdapterItem[]} */
142
+ const optionalItems = [];
143
+ for (const raw of q) {
144
+ if (!raw || typeof raw !== 'object') continue;
145
+ const r = /** @type {Record<string, unknown>} */ (raw);
146
+ const checkId = typeof r.checkId === 'string'
147
+ ? r.checkId
148
+ : (typeof r.check === 'string' ? r.check : '');
149
+ const stage = typeof r.stage === 'string' ? r.stage : '';
150
+ const itemId = pickItemId(r);
151
+ const label = pickLabel(r, { checkId, itemId });
152
+ const pb = phrasebookEntry(checkId, itemId);
153
+ const heading = typeof r.heading === 'string' && r.heading.length > 0 ? r.heading : pb.heading;
154
+ const ask = typeof r.ask === 'string' && r.ask.length > 0 ? r.ask : pb.ask;
155
+ const hint = typeof r.hint === 'string' && r.hint.length > 0 ? r.hint : pb.hint;
156
+ const optional = typeof r.optional === 'boolean' ? r.optional : Boolean(pb.optional);
157
+ const why = typeof r.why === 'string' ? r.why : '';
158
+ const detail = buildDetail({ stage, checkId, itemId, why });
159
+ const item = { heading, ask, hint, detail, checkId: `${stage}/${checkId}`, itemId, optional };
160
+ if (optional) {
161
+ optionalItems.push(item);
162
+ continue;
163
+ }
164
+ if (!byLabel.has(label)) byLabel.set(label, { label, items: [] });
165
+ byLabel.get(label).items.push(item);
166
+ }
167
+ return {
168
+ groups: Array.from(byLabel.values()),
169
+ optional: optionalItems.length > 0
170
+ ? { label: 'Optional: decisions with a default (they do not hold up the hand-over)', items: optionalItems, optional: true }
171
+ : null,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Pick the human itemId from a question record. Prefers an explicit
177
+ * `.itemId` (the real computeQuestions shape); falls back to `.id`
178
+ * only when it looks like a bare itemId (brief:*, profile:*,
179
+ * decision:*, REQ-*, AC-*), never a Q-... question id, so the
180
+ * phrasebook sub-check keying still works.
181
+ */
182
+ function pickItemId(r) {
183
+ if (typeof r.itemId === 'string' && r.itemId.length > 0) return r.itemId;
184
+ if (typeof r.id === 'string' && /^(brief:|profile:|decision:|REQ-|AC-)/.test(r.id)) return r.id;
185
+ return undefined;
186
+ }
187
+
188
+ /**
189
+ * Build the group label for a question. Honours an explicit
190
+ * `.sourceSpanLabel` first; then uses `.sourceSpan` or
191
+ * `.context.sourceSpan` / `.context.statement.source` (the real
192
+ * computeQuestions shape); then falls back to a plain prose sentence
193
+ * for brief:N / profile:* / REQ-* / stories:reqHasUs, matching the
194
+ * blockers-path labels so the PO layer reads the same way in both
195
+ * modes.
196
+ */
197
+ function pickLabel(r, { checkId, itemId }) {
198
+ if (typeof r.sourceSpanLabel === 'string' && r.sourceSpanLabel.length > 0) return r.sourceSpanLabel;
199
+ const ctx = (r && typeof r.context === 'object' && r.context) ? r.context : null;
200
+ const stmtSrc = ctx && typeof ctx === 'object' && ctx.statement && typeof ctx.statement === 'object'
201
+ ? ctx.statement.source
202
+ : null;
203
+ const span = typeof r.sourceSpan === 'string' && r.sourceSpan.length > 0
204
+ ? r.sourceSpan
205
+ : (typeof stmtSrc === 'string' && stmtSrc.length > 0
206
+ ? stmtSrc
207
+ : (ctx && typeof ctx.sourceSpan === 'string' ? ctx.sourceSpan : ''));
208
+ if (span.startsWith('brief:')) {
209
+ const rest = span.slice('brief:'.length);
210
+ return /^\d+$/.test(rest) ? `Your document, statement ${rest}` : 'Your document';
211
+ }
212
+ if (/^REQ-/.test(span)) return `Requirement ${span}`;
213
+ // Fall back to the same mapping the blockers path uses, so the two
214
+ // sources render identical group labels on the dogfood tree.
215
+ return labelForBlocker({ stage: typeof r.stage === 'string' ? r.stage : '', check: checkId }, itemId);
216
+ }
217
+
218
+ // ---- source B: personas.productOwner.blockers (today's shape) ----------
219
+
220
+ /**
221
+ * The source span label for a PO blocker. Brief checks name a brief
222
+ * statement number (`brief:7`); resolvedBy and reqIntent name a REQ;
223
+ * reqHasUs sits under "Requirements without a story yet"; everything
224
+ * else falls back to the plain stage label.
225
+ *
226
+ * @param {{ stage: string, check: string }} b
227
+ * @param {string} itemId
228
+ * @returns {string}
229
+ */
230
+ function labelForBlocker(b, itemId) {
231
+ if (b.check === 'stories:reqHasUs') return 'Requirements without a story yet';
232
+ if (typeof itemId === 'string' && itemId.startsWith('brief:')) {
233
+ const n = itemId.slice('brief:'.length);
234
+ return /^\d+$/.test(n) ? `Your document, statement ${n}` : 'Your document';
235
+ }
236
+ if (b.check === 'brief:sinceFreeze') return 'About this project';
237
+ if (b.check === 'brief:profile') return 'How you want to review';
238
+ if (b.check === 'brief:openQuestions') return 'Open questions in your document';
239
+ if (b.check === 'brief:kinds') return 'Statements in your document';
240
+ if (b.check === 'skeleton:resolvedBy') return 'Your document, statements without a home';
241
+ if (b.check === 'skeleton:reqIntent') return 'Requirements that need a plain description';
242
+ if (b.check === 'decisions:wellFormed') return 'Decisions without options yet';
243
+ if (typeof itemId === 'string' && /^REQ-/.test(itemId)) return `Requirement ${itemId}`;
244
+ return `Stage ${b.stage || 'other'}`;
245
+ }
246
+
247
+ /**
248
+ * Build an engineer-style detail line for the inner "Show detail"
249
+ * block (never shown as the PO ask). This stays private to the PO
250
+ * layer; the For-engineers DocRow carries the full detail.
251
+ */
252
+ function buildDetail({ stage, checkId, itemId, why }) {
253
+ const parts = [];
254
+ if (stage) parts.push(stage);
255
+ if (checkId) parts.push(checkId);
256
+ if (itemId) parts.push(`id ${itemId}`);
257
+ if (why) parts.push(why);
258
+ return parts.join(' / ');
259
+ }
260
+
261
+ function fromBlockers(readiness) {
262
+ const po = /** @type {{ personas?: { productOwner?: { blockers?: Array<unknown> } } }} */ (readiness);
263
+ const blockers = Array.isArray(po.personas?.productOwner?.blockers)
264
+ ? po.personas.productOwner.blockers
265
+ : [];
266
+ const stages = Array.isArray(/** @type {{stages?:unknown}} */(readiness).stages)
267
+ ? /** @type {Array<Record<string, unknown>>} */ (readiness.stages)
268
+ : [];
269
+ /** @type {Map<string, AdapterGroup>} */
270
+ const byLabel = new Map();
271
+ /** @type {AdapterItem[]} */
272
+ const optionalItems = [];
273
+ for (const raw of blockers) {
274
+ if (!raw || typeof raw !== 'object') continue;
275
+ const b = /** @type {Record<string, unknown>} */ (raw);
276
+ const stage = typeof b.stage === 'string' ? b.stage : '';
277
+ const checkId = typeof b.check === 'string' ? b.check : '';
278
+ const ids = Array.isArray(b.ids) ? b.ids : [];
279
+ const whyById = buildWhyById(stages, stage, checkId);
280
+ const items = ids.length > 0 ? ids : [null];
281
+ for (const idRaw of items) {
282
+ const itemId = typeof idRaw === 'string' ? idRaw : undefined;
283
+ const why = itemId && whyById[itemId] ? whyById[itemId] : '';
284
+ const pb = phrasebookEntry(checkId, itemId);
285
+ const label = labelForBlocker({ stage, check: checkId }, itemId);
286
+ const optional = Boolean(pb.optional);
287
+ const item = {
288
+ heading: pb.heading,
289
+ ask: pb.ask,
290
+ hint: pb.hint,
291
+ detail: buildDetail({ stage, checkId, itemId, why }),
292
+ checkId: `${stage}/${checkId}`,
293
+ itemId,
294
+ optional,
295
+ };
296
+ if (optional) {
297
+ optionalItems.push(item);
298
+ continue;
299
+ }
300
+ if (!byLabel.has(label)) byLabel.set(label, { label, items: [] });
301
+ byLabel.get(label).items.push(item);
302
+ }
303
+ }
304
+ return {
305
+ groups: Array.from(byLabel.values()),
306
+ optional: optionalItems.length > 0
307
+ ? { label: 'Optional: decisions with a default (they do not hold up the hand-over)', items: optionalItems, optional: true }
308
+ : null,
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Build an id -> why map by walking stages[stage].checks[check].failing[].
314
+ */
315
+ function buildWhyById(stages, stage, checkName) {
316
+ /** @type {Record<string, string>} */
317
+ const out = {};
318
+ const s = stages.find((row) => row && row.stage === stage);
319
+ if (!s || !Array.isArray(s.checks)) return out;
320
+ const c = s.checks.find((row) => row && row.name === checkName);
321
+ if (!c || !Array.isArray(c.failing)) return out;
322
+ for (const f of c.failing) {
323
+ if (!f || typeof f !== 'object') continue;
324
+ const id = typeof f.id === 'string' ? f.id : null;
325
+ if (!id) continue;
326
+ out[id] = typeof f.why === 'string' ? f.why : '';
327
+ }
328
+ return out;
329
+ }
@@ -0,0 +1,71 @@
1
+ // Readiness StageLegend (viewer UI refresh PR 8, decision 16).
2
+ //
3
+ // Renders a `<dialog>` listing D1..D8 by name, one-line meaning and
4
+ // posture (blocks the freeze, or warns until acknowledged). Opens
5
+ // from any D-chip or D-prefixed check name; client wiring lives in
6
+ // page-init.js (`wireStageLegend`). The dialog is server-rendered
7
+ // HTML so the shell carries no inline script.
8
+
9
+ import { escapeHtml } from '../doc-renderers/helpers.js';
10
+ import { STAGE_GATES, STAGE_ORDER } from '../../query/gates.js';
11
+
12
+ /**
13
+ * Human-readable name and one-line meaning per stage, aligned with
14
+ * design doc section 4 and the readiness wireframe (STAGE_GATES).
15
+ *
16
+ * @type {Record<string, { name: string, meaning: string, posture: 'blocking'|'warn-with-ack' }>}
17
+ */
18
+ export const STAGE_LEGEND = {
19
+ D1: { name: 'Brief', meaning: 'Brief intake and ledger: the owner\'s statements numbered and kinded, open questions answered or promoted, profile markers set.', posture: 'blocking' },
20
+ D2: { name: 'Skeleton', meaning: 'Requirements and architecture skeleton: every statement resolves to a requirement, an entity, an actor, a system or a surface; TAD persistence and a deploy decision exist.', posture: 'blocking' },
21
+ D3: { name: 'Shapes', meaning: 'Interface contracts and shapes on components and on shaped requirements (http, persistence, auth).', posture: 'warn-with-ack' },
22
+ D4: { name: 'Stories', meaning: 'Stories and criteria floors: every requirement has a story; stories carry failure and must-not criteria.', posture: 'blocking' },
23
+ D5: { name: 'Crosscut', meaning: 'Cross-cutting weave: security, operations and the other concerns are present where the requirements imply them.', posture: 'warn-with-ack' },
24
+ D6: { name: 'Consistency', meaning: 'Consistency and satisfiability probe: the tree validates clean and no probe is left open.', posture: 'warn-with-ack' },
25
+ D7: { name: 'Decisions', meaning: 'Decisions and review: every decision enumerated with options and a default; none open at freeze.', posture: 'blocking' },
26
+ D8: { name: 'Freeze', meaning: 'Freeze: prior stages passed or acknowledged, every criterion owned by one build spec, the queue head actionable, validate clean.', posture: 'blocking' },
27
+ };
28
+
29
+ /**
30
+ * Short title for a stage, used as the `title` attribute on D-chips
31
+ * and D-prefixed check names (hover-to-explain).
32
+ *
33
+ * @param {string} stage
34
+ * @returns {string}
35
+ */
36
+ export function stageTitle(stage) {
37
+ const row = STAGE_LEGEND[stage];
38
+ if (!row) return stage;
39
+ return `${stage} ${row.name}: ${row.meaning.split(':')[0]}`;
40
+ }
41
+
42
+ /**
43
+ * Render the StageLegend `<dialog>` HTML.
44
+ *
45
+ * @returns {string}
46
+ */
47
+ export function renderStageLegend() {
48
+ const rows = STAGE_ORDER.map((stage) => {
49
+ const row = STAGE_LEGEND[stage];
50
+ const gate = STAGE_GATES[stage];
51
+ const postureClass = row.posture === 'blocking' ? 'fail' : 'warn';
52
+ const postureLabel = row.posture === 'blocking' ? 'blocks freeze' : 'warns, acknowledge to pass';
53
+ return `<tr data-stage="${escapeHtml(stage)}">`
54
+ + `<td class="rcf-stage-legend__code"><code>${escapeHtml(stage)}</code></td>`
55
+ + `<td class="rcf-stage-legend__name"><strong>${escapeHtml(row.name)}</strong> <span class="muted"><code>${escapeHtml(gate)}</code></span></td>`
56
+ + `<td class="rcf-stage-legend__meaning">${escapeHtml(row.meaning)}</td>`
57
+ + `<td class="rcf-stage-legend__posture"><span class="rcf-pill rcf-pill--${postureClass}">${escapeHtml(postureLabel)}</span></td>`
58
+ + `</tr>`;
59
+ }).join('');
60
+ return `<dialog id="rcf-stage-legend" class="rcf-stage-legend" aria-labelledby="rcf-stage-legend-title">
61
+ <header class="rcf-stage-legend__head">
62
+ <h2 id="rcf-stage-legend-title">Stage guide: D1 to D8</h2>
63
+ <form method="dialog"><button type="submit" class="rcf-stage-legend__close">Close</button></form>
64
+ </header>
65
+ <p class="rcf-stage-legend__intro muted">The eight DEFINE stages the readiness object reports on. A blocking stage must pass before the tree can freeze; a warning stage passes once its findings are acknowledged. Codes are the chain's names; the viewer never asks a product owner to read them.</p>
66
+ <table class="rcf-stage-legend__table">
67
+ <thead><tr><th>Stage</th><th>Name</th><th>What it checks</th><th>Posture</th></tr></thead>
68
+ <tbody>${rows}</tbody>
69
+ </table>
70
+ </dialog>`;
71
+ }