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,285 @@
1
+ // Attestation × Profile enforcement matrix
2
+ // (verification-integrity-cluster-spec §3.5, §5.2).
3
+ //
4
+ // Pure data + pure classifiers. Consumers:
5
+ // - `coverage --strict` gates on the matrix at build time (§5.1, §5.2);
6
+ // - the Review-stage test-theatre audit re-runs the matrix at
7
+ // Stage 3 as one of its finding categories (§5.5, `attestationDrift`);
8
+ // - verify's deployed-verdict gate reads the same matrix inputs
9
+ // (aggregated attestations per AC) at chain-read time (§9.2 seam).
10
+ //
11
+ // The matrix returns a verdict + a per-cell reason string that
12
+ // downstream consumers can render in table / JSON / mermaid without
13
+ // re-deriving the semantics.
14
+
15
+ /**
16
+ * @typedef {'live'|'sandboxed'|'mocked'|'declaredMockOnly'|'notShipped'} AttestationMode
17
+ * @typedef {'mock'|'stub'|'fixture'|'live'|'mixed'} ProvenanceProfile
18
+ * @typedef {'pass'|'passWithWarn'|'refuse'} MatrixVerdict
19
+ */
20
+
21
+ /**
22
+ * The matrix cell.
23
+ * @typedef {object} MatrixCell
24
+ * @property {MatrixVerdict} verdict
25
+ * @property {string} reason
26
+ */
27
+
28
+ /**
29
+ * Classify one (attestationMode, profile) pair.
30
+ *
31
+ * @param {AttestationMode} attestation
32
+ * @param {ProvenanceProfile} profile
33
+ * @returns {MatrixCell}
34
+ */
35
+ export function classifyAttestationProfile(attestation, profile) {
36
+ // `mixed` in a TC is an anti-pattern flag by design: it should be
37
+ // expanded into finer TCs. coverage --strict refuses it here on the
38
+ // build side (§3.5 note on mixed) regardless of the covering AC's
39
+ // attestation.
40
+ if (profile === 'mixed') {
41
+ return {
42
+ verdict: 'refuse',
43
+ reason: 'TC profile is `mixed`; expand into finer TCs before coverage --strict will accept it.',
44
+ };
45
+ }
46
+ if (attestation === 'notShipped') {
47
+ return { verdict: 'pass', reason: 'AC does not gate ship; attestation notShipped.' };
48
+ }
49
+ if (attestation === 'mocked') {
50
+ return { verdict: 'pass', reason: 'AC attests mocked; any TC profile passes.' };
51
+ }
52
+ if (attestation === 'declaredMockOnly') {
53
+ if (profile === 'live') {
54
+ return {
55
+ verdict: 'passWithWarn',
56
+ reason: 'AC attests declaredMockOnly but TC went live; recorded as attestationDrift.',
57
+ };
58
+ }
59
+ return { verdict: 'pass', reason: 'AC attests declaredMockOnly; mock / stub / fixture is expected.' };
60
+ }
61
+ if (attestation === 'live') {
62
+ if (profile === 'live') return { verdict: 'pass', reason: 'live × live.' };
63
+ return {
64
+ verdict: 'refuse',
65
+ reason: `AC attests live; TC provenance is \`${profile}\`. Elevate the TC to live OR downgrade the AC attestation OR add a live-profile TC alongside the mock one.`,
66
+ };
67
+ }
68
+ if (attestation === 'sandboxed') {
69
+ if (profile === 'live' || profile === 'stub') return { verdict: 'pass', reason: 'sandboxed × live/stub (points at sandbox).' };
70
+ return {
71
+ verdict: 'refuse',
72
+ reason: `AC attests sandboxed; TC provenance is \`${profile}\`. Elevate the TC to live/stub against the sandbox OR downgrade the AC attestation.`,
73
+ };
74
+ }
75
+ return { verdict: 'refuse', reason: `Unknown attestation mode \`${attestation}\`.` };
76
+ }
77
+
78
+ /**
79
+ * Aggregate the `dependsOnServices` entries across every FBS that binds
80
+ * each AC. Returns a map `acId -> Array<{ serviceId, attestationMode,
81
+ * fbsId }>`. This is the shape verify's chain reader will surface via
82
+ * core's walker (spec §5.2 / §9.2); build re-computes it locally for
83
+ * `coverage --strict`.
84
+ *
85
+ * @param {import('#core/store/walker.js').TreeModel} tree
86
+ * @returns {Map<string, Array<{ serviceId: string, attestationMode: AttestationMode, fbsId: string }>>}
87
+ */
88
+ export function aggregateAttestationsByAc(tree) {
89
+ /** @type {Map<string, Array<{ serviceId: string, attestationMode: AttestationMode, fbsId: string }>>} */
90
+ const out = new Map();
91
+ for (const fbs of tree.fbsItems ?? []) {
92
+ const services = Array.isArray(fbs.dependsOnServices) ? fbs.dependsOnServices : [];
93
+ for (const svc of services) {
94
+ if (!svc?.id || !svc?.attestationMode || !Array.isArray(svc.acIds)) continue;
95
+ for (const acId of svc.acIds) {
96
+ if (!out.has(acId)) out.set(acId, []);
97
+ out.get(acId).push({
98
+ serviceId: svc.id,
99
+ attestationMode: svc.attestationMode,
100
+ fbsId: fbs.fbsId,
101
+ });
102
+ }
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+
108
+ /**
109
+ * Given a walker tree, produce the pre-flight service coverage: for
110
+ * every service the preFlightConfig[] records name (except notShipped)
111
+ * whose entry carries a non-empty `affectedFbsIds`, cross-check that
112
+ * every affected FBS carries a matching `dependsOnServices[]` entry.
113
+ * The `affectedFbsIds` back-reference is the load-bearing link between
114
+ * a preflight ruling and the FBSes that inherit it (spec §3.3); when
115
+ * empty we skip the check for that service rather than raise on every
116
+ * FBS — v1 stays honest about what it can and cannot infer.
117
+ *
118
+ * Used by `coverage --strict` to detect "attestation missing" (spec
119
+ * §5.2) — an FBS listed in `affectedFbsIds` for a pre-flight-declared
120
+ * service must itself carry the `dependsOnServices` entry naming that
121
+ * service.
122
+ *
123
+ * @param {import('#core/store/walker.js').TreeModel} tree
124
+ * @returns {Array<{ serviceId: string, fbsId: string, attestationMode: AttestationMode }>}
125
+ */
126
+ export function findAttestationMissing(tree) {
127
+ /** @type {Array<{ serviceId: string, fbsId: string, attestationMode: AttestationMode }>} */
128
+ const missing = [];
129
+ const preflight = Array.isArray(tree.manifest?.preFlightConfig) ? tree.manifest.preFlightConfig : [];
130
+ if (preflight.length === 0) return missing;
131
+
132
+ for (const pfc of preflight) {
133
+ for (const s of pfc.servicesInScope ?? []) {
134
+ if (!s?.id) continue;
135
+ if (s.attestationMode === 'notShipped') continue;
136
+ const affected = Array.isArray(s.affectedFbsIds) ? s.affectedFbsIds : [];
137
+ if (affected.length === 0) continue;
138
+ for (const fbsId of affected) {
139
+ const fbs = tree.byId.get(fbsId);
140
+ if (!fbs || tree.kindById.get(fbsId) !== 'fbs') continue;
141
+ const hasEntry = (fbs.dependsOnServices ?? []).some((e) => e.id === s.id);
142
+ if (!hasEntry) {
143
+ missing.push({ serviceId: s.id, fbsId, attestationMode: s.attestationMode });
144
+ }
145
+ }
146
+ }
147
+ }
148
+ return missing;
149
+ }
150
+
151
+ /**
152
+ * Given a walker tree, list every preFlightConfig service whose
153
+ * `affectedFbsIds` back-reference is empty. The `attestation-missing`
154
+ * detector (findAttestationMissing) skips those services on purpose:
155
+ * without the back-reference there is nothing to cross-check against,
156
+ * but the honest posture is to surface the skip so the operator can
157
+ * back-fill the field (review N-3 non-blocking finding).
158
+ *
159
+ * notShipped services are excluded: they never gate ship and never
160
+ * contribute an FBS-level attestation, so an empty back-reference on
161
+ * them is fine.
162
+ *
163
+ * @param {import('#core/store/walker.js').TreeModel} tree
164
+ * @returns {Array<{ serviceId: string, preFlightConfigId: string, attestationMode: AttestationMode }>}
165
+ */
166
+ export function findServicesWithEmptyAffectedFbsIds(tree) {
167
+ /** @type {Array<{ serviceId: string, preFlightConfigId: string, attestationMode: AttestationMode }>} */
168
+ const out = [];
169
+ const preflight = Array.isArray(tree.manifest?.preFlightConfig) ? tree.manifest.preFlightConfig : [];
170
+ for (const pfc of preflight) {
171
+ for (const s of pfc.servicesInScope ?? []) {
172
+ if (!s?.id) continue;
173
+ if (s.attestationMode === 'notShipped') continue;
174
+ const affected = Array.isArray(s.affectedFbsIds) ? s.affectedFbsIds : [];
175
+ if (affected.length === 0) {
176
+ out.push({ serviceId: s.id, preFlightConfigId: pfc.id, attestationMode: s.attestationMode });
177
+ }
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+
183
+ /**
184
+ * Given a walker tree and a target FBS id, return the FBS's
185
+ * `dependsOnServices[]` entries whose service `id` is not named in
186
+ * ANY `preFlightConfig[].servicesInScope[].id`. Powers the `rcf build
187
+ * --next` preflight-warning that the elicitation and build-cycle
188
+ * playbooks already advertise (spec section 4.2, review N-1
189
+ * non-blocking finding); the warning nudges the operator to run `rcf
190
+ * preflight` before the coverage-strict gate refuses at Stage 4.
191
+ *
192
+ * Returns an empty array when the FBS has no dependsOnServices,
193
+ * when the FBS is not found, or when every service is preflight-backed.
194
+ *
195
+ * @param {import('#core/store/walker.js').TreeModel} tree
196
+ * @param {string} fbsId
197
+ * @returns {Array<{ serviceId: string, displayName?: string, attestationMode?: AttestationMode }>}
198
+ */
199
+ export function scanUnbackedServices(tree, fbsId) {
200
+ const fbs = tree?.byId?.get?.(fbsId);
201
+ if (!fbs || tree?.kindById?.get?.(fbsId) !== 'fbs') return [];
202
+ const services = Array.isArray(fbs.dependsOnServices) ? fbs.dependsOnServices : [];
203
+ if (services.length === 0) return [];
204
+ const preflight = Array.isArray(tree.manifest?.preFlightConfig) ? tree.manifest.preFlightConfig : [];
205
+ const covered = new Set();
206
+ for (const pfc of preflight) {
207
+ for (const s of pfc.servicesInScope ?? []) {
208
+ if (s?.id) covered.add(s.id);
209
+ }
210
+ }
211
+ /** @type {Array<{ serviceId: string, displayName?: string, attestationMode?: AttestationMode }>} */
212
+ const unbacked = [];
213
+ for (const s of services) {
214
+ if (!s?.id || covered.has(s.id)) continue;
215
+ const entry = { serviceId: s.id };
216
+ if (typeof s.displayName === 'string') entry.displayName = s.displayName;
217
+ if (typeof s.attestationMode === 'string') entry.attestationMode = s.attestationMode;
218
+ unbacked.push(entry);
219
+ }
220
+ return unbacked;
221
+ }
222
+
223
+ /**
224
+ * Given a walker tree, list every TC that lacks `runtimeProvenance`
225
+ * where the covering AC binds a dependsOnServices entry. This is the
226
+ * §5.1 gate: provenance is authored, not remembered, on any AC that
227
+ * binds a service — coverage --strict refuses without it.
228
+ *
229
+ * @param {import('#core/store/walker.js').TreeModel} tree
230
+ * @returns {Array<{ tsId: string, tcId: string, acId: string }>}
231
+ */
232
+ export function findProvenanceMissing(tree) {
233
+ const acsWithServices = new Set(aggregateAttestationsByAc(tree).keys());
234
+ /** @type {Array<{ tsId: string, tcId: string, acId: string }>} */
235
+ const missing = [];
236
+ for (const ts of tree.testSuites ?? []) {
237
+ for (const tc of ts.testCases ?? []) {
238
+ if (!tc?.acId) continue;
239
+ if (!acsWithServices.has(tc.acId)) continue;
240
+ if (!tc.runtimeProvenance || typeof tc.runtimeProvenance.profile !== 'string') {
241
+ missing.push({ tsId: ts.id, tcId: tc.id, acId: tc.acId });
242
+ }
243
+ }
244
+ }
245
+ return missing;
246
+ }
247
+
248
+ /**
249
+ * Given a walker tree, find every TC whose runtimeProvenance profile
250
+ * contradicts the covering AC's aggregated attestation, per the §3.5
251
+ * matrix. Returns one entry per offending TC × service pair, so a TC
252
+ * covering an AC governed by two services can raise twice (both must
253
+ * be resolved before ship).
254
+ *
255
+ * @param {import('#core/store/walker.js').TreeModel} tree
256
+ * @returns {Array<{ tsId: string, tcId: string, acId: string, serviceId: string, attestationMode: AttestationMode, profile: ProvenanceProfile, verdict: MatrixVerdict, reason: string }>}
257
+ */
258
+ export function findAttestationDrift(tree) {
259
+ const attByAc = aggregateAttestationsByAc(tree);
260
+ /** @type {Array<{ tsId: string, tcId: string, acId: string, serviceId: string, attestationMode: AttestationMode, profile: ProvenanceProfile, verdict: MatrixVerdict, reason: string }>} */
261
+ const drift = [];
262
+ for (const ts of tree.testSuites ?? []) {
263
+ for (const tc of ts.testCases ?? []) {
264
+ const profile = tc?.runtimeProvenance?.profile;
265
+ if (typeof profile !== 'string') continue;
266
+ const bindings = attByAc.get(tc.acId) ?? [];
267
+ for (const { serviceId, attestationMode } of bindings) {
268
+ const cell = classifyAttestationProfile(attestationMode, profile);
269
+ if (cell.verdict === 'refuse' || cell.verdict === 'passWithWarn') {
270
+ drift.push({
271
+ tsId: ts.id,
272
+ tcId: tc.id,
273
+ acId: tc.acId,
274
+ serviceId,
275
+ attestationMode,
276
+ profile,
277
+ verdict: cell.verdict,
278
+ reason: cell.reason,
279
+ });
280
+ }
281
+ }
282
+ }
283
+ }
284
+ return drift;
285
+ }
@@ -0,0 +1,308 @@
1
+ // Pure coverage compute. Given a walker-produced TreeModel, walk the
2
+ // REQ chain (PRD -> REQ -> US -> AC -> TS -> TC) and report whether at
3
+ // least one full chain reaches a TC leaf per REQ.
4
+ //
5
+ // w-2026-07-28-005: the TC leaf test is RESOLUTION-GATED. A TC counts as
6
+ // covering its AC only when its `testPointer` resolves to a real test in
7
+ // the working tree (see core's tp-resolve.js). Callers resolve pointers
8
+ // first (async, fs-touching) and pass the result in via
9
+ // `opts.testPointers`; this function stays pure. When no resolution map
10
+ // is supplied the compute FAILS CLOSED - every TC is treated as
11
+ // unresolved - because an unverified pointer must never count as
12
+ // coverage. A TC row whose pointer does not resolve is reported as its
13
+ // own class (`covered-unresolved`), never silently folded into either
14
+ // covered or uncovered.
15
+ //
16
+ // Phase 5 §D2: shallow-any default (any AC covered by any TC = REQ
17
+ // covered); `--strict` flips to per-AC-strict (every AC has TC coverage).
18
+ // Phase-boundary note (§1.4, §D2): this is a MECHANICAL / DETERMINISTIC
19
+ // structural check. It does NOT answer "does the AC set adequately
20
+ // capture the REQ's intent?" - that non-deterministic question belongs
21
+ // to a later prompting + MCP resources phase (Phase 7+).
22
+ //
23
+ // Phase 5 §D10 scoping: no positional -> tree-wide; positional PRD id
24
+ // -> scope to REQs whose prdId matches; positional REQ id -> scope to
25
+ // that REQ; positional US id -> scope to the REQ that owns that US.
26
+ // Below-AC / cross-chain positionals (AC / TS / TC / FBS / TAC / ADR /
27
+ // BS / TAD) are refused at the handler layer (exit 2).
28
+
29
+ import { testCaseKey } from '#core/store';
30
+
31
+ /**
32
+ * @typedef {import('#core/store/walker.js').TreeModel} TreeModel
33
+ */
34
+
35
+ /**
36
+ * @typedef {object} AcCoverage
37
+ * @property {string} id
38
+ * @property {boolean} covered - at least one TC whose pointer RESOLVES
39
+ * @property {string[]} testCases - all TC ids that cross-reference this AC
40
+ * @property {string[]} unresolvedTestCases - the subset whose pointer does not resolve
41
+ */
42
+
43
+ /**
44
+ * @typedef {object} ReqCoverage
45
+ * @property {string} id
46
+ * @property {boolean} covered - true only for coverageClass 'covered'
47
+ * @property {'covered'|'covered-unresolved'|'uncovered'} coverageClass -
48
+ * 'covered-unresolved' = the TC rows exist to claim coverage, but the
49
+ * claim rests on at least one pointer that does not resolve
50
+ * @property {AcCoverage[]} acs
51
+ */
52
+
53
+ /**
54
+ * @typedef {object} CoverageResult
55
+ * @property {boolean} ok - true when every requirement in scope is covered
56
+ * by RESOLVING test cases (covered-unresolved is a gap, not a pass)
57
+ * @property {boolean} strict - the --strict flag echoed in the envelope
58
+ * @property {{requirements: number, covered: number, coveredUnresolved: number, uncovered: number}} totals
59
+ * @property {ReqCoverage[]} requirements
60
+ * @property {Array<{tsId: string, tcId: string, testPointer: string|null, reason: string}>} unresolvedTestPointers -
61
+ * every in-scope TC whose pointer failed to resolve, with why
62
+ */
63
+
64
+ /**
65
+ * Compute coverage over the tree. Returns a stable CoverageResult
66
+ * envelope suitable for the JSON / table / mermaid emitters.
67
+ *
68
+ * Phase 10 (X2 CodeNode bridge, D11): `opts.withCode` layers an
69
+ * INFORMATIONAL code axis onto every AC - `codeClass` is one of
70
+ * `implemented-and-covered` / `implemented-uncovered` / `unimplemented`
71
+ * (deterministic edge counting via `tree.cnByAcId` / `tree.tcsByAcId`,
72
+ * with the covered leg gated on pointer resolution per w-2026-07-28-005),
73
+ * plus a tree-wide `codeNodeOrphans` list (CN docs with empty
74
+ * `implementsAcIds`). None of this blocks - `ok` / exit code are
75
+ * unaffected by the code axis (spec D11: the mark-complete gate, not
76
+ * coverage, is where CN completeness is enforced).
77
+ *
78
+ * @param {TreeModel} tree
79
+ * @param {object} [opts]
80
+ * @param {boolean} [opts.strict] - per-AC-strict mode
81
+ * @param {string} [opts.scopeId] - optional PRD / REQ / US id to scope
82
+ * @param {boolean} [opts.withCode] - layer the code-axis classification
83
+ * @param {Map<string, import('#core/store/tp-resolve.js').TestPointerResolution>} [opts.testPointers] -
84
+ * per-TC pointer resolution keyed by `testCaseKey(tsId, tcId)` (from
85
+ * core's `resolveTestPointers`). Absent entries / an absent map fail
86
+ * closed: the TC counts as unresolved.
87
+ * @returns {CoverageResult}
88
+ */
89
+ export function computeCoverage(tree, opts = {}) {
90
+ const strict = Boolean(opts.strict);
91
+ const scopeId = opts.scopeId ?? null;
92
+ const withCode = Boolean(opts.withCode);
93
+ const testPointers = opts.testPointers ?? new Map();
94
+ const reqs = selectRequirements(tree, scopeId);
95
+
96
+ /** @type {ReqCoverage[]} */
97
+ const requirements = [];
98
+ /** @type {Map<string, {tsId: string, tcId: string, testPointer: string|null, reason: string}>} */
99
+ const unresolvedByKey = new Map();
100
+ let covered = 0;
101
+ let coveredUnresolved = 0;
102
+ let uncovered = 0;
103
+
104
+ for (const req of reqs) {
105
+ const acs = collectAcs(tree, req.reqId, { withCode, testPointers, unresolvedByKey });
106
+ // The real verdict counts only resolving TCs; the structural verdict
107
+ // is the pre-w-2026-07-28-005 rule (a TC row exists). A REQ that
108
+ // passes structurally but not really is 'covered-unresolved' - the
109
+ // stub-TC state, made visible instead of counted.
110
+ const isCovered = decideReqCoverage(acs, strict, (ac) => ac.covered);
111
+ const structurallyCovered = decideReqCoverage(acs, strict, (ac) => ac.testCases.length > 0);
112
+ const coverageClass = isCovered ? 'covered' : structurallyCovered ? 'covered-unresolved' : 'uncovered';
113
+ if (coverageClass === 'covered') covered += 1;
114
+ else if (coverageClass === 'covered-unresolved') coveredUnresolved += 1;
115
+ else uncovered += 1;
116
+ requirements.push({ id: req.reqId, covered: isCovered, coverageClass, acs });
117
+ }
118
+
119
+ const result = {
120
+ ok: coveredUnresolved === 0 && uncovered === 0,
121
+ strict,
122
+ totals: {
123
+ requirements: requirements.length,
124
+ covered,
125
+ coveredUnresolved,
126
+ uncovered,
127
+ },
128
+ requirements,
129
+ unresolvedTestPointers: [...unresolvedByKey.values()]
130
+ .sort((a, b) => (a.tsId + a.tcId).localeCompare(b.tsId + b.tcId)),
131
+ };
132
+
133
+ if (withCode) {
134
+ result.withCode = true;
135
+ result.codeNodeOrphans = collectCodeNodeOrphans(tree);
136
+ result.codeTotals = summariseCodeClasses(requirements);
137
+ }
138
+
139
+ return result;
140
+ }
141
+
142
+ /**
143
+ * CN docs whose `implementsAcIds` is empty - a legitimate, common state
144
+ * (utilities, glue, wiring), reported informationally (D3/D11).
145
+ * @param {TreeModel} tree
146
+ * @returns {string[]}
147
+ */
148
+ function collectCodeNodeOrphans(tree) {
149
+ return (tree.codeNodes ?? [])
150
+ .filter((cn) => (cn.implementsAcIds ?? []).length === 0)
151
+ .map((cn) => cn.cnId)
152
+ .sort();
153
+ }
154
+
155
+ /**
156
+ * @param {ReqCoverage[]} requirements
157
+ * @returns {{ implementedAndCovered: number, implementedUncovered: number, unimplemented: number }}
158
+ */
159
+ function summariseCodeClasses(requirements) {
160
+ const totals = { implementedAndCovered: 0, implementedUncovered: 0, unimplemented: 0 };
161
+ for (const req of requirements) {
162
+ for (const ac of req.acs) {
163
+ if (ac.codeClass === 'implemented-and-covered') totals.implementedAndCovered += 1;
164
+ else if (ac.codeClass === 'implemented-uncovered') totals.implementedUncovered += 1;
165
+ else if (ac.codeClass === 'unimplemented') totals.unimplemented += 1;
166
+ }
167
+ }
168
+ return totals;
169
+ }
170
+
171
+ /**
172
+ * Select the requirements in scope for coverage. No scope = every REQ
173
+ * in the tree. PRD scope = REQs whose prdId matches. REQ scope = just
174
+ * that REQ. US scope = the REQ that owns that US.
175
+ *
176
+ * @param {TreeModel} tree
177
+ * @param {string | null} scopeId
178
+ * @returns {object[]}
179
+ */
180
+ function selectRequirements(tree, scopeId) {
181
+ if (!scopeId) return [...tree.requirements];
182
+ const kind = tree.kindById.get(scopeId);
183
+ if (kind === 'prd') {
184
+ return tree.requirements.filter((r) => r.prdId === scopeId);
185
+ }
186
+ if (kind === 'req') {
187
+ const req = tree.requirements.find((r) => r.reqId === scopeId);
188
+ return req ? [req] : [];
189
+ }
190
+ if (kind === 'userStory') {
191
+ const us = tree.userStories.find((u) => u.usId === scopeId);
192
+ if (!us) return [];
193
+ const req = tree.requirements.find((r) => r.reqId === us.reqId);
194
+ return req ? [req] : [];
195
+ }
196
+ // Unknown / unscopeable id: caller (handler) refuses with exit 2. The
197
+ // pure function returns an empty scope so callers that pass a bad id
198
+ // without pre-flighting still get an empty envelope back.
199
+ return [];
200
+ }
201
+
202
+ /**
203
+ * Collect every AC under a REQ (across all its USs) with per-AC coverage
204
+ * signal + the list of TC ids referencing that AC. A TC only counts
205
+ * towards `covered` when its pointer resolves (w-2026-07-28-005); the
206
+ * unresolved subset is carried per AC and accumulated into the caller's
207
+ * `unresolvedByKey` detail map. Phase 10: when `withCode`, also attaches
208
+ * `cnIds` and the D11 `codeClass` (the covered leg is resolution-gated
209
+ * too - a stub TC never manufactures 'implemented-and-covered').
210
+ *
211
+ * @param {TreeModel} tree
212
+ * @param {string} reqId
213
+ * @param {{ withCode?: boolean, testPointers?: Map<string, object>, unresolvedByKey?: Map<string, object> }} [opts]
214
+ * @returns {AcCoverage[]}
215
+ */
216
+ function collectAcs(tree, reqId, opts = {}) {
217
+ const withCode = Boolean(opts.withCode);
218
+ const testPointers = opts.testPointers ?? new Map();
219
+ const unresolvedByKey = opts.unresolvedByKey ?? new Map();
220
+ /** @type {AcCoverage[]} */
221
+ const acs = [];
222
+ const usIds = tree.childrenByParent.get(reqId) ?? [];
223
+ for (const usId of usIds) {
224
+ const us = tree.byId.get(usId);
225
+ if (!us) continue;
226
+ for (const ac of us.acceptanceCriteria ?? []) {
227
+ if (!ac?.id) continue;
228
+ const tcEntries = tree.tcsByAcId.get(ac.id) ?? [];
229
+ const resolvedTcs = [];
230
+ const unresolvedTcs = [];
231
+ for (const { tsId, tcId } of tcEntries) {
232
+ const key = testCaseKey(tsId, tcId);
233
+ const resolution = testPointers.get(key);
234
+ if (resolution?.resolved === true) {
235
+ resolvedTcs.push(tcId);
236
+ } else {
237
+ unresolvedTcs.push(tcId);
238
+ if (!unresolvedByKey.has(key)) {
239
+ unresolvedByKey.set(key, {
240
+ tsId,
241
+ tcId,
242
+ testPointer: resolution?.testPointer ?? null,
243
+ reason: resolution?.reason ?? 'missing-pointer',
244
+ });
245
+ }
246
+ }
247
+ }
248
+ const entry = {
249
+ id: ac.id,
250
+ covered: resolvedTcs.length > 0,
251
+ testCases: tcEntries.map((e) => e.tcId).sort(),
252
+ unresolvedTestCases: [...unresolvedTcs].sort(),
253
+ };
254
+ if (withCode) {
255
+ const cnIds = [...(tree.cnByAcId?.get(ac.id) ?? [])].sort();
256
+ entry.cnIds = cnIds;
257
+ entry.codeClass = cnIds.length === 0
258
+ ? 'unimplemented'
259
+ : resolvedTcs.length > 0
260
+ ? 'implemented-and-covered'
261
+ : 'implemented-uncovered';
262
+ }
263
+ acs.push(entry);
264
+ }
265
+ }
266
+ return acs;
267
+ }
268
+
269
+ /**
270
+ * shallow-any: any one AC passing = REQ passing.
271
+ * strict: every AC passing = REQ passing. REQ with zero ACs is uncovered
272
+ * under either mode (no chain to walk). The per-AC predicate is supplied
273
+ * by the caller so the same rule serves both the resolution-gated verdict
274
+ * and the structural (TC-row-exists) verdict.
275
+ *
276
+ * @param {AcCoverage[]} acs
277
+ * @param {boolean} strict
278
+ * @param {(ac: AcCoverage) => boolean} acPasses
279
+ * @returns {boolean}
280
+ */
281
+ function decideReqCoverage(acs, strict, acPasses) {
282
+ if (acs.length === 0) return false;
283
+ if (strict) return acs.every(acPasses);
284
+ return acs.some(acPasses);
285
+ }
286
+
287
+ /**
288
+ * Detect whether the positional is a scopeable id (PRD / REQ / US) or
289
+ * a below-AC id that the handler must refuse with exit 2 per D10.
290
+ * Returns 'valid' | 'below-ac' | 'unknown-kind' | 'not-found'.
291
+ *
292
+ * @param {TreeModel} tree
293
+ * @param {string} id
294
+ * @returns {'valid' | 'below-ac' | 'unknown-kind' | 'not-found'}
295
+ */
296
+ export function classifyCoverageScope(tree, id) {
297
+ const kind = tree.kindById.get(id);
298
+ if (!kind) {
299
+ // Inline AC / TC ids are below-AC by definition.
300
+ if (/^AC-/.test(id) || /^TC-/.test(id)) return 'below-ac';
301
+ return 'not-found';
302
+ }
303
+ if (kind === 'prd' || kind === 'req' || kind === 'userStory') return 'valid';
304
+ // Below-AC or off-chain kinds. TAC / ADR / FBS / BS / TAD / TS all
305
+ // refuse: coverage backbone is PRD -> REQ -> US -> AC -> TS -> TC,
306
+ // scoping below AC has no meaningful reduction.
307
+ return 'below-ac';
308
+ }
@@ -0,0 +1,21 @@
1
+ // JSON formatter for coverage / trace / impact result envelopes.
2
+ // Phase 5 §D15: shape is stable-by-convention. Downstream consumers
3
+ // (Phase 7 MCP layer) will consume these envelopes; no in-place
4
+ // reshape without a version bump.
5
+ //
6
+ // The `verb` tag is currently unused - every envelope is already
7
+ // self-describing (`ok`, `pivot`, `direction`, etc.) - but it's
8
+ // retained on the API so future formatter dispatch stays uniform
9
+ // across the three verbs.
10
+
11
+ /**
12
+ * Format a query result as a JSON envelope.
13
+ *
14
+ * @param {object} result - CoverageResult | TraceResult | ImpactResult
15
+ * @param {string} verb - 'coverage' | 'trace' | 'impact'
16
+ * @returns {string}
17
+ */
18
+ export function formatJson(result, verb) {
19
+ void verb;
20
+ return `${JSON.stringify(result, null, 2)}\n`;
21
+ }