rcf-lite 0.0.1 → 0.8.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 (349) hide show
  1. package/CHANGELOG.md +344 -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 +71 -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/admissibility/enforce.js +142 -0
  177. package/src/admissibility/index.js +8 -0
  178. package/src/admissibility/markers.js +104 -0
  179. package/src/admissibility/scope-lint.js +163 -0
  180. package/src/browser-verify/auth-smoke.js +109 -0
  181. package/src/browser-verify/index.js +29 -0
  182. package/src/browser-verify/invariants.js +336 -0
  183. package/src/browser-verify/manifest-writer.js +189 -0
  184. package/src/browser-verify/runner.js +171 -0
  185. package/src/build/bundle.js +198 -0
  186. package/src/build/formatters/json.js +18 -0
  187. package/src/build/formatters/markdown.js +406 -0
  188. package/src/build/index.js +14 -0
  189. package/src/build/mark.js +177 -0
  190. package/src/build/queue.js +285 -0
  191. package/src/cli/browser-verify.js +231 -0
  192. package/src/cli/build.js +584 -0
  193. package/src/cli/coverage.js +219 -0
  194. package/src/cli/create.js +358 -0
  195. package/src/cli/delete.js +127 -0
  196. package/src/cli/design.js +353 -0
  197. package/src/cli/doctor.js +548 -0
  198. package/src/cli/fbs.js +171 -0
  199. package/src/cli/finalise.js +377 -0
  200. package/src/cli/guidance.js +206 -0
  201. package/src/cli/help.js +156 -0
  202. package/src/cli/impact.js +119 -0
  203. package/src/cli/init.js +282 -0
  204. package/src/cli/intake.js +153 -0
  205. package/src/cli/link.js +128 -0
  206. package/src/cli/mcp.js +160 -0
  207. package/src/cli/preflight.js +220 -0
  208. package/src/cli/read.js +168 -0
  209. package/src/cli/req-baseline.js +269 -0
  210. package/src/cli/req-classify.js +135 -0
  211. package/src/cli/review.js +295 -0
  212. package/src/cli/test-suite.js +221 -0
  213. package/src/cli/trace.js +172 -0
  214. package/src/cli/ui-baseline.js +292 -0
  215. package/src/cli/ui-classify.js +108 -0
  216. package/src/cli/update.js +197 -0
  217. package/src/cli/validate.js +168 -0
  218. package/src/cli/view.js +452 -0
  219. package/src/core/baseline-catalog/data/auth.json +42 -0
  220. package/src/core/baseline-catalog/data/http-api.json +42 -0
  221. package/src/core/baseline-catalog/data/notifications.json +33 -0
  222. package/src/core/baseline-catalog/data/persistence.json +33 -0
  223. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  224. package/src/core/baseline-catalog/index.js +121 -0
  225. package/src/core/errors/index.js +167 -0
  226. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  227. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  228. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  229. package/src/core/isolation/index.js +60 -0
  230. package/src/core/mcp/framing.js +103 -0
  231. package/src/core/mcp/index.js +8 -0
  232. package/src/core/mcp/server.js +228 -0
  233. package/src/core/patterns/register-canary.js +209 -0
  234. package/src/core/patterns/req-shapes.js +158 -0
  235. package/src/core/patterns/services.js +358 -0
  236. package/src/core/patterns/ui-shapes.js +166 -0
  237. package/src/core/store/cn-resolve.js +134 -0
  238. package/src/core/store/derive-deps.js +93 -0
  239. package/src/core/store/ids.js +78 -0
  240. package/src/core/store/index.js +20 -0
  241. package/src/core/store/init.js +255 -0
  242. package/src/core/store/loader.js +214 -0
  243. package/src/core/store/tp-resolve.js +176 -0
  244. package/src/core/store/validator.js +191 -0
  245. package/src/core/store/walker.js +944 -0
  246. package/src/core/store/writer.js +1879 -0
  247. package/src/design/index.js +11 -0
  248. package/src/design/writer.js +271 -0
  249. package/src/finalise/detect.js +151 -0
  250. package/src/finalise/index.js +31 -0
  251. package/src/finalise/ingest.js +160 -0
  252. package/src/finalise/install.js +119 -0
  253. package/src/finalise/ship-without-verified.js +131 -0
  254. package/src/finalise/spawn.js +84 -0
  255. package/src/intake/fidelity.js +105 -0
  256. package/src/intake/index.js +6 -0
  257. package/src/intake/manifest-writer.js +100 -0
  258. package/src/intake/orchestrator.js +138 -0
  259. package/src/intake/validate.js +80 -0
  260. package/src/mcp/map-errors.js +131 -0
  261. package/src/mcp/prompts.js +49 -0
  262. package/src/mcp/resources.js +244 -0
  263. package/src/mcp/tools.js +1212 -0
  264. package/src/preflight/design-shapes.js +185 -0
  265. package/src/preflight/index.js +90 -0
  266. package/src/preflight/manifest-writer.js +264 -0
  267. package/src/preflight/scanner.js +206 -0
  268. package/src/preflight/secrets.js +134 -0
  269. package/src/preflight/session.js +246 -0
  270. package/src/query/attestation.js +285 -0
  271. package/src/query/coverage.js +308 -0
  272. package/src/query/formatters/json.js +21 -0
  273. package/src/query/formatters/mermaid.js +209 -0
  274. package/src/query/formatters/table.js +203 -0
  275. package/src/query/impact.js +173 -0
  276. package/src/query/index.js +13 -0
  277. package/src/query/refuse-on-admissibility.js +73 -0
  278. package/src/query/trace.js +345 -0
  279. package/src/register-canary/fixture-loader.js +87 -0
  280. package/src/register-canary/index.js +10 -0
  281. package/src/register-canary/record-writer.js +132 -0
  282. package/src/register-canary/runner.js +156 -0
  283. package/src/req-baseline/gate.js +86 -0
  284. package/src/req-baseline/index.js +27 -0
  285. package/src/req-baseline/open-candidates.js +143 -0
  286. package/src/req-baseline/opt-out.js +195 -0
  287. package/src/req-baseline/sweep.js +230 -0
  288. package/src/req-detection/classifier.js +181 -0
  289. package/src/req-detection/index.js +9 -0
  290. package/src/req-detection/persist.js +55 -0
  291. package/src/review/index.js +325 -0
  292. package/src/review/mutation.js +117 -0
  293. package/src/review/ui-baseline-drift.js +138 -0
  294. package/src/ruleset/index.js +140 -0
  295. package/src/ruleset/ruleset.json +146 -0
  296. package/src/server/index.js +178 -0
  297. package/src/server/routes.js +110 -0
  298. package/src/server/sse.js +118 -0
  299. package/src/setup/agent-setup.js +362 -0
  300. package/src/setup/identity-seed.js +104 -0
  301. package/src/setup/knowledge-seed.js +123 -0
  302. package/src/setup/managed-block.js +193 -0
  303. package/src/setup/managed-gitignore.js +166 -0
  304. package/src/setup/managed-markers.js +49 -0
  305. package/src/ui-baseline/defaults.js +119 -0
  306. package/src/ui-baseline/index.js +25 -0
  307. package/src/ui-baseline/manifest-writer.js +282 -0
  308. package/src/ui-baseline/session.js +178 -0
  309. package/src/ui-detection/classifier.js +192 -0
  310. package/src/verify/chain/index.js +221 -0
  311. package/src/verify/cli/cleanup.js +61 -0
  312. package/src/verify/cli/help.js +56 -0
  313. package/src/verify/cli/mcp.js +98 -0
  314. package/src/verify/cli/provision.js +71 -0
  315. package/src/verify/cli/report.js +71 -0
  316. package/src/verify/cli/run.js +155 -0
  317. package/src/verify/engine/brief.js +87 -0
  318. package/src/verify/engine/index.js +177 -0
  319. package/src/verify/engine/launcher.js +307 -0
  320. package/src/verify/mcp/tools.js +107 -0
  321. package/src/verify/profile/index.js +146 -0
  322. package/src/verify/provision/index.js +256 -0
  323. package/src/verify/report/index.js +139 -0
  324. package/src/verify/report/renderer.js +118 -0
  325. package/src/verify/verdict/index.js +313 -0
  326. package/src/view/doc-renderers/adr.js +44 -0
  327. package/src/view/doc-renderers/build-sequence.js +40 -0
  328. package/src/view/doc-renderers/fbs.js +128 -0
  329. package/src/view/doc-renderers/helpers.js +159 -0
  330. package/src/view/doc-renderers/index.js +12 -0
  331. package/src/view/doc-renderers/prd.js +45 -0
  332. package/src/view/doc-renderers/req.js +43 -0
  333. package/src/view/doc-renderers/tac.js +38 -0
  334. package/src/view/doc-renderers/tad.js +74 -0
  335. package/src/view/doc-renderers/test-suite.js +45 -0
  336. package/src/view/doc-renderers/user-story.js +63 -0
  337. package/src/view/html-page.js +462 -0
  338. package/src/view/index.js +63 -0
  339. package/src/view/live-client.js +338 -0
  340. package/src/view/mermaid-diagram.js +178 -0
  341. package/src/view/style.css +735 -0
  342. package/src/view/tree-model.js +152 -0
  343. package/src/view/vendored/mermaid.min.js +2607 -0
  344. package/src/view-supervisor/index.js +26 -0
  345. package/src/view-supervisor/logs.js +32 -0
  346. package/src/view-supervisor/manifest-writer.js +178 -0
  347. package/src/view-supervisor/persist-until.js +85 -0
  348. package/src/view-supervisor/supervisor.js +276 -0
  349. package/src/watch/index.js +152 -0
@@ -0,0 +1,313 @@
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
+ * 0.8.0 slug-train car 4 addition (NV-BL-GATE-01 + NV-BL-ADM-03):
55
+ * - SCOPE-MISMATCH: an AC whose declared scope tag exceeds the scope
56
+ * of every bound TC. Verify emits this at REVIEW time so the pull-in
57
+ * from the finalise-time profile-vs-AC check fires per FBS rather
58
+ * than only at finalise.
59
+ */
60
+ export const PER_AC_VERDICTS = Object.freeze([
61
+ 'MOCK-ONLY-DECLARED',
62
+ 'BLOCKED-BY-DECLARATION',
63
+ 'UI-BASELINE-UNMET',
64
+ 'BROWSER-VERIFICATION-MISSING',
65
+ 'SCOPE-MISMATCH',
66
+ ]);
67
+
68
+ /**
69
+ * Required fields on every finding (spec §5.2): the RCF payoff is that every
70
+ * defect maps to a contract line (acId / chain node), never a free-floating
71
+ * bug.
72
+ *
73
+ * @param {object} finding
74
+ * @returns {import('#core/errors').RcfError | null} error as data, or null if valid
75
+ */
76
+ export function validateFinding(finding) {
77
+ if (!finding || typeof finding !== 'object') {
78
+ return rcfError({ kind: 'validation', message: 'finding must be an object' });
79
+ }
80
+ if (!FINDING_SEVERITIES.includes(finding.severity)) {
81
+ return rcfError({ kind: 'validation', message: `finding.severity must be one of ${FINDING_SEVERITIES.join('/')}`, field: 'severity' });
82
+ }
83
+ if (typeof finding.acId !== 'string' || finding.acId.length === 0) {
84
+ return rcfError({ kind: 'validation', message: 'finding.acId (chain-node reference) is required', field: 'acId' });
85
+ }
86
+ if (typeof finding.journey !== 'string' || finding.journey.length === 0) {
87
+ return rcfError({ kind: 'validation', message: 'finding.journey is required', field: 'journey' });
88
+ }
89
+ if (!Array.isArray(finding.reproSteps)) {
90
+ return rcfError({ kind: 'validation', message: 'finding.reproSteps must be an array', field: 'reproSteps' });
91
+ }
92
+ if (!finding.evidence || typeof finding.evidence !== 'object') {
93
+ return rcfError({ kind: 'validation', message: 'finding.evidence must be an object', field: 'evidence' });
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * The worst (max) severity across findings. Empty → PASS. This is the
100
+ * split-not-averaged rule: the single worst finding drives the class.
101
+ *
102
+ * @param {Array<{severity: string}>} findings
103
+ * @returns {'PASS'|'COSMETIC'|'DEGRADED'|'BROKEN'}
104
+ */
105
+ export function aggregateSeverity(findings = []) {
106
+ let worst = 'PASS';
107
+ for (const f of findings) {
108
+ if ((SEVERITY_ORDER[f.severity] ?? -1) > SEVERITY_ORDER[worst]) worst = f.severity;
109
+ }
110
+ return worst;
111
+ }
112
+
113
+ /**
114
+ * The overall run verdict (spec §5.1). NOT-DEPLOYED and a fully-blocked run
115
+ * are structural verdicts; otherwise the worst finding severity wins
116
+ * (split-not-averaged). A run with SOME findings and SOME blocked ACs is a
117
+ * partial verification: the verdict reflects what WAS exercised, and the
118
+ * blocked ACs are named separately in the report.
119
+ *
120
+ * @param {object} opts
121
+ * @param {Array<{severity: string}>} [opts.findings]
122
+ * @param {Array<object>} [opts.blockedAcs]
123
+ * @param {boolean} [opts.notDeployed]
124
+ * @returns {string}
125
+ */
126
+ export function aggregateVerdict({ findings = [], blockedAcs = [], notDeployed = false } = {}) {
127
+ if (notDeployed) return 'NOT-DEPLOYED';
128
+ if (findings.length === 0 && blockedAcs.length > 0) return 'BLOCKED';
129
+ return aggregateSeverity(findings);
130
+ }
131
+
132
+ /**
133
+ * Whether the severity gate is tripped → the process exits non-zero
134
+ * (spec §3 rule 5, §8.2). NOT-DEPLOYED, BLOCKED and LAUNCH-FAILURE always trip
135
+ * (ship cannot be confirmed); otherwise the worst finding severity is compared
136
+ * against the gate. With no gate configured, nothing trips — the report is
137
+ * still written.
138
+ *
139
+ * @param {object} opts
140
+ * @param {string} opts.verdict
141
+ * @param {Array<{severity: string}>} [opts.findings]
142
+ * @param {string|null} [opts.gate] - one of FINDING_SEVERITIES, or null/undefined
143
+ * @returns {boolean}
144
+ */
145
+ export function gateTripped({ verdict, findings = [], gate }) {
146
+ if (verdict === 'NOT-DEPLOYED' || verdict === 'BLOCKED' || verdict === 'LAUNCH-FAILURE') return true;
147
+ if (!gate) return false;
148
+ const worst = aggregateSeverity(findings);
149
+ return SEVERITY_ORDER[worst] >= SEVERITY_ORDER[gate];
150
+ }
151
+
152
+ /**
153
+ * Resolve the Track A per-AC verdict from an AC's aggregated service
154
+ * attestations. Priority: `declaredMockOnly` wins over `mocked`, because
155
+ * a chain that explicitly declared mock-only overrides an implicit mock.
156
+ * `live` / `sandboxed` / `notShipped` never emit a per-AC verdict here —
157
+ * they either mean a live path exists (verify's normal findings pipeline
158
+ * handles them) or the AC does not gate ship at all.
159
+ *
160
+ * @param {Array<{serviceId: string, attestationMode: string}>} attestations
161
+ * @returns {{ verdict: 'MOCK-ONLY-DECLARED'|'BLOCKED-BY-DECLARATION', reason: string } | null}
162
+ */
163
+ export function attestationPerAcVerdict(attestations = []) {
164
+ if (!Array.isArray(attestations) || attestations.length === 0) return null;
165
+ const declared = attestations.find((a) => a && a.attestationMode === 'declaredMockOnly');
166
+ if (declared) {
167
+ return {
168
+ verdict: 'BLOCKED-BY-DECLARATION',
169
+ reason: `operator declared mock-only at pre-flight for service ${declared.serviceId}; verify refused to issue a live verdict rather than fabricate a PASS.`,
170
+ };
171
+ }
172
+ const mocked = attestations.find((a) => a && a.attestationMode === 'mocked');
173
+ if (mocked) {
174
+ return {
175
+ verdict: 'MOCK-ONLY-DECLARED',
176
+ 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.`,
177
+ };
178
+ }
179
+ return null;
180
+ }
181
+
182
+ /**
183
+ * Resolve the Track B per-AC UI verdict for an AC. UI verdicts fire only
184
+ * for `fbsUiBearing: true` ACs (the chain-derived flag from
185
+ * `packages/rcf-lite/src/verify/chain/index.js`). Priority:
186
+ * 1. Any bound FBS has NO browserVerification entry → BROWSER-VERIFICATION-MISSING.
187
+ * 2. Any bound FBS's browserVerification.verdict is `block` → UI-BASELINE-UNMET.
188
+ * 3. Otherwise no per-AC UI verdict is emitted.
189
+ *
190
+ * @param {object} ac - a flattened AC with `fbsUiBearing` and `fbsIds`
191
+ * @param {object[]} [browserVerification] - manifest.browserVerification[]
192
+ * @returns {{ verdict: 'UI-BASELINE-UNMET'|'BROWSER-VERIFICATION-MISSING', reason: string } | null}
193
+ */
194
+ export function uiPerAcVerdict(ac, browserVerification = []) {
195
+ if (!ac || ac.fbsUiBearing !== true) return null;
196
+ const fbsIds = Array.isArray(ac.fbsIds) ? ac.fbsIds : [];
197
+ if (fbsIds.length === 0) return null;
198
+ const bv = Array.isArray(browserVerification) ? browserVerification : [];
199
+ const missing = [];
200
+ const blocked = [];
201
+ for (const fbsId of fbsIds) {
202
+ const records = bv.filter((r) => r && r.fbsId === fbsId);
203
+ if (records.length === 0) {
204
+ missing.push(fbsId);
205
+ continue;
206
+ }
207
+ // Take the freshest record — records land per-run and the latest verdict
208
+ // is the one that gates ship. `createdAt` sorts lexicographically for ISO
209
+ // timestamps; when it is missing fall back to array order (the last
210
+ // written entry wins).
211
+ const latest = [...records].sort((a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? '')).pop();
212
+ if (latest && latest.verdict === 'block') blocked.push({ fbsId, id: latest.id ?? null });
213
+ }
214
+ if (missing.length > 0) {
215
+ return {
216
+ verdict: 'BROWSER-VERIFICATION-MISSING',
217
+ reason: `UI-bearing FBS(es) ${missing.join(', ')} have no browserVerification record on the manifest; verify has nothing to read for the baseline check.`,
218
+ };
219
+ }
220
+ if (blocked.length > 0) {
221
+ const detail = blocked.map((b) => `${b.fbsId}${b.id ? ` (${b.id})` : ''}`).join(', ');
222
+ return {
223
+ verdict: 'UI-BASELINE-UNMET',
224
+ reason: `browserVerification recorded verdict \`block\` for ${detail}; the deployed UI failed at least one baseline invariant.`,
225
+ };
226
+ }
227
+ return null;
228
+ }
229
+
230
+ /**
231
+ * Rank each scope tag so we can compare "TC scope >= AC scope"
232
+ * numerically. Mirrors `packages/rcf-lite/src/admissibility/scope-lint.js`
233
+ * (single source of truth on rcf-schemas 0.4.3's scopeTag values;
234
+ * this copy stays deliberately local so the verdict layer does not
235
+ * depend on the admissibility module -- both consume the same schemas
236
+ * vocabulary and the same values, and duplicating the ranking here
237
+ * keeps the verdict layer standalone).
238
+ */
239
+ const SCOPE_RANK = Object.freeze({
240
+ library: 1,
241
+ runtime: 2,
242
+ deployed: 3,
243
+ });
244
+
245
+ /**
246
+ * 0.8.0 slug-train car 4 (NV-BL-GATE-01 + NV-BL-ADM-03): the profile-vs-AC
247
+ * scope-mismatch check, pulled from finalise into REVIEW so it fires
248
+ * per FBS. An AC declared runtime-scope bound only to library-scope TCs
249
+ * emits SCOPE-MISMATCH; a wider TC (deployed covering runtime) is fine;
250
+ * an AC with no scope declaration is silent here (NV-BL-ADM-02 catches
251
+ * it at admissibility); a bound TC with no declared scope is silent
252
+ * here (bootstrap window; NV-BL-ADM-03 in the admissibility lint
253
+ * catches it there).
254
+ *
255
+ * @param {object} ac - flattened AC with `scope` and `boundTcs`
256
+ * @returns {{ verdict: 'SCOPE-MISMATCH', reason: string } | null}
257
+ */
258
+ export function scopePerAcVerdict(ac) {
259
+ if (!ac || typeof ac.scope !== 'string') return null;
260
+ if (!Object.prototype.hasOwnProperty.call(SCOPE_RANK, ac.scope)) return null;
261
+ const boundTcs = Array.isArray(ac.boundTcs) ? ac.boundTcs : [];
262
+ if (boundTcs.length === 0) return null; // coverage rule handles "no TC at all".
263
+ const acRank = SCOPE_RANK[ac.scope];
264
+ const narrower = [];
265
+ let anyAtLeastAsWide = false;
266
+ for (const tc of boundTcs) {
267
+ if (typeof tc?.scope !== 'string') continue;
268
+ if (!Object.prototype.hasOwnProperty.call(SCOPE_RANK, tc.scope)) continue;
269
+ const tcRank = SCOPE_RANK[tc.scope];
270
+ if (tcRank >= acRank) {
271
+ anyAtLeastAsWide = true;
272
+ break;
273
+ }
274
+ narrower.push({ tsId: tc.tsId, tcId: tc.tcId, scope: tc.scope });
275
+ }
276
+ if (anyAtLeastAsWide) return null;
277
+ if (narrower.length === 0) return null;
278
+ const detail = narrower.map((n) => `${n.tcId} on ${n.tsId} (scope=${n.scope})`).join(', ');
279
+ return {
280
+ verdict: 'SCOPE-MISMATCH',
281
+ reason: `AC ${ac.acId} is scope=${ac.scope} but every bound TC is narrower: ${detail}. NV-BL-GATE-01 pulls this check into REVIEW; NV-BL-ADM-03 refuses at admissibility.`,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Emit per-AC verdicts across the whole chain. Combines Track A's service
287
+ * attestation verdicts with Track B's UI-baseline verdicts. An AC can carry
288
+ * BOTH a service-attestation verdict AND a UI verdict (a UI-bearing FBS
289
+ * that also depends on a mocked service produces two per-AC entries — one
290
+ * per class). This matches the finalise gate contract in
291
+ * `packages/rcf-lite/src/finalise/ingest.js:findMockOnlyDeclaredAcs`, which
292
+ * filters on verdict class and does not deduplicate by acId.
293
+ *
294
+ * 0.8.0 slug-train car 4: SCOPE-MISMATCH runs here too, alongside the
295
+ * attestation and UI verdicts.
296
+ *
297
+ * @param {object} opts
298
+ * @param {Array<object>} opts.acs - flattened ACs from `readChain`
299
+ * @param {object[]} [opts.browserVerification] - manifest.browserVerification[]
300
+ * @returns {Array<{ acId: string, verdict: string, reason: string }>}
301
+ */
302
+ export function derivePerAcVerdicts({ acs = [], browserVerification = [] } = {}) {
303
+ const out = [];
304
+ for (const ac of acs) {
305
+ const attest = attestationPerAcVerdict(ac.serviceAttestations);
306
+ if (attest) out.push({ acId: ac.acId, verdict: attest.verdict, reason: attest.reason });
307
+ const ui = uiPerAcVerdict(ac, browserVerification);
308
+ if (ui) out.push({ acId: ac.acId, verdict: ui.verdict, reason: ui.reason });
309
+ const scopeMismatch = scopePerAcVerdict(ac);
310
+ if (scopeMismatch) out.push({ acId: ac.acId, verdict: scopeMismatch.verdict, reason: scopeMismatch.reason });
311
+ }
312
+ return out;
313
+ }
@@ -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
+ }