rcf-lite 0.0.1 → 0.7.1

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 (342) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/LICENSE +202 -0
  3. package/README.md +92 -4
  4. package/bin/rcf-verify.js +122 -0
  5. package/bin/rcf.js +174 -0
  6. package/bin/view-supervisor-child.mjs +14 -0
  7. package/fixtures/canary-manifest.json +103 -0
  8. package/guidance/README.md +25 -0
  9. package/guidance/build-cycle-playbook.md +492 -0
  10. package/guidance/build-cycle.md +40 -0
  11. package/guidance/document-model.md +45 -0
  12. package/guidance/elicitation-playbook.md +442 -0
  13. package/guidance/harness-template.md +246 -0
  14. package/guidance/managed/README.md +63 -0
  15. package/guidance/managed/agent-instructions-block.hash +1 -0
  16. package/guidance/managed/agent-instructions-block.md +220 -0
  17. package/guidance/managed/legacy-fragment-hashes.json +22 -0
  18. package/guidance/manifest.json +21 -0
  19. package/guidance/overview.md +48 -0
  20. package/guidance/persona-programme.md +44 -0
  21. package/package.json +69 -8
  22. package/rcf/adrs/adr-001.json +25 -0
  23. package/rcf/adrs/adr-002.json +25 -0
  24. package/rcf/adrs/adr-003.json +25 -0
  25. package/rcf/adrs/adr-004.json +20 -0
  26. package/rcf/adrs/adr-005.json +20 -0
  27. package/rcf/adrs/adr-006.json +25 -0
  28. package/rcf/adrs/adr-007.json +25 -0
  29. package/rcf/adrs/adr-008.json +25 -0
  30. package/rcf/adrs/adr-009.json +25 -0
  31. package/rcf/build-sequence.json +11 -0
  32. package/rcf/code-nodes/cn-001.json +14 -0
  33. package/rcf/code-nodes/cn-002.json +14 -0
  34. package/rcf/code-nodes/cn-003.json +15 -0
  35. package/rcf/code-nodes/cn-004.json +17 -0
  36. package/rcf/code-nodes/cn-005.json +19 -0
  37. package/rcf/code-nodes/cn-006.json +17 -0
  38. package/rcf/code-nodes/cn-007.json +16 -0
  39. package/rcf/code-nodes/cn-008.json +19 -0
  40. package/rcf/code-nodes/cn-009.json +16 -0
  41. package/rcf/code-nodes/cn-010.json +18 -0
  42. package/rcf/code-nodes/cn-011.json +18 -0
  43. package/rcf/code-nodes/cn-012.json +18 -0
  44. package/rcf/code-nodes/cn-013.json +19 -0
  45. package/rcf/code-nodes/cn-014.json +16 -0
  46. package/rcf/code-nodes/cn-015.json +16 -0
  47. package/rcf/code-nodes/cn-016.json +16 -0
  48. package/rcf/code-nodes/cn-017.json +16 -0
  49. package/rcf/code-nodes/cn-018.json +15 -0
  50. package/rcf/code-nodes/cn-019.json +16 -0
  51. package/rcf/code-nodes/cn-020.json +16 -0
  52. package/rcf/code-nodes/cn-021.json +16 -0
  53. package/rcf/code-nodes/cn-022.json +16 -0
  54. package/rcf/code-nodes/cn-023.json +16 -0
  55. package/rcf/code-nodes/cn-024.json +16 -0
  56. package/rcf/code-nodes/cn-025.json +16 -0
  57. package/rcf/code-nodes/cn-026.json +16 -0
  58. package/rcf/code-nodes/cn-027.json +16 -0
  59. package/rcf/code-nodes/cn-028.json +16 -0
  60. package/rcf/code-nodes/cn-029.json +19 -0
  61. package/rcf/code-nodes/cn-030.json +14 -0
  62. package/rcf/code-nodes/cn-031.json +14 -0
  63. package/rcf/code-nodes/cn-032.json +14 -0
  64. package/rcf/code-nodes/cn-033.json +14 -0
  65. package/rcf/code-nodes/cn-034.json +14 -0
  66. package/rcf/code-nodes/cn-035.json +14 -0
  67. package/rcf/code-nodes/cn-036.json +14 -0
  68. package/rcf/code-nodes/cn-037.json +14 -0
  69. package/rcf/code-nodes/cn-038.json +14 -0
  70. package/rcf/code-nodes/cn-039.json +14 -0
  71. package/rcf/code-nodes/cn-040.json +14 -0
  72. package/rcf/code-nodes/cn-041.json +14 -0
  73. package/rcf/code-nodes/cn-042.json +14 -0
  74. package/rcf/code-nodes/cn-043.json +14 -0
  75. package/rcf/code-nodes/cn-044.json +14 -0
  76. package/rcf/code-nodes/cn-045.json +14 -0
  77. package/rcf/code-nodes/cn-046.json +14 -0
  78. package/rcf/code-nodes/cn-047.json +14 -0
  79. package/rcf/code-nodes/cn-048.json +14 -0
  80. package/rcf/code-nodes/cn-049.json +14 -0
  81. package/rcf/code-nodes/cn-050.json +14 -0
  82. package/rcf/code-nodes/cn-051.json +14 -0
  83. package/rcf/code-nodes/cn-052.json +14 -0
  84. package/rcf/code-nodes/cn-053.json +14 -0
  85. package/rcf/code-nodes/cn-054.json +14 -0
  86. package/rcf/code-nodes/cn-055.json +14 -0
  87. package/rcf/code-nodes/cn-056.json +14 -0
  88. package/rcf/code-nodes/cn-057.json +14 -0
  89. package/rcf/fbs/fbs-001.json +49 -0
  90. package/rcf/fbs/fbs-002.json +42 -0
  91. package/rcf/fbs/fbs-003.json +37 -0
  92. package/rcf/fbs/fbs-004.json +39 -0
  93. package/rcf/fbs/fbs-005.json +38 -0
  94. package/rcf/fbs/fbs-006.json +48 -0
  95. package/rcf/fbs/fbs-007.json +39 -0
  96. package/rcf/fbs/fbs-008.json +40 -0
  97. package/rcf/fbs/fbs-009.json +36 -0
  98. package/rcf/fbs/fbs-010.json +41 -0
  99. package/rcf/fbs/fbs-011.json +36 -0
  100. package/rcf/fbs/fbs-012.json +46 -0
  101. package/rcf/fbs/fbs-013.json +42 -0
  102. package/rcf/fbs/fbs-014.json +49 -0
  103. package/rcf/fbs/fbs-015.json +32 -0
  104. package/rcf/manifest.json +17 -0
  105. package/rcf/prd.json +47 -0
  106. package/rcf/requirements/req-001.json +19 -0
  107. package/rcf/requirements/req-002.json +19 -0
  108. package/rcf/requirements/req-003.json +19 -0
  109. package/rcf/requirements/req-004.json +19 -0
  110. package/rcf/requirements/req-005.json +19 -0
  111. package/rcf/requirements/req-006.json +19 -0
  112. package/rcf/requirements/req-007.json +19 -0
  113. package/rcf/requirements/req-008.json +19 -0
  114. package/rcf/requirements/req-009.json +19 -0
  115. package/rcf/tacs/tac-001.json +45 -0
  116. package/rcf/tacs/tac-002.json +109 -0
  117. package/rcf/tacs/tac-003.json +40 -0
  118. package/rcf/tacs/tac-004.json +51 -0
  119. package/rcf/tacs/tac-005.json +52 -0
  120. package/rcf/tacs/tac-006.json +104 -0
  121. package/rcf/tacs/tac-007.json +38 -0
  122. package/rcf/tacs/tac-008.json +51 -0
  123. package/rcf/tad.json +51 -0
  124. package/rcf/test-suites/PENDING.md +23 -0
  125. package/rcf/test-suites/ts-001.json +38 -0
  126. package/rcf/test-suites/ts-002.json +38 -0
  127. package/rcf/test-suites/ts-003.json +43 -0
  128. package/rcf/test-suites/ts-004.json +44 -0
  129. package/rcf/test-suites/ts-005.json +30 -0
  130. package/rcf/test-suites/ts-006.json +36 -0
  131. package/rcf/test-suites/ts-007.json +43 -0
  132. package/rcf/test-suites/ts-008.json +37 -0
  133. package/rcf/test-suites/ts-009.json +38 -0
  134. package/rcf/test-suites/ts-010.json +38 -0
  135. package/rcf/test-suites/ts-011.json +44 -0
  136. package/rcf/test-suites/ts-012.json +36 -0
  137. package/rcf/test-suites/ts-013.json +38 -0
  138. package/rcf/test-suites/ts-014.json +38 -0
  139. package/rcf/test-suites/ts-015.json +38 -0
  140. package/rcf/test-suites/ts-016.json +37 -0
  141. package/rcf/test-suites/ts-017.json +38 -0
  142. package/rcf/test-suites/ts-018.json +38 -0
  143. package/rcf/test-suites/ts-019.json +37 -0
  144. package/rcf/test-suites/ts-020.json +46 -0
  145. package/rcf/test-suites/ts-021.json +46 -0
  146. package/rcf/test-suites/ts-022.json +46 -0
  147. package/rcf/test-suites/ts-023.json +46 -0
  148. package/rcf/test-suites/ts-024.json +46 -0
  149. package/rcf/test-suites/ts-025.json +52 -0
  150. package/rcf/user-stories/us-101.json +40 -0
  151. package/rcf/user-stories/us-102.json +40 -0
  152. package/rcf/user-stories/us-201.json +40 -0
  153. package/rcf/user-stories/us-202.json +40 -0
  154. package/rcf/user-stories/us-203.json +32 -0
  155. package/rcf/user-stories/us-301.json +40 -0
  156. package/rcf/user-stories/us-302.json +40 -0
  157. package/rcf/user-stories/us-303.json +40 -0
  158. package/rcf/user-stories/us-304.json +40 -0
  159. package/rcf/user-stories/us-401.json +40 -0
  160. package/rcf/user-stories/us-402.json +40 -0
  161. package/rcf/user-stories/us-403.json +40 -0
  162. package/rcf/user-stories/us-501.json +40 -0
  163. package/rcf/user-stories/us-502.json +40 -0
  164. package/rcf/user-stories/us-503.json +40 -0
  165. package/rcf/user-stories/us-601.json +40 -0
  166. package/rcf/user-stories/us-602.json +40 -0
  167. package/rcf/user-stories/us-701.json +40 -0
  168. package/rcf/user-stories/us-702.json +40 -0
  169. package/rcf/user-stories/us-801.json +49 -0
  170. package/rcf/user-stories/us-802.json +49 -0
  171. package/rcf/user-stories/us-803.json +49 -0
  172. package/rcf/user-stories/us-804.json +49 -0
  173. package/rcf/user-stories/us-805.json +49 -0
  174. package/rcf/user-stories/us-901.json +40 -0
  175. package/src/.gitkeep +0 -0
  176. package/src/browser-verify/auth-smoke.js +109 -0
  177. package/src/browser-verify/index.js +29 -0
  178. package/src/browser-verify/invariants.js +336 -0
  179. package/src/browser-verify/manifest-writer.js +189 -0
  180. package/src/browser-verify/runner.js +171 -0
  181. package/src/build/bundle.js +198 -0
  182. package/src/build/formatters/json.js +18 -0
  183. package/src/build/formatters/markdown.js +406 -0
  184. package/src/build/index.js +14 -0
  185. package/src/build/mark.js +177 -0
  186. package/src/build/queue.js +285 -0
  187. package/src/cli/browser-verify.js +231 -0
  188. package/src/cli/build.js +584 -0
  189. package/src/cli/coverage.js +219 -0
  190. package/src/cli/create.js +355 -0
  191. package/src/cli/delete.js +127 -0
  192. package/src/cli/design.js +353 -0
  193. package/src/cli/doctor.js +548 -0
  194. package/src/cli/fbs.js +171 -0
  195. package/src/cli/finalise.js +377 -0
  196. package/src/cli/guidance.js +206 -0
  197. package/src/cli/help.js +156 -0
  198. package/src/cli/impact.js +119 -0
  199. package/src/cli/init.js +282 -0
  200. package/src/cli/intake.js +153 -0
  201. package/src/cli/link.js +128 -0
  202. package/src/cli/mcp.js +160 -0
  203. package/src/cli/preflight.js +220 -0
  204. package/src/cli/read.js +162 -0
  205. package/src/cli/req-baseline.js +269 -0
  206. package/src/cli/req-classify.js +135 -0
  207. package/src/cli/review.js +295 -0
  208. package/src/cli/test-suite.js +216 -0
  209. package/src/cli/trace.js +172 -0
  210. package/src/cli/ui-baseline.js +292 -0
  211. package/src/cli/ui-classify.js +108 -0
  212. package/src/cli/update.js +197 -0
  213. package/src/cli/validate.js +168 -0
  214. package/src/cli/view.js +452 -0
  215. package/src/core/baseline-catalog/data/auth.json +42 -0
  216. package/src/core/baseline-catalog/data/http-api.json +42 -0
  217. package/src/core/baseline-catalog/data/notifications.json +33 -0
  218. package/src/core/baseline-catalog/data/persistence.json +33 -0
  219. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  220. package/src/core/baseline-catalog/index.js +121 -0
  221. package/src/core/errors/index.js +167 -0
  222. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  223. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  224. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  225. package/src/core/isolation/index.js +60 -0
  226. package/src/core/mcp/framing.js +103 -0
  227. package/src/core/mcp/index.js +8 -0
  228. package/src/core/mcp/server.js +228 -0
  229. package/src/core/patterns/register-canary.js +209 -0
  230. package/src/core/patterns/req-shapes.js +158 -0
  231. package/src/core/patterns/services.js +358 -0
  232. package/src/core/patterns/ui-shapes.js +166 -0
  233. package/src/core/store/cn-resolve.js +134 -0
  234. package/src/core/store/derive-deps.js +93 -0
  235. package/src/core/store/ids.js +78 -0
  236. package/src/core/store/index.js +20 -0
  237. package/src/core/store/init.js +255 -0
  238. package/src/core/store/loader.js +211 -0
  239. package/src/core/store/tp-resolve.js +176 -0
  240. package/src/core/store/validator.js +191 -0
  241. package/src/core/store/walker.js +898 -0
  242. package/src/core/store/writer.js +1849 -0
  243. package/src/design/index.js +11 -0
  244. package/src/design/writer.js +271 -0
  245. package/src/finalise/detect.js +129 -0
  246. package/src/finalise/index.js +17 -0
  247. package/src/finalise/ingest.js +119 -0
  248. package/src/finalise/install.js +119 -0
  249. package/src/finalise/ship-without-verified.js +131 -0
  250. package/src/finalise/spawn.js +84 -0
  251. package/src/intake/fidelity.js +105 -0
  252. package/src/intake/index.js +6 -0
  253. package/src/intake/manifest-writer.js +100 -0
  254. package/src/intake/orchestrator.js +138 -0
  255. package/src/intake/validate.js +80 -0
  256. package/src/mcp/map-errors.js +131 -0
  257. package/src/mcp/prompts.js +49 -0
  258. package/src/mcp/resources.js +244 -0
  259. package/src/mcp/tools.js +1204 -0
  260. package/src/preflight/design-shapes.js +185 -0
  261. package/src/preflight/index.js +90 -0
  262. package/src/preflight/manifest-writer.js +264 -0
  263. package/src/preflight/scanner.js +206 -0
  264. package/src/preflight/secrets.js +134 -0
  265. package/src/preflight/session.js +246 -0
  266. package/src/query/attestation.js +285 -0
  267. package/src/query/coverage.js +308 -0
  268. package/src/query/formatters/json.js +21 -0
  269. package/src/query/formatters/mermaid.js +209 -0
  270. package/src/query/formatters/table.js +203 -0
  271. package/src/query/impact.js +173 -0
  272. package/src/query/index.js +9 -0
  273. package/src/query/trace.js +345 -0
  274. package/src/register-canary/fixture-loader.js +87 -0
  275. package/src/register-canary/index.js +10 -0
  276. package/src/register-canary/record-writer.js +132 -0
  277. package/src/register-canary/runner.js +156 -0
  278. package/src/req-baseline/gate.js +86 -0
  279. package/src/req-baseline/index.js +27 -0
  280. package/src/req-baseline/open-candidates.js +143 -0
  281. package/src/req-baseline/opt-out.js +195 -0
  282. package/src/req-baseline/sweep.js +230 -0
  283. package/src/req-detection/classifier.js +181 -0
  284. package/src/req-detection/index.js +9 -0
  285. package/src/req-detection/persist.js +55 -0
  286. package/src/review/index.js +325 -0
  287. package/src/review/mutation.js +117 -0
  288. package/src/review/ui-baseline-drift.js +138 -0
  289. package/src/server/index.js +178 -0
  290. package/src/server/routes.js +110 -0
  291. package/src/server/sse.js +118 -0
  292. package/src/setup/agent-setup.js +362 -0
  293. package/src/setup/identity-seed.js +104 -0
  294. package/src/setup/knowledge-seed.js +123 -0
  295. package/src/setup/managed-block.js +193 -0
  296. package/src/setup/managed-gitignore.js +166 -0
  297. package/src/setup/managed-markers.js +49 -0
  298. package/src/ui-baseline/defaults.js +119 -0
  299. package/src/ui-baseline/index.js +25 -0
  300. package/src/ui-baseline/manifest-writer.js +282 -0
  301. package/src/ui-baseline/session.js +178 -0
  302. package/src/ui-detection/classifier.js +192 -0
  303. package/src/verify/chain/index.js +190 -0
  304. package/src/verify/cli/cleanup.js +61 -0
  305. package/src/verify/cli/help.js +56 -0
  306. package/src/verify/cli/mcp.js +98 -0
  307. package/src/verify/cli/provision.js +71 -0
  308. package/src/verify/cli/report.js +71 -0
  309. package/src/verify/cli/run.js +155 -0
  310. package/src/verify/engine/brief.js +87 -0
  311. package/src/verify/engine/index.js +177 -0
  312. package/src/verify/engine/launcher.js +307 -0
  313. package/src/verify/mcp/tools.js +107 -0
  314. package/src/verify/profile/index.js +146 -0
  315. package/src/verify/provision/index.js +256 -0
  316. package/src/verify/report/index.js +139 -0
  317. package/src/verify/report/renderer.js +118 -0
  318. package/src/verify/verdict/index.js +246 -0
  319. package/src/view/doc-renderers/adr.js +44 -0
  320. package/src/view/doc-renderers/build-sequence.js +40 -0
  321. package/src/view/doc-renderers/fbs.js +128 -0
  322. package/src/view/doc-renderers/helpers.js +159 -0
  323. package/src/view/doc-renderers/index.js +12 -0
  324. package/src/view/doc-renderers/prd.js +45 -0
  325. package/src/view/doc-renderers/req.js +43 -0
  326. package/src/view/doc-renderers/tac.js +38 -0
  327. package/src/view/doc-renderers/tad.js +74 -0
  328. package/src/view/doc-renderers/test-suite.js +45 -0
  329. package/src/view/doc-renderers/user-story.js +63 -0
  330. package/src/view/html-page.js +462 -0
  331. package/src/view/index.js +63 -0
  332. package/src/view/live-client.js +338 -0
  333. package/src/view/mermaid-diagram.js +178 -0
  334. package/src/view/style.css +735 -0
  335. package/src/view/tree-model.js +152 -0
  336. package/src/view/vendored/mermaid.min.js +2607 -0
  337. package/src/view-supervisor/index.js +26 -0
  338. package/src/view-supervisor/logs.js +32 -0
  339. package/src/view-supervisor/manifest-writer.js +178 -0
  340. package/src/view-supervisor/persist-until.js +85 -0
  341. package/src/view-supervisor/supervisor.js +276 -0
  342. package/src/watch/index.js +152 -0
@@ -0,0 +1,246 @@
1
+ // Verdict taxonomy + aggregation (spec §5.1, §5.2). Mirrors the persona
2
+ // programme's PASS/BROKEN/DEGRADED/COSMETIC, plus the structural verdicts
3
+ // NOT-DEPLOYED (§4 refusal), BLOCKED (§6 unprovisionable), and LAUNCH-FAILURE
4
+ // (the verifier agent could not run or its output could not be ingested — a
5
+ // refusal to issue a verdict on the app, never a soft pass; see engine catch).
6
+ //
7
+ // Split verdicts are held split, NEVER averaged (§5.1): a run is BROKEN if
8
+ // ANY finding is BROKEN, regardless of how many ACs passed.
9
+ //
10
+ // 0.7.0 additions:
11
+ // - MOCK-ONLY-DECLARED (verification-integrity-cluster-spec §5.2): an AC
12
+ // whose aggregated service attestation resolves to `mocked` for at
13
+ // least one service. Verify has no live path to make it real-hit; the
14
+ // verdict is the honest alternative to a false PASS.
15
+ // - BLOCKED-BY-DECLARATION (verification-integrity-cluster-spec §5.2):
16
+ // an AC whose aggregated service attestation contains `declaredMockOnly`
17
+ // for at least one service. The operator declared mock-only at pre-flight;
18
+ // verify refuses to issue a live verdict and the chain records the
19
+ // decision.
20
+ // - UI-BASELINE-UNMET (ui-design-gate §8.7): a UI-bearing AC whose
21
+ // browser-verification record for a bound FBS came back `block`.
22
+ // - BROWSER-VERIFICATION-MISSING (ui-design-gate §8.7): a UI-bearing AC
23
+ // bound to an FBS with no browserVerification[] entry on the manifest.
24
+ //
25
+ // The four new classes are PER-AC verdicts emitted on the report's
26
+ // `perAcVerdicts[]` array. They do NOT replace the top-level `verdict`
27
+ // (which stays BROKEN/DEGRADED/… by finding severity + provisioning
28
+ // blocked); they run alongside so a finalise-gate consumer can refuse
29
+ // `verified` on an AC-level basis even when the run's aggregate is PASS.
30
+
31
+ import { rcfError } from '#core/errors';
32
+
33
+ /** Finding severities, low → high. */
34
+ export const FINDING_SEVERITIES = Object.freeze(['PASS', 'COSMETIC', 'DEGRADED', 'BROKEN']);
35
+
36
+ /** Severity rank for the split-not-averaged max and the severity gate. */
37
+ export const SEVERITY_ORDER = Object.freeze({ PASS: 0, COSMETIC: 1, DEGRADED: 2, BROKEN: 3 });
38
+
39
+ /**
40
+ * All overall-verdict classes: findings severities + the three structural
41
+ * verdicts. The 0.7.0 per-AC verdict classes are NOT in this set — they
42
+ * ride on `perAcVerdicts[]`, not the run-level `verdict` field. Keeping
43
+ * them out preserves backward compatibility on `validateReportShape`
44
+ * (§5.3) for existing consumers that only knew the pre-0.7.0 classes.
45
+ */
46
+ export const VERDICTS = Object.freeze([...FINDING_SEVERITIES, 'NOT-DEPLOYED', 'BLOCKED', 'LAUNCH-FAILURE']);
47
+
48
+ /**
49
+ * Per-AC verdict classes emitted alongside the top-level verdict on
50
+ * `report.perAcVerdicts[]`. Consumed by `rcf finalise` to refuse promotion
51
+ * to `verified` on any of these AC-level verdicts (see
52
+ * `packages/rcf-lite/src/finalise/ingest.js:findMockOnlyDeclaredAcs`).
53
+ */
54
+ export const PER_AC_VERDICTS = Object.freeze([
55
+ 'MOCK-ONLY-DECLARED',
56
+ 'BLOCKED-BY-DECLARATION',
57
+ 'UI-BASELINE-UNMET',
58
+ 'BROWSER-VERIFICATION-MISSING',
59
+ ]);
60
+
61
+ /**
62
+ * Required fields on every finding (spec §5.2): the RCF payoff is that every
63
+ * defect maps to a contract line (acId / chain node), never a free-floating
64
+ * bug.
65
+ *
66
+ * @param {object} finding
67
+ * @returns {import('#core/errors').RcfError | null} error as data, or null if valid
68
+ */
69
+ export function validateFinding(finding) {
70
+ if (!finding || typeof finding !== 'object') {
71
+ return rcfError({ kind: 'validation', message: 'finding must be an object' });
72
+ }
73
+ if (!FINDING_SEVERITIES.includes(finding.severity)) {
74
+ return rcfError({ kind: 'validation', message: `finding.severity must be one of ${FINDING_SEVERITIES.join('/')}`, field: 'severity' });
75
+ }
76
+ if (typeof finding.acId !== 'string' || finding.acId.length === 0) {
77
+ return rcfError({ kind: 'validation', message: 'finding.acId (chain-node reference) is required', field: 'acId' });
78
+ }
79
+ if (typeof finding.journey !== 'string' || finding.journey.length === 0) {
80
+ return rcfError({ kind: 'validation', message: 'finding.journey is required', field: 'journey' });
81
+ }
82
+ if (!Array.isArray(finding.reproSteps)) {
83
+ return rcfError({ kind: 'validation', message: 'finding.reproSteps must be an array', field: 'reproSteps' });
84
+ }
85
+ if (!finding.evidence || typeof finding.evidence !== 'object') {
86
+ return rcfError({ kind: 'validation', message: 'finding.evidence must be an object', field: 'evidence' });
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * The worst (max) severity across findings. Empty → PASS. This is the
93
+ * split-not-averaged rule: the single worst finding drives the class.
94
+ *
95
+ * @param {Array<{severity: string}>} findings
96
+ * @returns {'PASS'|'COSMETIC'|'DEGRADED'|'BROKEN'}
97
+ */
98
+ export function aggregateSeverity(findings = []) {
99
+ let worst = 'PASS';
100
+ for (const f of findings) {
101
+ if ((SEVERITY_ORDER[f.severity] ?? -1) > SEVERITY_ORDER[worst]) worst = f.severity;
102
+ }
103
+ return worst;
104
+ }
105
+
106
+ /**
107
+ * The overall run verdict (spec §5.1). NOT-DEPLOYED and a fully-blocked run
108
+ * are structural verdicts; otherwise the worst finding severity wins
109
+ * (split-not-averaged). A run with SOME findings and SOME blocked ACs is a
110
+ * partial verification: the verdict reflects what WAS exercised, and the
111
+ * blocked ACs are named separately in the report.
112
+ *
113
+ * @param {object} opts
114
+ * @param {Array<{severity: string}>} [opts.findings]
115
+ * @param {Array<object>} [opts.blockedAcs]
116
+ * @param {boolean} [opts.notDeployed]
117
+ * @returns {string}
118
+ */
119
+ export function aggregateVerdict({ findings = [], blockedAcs = [], notDeployed = false } = {}) {
120
+ if (notDeployed) return 'NOT-DEPLOYED';
121
+ if (findings.length === 0 && blockedAcs.length > 0) return 'BLOCKED';
122
+ return aggregateSeverity(findings);
123
+ }
124
+
125
+ /**
126
+ * Whether the severity gate is tripped → the process exits non-zero
127
+ * (spec §3 rule 5, §8.2). NOT-DEPLOYED, BLOCKED and LAUNCH-FAILURE always trip
128
+ * (ship cannot be confirmed); otherwise the worst finding severity is compared
129
+ * against the gate. With no gate configured, nothing trips — the report is
130
+ * still written.
131
+ *
132
+ * @param {object} opts
133
+ * @param {string} opts.verdict
134
+ * @param {Array<{severity: string}>} [opts.findings]
135
+ * @param {string|null} [opts.gate] - one of FINDING_SEVERITIES, or null/undefined
136
+ * @returns {boolean}
137
+ */
138
+ export function gateTripped({ verdict, findings = [], gate }) {
139
+ if (verdict === 'NOT-DEPLOYED' || verdict === 'BLOCKED' || verdict === 'LAUNCH-FAILURE') return true;
140
+ if (!gate) return false;
141
+ const worst = aggregateSeverity(findings);
142
+ return SEVERITY_ORDER[worst] >= SEVERITY_ORDER[gate];
143
+ }
144
+
145
+ /**
146
+ * Resolve the Track A per-AC verdict from an AC's aggregated service
147
+ * attestations. Priority: `declaredMockOnly` wins over `mocked`, because
148
+ * a chain that explicitly declared mock-only overrides an implicit mock.
149
+ * `live` / `sandboxed` / `notShipped` never emit a per-AC verdict here —
150
+ * they either mean a live path exists (verify's normal findings pipeline
151
+ * handles them) or the AC does not gate ship at all.
152
+ *
153
+ * @param {Array<{serviceId: string, attestationMode: string}>} attestations
154
+ * @returns {{ verdict: 'MOCK-ONLY-DECLARED'|'BLOCKED-BY-DECLARATION', reason: string } | null}
155
+ */
156
+ export function attestationPerAcVerdict(attestations = []) {
157
+ if (!Array.isArray(attestations) || attestations.length === 0) return null;
158
+ const declared = attestations.find((a) => a && a.attestationMode === 'declaredMockOnly');
159
+ if (declared) {
160
+ return {
161
+ verdict: 'BLOCKED-BY-DECLARATION',
162
+ reason: `operator declared mock-only at pre-flight for service ${declared.serviceId}; verify refused to issue a live verdict rather than fabricate a PASS.`,
163
+ };
164
+ }
165
+ const mocked = attestations.find((a) => a && a.attestationMode === 'mocked');
166
+ if (mocked) {
167
+ return {
168
+ verdict: 'MOCK-ONLY-DECLARED',
169
+ reason: `service ${mocked.serviceId} is chain-attested \`mocked\`; verify has no live path to the third party and no observable delivery record on the running app.`,
170
+ };
171
+ }
172
+ return null;
173
+ }
174
+
175
+ /**
176
+ * Resolve the Track B per-AC UI verdict for an AC. UI verdicts fire only
177
+ * for `fbsUiBearing: true` ACs (the chain-derived flag from
178
+ * `packages/rcf-lite/src/verify/chain/index.js`). Priority:
179
+ * 1. Any bound FBS has NO browserVerification entry → BROWSER-VERIFICATION-MISSING.
180
+ * 2. Any bound FBS's browserVerification.verdict is `block` → UI-BASELINE-UNMET.
181
+ * 3. Otherwise no per-AC UI verdict is emitted.
182
+ *
183
+ * @param {object} ac - a flattened AC with `fbsUiBearing` and `fbsIds`
184
+ * @param {object[]} [browserVerification] - manifest.browserVerification[]
185
+ * @returns {{ verdict: 'UI-BASELINE-UNMET'|'BROWSER-VERIFICATION-MISSING', reason: string } | null}
186
+ */
187
+ export function uiPerAcVerdict(ac, browserVerification = []) {
188
+ if (!ac || ac.fbsUiBearing !== true) return null;
189
+ const fbsIds = Array.isArray(ac.fbsIds) ? ac.fbsIds : [];
190
+ if (fbsIds.length === 0) return null;
191
+ const bv = Array.isArray(browserVerification) ? browserVerification : [];
192
+ const missing = [];
193
+ const blocked = [];
194
+ for (const fbsId of fbsIds) {
195
+ const records = bv.filter((r) => r && r.fbsId === fbsId);
196
+ if (records.length === 0) {
197
+ missing.push(fbsId);
198
+ continue;
199
+ }
200
+ // Take the freshest record — records land per-run and the latest verdict
201
+ // is the one that gates ship. `createdAt` sorts lexicographically for ISO
202
+ // timestamps; when it is missing fall back to array order (the last
203
+ // written entry wins).
204
+ const latest = [...records].sort((a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? '')).pop();
205
+ if (latest && latest.verdict === 'block') blocked.push({ fbsId, id: latest.id ?? null });
206
+ }
207
+ if (missing.length > 0) {
208
+ return {
209
+ verdict: 'BROWSER-VERIFICATION-MISSING',
210
+ reason: `UI-bearing FBS(es) ${missing.join(', ')} have no browserVerification record on the manifest; verify has nothing to read for the baseline check.`,
211
+ };
212
+ }
213
+ if (blocked.length > 0) {
214
+ const detail = blocked.map((b) => `${b.fbsId}${b.id ? ` (${b.id})` : ''}`).join(', ');
215
+ return {
216
+ verdict: 'UI-BASELINE-UNMET',
217
+ reason: `browserVerification recorded verdict \`block\` for ${detail}; the deployed UI failed at least one baseline invariant.`,
218
+ };
219
+ }
220
+ return null;
221
+ }
222
+
223
+ /**
224
+ * Emit per-AC verdicts across the whole chain. Combines Track A's service
225
+ * attestation verdicts with Track B's UI-baseline verdicts. An AC can carry
226
+ * BOTH a service-attestation verdict AND a UI verdict (a UI-bearing FBS
227
+ * that also depends on a mocked service produces two per-AC entries — one
228
+ * per class). This matches the finalise gate contract in
229
+ * `packages/rcf-lite/src/finalise/ingest.js:findMockOnlyDeclaredAcs`, which
230
+ * filters on verdict class and does not deduplicate by acId.
231
+ *
232
+ * @param {object} opts
233
+ * @param {Array<object>} opts.acs - flattened ACs from `readChain`
234
+ * @param {object[]} [opts.browserVerification] - manifest.browserVerification[]
235
+ * @returns {Array<{ acId: string, verdict: string, reason: string }>}
236
+ */
237
+ export function derivePerAcVerdicts({ acs = [], browserVerification = [] } = {}) {
238
+ const out = [];
239
+ for (const ac of acs) {
240
+ const attest = attestationPerAcVerdict(ac.serviceAttestations);
241
+ if (attest) out.push({ acId: ac.acId, verdict: attest.verdict, reason: attest.reason });
242
+ const ui = uiPerAcVerdict(ac, browserVerification);
243
+ if (ui) out.push({ acId: ac.acId, verdict: ui.verdict, reason: ui.reason });
244
+ }
245
+ return out;
246
+ }
@@ -0,0 +1,44 @@
1
+ // ADR renderer.
2
+
3
+ import {
4
+ anchorIdFor,
5
+ brokenBanner,
6
+ docLink,
7
+ docLinkList,
8
+ escapeHtml,
9
+ fieldPara,
10
+ rawJsonDisclosure,
11
+ } from './helpers.js';
12
+
13
+ /**
14
+ * @param {object} adr
15
+ * @param {object} ctx
16
+ * @param {string|undefined} ctx.raw
17
+ * @param {import('#core/errors').RcfError[]} [ctx.errors]
18
+ * @returns {string}
19
+ */
20
+ export function renderAdr(adr, ctx) {
21
+ if (!adr) return '';
22
+ const anchor = anchorIdFor(adr.adrId ?? 'ADR');
23
+ const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
24
+ const alts = (adr.alternativesConsidered ?? []).map((a) => `<li><strong>${escapeHtml(a.name ?? '')}</strong> - ${escapeHtml(a.summary ?? '')}<br/><em>Not chosen because:</em> ${escapeHtml(a.reasonNotChosen ?? '')}</li>`).join('');
25
+ const supersededBy = adr.supersededBy
26
+ ? `<p><strong>Superseded by:</strong> ${docLink(adr.supersededBy)}</p>`
27
+ : '';
28
+ const related = Array.isArray(adr.relatedAdrs) && adr.relatedAdrs.length > 0
29
+ ? `<p><strong>Related ADRs:</strong> ${docLinkList(adr.relatedAdrs)}</p>`
30
+ : '';
31
+ return `
32
+ <article id="${anchor}" class="doc doc-adr">
33
+ <h3>${escapeHtml(adr.adrId ?? 'ADR')} - ${escapeHtml(adr.title ?? '')}</h3>
34
+ ${broken}
35
+ ${fieldPara('Status', adr.status)}
36
+ ${fieldPara('Context', adr.context)}
37
+ ${fieldPara('Decision', adr.decision)}
38
+ ${fieldPara('Consequences', adr.consequences)}
39
+ ${alts ? `<section class="field-list"><h4>Alternatives considered</h4><ul>${alts}</ul></section>` : ''}
40
+ ${supersededBy}
41
+ ${related}
42
+ ${rawJsonDisclosure(ctx.raw, adr, adr.adrId)}
43
+ </article>`.trim();
44
+ }
@@ -0,0 +1,40 @@
1
+ // Build Sequence renderer. Post-3.7 (D15) the ordered FBS slot list is not
2
+ // read from the removed `bs.fbs[]` array but computed by the caller and
3
+ // passed via `ctx.slots` -- a list of `{ fbsId, buildOrder, executionStatus,
4
+ // title? }` sorted by buildOrder ascending.
5
+
6
+ import {
7
+ anchorIdFor,
8
+ brokenBanner,
9
+ docLink,
10
+ escapeHtml,
11
+ fieldPara,
12
+ rawJsonDisclosure,
13
+ } from './helpers.js';
14
+
15
+ /**
16
+ * @param {object} bs
17
+ * @param {object} ctx
18
+ * @param {string|undefined} ctx.raw
19
+ * @param {import('#core/errors').RcfError[]} [ctx.errors]
20
+ * @param {Array<{ fbsId: string, buildOrder: number, executionStatus?: string, title?: string }>} [ctx.slots]
21
+ * @returns {string}
22
+ */
23
+ export function renderBuildSequence(bs, ctx) {
24
+ if (!bs) return '';
25
+ const anchor = anchorIdFor(bs.bsId ?? 'BS');
26
+ const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
27
+ const slots = Array.isArray(ctx.slots)
28
+ ? [...ctx.slots].sort((a, b) => (a.buildOrder ?? 0) - (b.buildOrder ?? 0))
29
+ : [];
30
+ const slotList = slots.map((s) => `<li><strong>${s.buildOrder ?? '?'}.</strong> ${docLink(s.fbsId)} - <span class="status ${escapeHtml(s.executionStatus ?? '')}">${escapeHtml(s.executionStatus ?? 'unknown')}</span>${s.title ? ` - ${escapeHtml(s.title)}` : ''}</li>`).join('');
31
+ return `
32
+ <article id="${anchor}" class="doc doc-bs">
33
+ <h3>${escapeHtml(bs.bsId ?? 'BS')} - ${escapeHtml(bs.title ?? 'Build sequence')}</h3>
34
+ ${broken}
35
+ ${fieldPara('Build philosophy', bs.buildPhilosophy)}
36
+ ${fieldPara('Generation strategy', bs.generationStrategy)}
37
+ <section class="field-list"><h4>FBS slots</h4><ol>${slotList}</ol></section>
38
+ ${rawJsonDisclosure(ctx.raw, bs, bs.bsId)}
39
+ </article>`.trim();
40
+ }
@@ -0,0 +1,128 @@
1
+ // FBS renderer. Resolves AC ids into their Given/When/Then text rather
2
+ // than rendering just the id, so an owner reviewing an FBS section can read
3
+ // what each AC requires without jumping back to the User stories area.
4
+ // Phase 3.2: `acIds` are also rendered as clickable pills at the top so the
5
+ // operator can jump directly across into Requirements-tab context (D8).
6
+
7
+ import {
8
+ anchorIdFor,
9
+ brokenBanner,
10
+ docLink,
11
+ docLinkList,
12
+ escapeHtml,
13
+ fieldList,
14
+ fieldPara,
15
+ rawJsonDisclosure,
16
+ } from './helpers.js';
17
+
18
+ /**
19
+ * @param {object} fbs
20
+ * @param {object} ctx
21
+ * @param {string|undefined} ctx.raw
22
+ * @param {import('#core/errors').RcfError[]} [ctx.errors]
23
+ * @param {Map<string, object>} [ctx.usByAcId]
24
+ * @returns {string}
25
+ */
26
+ export function renderFbs(fbs, ctx) {
27
+ if (!fbs) return '';
28
+ const anchor = anchorIdFor(fbs.fbsId ?? 'FBS');
29
+ const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
30
+ const acPills = renderAcPills(fbs.acIds ?? []);
31
+ const acBlocks = (fbs.acIds ?? []).map((acId) => renderResolvedAc(acId, ctx)).join('\n');
32
+ const ctxReq = fbs.contextRequirements ?? {};
33
+ const ctxBlocks = renderContextRequirements(ctxReq);
34
+ const designBlock = renderDesignBlock(fbs);
35
+ const designSlot = designBlock ? `\n ${designBlock}` : '';
36
+ return `
37
+ <article id="${anchor}" class="doc doc-fbs">
38
+ <h3>${escapeHtml(fbs.fbsId ?? 'FBS')} - ${escapeHtml(fbs.title ?? '')}</h3>
39
+ ${broken}
40
+ ${fieldPara('Summary', fbs.summary)}
41
+ ${fieldPara('Approach', fbs.approach)}
42
+ ${acPills}
43
+ <section class="field-list"><h4>Acceptance criteria delivered</h4>${acBlocks}</section>
44
+ ${ctxBlocks}${designSlot}
45
+ ${fbs.dependsOnFbsIds?.length ? `<section class="field-list"><h4>Depends on</h4><p>${docLinkList(fbs.dependsOnFbsIds)}</p></section>` : ''}
46
+ ${fieldPara('Estimated size', fbs.estimatedSize)}
47
+ ${fieldPara('Estimated hours', fbs.estimatedHours)}
48
+ ${fieldList('Deliverables', fbs.deliverables)}
49
+ ${fieldPara('Risk level', fbs.riskLevel)}
50
+ ${fieldPara('Build order', fbs.buildOrder)}
51
+ ${fieldPara('Execution status', fbs.executionStatus)}
52
+ ${fieldPara('Notes', fbs.notes)}
53
+ ${rawJsonDisclosure(ctx.raw, fbs, fbs.fbsId)}
54
+ </article>`.trim();
55
+ }
56
+
57
+ /**
58
+ * Design substage block (Track B, ui-design-gate-0.7.0-spec §5.5
59
+ * "Honest cost" list: FBS view renderer surfaces designStageComplete +
60
+ * the designStage artefacts). Renders nothing for non-UI FBS.
61
+ *
62
+ * @param {object} fbs
63
+ * @returns {string}
64
+ */
65
+ function renderDesignBlock(fbs) {
66
+ if (fbs?.uiBearing !== true && !fbs?.designStage && fbs?.designStageComplete !== true) return '';
67
+ const stage = fbs.designStage ?? {};
68
+ const journeyCount = Array.isArray(stage.journeys) ? stage.journeys.length : 0;
69
+ const navShape = stage.navModel?.shape ?? '(none)';
70
+ const routeCount = Array.isArray(stage.navModel?.routes) ? stage.navModel.routes.length : 0;
71
+ const themeMode = stage.themeAndA11y?.themeMode ?? '(none)';
72
+ const complete = fbs?.designStageComplete === true;
73
+ return `<section class="field-list"><h4>Design substage</h4>
74
+ <p><strong>UI-bearing:</strong> ${escapeHtml(String(fbs?.uiBearing ?? false))}</p>
75
+ <p><strong>Design stage complete:</strong> ${escapeHtml(String(complete))}</p>
76
+ <p><strong>Journeys:</strong> ${escapeHtml(String(journeyCount))}</p>
77
+ <p><strong>Nav model:</strong> ${escapeHtml(String(navShape))} (${escapeHtml(String(routeCount))} route(s))</p>
78
+ <p><strong>Theme and a11y:</strong> ${escapeHtml(String(themeMode))}</p>
79
+ </section>`;
80
+ }
81
+
82
+ function renderAcPills(acIds) {
83
+ if (!Array.isArray(acIds) || acIds.length === 0) return '';
84
+ const pills = acIds.map((id) => `<a class="ac-pill" href="#${escapeHtml(id)}">${escapeHtml(id)}</a>`).join('');
85
+ return `<div class="ac-pills">${pills}</div>`;
86
+ }
87
+
88
+ function renderResolvedAc(acId, ctx) {
89
+ const us = ctx.usByAcId?.get(acId);
90
+ const ac = us?.acceptanceCriteria?.find((a) => a.id === acId);
91
+ if (!ac) {
92
+ return `<div class="ac-resolved broken"><p><strong>${escapeHtml(acId)}</strong> (unresolved)</p></div>`;
93
+ }
94
+ return `
95
+ <div class="ac-resolved">
96
+ <p><strong>${docLink(acId)}</strong> - ${escapeHtml(ac.description ?? '')}</p>
97
+ ${ac.given ? `<p><em>Given</em> ${escapeHtml(ac.given)}</p>` : ''}
98
+ ${ac.when ? `<p><em>When</em> ${escapeHtml(ac.when)}</p>` : ''}
99
+ ${ac.then ? `<p><em>Then</em> ${escapeHtml(ac.then)}</p>` : ''}
100
+ </div>`.trim();
101
+ }
102
+
103
+ function renderContextRequirements(ctx) {
104
+ const parts = [];
105
+ if (Array.isArray(ctx.tadSections) && ctx.tadSections.length > 0) {
106
+ parts.push(`<p><strong>TAD sections:</strong> ${ctx.tadSections.map((s) => escapeHtml(s)).join(', ')}</p>`);
107
+ }
108
+ if (Array.isArray(ctx.tacIds) && ctx.tacIds.length > 0) {
109
+ parts.push(`<p><strong>TACs:</strong> ${docLinkList(ctx.tacIds)}</p>`);
110
+ }
111
+ if (Array.isArray(ctx.adrIds) && ctx.adrIds.length > 0) {
112
+ parts.push(`<p><strong>ADRs:</strong> ${docLinkList(ctx.adrIds)}</p>`);
113
+ }
114
+ if (Array.isArray(ctx.schemas) && ctx.schemas.length > 0) {
115
+ parts.push(`<p><strong>Schemas:</strong> ${ctx.schemas.map((s) => `<code>${escapeHtml(s)}</code>`).join(', ')}</p>`);
116
+ }
117
+ if (Array.isArray(ctx.externalDocs) && ctx.externalDocs.length > 0) {
118
+ parts.push(`<p><strong>External docs:</strong> ${ctx.externalDocs.map((s) => `<code>${escapeHtml(s)}</code>`).join(', ')}</p>`);
119
+ }
120
+ if (Array.isArray(ctx.existingModules) && ctx.existingModules.length > 0) {
121
+ parts.push(`<p><strong>Existing modules:</strong> ${ctx.existingModules.map((s) => `<code>${escapeHtml(s)}</code>`).join(', ')}</p>`);
122
+ }
123
+ if (Array.isArray(ctx.other) && ctx.other.length > 0) {
124
+ parts.push(`<p><strong>Other:</strong> ${ctx.other.map((s) => escapeHtml(s)).join(', ')}</p>`);
125
+ }
126
+ if (parts.length === 0) return '';
127
+ return `<section class="field-list"><h4>Context requirements</h4>${parts.join('\n')}</section>`;
128
+ }
@@ -0,0 +1,159 @@
1
+ // Small helpers shared across the per-document renderers. Vanilla template
2
+ // strings; no template engine.
3
+
4
+ /**
5
+ * HTML-escape a string for safe injection between tags.
6
+ *
7
+ * @param {unknown} value
8
+ * @returns {string}
9
+ */
10
+ export function escapeHtml(value) {
11
+ if (value === null || value === undefined) return '';
12
+ const s = String(value);
13
+ return s
14
+ .replace(/&/g, '&amp;')
15
+ .replace(/</g, '&lt;')
16
+ .replace(/>/g, '&gt;')
17
+ .replace(/"/g, '&quot;')
18
+ .replace(/'/g, '&#39;');
19
+ }
20
+
21
+ /**
22
+ * Return the raw document id as an anchor id. Phase 3.2 unified the anchor
23
+ * convention around the display id (e.g. "REQ-002") so Mermaid click targets
24
+ * and internal doc links resolve to the same DOM node.
25
+ *
26
+ * @param {string} id
27
+ * @returns {string}
28
+ */
29
+ export function anchorIdFor(id) {
30
+ return String(id);
31
+ }
32
+
33
+ /**
34
+ * Render an `<a>` link to the section of another document.
35
+ *
36
+ * @param {string} id
37
+ * @param {string} [label]
38
+ * @returns {string}
39
+ */
40
+ export function docLink(id, label) {
41
+ return `<a href="#${anchorIdFor(id)}">${escapeHtml(label ?? id)}</a>`;
42
+ }
43
+
44
+ /**
45
+ * Render a list of ids as comma-separated doc links.
46
+ *
47
+ * @param {string[] | undefined} ids
48
+ * @returns {string}
49
+ */
50
+ export function docLinkList(ids) {
51
+ if (!Array.isArray(ids) || ids.length === 0) return '<em>none</em>';
52
+ return ids.map((id) => docLink(id)).join(', ');
53
+ }
54
+
55
+ /**
56
+ * Render a paragraph if value is present.
57
+ *
58
+ * @param {string} label
59
+ * @param {unknown} value
60
+ * @returns {string}
61
+ */
62
+ export function fieldPara(label, value) {
63
+ if (value === undefined || value === null || value === '') return '';
64
+ return `<p><strong>${escapeHtml(label)}:</strong> ${escapeHtml(value)}</p>`;
65
+ }
66
+
67
+ /**
68
+ * Render an unordered list if items are present.
69
+ *
70
+ * @param {string} label
71
+ * @param {string[] | undefined} items
72
+ * @returns {string}
73
+ */
74
+ export function fieldList(label, items) {
75
+ if (!Array.isArray(items) || items.length === 0) return '';
76
+ const li = items.map((s) => `<li>${escapeHtml(s)}</li>`).join('');
77
+ return `<section class="field-list"><h4>${escapeHtml(label)}</h4><ul>${li}</ul></section>`;
78
+ }
79
+
80
+ /**
81
+ * Render the "Show raw JSON" disclosure block for a document.
82
+ *
83
+ * Phase 3.8 D13b: the raw-JSON disclosure now carries a stable
84
+ * `data-doc-id="{parentDocId}::raw"` attribute so the live-client can
85
+ * persist its open state across SSE swaps and page reloads. The main
86
+ * doc-details already get a `data-doc-id` via `detailsWrap`; this closes
87
+ * the last state-persistence gap.
88
+ *
89
+ * @param {string|undefined} raw
90
+ * @param {object} doc
91
+ * @param {string} parentDocId - the enclosing doc's display id (e.g. "REQ-002")
92
+ * @returns {string}
93
+ */
94
+ export function rawJsonDisclosure(raw, doc, parentDocId) {
95
+ const json = raw ?? JSON.stringify(doc, null, 2);
96
+ const rawId = `${parentDocId ?? 'doc'}::raw`;
97
+ return `<details class="raw-json" data-doc-id="${escapeHtml(rawId)}"><summary>Show raw JSON</summary><pre>${escapeHtml(json)}</pre></details>`;
98
+ }
99
+
100
+ /**
101
+ * Render a broken-document banner above the rest of a document section.
102
+ *
103
+ * @param {import('#core/errors').RcfError[]} errors
104
+ * @returns {string}
105
+ */
106
+ export function brokenBanner(errors) {
107
+ if (!errors || errors.length === 0) return '';
108
+ const items = errors
109
+ .map((e) => `<li><code>${escapeHtml(e.kind)}</code> ${escapeHtml(e.message)}</li>`)
110
+ .join('');
111
+ return `
112
+ <aside class="broken" role="alert">
113
+ <p><strong>Broken document</strong> - schema validation or reference failure.</p>
114
+ <ul>${items}</ul>
115
+ </aside>`.trim();
116
+ }
117
+
118
+ /**
119
+ * Render a broken-reference placeholder when a referenced id has no file.
120
+ *
121
+ * @param {string} id
122
+ * @returns {string}
123
+ */
124
+ export function brokenReferenceSection(id) {
125
+ const anchor = anchorIdFor(id);
126
+ return `
127
+ <article id="${anchor}" class="doc broken-doc">
128
+ <h3>${escapeHtml(id)} - broken reference</h3>
129
+ <aside class="broken" role="alert">
130
+ <p>Referenced by a parent document but no file was found at the expected path.</p>
131
+ </aside>
132
+ </article>`.trim();
133
+ }
134
+
135
+ /**
136
+ * Wrap a doc's rendered body in a `<details data-doc-id>` so it can be
137
+ * drilled-in from the tab tree. Closed by default. The `data-doc-id`
138
+ * attribute is what the hash-routing script targets.
139
+ *
140
+ * @param {object} args
141
+ * @param {string} args.id - display doc id (e.g. "REQ-001")
142
+ * @param {string} args.summary - short label shown in the summary line
143
+ * @param {string} args.className - class applied to the details wrapper
144
+ * @param {string} args.body - the rendered body HTML
145
+ * @param {string} [args.status] - optional doc status, rendered as a pill
146
+ * @returns {string}
147
+ */
148
+ export function detailsWrap({
149
+ id, summary, className, body, status,
150
+ }) {
151
+ const statusPill = status
152
+ ? `<span class="status ${escapeHtml(status)}">${escapeHtml(status)}</span>`
153
+ : '';
154
+ return `
155
+ <details class="doc-details ${className}" data-doc-id="${escapeHtml(id)}">
156
+ <summary><span class="summary-label">${escapeHtml(summary)}</span>${statusPill}</summary>
157
+ ${body}
158
+ </details>`.trim();
159
+ }
@@ -0,0 +1,12 @@
1
+ // Doc renderer index. Re-exports each per-type renderer so html-page.js can
2
+ // import them through a single module.
3
+
4
+ export { renderPrd } from './prd.js';
5
+ export { renderReq } from './req.js';
6
+ export { renderUserStory } from './user-story.js';
7
+ export { renderTad } from './tad.js';
8
+ export { renderTac } from './tac.js';
9
+ export { renderAdr } from './adr.js';
10
+ export { renderBuildSequence } from './build-sequence.js';
11
+ export { renderFbs } from './fbs.js';
12
+ export { renderTestSuite } from './test-suite.js';
@@ -0,0 +1,45 @@
1
+ // PRD renderer. Curated fields per spec D11. Post-3.7 (D15) the child REQ
2
+ // list is not read from `prd.requirementIds` (removed in 0.2.0) but from
3
+ // the computed `childrenByParent` map on the tree model.
4
+
5
+ import {
6
+ anchorIdFor,
7
+ brokenBanner,
8
+ docLinkList,
9
+ escapeHtml,
10
+ fieldList,
11
+ fieldPara,
12
+ rawJsonDisclosure,
13
+ } from './helpers.js';
14
+
15
+ /**
16
+ * @param {object} prd
17
+ * @param {object} ctx
18
+ * @param {string|undefined} ctx.raw
19
+ * @param {import('#core/errors').RcfError[]} [ctx.errors]
20
+ * @param {string[]} [ctx.requirementIds] - computed REQ children for this PRD
21
+ * @returns {string}
22
+ */
23
+ export function renderPrd(prd, ctx) {
24
+ if (!prd) return '';
25
+ const anchor = anchorIdFor(prd.prdId ?? 'PRD');
26
+ const broken = ctx.errors?.length ? brokenBanner(ctx.errors) : '';
27
+ const requirementIds = ctx.requirementIds ?? [];
28
+ const sections = [
29
+ fieldPara('Executive summary', prd.executiveSummary),
30
+ fieldPara('Problem statement', prd.problemStatement),
31
+ fieldList('Target users', prd.targetUsers),
32
+ fieldList('In scope', prd.inScope),
33
+ fieldList('Out of scope', prd.outOfScope),
34
+ fieldList('Objectives', prd.objectives),
35
+ fieldList('Constraints', prd.constraints),
36
+ `<section class="field-list"><h4>Requirements</h4><p>${docLinkList(requirementIds)}</p></section>`,
37
+ ].filter(Boolean).join('\n');
38
+ return `
39
+ <article id="${anchor}" class="doc doc-prd">
40
+ <h3>${escapeHtml(prd.prdId ?? 'PRD')} - ${escapeHtml(prd.productName ?? '')}</h3>
41
+ ${broken}
42
+ ${sections}
43
+ ${rawJsonDisclosure(ctx.raw, prd, prd.prdId)}
44
+ </article>`.trim();
45
+ }