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,505 @@
1
+ // Readiness tab renderer (REQ-180, TAC-4127-readiness-view, ADR-4127).
2
+ // Pure HTML string builder: takes the ReadinessResult (as produced by
3
+ // `computeReadiness` in `src/query/readiness.js`) and the operator
4
+ // profile text, emits the tabpanel body. Never recomputes readiness
5
+ // and never touches disk; the server loads the inputs once per
6
+ // rewalk (see `renderModelToPage`) so the Readiness tab and
7
+ // `rcf define readiness --json` cannot disagree for the same tree.
8
+ //
9
+ // Layout follows DEFINE step 2 spec section 5, top to bottom:
10
+ // 1 tree line; 2 verdict pills; 3 next-action per persona;
11
+ // 4 D1..D8 state chips; 5 blockers by persona; 6 delta list;
12
+ // 7 per-stage check detail; 8 coverage tree-vs-delta;
13
+ // 9 decisions outstanding; 10 freeze-now control; 11 freeze record.
14
+
15
+ import { escapeHtml } from './doc-renderers/helpers.js';
16
+ import { formatTreeLine, formatVerdictLines, shortHash } from '../query/readiness.js';
17
+ import { pill } from './components/pill.js';
18
+ import { findingsList } from './components/findings-list.js';
19
+ import { diff } from './components/diff.js';
20
+ import { renderReadinessPO } from './readiness/po-layer.js';
21
+ import { renderStageLegend, stageTitle } from './readiness/stage-legend.js';
22
+
23
+ /**
24
+ * @typedef {import('../query/readiness.js').ReadinessResult} ReadinessResult
25
+ * @typedef {import('../query/readiness.js').StageResult} StageResult
26
+ * @typedef {import('../query/readiness.js').CheckResult} CheckResult
27
+ * @typedef {import('../query/readiness.js').Blocker} Blocker
28
+ */
29
+
30
+ /**
31
+ * Decide the register ordering from `profile.md` text. Same contract
32
+ * as the CLI's `pickRegister` helper: first marker in the text wins;
33
+ * absent / unrecognised yields `unstated`, which the tab orders as
34
+ * productOwner-first (spec section 5).
35
+ *
36
+ * @param {string | null | undefined} text
37
+ * @returns {'productOwner'|'engineer'|'unstated'}
38
+ */
39
+ export function pickRegister(text) {
40
+ if (typeof text !== 'string' || text.length === 0) return 'unstated';
41
+ const markers = ['productOwner', 'engineer', 'unstated'];
42
+ let best = null;
43
+ let bestIdx = Infinity;
44
+ for (const m of markers) {
45
+ const idx = text.indexOf(m);
46
+ if (idx !== -1 && idx < bestIdx) {
47
+ bestIdx = idx;
48
+ best = m;
49
+ }
50
+ }
51
+ if (best === 'productOwner' || best === 'engineer') return best;
52
+ return 'unstated';
53
+ }
54
+
55
+ /**
56
+ * Build the full Readiness tabpanel inner HTML.
57
+ *
58
+ * @param {ReadinessResult | null} result - the computed readiness
59
+ * object, or null when the tree could not be computed
60
+ * @param {object} [opts]
61
+ * @param {string | null} [opts.profile] - profile.md text (register)
62
+ * @param {object | null} [opts.freezeRecord] - loaded freeze record,
63
+ * for the Freeze record block (section 11)
64
+ * @returns {string}
65
+ */
66
+ export function renderReadinessPanel(result, opts = {}) {
67
+ if (!result) {
68
+ return '<p><em>Readiness could not be computed for this tree.</em></p>';
69
+ }
70
+ const register = pickRegister(opts.profile ?? null);
71
+ const freezeRecord = opts.freezeRecord ?? null;
72
+ const verdictLines = formatVerdictLines(result);
73
+
74
+ const engineerBody = [
75
+ renderTreeLine(result),
76
+ renderVerdicts(result, verdictLines),
77
+ renderNextActions(result, register),
78
+ renderStageChips(result, freezeRecord),
79
+ renderBlockersByPersona(result, register),
80
+ renderDelta(result, freezeRecord),
81
+ renderStageDetail(result),
82
+ renderCoverage(result),
83
+ renderDecisions(result),
84
+ renderFreezeNow(result),
85
+ renderFreezeRecord(freezeRecord),
86
+ ].join('\n');
87
+
88
+ const poLayer = renderReadinessPO(result, { persona: register, engineerBody });
89
+ const legend = renderStageLegend();
90
+ return `${poLayer}\n${legend}`;
91
+ }
92
+
93
+ // --- Block 1: tree line ------------------------------------------------
94
+
95
+ function renderTreeLine(result) {
96
+ return `<div class="rcf-readiness-tree"><p>${escapeHtml(formatTreeLine(result))}</p></div>`;
97
+ }
98
+
99
+ // --- Block 2: verdict pills --------------------------------------------
100
+
101
+ function renderVerdicts(result, verdictLines) {
102
+ const l = result.levels;
103
+ const intentValue = l.intentComplete.ok ? 'yes' : 'no';
104
+ const buildValue = l.readyToBuild.ok ? 'yes' : 'no';
105
+ const intentPill = pill({
106
+ value: verdictLines.intentComplete,
107
+ variant: 'level-verdict-intent',
108
+ title: `Intent complete: ${intentValue} (${l.intentComplete.blockedBy.length} blocker${l.intentComplete.blockedBy.length === 1 ? '' : 's'})`,
109
+ });
110
+ const buildPill = pill({
111
+ value: verdictLines.readyToBuild,
112
+ variant: 'level-verdict-build',
113
+ title: `Ready to build: ${buildValue} (${l.readyToBuild.blockedBy.length} blocker${l.readyToBuild.blockedBy.length === 1 ? '' : 's'})`,
114
+ });
115
+ return `<div class="rcf-readiness-verdicts" data-rcf-intent-ok="${intentValue}" data-rcf-build-ok="${buildValue}">`
116
+ + `<div class="rcf-readiness-verdict rcf-readiness-verdict--intent">${intentPill}`
117
+ + ` <span class="rcf-readiness-verdict__count">${l.intentComplete.blockedBy.length}</span></div>`
118
+ + `<div class="rcf-readiness-verdict rcf-readiness-verdict--build">${buildPill}`
119
+ + ` <span class="rcf-readiness-verdict__count">${l.readyToBuild.blockedBy.length}</span></div>`
120
+ + `</div>`;
121
+ }
122
+
123
+ // --- Block 3: next action per persona ----------------------------------
124
+
125
+ function renderNextActions(result, register) {
126
+ const po = result.personas.productOwner.nextAction;
127
+ const eng = result.personas.engineer.nextAction;
128
+ const po_line = renderNextActionLine('product owner', po, 'productOwner');
129
+ const eng_line = renderNextActionLine('engineer', eng, 'engineer');
130
+ const order = register === 'engineer' ? [eng_line, po_line] : [po_line, eng_line];
131
+ return `<div class="rcf-readiness-next-actions" data-rcf-register="${register}">`
132
+ + order.join('')
133
+ + `</div>`;
134
+ }
135
+
136
+ function renderNextActionLine(label, action, persona) {
137
+ if (!action) {
138
+ return `<p class="rcf-readiness-next-action rcf-readiness-next-action--${persona}">`
139
+ + `<strong>Next action (${escapeHtml(label)}):</strong> none.`
140
+ + `</p>`;
141
+ }
142
+ const ids = action.ids && action.ids.length > 0 ? ` ids: ${action.ids.join(', ')}` : '';
143
+ const anchor = `#rcf-readiness-check-${encodeURIComponent(`${action.stage}:${action.check}`)}`;
144
+ return `<p class="rcf-readiness-next-action rcf-readiness-next-action--${persona}">`
145
+ + `<strong>Next action (${escapeHtml(label)}):</strong> `
146
+ + `<a href="${escapeHtml(anchor)}">${escapeHtml(action.stage)} / ${escapeHtml(action.check)}</a>`
147
+ + `${escapeHtml(ids)}. `
148
+ + `Run <code>${escapeHtml(action.command)}</code> after editing.`
149
+ + `</p>`;
150
+ }
151
+
152
+ // --- Block 4: stage chips D1..D8 ---------------------------------------
153
+
154
+ function renderStageChips(result, freezeRecord) {
155
+ const chips = result.stages.map((s) => {
156
+ const stateClass = chipStateClass(s.state);
157
+ const legendTitle = stageTitle(s.stage);
158
+ let title = `${s.stage} ${s.gate}: ${s.state}`;
159
+ if (legendTitle && legendTitle !== s.stage) title = `${legendTitle} (${s.state})`;
160
+ if (s.state === 'notApplicable' && s.reason) title = `${title} - ${s.reason}`;
161
+ if (s.state === 'acknowledged' && freezeRecord && freezeRecord.gates && freezeRecord.gates[s.gate]) {
162
+ const ack = freezeRecord.gates[s.gate];
163
+ if (ack && typeof ack.reason === 'string' && ack.reason.length > 0) {
164
+ title = `${title} - ${ack.reason}`;
165
+ }
166
+ }
167
+ const label = `${s.stage}: ${s.state}`;
168
+ return `<li class="rcf-readiness-chip rcf-readiness-chip--${stateClass}" data-rcf-stage-ref="${s.stage}">`
169
+ + pill({ value: label, variant: 'gate-state', title })
170
+ + `</li>`;
171
+ }).join('');
172
+ return `<ul class="rcf-readiness-chips">${chips}</ul>`;
173
+ }
174
+
175
+ function chipStateClass(state) {
176
+ switch (state) {
177
+ case 'passed':
178
+ return 'green';
179
+ case 'failing':
180
+ return 'red';
181
+ case 'acknowledged':
182
+ return 'amber';
183
+ case 'notApplicable':
184
+ return 'grey';
185
+ default:
186
+ return 'grey';
187
+ }
188
+ }
189
+
190
+ // --- Block 5: blockers by persona --------------------------------------
191
+
192
+ export function renderBlockersByPersona(result, register) {
193
+ const po = result.personas.productOwner.blockers;
194
+ const eng = result.personas.engineer.blockers;
195
+ const stages = Array.isArray(result.stages) ? result.stages : [];
196
+ const poBlock = renderPersonaGroup('productOwner', 'Product owner', 'question', po, register, 'open', stages);
197
+ const engBlock = renderPersonaGroup('engineer', 'Engineer', 'check blocking', eng, register, 'closed', stages);
198
+ // When the register is engineer, the engineer group opens first and
199
+ // the PO group collapses; `unstated` and `productOwner` keep PO open.
200
+ const [first, second] = register === 'engineer'
201
+ ? [renderPersonaGroup('engineer', 'Engineer', 'check blocking', eng, register, 'open', stages),
202
+ renderPersonaGroup('productOwner', 'Product owner', 'question', po, register, 'closed', stages)]
203
+ : [poBlock, engBlock];
204
+ return `<section class="rcf-readiness-blockers-by-persona" data-rcf-register="${register}">`
205
+ + first + second
206
+ + `</section>`;
207
+ }
208
+
209
+ /**
210
+ * @param {'productOwner'|'engineer'} persona
211
+ * @param {string} heading
212
+ * @param {string} singular - 'question' | 'check blocking'
213
+ * @param {Blocker[]} blockers
214
+ * @param {'productOwner'|'engineer'|'unstated'} register
215
+ * @param {'open'|'closed'} state
216
+ * @param {StageResult[]} stages - threaded so each blocker's ids can
217
+ * carry the matching failing `why` from `stages[].checks[].failing[]`
218
+ * (spec section 5 block 5).
219
+ */
220
+ function renderPersonaGroup(persona, heading, singular, blockers, register, state, stages) {
221
+ const plural = singular === 'question' ? 'questions' : 'checks blocking';
222
+ const count = blockers.length;
223
+ const noun = count === 1 ? singular : plural;
224
+ const summary = `${heading}: ${count} ${noun}`;
225
+ const openAttr = state === 'open' ? ' open' : '';
226
+ if (count === 0) {
227
+ return `<details class="rcf-readiness-persona rcf-readiness-persona--${persona}" data-rcf-persona-state="${state}"${openAttr}>`
228
+ + `<summary><strong>${escapeHtml(heading)}:</strong> 0 ${plural}.</summary>`
229
+ + `</details>`;
230
+ }
231
+ const items = blockers.map((b) => renderBlockerCard(b, stages)).join('');
232
+ return `<details class="rcf-readiness-persona rcf-readiness-persona--${persona}" data-rcf-persona-state="${state}"${openAttr}>`
233
+ + `<summary><strong>${escapeHtml(summary)}</strong></summary>`
234
+ + items
235
+ + `</details>`;
236
+ }
237
+
238
+ /**
239
+ * Build an id→why map for a blocker by looking up the matching
240
+ * `stages[].checks[].failing[]` entries. Ids missing a `why` map to
241
+ * the empty string so the findings-list renders the id alone.
242
+ *
243
+ * @param {Blocker} b
244
+ * @param {StageResult[]} stages
245
+ * @returns {Record<string, string>}
246
+ */
247
+ function buildIdWhyMap(b, stages) {
248
+ /** @type {Record<string, string>} */
249
+ const map = {};
250
+ const stage = Array.isArray(stages) ? stages.find((s) => s && s.stage === b.stage) : null;
251
+ if (!stage || !Array.isArray(stage.checks)) return map;
252
+ const check = stage.checks.find((c) => c && c.name === b.check);
253
+ if (!check || !Array.isArray(check.failing)) return map;
254
+ for (const f of check.failing) {
255
+ if (!f || typeof f !== 'object') continue;
256
+ const id = typeof f.id === 'string' ? f.id : null;
257
+ if (!id) continue;
258
+ map[id] = typeof f.why === 'string' ? f.why : '';
259
+ }
260
+ return map;
261
+ }
262
+
263
+ /**
264
+ * @param {Blocker} b
265
+ * @param {StageResult[]} stages
266
+ */
267
+ function renderBlockerCard(b, stages) {
268
+ // Each id is a document id; the hash routing in html-page.js resolves
269
+ // `#<id>` to the right tab. The `why` per id lives on the matching
270
+ // `stages[].checks[].failing[]` entry — spec section 5 block 5.
271
+ const whyById = buildIdWhyMap(b, stages);
272
+ const items = (b.ids ?? []).slice(0, 20).map((id) => ({
273
+ id,
274
+ why: whyById[id] ?? '',
275
+ href: `#${id}`,
276
+ }));
277
+ return `<div class="rcf-readiness-blocker" data-rcf-stage="${escapeHtml(b.stage)}" data-rcf-check="${escapeHtml(b.check)}" data-rcf-persona="${escapeHtml(b.persona)}">`
278
+ + `<div class="rcf-readiness-blocker__meta">`
279
+ + `<span class="rcf-readiness-blocker__stage">${escapeHtml(b.stage)}</span> `
280
+ + `<span class="rcf-readiness-blocker__check">${escapeHtml(b.check)}</span> `
281
+ + pill({ value: b.persona === 'productOwner' ? 'PO' : 'eng', variant: 'persona', title: b.persona })
282
+ + ` <span class="rcf-readiness-blocker__over">over ${escapeHtml(b.over)}</span>`
283
+ + `</div>`
284
+ + findingsList({ heading: b.question, count: b.failingCount, items })
285
+ + `</div>`;
286
+ }
287
+
288
+ // --- Block 6: delta list -----------------------------------------------
289
+
290
+ function renderDelta(result, freezeRecord) {
291
+ const d = result.delta;
292
+ const briefCount = Array.isArray(d.briefSince) ? d.briefSince.length : 0;
293
+ const changedCount = (d.changed?.length ?? 0) + (d.added?.length ?? 0) + (d.removed?.length ?? 0);
294
+ const frozenHashes = (freezeRecord && typeof freezeRecord === 'object' && freezeRecord.docHashes && typeof freezeRecord.docHashes === 'object')
295
+ ? freezeRecord.docHashes
296
+ : {};
297
+ const currentHashes = (d && typeof d.currentDocHashes === 'object' && d.currentDocHashes !== null)
298
+ ? d.currentDocHashes
299
+ : {};
300
+ const briefBlock = briefCount > 0
301
+ ? `<section class="rcf-readiness-delta__group rcf-readiness-delta__group--brief">`
302
+ + `<h4>Brief statements since freeze <span class="rcf-readiness-delta__count">${briefCount}</span></h4>`
303
+ + `<ul>${d.briefSince.map((n) => `<li>#${escapeHtml(String(n))}</li>`).join('')}</ul>`
304
+ + `</section>`
305
+ : '';
306
+ const changedBlock = changedCount > 0
307
+ ? `<section class="rcf-readiness-delta__group rcf-readiness-delta__group--documents">`
308
+ + `<h4>Documents changed <span class="rcf-readiness-delta__count">${changedCount}</span></h4>`
309
+ + `<ul>`
310
+ + (d.added ?? []).map((id) => `<li><a href="#${escapeHtml(id)}">${escapeHtml(id)}</a> <em>added</em>${renderDocDiff(id, frozenHashes, currentHashes, 'added')}</li>`).join('')
311
+ + (d.changed ?? []).map((id) => `<li><a href="#${escapeHtml(id)}">${escapeHtml(id)}</a> <em>changed</em>${renderDocDiff(id, frozenHashes, currentHashes, 'changed')}</li>`).join('')
312
+ + (d.removed ?? []).map((id) => `<li>${escapeHtml(id)} <em>removed</em>${renderDocDiff(id, frozenHashes, currentHashes, 'removed')}</li>`).join('')
313
+ + `</ul>`
314
+ + `</section>`
315
+ : '';
316
+ if (!briefBlock && !changedBlock) {
317
+ return `<section class="rcf-readiness-delta"><p><em>No delta since the last freeze.</em></p></section>`;
318
+ }
319
+ return `<section class="rcf-readiness-delta">${briefBlock}${changedBlock}</section>`;
320
+ }
321
+
322
+ /**
323
+ * Document-level hash diff for one changed / added / removed id.
324
+ * Spec section 5 block 6 pins a real `diff` component here; section 10
325
+ * decision 1 scopes 0.29.0 to document-level only (criterion-level
326
+ * deferred). The freeze record does not persist document bodies in
327
+ * 0.29.0 (freeze-record.js schema: `docHashes` only — no `docs` /
328
+ * `snapshot` section), so this ships the hash-only variant: both
329
+ * columns show the short-hash subtitle and the frozen side carries
330
+ * a muted note stating the body was not captured. NOTES in the brief.
331
+ */
332
+ function renderDocDiff(id, frozenHashes, currentHashes, kind) {
333
+ const frozenHash = typeof frozenHashes?.[id] === 'string' ? frozenHashes[id] : null;
334
+ const currentHash = typeof currentHashes?.[id] === 'string' ? currentHashes[id] : null;
335
+ const beforeNote = 'frozen body not captured in freeze record 0.29.0';
336
+ const before = kind === 'added'
337
+ ? { note: 'not in the frozen tree' }
338
+ : { hash: frozenHash ?? '', note: beforeNote };
339
+ const after = kind === 'removed'
340
+ ? { note: 'removed from the live tree' }
341
+ : { hash: currentHash ?? '', note: 'current body available on the document tab' };
342
+ return ` ${diff(before, after)}`;
343
+ }
344
+
345
+ // --- Block 7: per-stage check detail -----------------------------------
346
+
347
+ function renderStageDetail(result) {
348
+ const stageBlocks = result.stages.map((s) => renderStage(s)).join('');
349
+ return `<section class="rcf-readiness-stage-detail">`
350
+ + `<h3>Stage detail</h3>`
351
+ + stageBlocks
352
+ + `</section>`;
353
+ }
354
+
355
+ /**
356
+ * @param {StageResult} s
357
+ */
358
+ function renderStage(s) {
359
+ const chip = pill({ value: s.state, variant: 'gate-state', title: `${s.stage} ${s.gate}` });
360
+ const checks = s.checks.map((c) => renderCheck(s, c)).join('');
361
+ return `<section class="rcf-readiness-stage" id="rcf-readiness-stage-${escapeHtml(s.stage)}">`
362
+ + `<h4>${escapeHtml(s.stage)} <code>${escapeHtml(s.gate)}</code> ${chip}</h4>`
363
+ + checks
364
+ + `</section>`;
365
+ }
366
+
367
+ /**
368
+ * @param {StageResult} s
369
+ * @param {CheckResult} c
370
+ */
371
+ function renderCheck(s, c) {
372
+ const checkId = `rcf-readiness-check-${encodeURIComponent(`${s.stage}:${c.name}`)}`;
373
+ const personaPill = pill({
374
+ value: c.persona === 'productOwner' ? 'PO' : 'eng',
375
+ variant: 'persona',
376
+ title: c.persona,
377
+ });
378
+ const statusLabel = c.ok ? 'ok' : 'fail';
379
+ const items = c.ok
380
+ ? ''
381
+ : findingsList({
382
+ heading: c.question,
383
+ count: c.failing.length,
384
+ items: c.failing.map((f) => ({ id: f.id, why: f.why, href: `#${f.id}` })),
385
+ });
386
+ return `<div class="rcf-readiness-check" id="${escapeHtml(checkId)}" data-rcf-check-ok="${c.ok ? 'yes' : 'no'}">`
387
+ + `<div class="rcf-readiness-check__meta">`
388
+ + `<strong>${escapeHtml(c.name)}</strong> ${personaPill} `
389
+ + `<span class="rcf-readiness-check__over">over ${escapeHtml(c.over)}</span> `
390
+ + `<span class="rcf-readiness-check__counts">${c.pass}/${c.total}</span> `
391
+ + `<span class="rcf-readiness-check__status rcf-readiness-check__status--${statusLabel}">${statusLabel}</span>`
392
+ + `</div>`
393
+ + items
394
+ + `</div>`;
395
+ }
396
+
397
+ // --- Block 8: coverage tree-vs-delta -----------------------------------
398
+
399
+ function renderCoverage(result) {
400
+ const t = result.coverage?.tree ?? null;
401
+ const d = result.coverage?.delta ?? [];
402
+ const impactedCount = result.delta?.impacted?.length ?? 0;
403
+ const impactedFbsCount = result.delta?.impactedFbs?.length ?? 0;
404
+ const treeCol = t
405
+ ? `<pre class="rcf-readiness-coverage__tree">pass ${escapeHtml(String(t.pass ?? ''))} / ${escapeHtml(String(t.total ?? ''))}</pre>`
406
+ : `<p><em>No tree-wide coverage result.</em></p>`;
407
+ // Filter rows where every meaningful field is nullish / empty — a
408
+ // coverage shim with no reqId/scope and no pass/total totals produces
409
+ // "delta: / " markup otherwise (P2 polish; spec §5 block 8).
410
+ const liveDelta = Array.isArray(d) ? d.filter(isMeaningfulCoverageRow) : [];
411
+ const deltaRows = liveDelta.length > 0
412
+ ? `<ul>${liveDelta.map((cv) => `<li>${escapeHtml(cv.reqId ?? cv.scope ?? 'delta')}: ${escapeHtml(String(cv.pass ?? ''))} / ${escapeHtml(String(cv.total ?? ''))}</li>`).join('')}</ul>`
413
+ : `<p><em>No per-REQ delta coverage.</em></p>`;
414
+ return `<section class="rcf-readiness-coverage">`
415
+ + `<h3>Coverage</h3>`
416
+ + `<div class="rcf-readiness-coverage__cols">`
417
+ + `<div class="rcf-readiness-coverage__col rcf-readiness-coverage__col--tree"><h4>Tree</h4>${treeCol}</div>`
418
+ + `<div class="rcf-readiness-coverage__col rcf-readiness-coverage__col--delta"><h4>Delta</h4>${deltaRows}</div>`
419
+ + `</div>`
420
+ + `<p class="rcf-readiness-coverage__counts">`
421
+ + `re-verify: ${impactedCount} &middot; re-execute: ${impactedFbsCount}`
422
+ + `</p>`
423
+ + `</section>`;
424
+ }
425
+
426
+ function isMeaningfulCoverageRow(cv) {
427
+ if (!cv || typeof cv !== 'object') return false;
428
+ const hasLabel = (typeof cv.reqId === 'string' && cv.reqId.length > 0)
429
+ || (typeof cv.scope === 'string' && cv.scope.length > 0);
430
+ const passSet = cv.pass !== null && cv.pass !== undefined && cv.pass !== '';
431
+ const totalSet = cv.total !== null && cv.total !== undefined && cv.total !== '';
432
+ return hasLabel || passSet || totalSet;
433
+ }
434
+
435
+ // --- Block 9: decisions outstanding ------------------------------------
436
+
437
+ function renderDecisions(result) {
438
+ const items = Array.isArray(result.decisions) ? result.decisions : [];
439
+ if (items.length === 0) {
440
+ return `<section class="rcf-readiness-decisions"><h3>Decisions outstanding</h3><p><em>No open decisions.</em></p></section>`;
441
+ }
442
+ const list = items.map((d, idx) => {
443
+ const n = typeof d.number === 'number' ? d.number : idx + 1;
444
+ const q = typeof d.question === 'string' ? d.question : '(no question)';
445
+ const def = typeof d.default === 'string' ? ` <em>default:</em> ${escapeHtml(d.default)}` : '';
446
+ const options = Array.isArray(d.options) && d.options.length > 0
447
+ ? ` <em>options:</em> ${d.options.map((o) => escapeHtml(String(o))).join(', ')}`
448
+ : '';
449
+ return `<li><strong>#${escapeHtml(String(n))}</strong> ${escapeHtml(q)}${options}${def}</li>`;
450
+ }).join('');
451
+ return `<section class="rcf-readiness-decisions"><h3>Decisions outstanding</h3><ol>${list}</ol></section>`;
452
+ }
453
+
454
+ // --- Block 10: freeze now ----------------------------------------------
455
+
456
+ function renderFreezeNow(result) {
457
+ const ok = result.levels.readyToBuild.ok;
458
+ const disabledAttr = ok ? '' : ' disabled';
459
+ const failingGates = ok
460
+ ? []
461
+ : [...new Set(result.levels.readyToBuild.blockedBy.map((b) => b.gate))].sort();
462
+ const failingList = failingGates.length > 0
463
+ ? ` <span class="rcf-readiness-freeze-now__failing">Failing gates: ${failingGates.map((g) => escapeHtml(g)).join(', ')}</span>`
464
+ : '';
465
+ const cmd = `rcf define freeze`;
466
+ const willRecord = ok
467
+ ? `<p>Will record: tree hash <code>${escapeHtml(shortHash(result.tree.currentTreeHash))}</code>, timestamp, note, counts and acknowledged gates with reasons.</p>`
468
+ : '';
469
+ return `<section class="rcf-readiness-freeze-now" data-rcf-freezeable="${ok ? 'yes' : 'no'}">`
470
+ + `<h3>Freeze now</h3>`
471
+ + `<button type="button" class="rcf-readiness-freeze-now__btn"${disabledAttr}>Freeze</button>`
472
+ + ` <code>${escapeHtml(cmd)}</code>`
473
+ + failingList
474
+ + willRecord
475
+ + `</section>`;
476
+ }
477
+
478
+ // --- Block 11: freeze record -------------------------------------------
479
+
480
+ function renderFreezeRecord(freezeRecord) {
481
+ if (!freezeRecord) {
482
+ return `<section class="rcf-readiness-freeze-record"><h3>Freeze record</h3><p><em>No freeze record on disk.</em></p></section>`;
483
+ }
484
+ const hash = typeof freezeRecord.treeHash === 'string' ? shortHash(freezeRecord.treeHash) : '(no hash)';
485
+ const when = typeof freezeRecord.frozenAt === 'string' ? freezeRecord.frozenAt : '(no timestamp)';
486
+ const note = typeof freezeRecord.note === 'string' ? freezeRecord.note : '';
487
+ const counts = freezeRecord.counts && typeof freezeRecord.counts === 'object'
488
+ ? Object.entries(freezeRecord.counts)
489
+ .map(([k, v]) => `<li>${escapeHtml(k)}: ${escapeHtml(String(v))}</li>`)
490
+ .join('')
491
+ : '';
492
+ const gates = freezeRecord.gates && typeof freezeRecord.gates === 'object'
493
+ ? Object.entries(freezeRecord.gates)
494
+ .map(([k, v]) => `<li><code>${escapeHtml(k)}</code>: ${escapeHtml(v && typeof v === 'object' && typeof v.reason === 'string' ? v.reason : String(v ?? ''))}</li>`)
495
+ .join('')
496
+ : '';
497
+ return `<section class="rcf-readiness-freeze-record">`
498
+ + `<h3>Freeze record</h3>`
499
+ + `<p><strong>Hash:</strong> <code>${escapeHtml(hash)}</code></p>`
500
+ + `<p><strong>When:</strong> ${escapeHtml(when)}</p>`
501
+ + (note ? `<p><strong>Note:</strong> ${escapeHtml(note)}</p>` : '')
502
+ + (counts ? `<h4>Counts</h4><ul>${counts}</ul>` : '')
503
+ + (gates ? `<h4>Acknowledged gates</h4><ul>${gates}</ul>` : '')
504
+ + `</section>`;
505
+ }