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,563 @@
1
+ // Build/DAG sub-tab renderer (viewer UI refresh PR 6; REQ-002, US-204,
2
+ // TAC-4131-build-dag-view, ADR-4133-dag-dependency-depth-layout).
3
+ //
4
+ // Pure HTML-string renderer for the Build tab's DAG sub-tab:
5
+ // - DagToolbar: Status chips, Area select, Buildable-now, Critical-path
6
+ // and Show-unconnected toggles plus a cycle finding (findingsList).
7
+ // - DagCanvas: HTML buttons for each FBS node + inline SVG edges, laid
8
+ // by `dependsOnFbsIds` depth (columns) and `buildOrder` (rows). The
9
+ // canvas owns its own overflow so the viewer never scrolls
10
+ // horizontally (ADR-4133).
11
+ // - DagInspector: a sticky side panel the client fills on select.
12
+ // - Unconnected lane: FBS with no incoming and no outgoing dependency,
13
+ // hidden by default; the toggle carries the count.
14
+ //
15
+ // Layout, closures and critical path come from `buildDagLayout` so the
16
+ // toolbar and the canvas see the same numbers (AC-204-1, AC-204-3). The
17
+ // inspector is HTML-only; the client in page-init.js (wireBuildDag)
18
+ // swaps its body on select without any outbound postMessage (AC-204-7).
19
+
20
+ import { escapeHtml } from './doc-renderers/helpers.js';
21
+ import { pill } from './components/pill.js';
22
+ import { renderBadge } from './components/badge.js';
23
+ import { findingsList } from './components/findings-list.js';
24
+
25
+ /**
26
+ * @typedef {object} DagNode
27
+ * @property {string} id
28
+ * @property {string} title
29
+ * @property {string} status
30
+ * @property {string} domain
31
+ * @property {string} size
32
+ * @property {number} buildOrder
33
+ * @property {number} acCount
34
+ * @property {number} depth
35
+ * @property {string[]} needs direct dependencies
36
+ * @property {string[]} waits direct dependants
37
+ * @property {string[]} upstream transitive closure of needs
38
+ * @property {string[]} downstream transitive closure of waits
39
+ * @property {boolean} buildable
40
+ * @property {boolean} critical
41
+ * @property {boolean} unconnected
42
+ */
43
+
44
+ /**
45
+ * @typedef {object} DagLayout
46
+ * @property {Map<string, DagNode>} nodes
47
+ * @property {Array<{ from: string, to: string }>} edges
48
+ * @property {string[][]} columns column index -> fbsIds (buildOrder asc)
49
+ * @property {string[]} unconnectedIds fbsIds with no in and no out edge
50
+ * @property {string[]} criticalPathIds longest chain by node count
51
+ * @property {string[]} cycleIds fbsIds that sit on a dependsOnFbsIds cycle
52
+ * @property {number} maxDepth
53
+ */
54
+
55
+ /**
56
+ * Pure projection of `fbsItems[].dependsOnFbsIds` into a DAG layout.
57
+ *
58
+ * - A node's depth is the longest chain length from `dependsOnFbsIds` to a
59
+ * root (0 for an FBS that declares no dependency).
60
+ * - Unconnected: no incoming and no outgoing edge.
61
+ * - Critical path: longest chain by node count across the DAG (ADR-4133).
62
+ * - Cycle members are excluded from the depth layout and surfaced as a
63
+ * finding by the toolbar (AC-204-6); non-cycle FBS still render.
64
+ *
65
+ * @param {object[]} fbsItems
66
+ * @param {Set<string>} [buildableIds] - the actionable set from computeQueue
67
+ * @returns {DagLayout}
68
+ */
69
+ export function buildDagLayout(fbsItems, buildableIds) {
70
+ const items = Array.isArray(fbsItems) ? fbsItems : [];
71
+ const buildable = buildableIds instanceof Set ? buildableIds : new Set();
72
+ const byId = new Map();
73
+ for (const f of items) {
74
+ if (!f || typeof f.fbsId !== 'string') continue;
75
+ byId.set(f.fbsId, f);
76
+ }
77
+
78
+ const needsById = new Map();
79
+ const waitsById = new Map();
80
+ for (const f of items) {
81
+ const id = f.fbsId;
82
+ if (!id) continue;
83
+ const deps = Array.isArray(f.dependsOnFbsIds) ? f.dependsOnFbsIds.filter((d) => byId.has(d)) : [];
84
+ needsById.set(id, deps);
85
+ if (!waitsById.has(id)) waitsById.set(id, []);
86
+ for (const d of deps) {
87
+ if (!waitsById.has(d)) waitsById.set(d, []);
88
+ waitsById.get(d).push(id);
89
+ }
90
+ }
91
+
92
+ const cycleIds = findCycleIds(byId, needsById);
93
+ const cycleSet = new Set(cycleIds);
94
+
95
+ const depth = new Map();
96
+ function computeDepth(id, seen) {
97
+ if (cycleSet.has(id)) return 0;
98
+ if (depth.has(id)) return depth.get(id);
99
+ if (seen.has(id)) return 0;
100
+ seen.add(id);
101
+ const deps = needsById.get(id) ?? [];
102
+ let d = 0;
103
+ for (const dep of deps) {
104
+ if (cycleSet.has(dep)) continue;
105
+ d = Math.max(d, computeDepth(dep, seen) + 1);
106
+ }
107
+ seen.delete(id);
108
+ depth.set(id, d);
109
+ return d;
110
+ }
111
+ for (const id of byId.keys()) {
112
+ if (!cycleSet.has(id)) computeDepth(id, new Set());
113
+ }
114
+
115
+ const upstream = new Map();
116
+ const downstream = new Map();
117
+ for (const id of byId.keys()) {
118
+ upstream.set(id, closure(id, needsById));
119
+ downstream.set(id, closure(id, waitsById));
120
+ }
121
+
122
+ const criticalPathIds = longestChain(byId, needsById, waitsById, cycleSet);
123
+ const criticalSet = new Set(criticalPathIds);
124
+
125
+ const unconnectedIds = [];
126
+ for (const id of byId.keys()) {
127
+ if (cycleSet.has(id)) continue;
128
+ const inDeg = (needsById.get(id) ?? []).length;
129
+ const outDeg = (waitsById.get(id) ?? []).length;
130
+ if (inDeg === 0 && outDeg === 0) unconnectedIds.push(id);
131
+ }
132
+ unconnectedIds.sort((a, b) => byBuildOrder(byId.get(a), byId.get(b)));
133
+
134
+ let maxDepth = 0;
135
+ for (const id of byId.keys()) {
136
+ if (cycleSet.has(id)) continue;
137
+ const inDeg = (needsById.get(id) ?? []).length;
138
+ const outDeg = (waitsById.get(id) ?? []).length;
139
+ if (inDeg === 0 && outDeg === 0) continue;
140
+ const d = depth.get(id) ?? 0;
141
+ if (d > maxDepth) maxDepth = d;
142
+ }
143
+
144
+ const columns = Array.from({ length: maxDepth + 1 }, () => []);
145
+ for (const id of byId.keys()) {
146
+ if (cycleSet.has(id)) continue;
147
+ const inDeg = (needsById.get(id) ?? []).length;
148
+ const outDeg = (waitsById.get(id) ?? []).length;
149
+ if (inDeg === 0 && outDeg === 0) continue;
150
+ const d = depth.get(id) ?? 0;
151
+ columns[d].push(id);
152
+ }
153
+ for (const col of columns) {
154
+ col.sort((a, b) => byBuildOrder(byId.get(a), byId.get(b)));
155
+ }
156
+
157
+ const nodes = new Map();
158
+ const unconnectedSet = new Set(unconnectedIds);
159
+ for (const [id, f] of byId.entries()) {
160
+ const node = {
161
+ id,
162
+ title: typeof f.title === 'string' ? f.title : '',
163
+ status: typeof f.executionStatus === 'string' ? f.executionStatus : '',
164
+ domain: typeof f.domain === 'string' ? f.domain : '',
165
+ size: typeof f.estimatedSize === 'string' ? f.estimatedSize : '',
166
+ buildOrder: typeof f.buildOrder === 'number' ? f.buildOrder : 0,
167
+ acCount: Array.isArray(f.acIds) ? f.acIds.length : 0,
168
+ depth: cycleSet.has(id) ? -1 : (depth.get(id) ?? 0),
169
+ needs: (needsById.get(id) ?? []).slice(),
170
+ waits: (waitsById.get(id) ?? []).slice(),
171
+ upstream: Array.from(upstream.get(id) ?? []).sort(idSort(byId)),
172
+ downstream: Array.from(downstream.get(id) ?? []).sort(idSort(byId)),
173
+ buildable: buildable.has(id),
174
+ critical: criticalSet.has(id),
175
+ unconnected: unconnectedSet.has(id),
176
+ };
177
+ nodes.set(id, node);
178
+ }
179
+
180
+ const edges = [];
181
+ for (const [to, deps] of needsById.entries()) {
182
+ for (const from of deps) {
183
+ edges.push({ from, to });
184
+ }
185
+ }
186
+
187
+ return { nodes, edges, columns, unconnectedIds, criticalPathIds, cycleIds, maxDepth };
188
+ }
189
+
190
+ function closure(start, edgesById) {
191
+ const out = new Set();
192
+ const stack = [start];
193
+ while (stack.length > 0) {
194
+ const cur = stack.pop();
195
+ const next = edgesById.get(cur) ?? [];
196
+ for (const n of next) {
197
+ if (out.has(n)) continue;
198
+ out.add(n);
199
+ stack.push(n);
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+
205
+ function byBuildOrder(a, b) {
206
+ const ao = a?.buildOrder ?? 0;
207
+ const bo = b?.buildOrder ?? 0;
208
+ if (ao !== bo) return ao - bo;
209
+ return String(a?.fbsId ?? '').localeCompare(String(b?.fbsId ?? ''));
210
+ }
211
+
212
+ function idSort(byId) {
213
+ return (a, b) => byBuildOrder(byId.get(a), byId.get(b));
214
+ }
215
+
216
+ function findCycleIds(byId, needsById) {
217
+ const WHITE = 0;
218
+ const GREY = 1;
219
+ const BLACK = 2;
220
+ const colour = new Map();
221
+ const onStack = new Map();
222
+ const stack = [];
223
+ const result = new Set();
224
+ for (const id of byId.keys()) colour.set(id, WHITE);
225
+ for (const start of byId.keys()) {
226
+ if (colour.get(start) !== WHITE) continue;
227
+ const work = [{ id: start, i: 0 }];
228
+ colour.set(start, GREY);
229
+ onStack.set(start, true);
230
+ stack.push(start);
231
+ while (work.length > 0) {
232
+ const top = work[work.length - 1];
233
+ const deps = needsById.get(top.id) ?? [];
234
+ if (top.i >= deps.length) {
235
+ colour.set(top.id, BLACK);
236
+ onStack.delete(top.id);
237
+ stack.pop();
238
+ work.pop();
239
+ continue;
240
+ }
241
+ const next = deps[top.i];
242
+ top.i += 1;
243
+ if (colour.get(next) === WHITE) {
244
+ colour.set(next, GREY);
245
+ onStack.set(next, true);
246
+ stack.push(next);
247
+ work.push({ id: next, i: 0 });
248
+ } else if (onStack.get(next)) {
249
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
250
+ result.add(stack[i]);
251
+ if (stack[i] === next) break;
252
+ }
253
+ }
254
+ }
255
+ }
256
+ return Array.from(result).sort();
257
+ }
258
+
259
+ function longestChain(byId, needsById, waitsById, cycleSet) {
260
+ const memo = new Map();
261
+ function longest(id) {
262
+ if (memo.has(id)) return memo.get(id);
263
+ const waits = (waitsById.get(id) ?? []).filter((d) => !cycleSet.has(d));
264
+ let best = [id];
265
+ for (const n of waits) {
266
+ const chain = [id, ...longest(n)];
267
+ if (chain.length > best.length) best = chain;
268
+ }
269
+ memo.set(id, best);
270
+ return best;
271
+ }
272
+ let overall = [];
273
+ for (const id of byId.keys()) {
274
+ if (cycleSet.has(id)) continue;
275
+ const deps = needsById.get(id) ?? [];
276
+ if (deps.filter((d) => !cycleSet.has(d)).length !== 0) continue;
277
+ const chain = longest(id);
278
+ if (chain.length > overall.length) overall = chain;
279
+ }
280
+ return overall;
281
+ }
282
+
283
+ // ------------------------------------------------------------------------
284
+ // HTML render
285
+ // ------------------------------------------------------------------------
286
+
287
+ const NODE_W = 220;
288
+ const NODE_H = 40;
289
+ const COL_GAP = 68;
290
+ const ROW_GAP = 10;
291
+ const PAD = 20;
292
+ const HEADER_H = 28;
293
+ const LANE_HEAD_H = 44;
294
+
295
+ /**
296
+ * @param {object} args
297
+ * @param {DagLayout} args.layout
298
+ * @param {Record<string, object>} [args.inspectorPayload] - rides on a
299
+ * `data-rcf-dag-inspector-data` attribute on the inspector shell (PR 1
300
+ * contract: component payload data on a `data-*` attribute, never an
301
+ * inline `<script>` body).
302
+ * @returns {string}
303
+ */
304
+ export function renderBuildDag({ layout, inspectorPayload }) {
305
+ const toolbar = renderDagToolbar(layout);
306
+ const canvas = renderDagCanvas(layout);
307
+ const inspector = renderDagInspector(inspectorPayload);
308
+ const legend = renderDagLegend();
309
+ return `<div class="rcf-dag" data-rcf-dag="build">
310
+ ${toolbar}
311
+ <div class="rcf-dag-grid">
312
+ <div class="rcf-dag-canvas-wrap card" data-rcf-dag-canvas-wrap>
313
+ ${canvas}
314
+ </div>
315
+ <aside class="rcf-dag-inspector card" data-rcf-dag-inspector>
316
+ ${inspector}
317
+ </aside>
318
+ </div>
319
+ ${legend}
320
+ </div>`;
321
+ }
322
+
323
+ function renderDagToolbar(layout) {
324
+ const uncCount = layout.unconnectedIds.length;
325
+ const critCount = layout.criticalPathIds.length;
326
+ const statusChips = ['notStarted', 'inProgress', 'complete', 'verified']
327
+ .map((s) => `<button type="button" class="rcf-dag-chip" data-rcf-dag-status="${escapeHtml(s)}" aria-pressed="true">${escapeHtml(statusLabel(s))}</button>`)
328
+ .join('');
329
+ const domainOptions = Array.from(new Set(
330
+ Array.from(layout.nodes.values()).map((n) => n.domain).filter((d) => typeof d === 'string' && d.length > 0),
331
+ )).sort();
332
+ const domainSelect = `<label class="rcf-dag-label"><span class="small muted">Area</span><select class="rcf-dag-select" data-rcf-dag-domain><option value="">All areas</option>${domainOptions.map((d) => `<option value="${escapeHtml(d)}">${escapeHtml(d)}</option>`).join('')}</select></label>`;
333
+ const toggleBuildable = `<button type="button" class="rcf-dag-chip" data-rcf-dag-toggle="buildable" aria-pressed="false">Buildable now</button>`;
334
+ const toggleCritical = `<button type="button" class="rcf-dag-chip" data-rcf-dag-toggle="critical" aria-pressed="false">Critical path <span class="rcf-badge rcf-badge--count">${critCount}</span></button>`;
335
+ const toggleUnconnected = `<button type="button" class="rcf-dag-chip" data-rcf-dag-toggle="unconnected" aria-pressed="false">Show unconnected <span class="rcf-badge rcf-badge--count">${uncCount}</span></button>`;
336
+ const findings = layout.cycleIds.length > 0
337
+ ? findingsList({
338
+ heading: 'Dependency cycle',
339
+ count: layout.cycleIds.length,
340
+ items: layout.cycleIds.map((id) => ({ id, why: 'sits on a dependsOnFbsIds cycle; excluded from the depth layout', href: `#tab=build&sub=specs&entity=${id}` })),
341
+ })
342
+ : '';
343
+ return `<div class="rcf-dag-toolbar" data-rcf-dag-toolbar>
344
+ <div class="rcf-dag-toolbar-row">
345
+ <span class="small muted">Status</span>
346
+ ${statusChips}
347
+ ${domainSelect}
348
+ ${toggleBuildable}
349
+ ${toggleCritical}
350
+ ${toggleUnconnected}
351
+ </div>
352
+ ${findings ? `<div class="rcf-dag-toolbar-findings">${findings}</div>` : ''}
353
+ </div>`;
354
+ }
355
+
356
+ function statusLabel(s) {
357
+ switch (s) {
358
+ case 'notStarted': return 'not started';
359
+ case 'inProgress': return 'in progress';
360
+ default: return s;
361
+ }
362
+ }
363
+
364
+ function renderDagCanvas(layout) {
365
+ const positions = layoutPositions(layout);
366
+ const connectedCols = layout.columns.length;
367
+ const connectedHeight = positions.connectedHeight;
368
+ const canvasW = Math.max(PAD * 2 + Math.max(1, connectedCols) * (NODE_W + COL_GAP), 900);
369
+ let canvasH = PAD + HEADER_H + connectedHeight + PAD;
370
+
371
+ const laneRows = [];
372
+ if (layout.unconnectedIds.length > 0) {
373
+ const perRow = Math.max(1, Math.floor((canvasW - PAD * 2) / (NODE_W + 12)));
374
+ const laneTop = canvasH + 8;
375
+ for (let i = 0; i < layout.unconnectedIds.length; i += 1) {
376
+ const id = layout.unconnectedIds[i];
377
+ const col = i % perRow;
378
+ const row = Math.floor(i / perRow);
379
+ positions.pos.set(id, {
380
+ x: PAD + col * (NODE_W + 12),
381
+ y: laneTop + LANE_HEAD_H + row * (NODE_H + ROW_GAP),
382
+ });
383
+ laneRows.push({ id, col, row });
384
+ }
385
+ const rows = Math.ceil(layout.unconnectedIds.length / perRow);
386
+ canvasH = laneTop + LANE_HEAD_H + rows * (NODE_H + ROW_GAP) + PAD;
387
+ positions.laneTop = laneTop;
388
+ }
389
+
390
+ const columnHeaders = layout.columns.map((_, ci) => {
391
+ const label = ci === 0 ? 'NO DEPENDENCIES' : `DEPTH ${ci}`;
392
+ return `<div class="rcf-dag-colhead" style="left:${PAD + ci * (NODE_W + COL_GAP)}px;top:${PAD}px;width:${NODE_W}px">${label}</div>`;
393
+ }).join('');
394
+
395
+ const laneHeader = layout.unconnectedIds.length > 0
396
+ ? `<div class="rcf-dag-lane-head" data-rcf-dag-lane-head style="left:${PAD}px;top:${positions.laneTop}px;right:${PAD}px">UNCONNECTED (<span data-rcf-dag-lane-count>${layout.unconnectedIds.length}</span>): no declared dependency, nothing depends on them; ordered by buildOrder</div>`
397
+ : '';
398
+
399
+ const edgesSvg = renderEdges(layout, positions, canvasW, canvasH);
400
+ const nodesHtml = renderNodes(layout, positions);
401
+
402
+ const laneDataAttrs = layout.unconnectedIds.length > 0 ? ' data-has-unconnected="1"' : '';
403
+
404
+ return `<div class="rcf-dag-canvas" data-rcf-dag-canvas${laneDataAttrs} style="width:${canvasW}px;height:${canvasH}px">
405
+ ${edgesSvg}
406
+ ${columnHeaders}
407
+ ${laneHeader}
408
+ ${nodesHtml}
409
+ </div>`;
410
+ }
411
+
412
+ function layoutPositions(layout) {
413
+ const pos = new Map();
414
+ let maxRows = 0;
415
+ layout.columns.forEach((col, ci) => {
416
+ col.forEach((id, ri) => {
417
+ pos.set(id, {
418
+ x: PAD + ci * (NODE_W + COL_GAP),
419
+ y: PAD + HEADER_H + ri * (NODE_H + ROW_GAP),
420
+ });
421
+ });
422
+ if (col.length > maxRows) maxRows = col.length;
423
+ });
424
+ const connectedHeight = Math.max(0, maxRows * (NODE_H + ROW_GAP));
425
+ return { pos, maxRows, connectedHeight, laneTop: 0 };
426
+ }
427
+
428
+ function renderEdges(layout, positions, w, h) {
429
+ const defs = `<defs>
430
+ <marker id="rcf-dag-arrow" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
431
+ <path d="M0,0 L7,3.5 L0,7 Z" fill="var(--rcf-dag-edge, currentColor)"/>
432
+ </marker>
433
+ <marker id="rcf-dag-arrow-up" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
434
+ <path d="M0,0 L7,3.5 L0,7 Z" fill="var(--sv-link)"/>
435
+ </marker>
436
+ <marker id="rcf-dag-arrow-down" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
437
+ <path d="M0,0 L7,3.5 L0,7 Z" fill="var(--sv-warning)"/>
438
+ </marker>
439
+ </defs>`;
440
+ const paths = layout.edges.map((e) => {
441
+ const a = positions.pos.get(e.from);
442
+ const b = positions.pos.get(e.to);
443
+ if (!a || !b) return '';
444
+ const x1 = a.x + NODE_W;
445
+ const y1 = a.y + NODE_H / 2;
446
+ const x2 = b.x;
447
+ const y2 = b.y + NODE_H / 2;
448
+ const c1x = x1 + COL_GAP / 2;
449
+ const c2x = x2 - COL_GAP / 2;
450
+ const d = `M${x1},${y1} C${c1x},${y1} ${c2x},${y2} ${x2},${y2}`;
451
+ return `<path class="rcf-dag-edge" d="${d}" data-rcf-dag-edge data-from="${escapeHtml(e.from)}" data-to="${escapeHtml(e.to)}" marker-end="url(#rcf-dag-arrow)"></path>`;
452
+ }).join('');
453
+ return `<svg class="rcf-dag-edges" width="${w}" height="${h}" aria-hidden="true" focusable="false">
454
+ ${defs}
455
+ ${paths}
456
+ </svg>`;
457
+ }
458
+
459
+ function renderNodes(layout, positions) {
460
+ const html = [];
461
+ for (const [id, node] of layout.nodes.entries()) {
462
+ const p = positions.pos.get(id);
463
+ if (!p) continue;
464
+ const dotClass = statusDot(node.status);
465
+ const title = `${node.id} ${node.title} (${node.status})`;
466
+ const buildableChip = node.buildable
467
+ ? '<span class="rcf-dag-node-tag" title="No unmet dependencies">ready</span>'
468
+ : '';
469
+ html.push(
470
+ `<button type="button" class="rcf-dag-node"`
471
+ + ` data-fbs-id="${escapeHtml(id)}"`
472
+ + ` data-status="${escapeHtml(node.status)}"`
473
+ + ` data-domain="${escapeHtml(node.domain)}"`
474
+ + ` data-depth="${node.depth}"`
475
+ + ` data-buildable="${node.buildable ? '1' : '0'}"`
476
+ + ` data-critical="${node.critical ? '1' : '0'}"`
477
+ + ` data-unconnected="${node.unconnected ? '1' : '0'}"`
478
+ + ` style="left:${p.x}px;top:${p.y}px;width:${NODE_W}px;height:${NODE_H}px"`
479
+ + ` title="${escapeHtml(title)}">`
480
+ + `<span class="rcf-dag-node-dot ${dotClass}" aria-hidden="true"></span>`
481
+ + `<span class="rcf-dag-node-id mono">${escapeHtml(id.replace(/^FBS-/, ''))}</span>`
482
+ + `<span class="rcf-dag-node-title">${escapeHtml(node.title)}</span>`
483
+ + buildableChip
484
+ + `</button>`,
485
+ );
486
+ }
487
+ return html.join('\n ');
488
+ }
489
+
490
+ function statusDot(status) {
491
+ if (status === 'verified' || status === 'complete') return 'rcf-dag-node-dot--ok';
492
+ if (status === 'inProgress') return 'rcf-dag-node-dot--warn';
493
+ return 'rcf-dag-node-dot--none';
494
+ }
495
+
496
+ function renderDagInspector(payload) {
497
+ // PR 1 contract (standing criterion): component payload rides on a
498
+ // `data-*` attribute on this shell, never an inline `<script>` body.
499
+ // The client in page-init.js reads `data-rcf-dag-inspector-data`
500
+ // directly off this element. No outbound postMessage, no fetch.
501
+ const safeJson = JSON.stringify(payload || {}).replace(/</g, '\\u003c');
502
+ const attr = safeJson.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
503
+ return `<div class="rcf-dag-inspector-body" data-rcf-dag-inspector-body data-rcf-dag-inspector-data="${attr}">
504
+ <div class="muted small">Select a build spec to see what it needs and what waits on it.</div>
505
+ </div>`;
506
+ }
507
+
508
+ function renderDagLegend() {
509
+ return `<p class="rcf-dag-legend small muted">`
510
+ + `Columns are dependency depth (left has no declared dependency), computed from <code>dependsOnFbsIds</code>; row order inside a column is <code>buildOrder</code>. `
511
+ + `Click a node: <span style="color:var(--sv-link)">upstream</span> outlines in link colour, <span style="color:var(--sv-warning)">downstream</span> in warning. `
512
+ + `Specs with no dependency and no dependant sit in the unconnected lane so they do not swamp the graph.`
513
+ + `</p>`;
514
+ }
515
+
516
+ /**
517
+ * Build an inspector payload the client can render after a selection
518
+ * without having to recompute the layout. One payload per selectable
519
+ * node (including unconnected); keyed by fbsId.
520
+ *
521
+ * The payload lives in a <script type="application/json"> tag beside
522
+ * the canvas so the DAG stays SSR-driven and the client wiring has no
523
+ * other data source.
524
+ *
525
+ * @param {DagLayout} layout
526
+ * @returns {Record<string, object>}
527
+ */
528
+ export function buildDagInspectorPayload(layout) {
529
+ const out = {};
530
+ for (const [id, node] of layout.nodes.entries()) {
531
+ out[id] = {
532
+ id: node.id,
533
+ title: node.title,
534
+ status: node.status,
535
+ domain: node.domain,
536
+ size: node.size,
537
+ buildOrder: node.buildOrder,
538
+ acCount: node.acCount,
539
+ buildable: node.buildable,
540
+ critical: node.critical,
541
+ unconnected: node.unconnected,
542
+ needs: node.needs,
543
+ waits: node.waits,
544
+ needsTotal: node.upstream.length,
545
+ waitsTotal: node.downstream.length,
546
+ upstream: node.upstream,
547
+ downstream: node.downstream,
548
+ needsMeta: node.needs.map((depId) => inspectorMeta(layout, depId)),
549
+ waitsMeta: node.waits.map((depId) => inspectorMeta(layout, depId)),
550
+ };
551
+ }
552
+ return out;
553
+ }
554
+
555
+ function inspectorMeta(layout, id) {
556
+ const n = layout.nodes.get(id);
557
+ return {
558
+ id,
559
+ title: n?.title ?? '',
560
+ status: n?.status ?? '',
561
+ };
562
+ }
563
+
@@ -0,0 +1,41 @@
1
+ // Shared Badge component (viewer UI refresh PR 2, design doc section 4):
2
+ // a small, muted count or facet marker used in a DocRow's `meta` slot or
3
+ // anywhere else a numeric count / tag needs to ride beside a label. One
4
+ // markup shape, variant-driven styling:
5
+ //
6
+ // <span class="rcf-badge rcf-badge--{variant}" [title?]>
7
+ // [label] [value]
8
+ // </span>
9
+ //
10
+ // Variants (and intent):
11
+ // - count : plain muted count ("3 US", "7 AC"); the default
12
+ // - facet : neutral tag chip (status filter facets, area labels)
13
+ // - accent : primary / link-coloured accent (buildable-now, open tally)
14
+ //
15
+ // `label` is optional and renders muted-left-of value when both are
16
+ // present ("US 3" -> label="US", value=3). An explicit `title` surfaces
17
+ // as a tooltip; the Pill component (shared PR 0.29) still handles
18
+ // pill-shaped status rendering.
19
+
20
+ import { escapeHtml } from '../doc-renderers/helpers.js';
21
+
22
+ const VARIANTS = new Set(['count', 'facet', 'accent']);
23
+
24
+ /**
25
+ * @param {object} args
26
+ * @param {string|number} args.value
27
+ * @param {string} [args.label]
28
+ * @param {string} [args.variant] one of count | facet | accent (default: count)
29
+ * @param {string} [args.title]
30
+ * @returns {string}
31
+ */
32
+ export function renderBadge({ value, label, variant = 'count', title }) {
33
+ const v = VARIANTS.has(variant) ? variant : 'count';
34
+ const classes = ['rcf-badge', `rcf-badge--${v}`];
35
+ const titleAttr = typeof title === 'string' && title.length > 0
36
+ ? ` title="${escapeHtml(title)}"`
37
+ : '';
38
+ const labelHtml = label ? `<span class="rcf-badge-label">${escapeHtml(label)}</span>` : '';
39
+ const valueHtml = `<span class="rcf-badge-value">${escapeHtml(String(value ?? ''))}</span>`;
40
+ return `<span class="${classes.join(' ')}"${titleAttr}>${labelHtml}${valueHtml}</span>`;
41
+ }
@@ -0,0 +1,72 @@
1
+ // Shared diff component (REQ-181, TAC-4127-readiness-view). Renders a
2
+ // two-column before-and-after of two document bodies for the delta
3
+ // list on the Readiness tab. Document-level only in 0.29.0;
4
+ // criterion-level before-and-after is deferred per ADR-4127 (open
5
+ // question 7 of the DEFINE step 2 spec).
6
+ //
7
+ // Each side may be:
8
+ // - null / undefined renders an `(absent)` placeholder
9
+ // - a string renders as a <pre> body
10
+ // - an object `{ hash?, text?, note? }` renders an optional
11
+ // short-hash subtitle, then `text` as a <pre>
12
+ // body, OR `note` as a muted <p>, OR the
13
+ // `(absent)` placeholder when both are empty.
14
+ // The object form is what the Readiness tab's hash-only document-level
15
+ // diff passes in 0.29.0: the frozen body is not persisted in the freeze
16
+ // record, so the before column renders the frozen short-hash + a
17
+ // "frozen body not captured" note; the after column renders the
18
+ // current short-hash + the serialised current body.
19
+
20
+ import { escapeHtml } from '../doc-renderers/helpers.js';
21
+
22
+ /**
23
+ * @typedef {object} DiffSide
24
+ * @property {string} [hash] full-length hash (e.g. 'sha256:abc...'),
25
+ * rendered as the short form in a subtitle
26
+ * @property {string} [text] serialised body
27
+ * @property {string} [note] muted note shown when `text` is absent
28
+ */
29
+
30
+ /**
31
+ * @param {string | DiffSide | null | undefined} before
32
+ * @param {string | DiffSide | null | undefined} after
33
+ * @returns {string}
34
+ */
35
+ export function diff(before, after) {
36
+ return `<div class="rcf-diff">`
37
+ + `<div class="rcf-diff__col rcf-diff__col--before"><h5>Before</h5>${renderSide(before)}</div>`
38
+ + `<div class="rcf-diff__col rcf-diff__col--after"><h5>After</h5>${renderSide(after)}</div>`
39
+ + `</div>`;
40
+ }
41
+
42
+ function renderSide(side) {
43
+ if (typeof side === 'string') {
44
+ return side.length > 0
45
+ ? `<pre class="rcf-diff__pre">${escapeHtml(side)}</pre>`
46
+ : `<p class="rcf-diff__absent"><em>(absent)</em></p>`;
47
+ }
48
+ if (side && typeof side === 'object') {
49
+ const parts = [];
50
+ if (typeof side.hash === 'string' && side.hash.length > 0) {
51
+ parts.push(`<p class="rcf-diff__hash"><code>${escapeHtml(shortHash(side.hash))}</code></p>`);
52
+ }
53
+ if (typeof side.text === 'string' && side.text.length > 0) {
54
+ parts.push(`<pre class="rcf-diff__pre">${escapeHtml(side.text)}</pre>`);
55
+ } else if (typeof side.note === 'string' && side.note.length > 0) {
56
+ parts.push(`<p class="rcf-diff__note"><em>${escapeHtml(side.note)}</em></p>`);
57
+ } else if (typeof side.hash !== 'string' || side.hash.length === 0) {
58
+ parts.push(`<p class="rcf-diff__absent"><em>(absent)</em></p>`);
59
+ }
60
+ return parts.join('');
61
+ }
62
+ return `<p class="rcf-diff__absent"><em>(absent)</em></p>`;
63
+ }
64
+
65
+ function shortHash(hash) {
66
+ if (typeof hash !== 'string') return '';
67
+ const colonIdx = hash.indexOf(':');
68
+ const body = colonIdx >= 0 ? hash.slice(colonIdx + 1) : hash;
69
+ const short = body.slice(0, 7);
70
+ const prefix = colonIdx >= 0 ? `${hash.slice(0, colonIdx)}:` : '';
71
+ return `${prefix}${short}`;
72
+ }