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,209 @@
1
+ // Mermaid formatter for coverage / trace / impact result envelopes.
2
+ // Phase 5 §D14: `flowchart LR` orientation, class palette matches
3
+ // `src/view/mermaid-diagram.js`. Text-only output; no HTML wrapper.
4
+ //
5
+ // The class-defs block, `classForId`, and label helpers are
6
+ // duplicated here rather than extracted from `src/view/mermaid-diagram.js`.
7
+ // Reason (per Phase 5 brief §5, spec §D16): view-side helpers take
8
+ // the view's `BuiltTreeModel` (title lookup via `model.byId`), while
9
+ // this formatter operates over a pure `computeTrace` result envelope
10
+ // that carries only node ids and kinds. Bridging the two contract
11
+ // shapes would need a wrapper in the view module - the "invasive"
12
+ // extraction the spec calls out. Duplicating ~30 LoC of trivial
13
+ // static defs is cheaper and keeps the view module unchanged.
14
+
15
+ const CLASS_DEFS = `
16
+ classDef prd fill:#fde68a,stroke:#92400e,color:#1f2937;
17
+ classDef req fill:#bbf7d0,stroke:#065f46,color:#1f2937;
18
+ classDef us fill:#bae6fd,stroke:#0c4a6e,color:#1f2937;
19
+ classDef ac fill:#e0e7ff,stroke:#3730a3,color:#1f2937;
20
+ classDef tad fill:#fecaca,stroke:#991b1b,color:#1f2937;
21
+ classDef tac fill:#fed7aa,stroke:#9a3412,color:#1f2937;
22
+ classDef adr fill:#fbcfe8,stroke:#9d174d,color:#1f2937;
23
+ classDef bs fill:#ddd6fe,stroke:#5b21b6,color:#1f2937;
24
+ classDef fbs fill:#c7d2fe,stroke:#3730a3,color:#1f2937;
25
+ classDef ts fill:#99f6e4,stroke:#115e59,color:#1f2937;
26
+ classDef tc fill:#d9f99d,stroke:#3f6212,color:#1f2937;
27
+ classDef cn fill:#a5b4fc,stroke:#312e81,color:#1f2937;
28
+ classDef broken stroke:#dc2626,stroke-width:2px,stroke-dasharray:5 5,color:#7f1d1d;
29
+ `.trim();
30
+
31
+ function classForId(id) {
32
+ if (id.startsWith('PRD-')) return 'prd';
33
+ if (id.startsWith('REQ-')) return 'req';
34
+ if (id.startsWith('US-')) return 'us';
35
+ if (id.startsWith('AC-')) return 'ac';
36
+ if (id.startsWith('TAD-')) return 'tad';
37
+ if (id.startsWith('TAC-')) return 'tac';
38
+ if (id.startsWith('ADR-')) return 'adr';
39
+ if (id.startsWith('BS-')) return 'bs';
40
+ if (id.startsWith('FBS-')) return 'fbs';
41
+ if (id.startsWith('TS-')) return 'ts';
42
+ if (id.startsWith('TC-')) return 'tc';
43
+ // Phase 10 (X2 CodeNode bridge, spec D12): CN cosmetic class.
44
+ if (id.startsWith('CN-')) return 'cn';
45
+ return 'unknown';
46
+ }
47
+
48
+ function nodeLabel(id) {
49
+ return `"${id}"`;
50
+ }
51
+
52
+ /**
53
+ * Emit `flowchart LR` mermaid text for a query result.
54
+ *
55
+ * @param {object} result
56
+ * @param {'coverage' | 'trace' | 'impact'} verb
57
+ * @returns {string}
58
+ */
59
+ export function formatMermaid(result, verb) {
60
+ if (verb === 'coverage') return renderCoverageMermaid(result);
61
+ if (verb === 'trace') return renderTraceMermaid(result);
62
+ if (verb === 'impact') return renderImpactMermaid(result);
63
+ return '';
64
+ }
65
+
66
+ function renderCoverageMermaid(result) {
67
+ const lines = ['flowchart LR'];
68
+ const seenIds = new Set();
69
+ // w-2026-07-28-005: TCs whose pointer does not resolve carry the
70
+ // `broken` class on top of `tc` - dashed red stroke, so a stub TC is
71
+ // visibly not the same thing as a resolving one.
72
+ const unresolvedTcIds = new Set();
73
+ const declare = (id) => {
74
+ if (seenIds.has(id)) return;
75
+ seenIds.add(id);
76
+ lines.push(` ${id}[${nodeLabel(id)}]`);
77
+ };
78
+ for (const req of result.requirements) {
79
+ declare(req.id);
80
+ for (const ac of req.acs) {
81
+ declare(ac.id);
82
+ lines.push(` ${req.id} --> ${ac.id}`);
83
+ const unresolved = new Set(ac.unresolvedTestCases ?? []);
84
+ for (const tc of ac.testCases) {
85
+ declare(tc);
86
+ lines.push(` ${ac.id} -.-> ${tc}`);
87
+ if (unresolved.has(tc)) unresolvedTcIds.add(tc);
88
+ }
89
+ }
90
+ }
91
+ for (const tc of unresolvedTcIds) {
92
+ lines.push(` class ${tc} broken;`);
93
+ }
94
+ return `${appendClassBlock(lines, seenIds)}\n`;
95
+ }
96
+
97
+ function renderTraceMermaid(result) {
98
+ if (!result.found) return `%% trace: id ${result.pivot} not found\n`;
99
+ if (result.direction === 'both') {
100
+ // Two adjacent flowchart LR blocks with the pivot appearing in both,
101
+ // so the pivot visually anchors between ancestors and descendants
102
+ // (spec §D9).
103
+ const pivotId = result.pivot;
104
+ const ancestors = result.ancestors ?? [];
105
+ const descendants = result.descendants ?? [];
106
+ const ancBlock = buildTraceBlock({
107
+ title: 'Ancestors',
108
+ pivot: pivotId,
109
+ nodes: [{ id: pivotId, kind: 'pivot', depth: 0 }, ...ancestors],
110
+ edges: pairToEdges(ancestors, pivotId, /*isAncestor*/ true),
111
+ });
112
+ const descBlock = buildTraceBlock({
113
+ title: 'Descendants',
114
+ pivot: pivotId,
115
+ nodes: [{ id: pivotId, kind: 'pivot', depth: 0 }, ...descendants],
116
+ edges: pairToEdges(descendants, pivotId, /*isAncestor*/ false),
117
+ });
118
+ return `${ancBlock}\n\n${descBlock}\n`;
119
+ }
120
+ const lines = ['flowchart LR'];
121
+ const seenIds = new Set();
122
+ const declare = (id) => {
123
+ if (seenIds.has(id)) return;
124
+ seenIds.add(id);
125
+ lines.push(` ${id}[${nodeLabel(id)}]`);
126
+ };
127
+ for (const n of result.nodes ?? []) declare(n.id);
128
+ for (const e of result.edges ?? []) {
129
+ const arrow = e.kind === 'crossLink' ? '-.->' : '-->';
130
+ lines.push(` ${e.from} ${arrow} ${e.to}`);
131
+ }
132
+ return `${appendClassBlock(lines, seenIds)}\n`;
133
+ }
134
+
135
+ function renderImpactMermaid(result) {
136
+ if (!result.found) return `%% impact: id ${result.pivot} not found\n`;
137
+ const lines = ['flowchart LR'];
138
+ const seenIds = new Set();
139
+ const declare = (id) => {
140
+ if (seenIds.has(id)) return;
141
+ seenIds.add(id);
142
+ lines.push(` ${id}[${nodeLabel(id)}]`);
143
+ };
144
+ for (const n of result.nodes ?? []) declare(n.id);
145
+ for (const e of result.edges ?? []) {
146
+ const arrow = e.kind === 'crossLink' ? '-.->' : '-->';
147
+ lines.push(` ${e.from} ${arrow} ${e.to}`);
148
+ }
149
+ return `${appendClassBlock(lines, seenIds)}\n`;
150
+ }
151
+
152
+ // -- helpers --
153
+
154
+ function buildTraceBlock({ nodes, edges }) {
155
+ const lines = ['flowchart LR'];
156
+ const seen = new Set();
157
+ const declare = (id) => {
158
+ if (seen.has(id)) return;
159
+ seen.add(id);
160
+ lines.push(` ${id}[${nodeLabel(id)}]`);
161
+ };
162
+ for (const n of nodes) declare(n.id);
163
+ for (const e of edges) {
164
+ const arrow = e.kind === 'crossLink' ? '-.->' : '-->';
165
+ lines.push(` ${e.from} ${arrow} ${e.to}`);
166
+ }
167
+ return appendClassBlock(lines, seen);
168
+ }
169
+
170
+ // The both-direction trace result does not preserve raw edges; synthesise
171
+ // a linear chain from the pivot through ancestors / descendants in
172
+ // depth order. Edge kind defaults to parentChild because we don't know
173
+ // the arrival relationship at this layer - the caller uses the raw
174
+ // forward / back envelopes when precise edge kinds matter.
175
+ function pairToEdges(list, pivot, isAncestor) {
176
+ const edges = [];
177
+ if (list.length === 0) return edges;
178
+ if (isAncestor) {
179
+ // Ancestors sorted by depth descending (closest first). Chain the
180
+ // pivot -> first ancestor -> next ancestor. Depth on the trace
181
+ // result is negative, closest ancestor has depth -1.
182
+ let prev = pivot;
183
+ const sorted = [...list].sort((a, b) => (b.depth ?? 0) - (a.depth ?? 0));
184
+ for (const n of sorted) {
185
+ edges.push({ from: n.id, to: prev, kind: 'parentChild' });
186
+ prev = n.id;
187
+ }
188
+ return edges;
189
+ }
190
+ // Descendants: pivot -> depth 1 -> depth 2 -> ...
191
+ const sorted = [...list].sort((a, b) => (a.depth ?? 0) - (b.depth ?? 0));
192
+ let prev = pivot;
193
+ for (const n of sorted) {
194
+ edges.push({ from: prev, to: n.id, kind: 'parentChild' });
195
+ prev = n.id;
196
+ }
197
+ return edges;
198
+ }
199
+
200
+ function appendClassBlock(lines, ids) {
201
+ for (const id of ids) {
202
+ const cls = classForId(id);
203
+ if (cls !== 'unknown') lines.push(` class ${id} ${cls};`);
204
+ }
205
+ lines.push(CLASS_DEFS);
206
+ return lines.join('\n');
207
+ }
208
+
209
+ export { classForId, CLASS_DEFS };
@@ -0,0 +1,203 @@
1
+ // ASCII table formatter for coverage / trace / impact result envelopes.
2
+ // Zero-dep, uses Node built-in string padding. Column widths adapt to
3
+ // the widest cell per column, capped at a sane per-column maximum so a
4
+ // single very long title doesn't force the whole table off-screen.
5
+ //
6
+ // Phase 5 §D3: default output format. British English column headers,
7
+ // ASCII hyphens only (§D19).
8
+
9
+ const COLUMN_CAP = 60;
10
+
11
+ /**
12
+ * Format a query result as an ASCII table (or two labelled blocks
13
+ * around a pivot for trace --both). Trailing newline included.
14
+ *
15
+ * @param {object} result
16
+ * @param {'coverage' | 'trace' | 'impact'} verb
17
+ * @returns {string}
18
+ */
19
+ export function formatTable(result, verb) {
20
+ if (verb === 'coverage') return formatCoverageTable(result);
21
+ if (verb === 'trace') return formatTraceTable(result);
22
+ if (verb === 'impact') return formatImpactTable(result);
23
+ return '';
24
+ }
25
+
26
+ // w-2026-07-28-005: three-state cell vocabulary. 'unresolved' = TC rows
27
+ // exist but none of their pointers resolves to a real test - the stub-TC
28
+ // state, shown as its own word rather than folded into yes/no.
29
+ function reqCoveredCell(req) {
30
+ if (req.covered) return 'yes';
31
+ return req.coverageClass === 'covered-unresolved' ? 'unresolved' : 'no';
32
+ }
33
+
34
+ function acCoveredCell(ac) {
35
+ if (ac.covered) return 'yes';
36
+ return ac.testCases.length > 0 ? 'unresolved' : 'no';
37
+ }
38
+
39
+ function testCasesCell(ac) {
40
+ if (ac.testCases.length === 0) return '-';
41
+ const unresolved = new Set(ac.unresolvedTestCases ?? []);
42
+ return ac.testCases
43
+ .map((tc) => (unresolved.has(tc) ? `${tc}[unresolved]` : tc))
44
+ .join(', ');
45
+ }
46
+
47
+ function formatCoverageTable(result) {
48
+ const lines = [];
49
+ const modeLabel = result.strict ? 'strict (per-AC)' : 'shallow-any';
50
+ lines.push(`Coverage mode: ${modeLabel}`);
51
+ lines.push(
52
+ `Requirements: ${result.totals.requirements} ` +
53
+ `covered: ${result.totals.covered} ` +
54
+ `covered-unresolved: ${result.totals.coveredUnresolved} ` +
55
+ `uncovered: ${result.totals.uncovered}`,
56
+ );
57
+ lines.push('');
58
+ const rows = [];
59
+ // Phase 10 (X2 CodeNode bridge, D11): --with-code appends a Code column.
60
+ const withCode = Boolean(result.withCode);
61
+ const header = ['Requirement', 'Covered', 'AC', 'AC covered', 'Test cases'];
62
+ if (withCode) header.push('Code');
63
+ rows.push(header);
64
+ for (const req of result.requirements) {
65
+ if (req.acs.length === 0) {
66
+ const row = [req.id, reqCoveredCell(req), '(no AC)', '-', '-'];
67
+ if (withCode) row.push('-');
68
+ rows.push(row);
69
+ continue;
70
+ }
71
+ for (const [i, ac] of req.acs.entries()) {
72
+ const row = [
73
+ i === 0 ? req.id : '',
74
+ i === 0 ? reqCoveredCell(req) : '',
75
+ ac.id,
76
+ acCoveredCell(ac),
77
+ testCasesCell(ac),
78
+ ];
79
+ if (withCode) row.push(ac.codeClass ?? '-');
80
+ rows.push(row);
81
+ }
82
+ }
83
+ lines.push(renderTable(rows));
84
+ const unresolvedPointers = result.unresolvedTestPointers ?? [];
85
+ if (unresolvedPointers.length > 0) {
86
+ lines.push('');
87
+ lines.push('Unresolved test pointers (never counted as coverage):');
88
+ for (const u of unresolvedPointers) {
89
+ const pointer = u.testPointer ?? '(no pointer)';
90
+ lines.push(` ${u.tcId} (${u.tsId}): ${pointer} - ${u.reason}`);
91
+ }
92
+ }
93
+ if (withCode) {
94
+ lines.push('');
95
+ lines.push(
96
+ `Code totals: implemented-and-covered=${result.codeTotals.implementedAndCovered} ` +
97
+ `implemented-uncovered=${result.codeTotals.implementedUncovered} ` +
98
+ `unimplemented=${result.codeTotals.unimplemented}`,
99
+ );
100
+ lines.push(
101
+ result.codeNodeOrphans.length > 0
102
+ ? `CN-orphaned (no implementsAcIds): ${result.codeNodeOrphans.join(', ')}`
103
+ : 'CN-orphaned (no implementsAcIds): (none)',
104
+ );
105
+ }
106
+ return `${lines.join('\n')}\n`;
107
+ }
108
+
109
+ function formatTraceTable(result) {
110
+ if (!result.found) return `trace: id ${result.pivot} not found\n`;
111
+ if (result.direction === 'both') {
112
+ return formatBothTraceTable(result);
113
+ }
114
+ const lines = [];
115
+ const directionLabel = result.direction === 'forward' ? 'Descendants' : 'Ancestors';
116
+ lines.push(`Trace pivot: ${result.pivot} direction: ${result.direction}`);
117
+ lines.push('');
118
+ const rows = [];
119
+ rows.push(['Depth', 'Id', 'Kind', 'Title']);
120
+ const showList = (result.nodes ?? []).slice();
121
+ if (showList.length <= 1) {
122
+ lines.push(`${directionLabel}: (none)`);
123
+ } else {
124
+ for (const n of showList) {
125
+ rows.push([String(n.depth), n.id, n.kind, cellTitle(n.id)]);
126
+ }
127
+ lines.push(renderTable(rows));
128
+ }
129
+ return `${lines.join('\n')}\n`;
130
+ }
131
+
132
+ function formatBothTraceTable(result) {
133
+ const lines = [];
134
+ lines.push(`Trace pivot: ${result.pivot} direction: both`);
135
+ lines.push('');
136
+ lines.push('Ancestors:');
137
+ const ancestors = result.ancestors ?? [];
138
+ if (ancestors.length === 0) {
139
+ lines.push(' (none)');
140
+ } else {
141
+ const rows = [['Depth', 'Id', 'Kind', 'Title']];
142
+ for (const n of ancestors) rows.push([String(n.depth), n.id, n.kind, '']);
143
+ lines.push(renderTable(rows));
144
+ }
145
+ lines.push('');
146
+ lines.push(`Pivot: ${result.pivot}`);
147
+ lines.push('');
148
+ lines.push('Descendants:');
149
+ const descendants = result.descendants ?? [];
150
+ if (descendants.length === 0) {
151
+ lines.push(' (none)');
152
+ } else {
153
+ const rows = [['Depth', 'Id', 'Kind', 'Title']];
154
+ for (const n of descendants) rows.push([String(n.depth), n.id, n.kind, '']);
155
+ lines.push(renderTable(rows));
156
+ }
157
+ return `${lines.join('\n')}\n`;
158
+ }
159
+
160
+ function formatImpactTable(result) {
161
+ if (!result.found) return `impact: id ${result.pivot} not found\n`;
162
+ const lines = [];
163
+ lines.push(`Impact pivot: ${result.pivot}`);
164
+ lines.push('');
165
+ const rows = [['Id', 'Kind', 'Role', 'Action needed']];
166
+ for (const n of result.nodes ?? []) {
167
+ rows.push([n.id, n.kind, n.role, n.actionNeeded ?? '-']);
168
+ }
169
+ lines.push(renderTable(rows));
170
+ return `${lines.join('\n')}\n`;
171
+ }
172
+
173
+ // Table renderer. Every cell is stringified, truncated at COLUMN_CAP,
174
+ // then padded to the column's widest observed cell. Deterministic
175
+ // spacing; no ANSI colours; two spaces between columns.
176
+
177
+ function renderTable(rows) {
178
+ const widths = new Array(rows[0].length).fill(0);
179
+ const cells = rows.map((r) => r.map((c) => {
180
+ let s = c == null ? '' : String(c);
181
+ if (s.length > COLUMN_CAP) s = `${s.slice(0, COLUMN_CAP - 3)}...`;
182
+ return s;
183
+ }));
184
+ for (const r of cells) {
185
+ for (let i = 0; i < r.length; i += 1) {
186
+ if (r[i].length > widths[i]) widths[i] = r[i].length;
187
+ }
188
+ }
189
+ const out = cells.map(
190
+ (r) => r.map((c, i) => c.padEnd(widths[i])).join(' ').replace(/ +$/, ''),
191
+ );
192
+ // Header separator: `-` under each column.
193
+ const sep = widths.map((w) => '-'.repeat(w)).join(' ').replace(/ +$/, '');
194
+ return [out[0], sep, ...out.slice(1)].join('\n');
195
+ }
196
+
197
+ // Placeholder for future title lookup - the pure trace result does not
198
+ // carry doc bodies, so title columns render blank unless the caller
199
+ // passes a title source. Left here for the trace `Title` column so the
200
+ // column stays if we ever wire in a title map without a schema change.
201
+ function cellTitle(_id) {
202
+ return '';
203
+ }
@@ -0,0 +1,173 @@
1
+ // Pure impact compute. Impact = trace-forward + trace-back + a
2
+ // labelled `actionNeeded` column per node kind + role (Phase 5 §D7).
3
+ //
4
+ // The delta over `computeTrace(..., 'both')` is the labelled
5
+ // `actionNeeded` column: it turns a raw traversal into an actionable
6
+ // re-verify / re-approve fan-out that the operator can walk through
7
+ // end-to-end.
8
+ //
9
+ // Action label rules (§D7):
10
+ // - PRD ancestor: "re-approve"
11
+ // - TAD ancestor: "review-arch"
12
+ // - BS ancestor: "review-plan"
13
+ // - REQ / US descendant: "review-scope"
14
+ // - AC descendant: "re-approve"
15
+ // - TS descendant: "re-verify"
16
+ // - TC descendant: "re-run"
17
+ // - FBS descendant: "re-execute" (cross-linked to an affected AC,
18
+ // or a dependent of an affected FBS)
19
+ // - TAC / ADR descendant: "review-context" (reached via a
20
+ // contextRequirements cross-link, which is what
21
+ // the current impact fan-out surfaces)
22
+ // - Pivot: null
23
+
24
+ import { computeTrace, kindOf } from './trace.js';
25
+
26
+ /**
27
+ * @typedef {import('#core/store/walker.js').TreeModel} TreeModel
28
+ */
29
+
30
+ /**
31
+ * @typedef {'pivot' | 'ancestor' | 'descendant'} ImpactRole
32
+ */
33
+
34
+ /**
35
+ * @typedef {object} ImpactNode
36
+ * @property {string} id
37
+ * @property {string} kind
38
+ * @property {ImpactRole} role
39
+ * @property {string | null} actionNeeded
40
+ */
41
+
42
+ /**
43
+ * @typedef {object} ImpactEdge
44
+ * @property {string} from
45
+ * @property {string} to
46
+ * @property {'parentChild' | 'crossLink'} kind
47
+ */
48
+
49
+ /**
50
+ * @typedef {object} ImpactResult
51
+ * @property {string} pivot
52
+ * @property {boolean} found - false when the pivot id is unknown
53
+ * @property {ImpactNode[]} [nodes]
54
+ * @property {ImpactEdge[]} [edges]
55
+ */
56
+
57
+ /**
58
+ * Compute impact for a pivot. Composes computeTrace('both') and layers
59
+ * the D7 actionNeeded column per (kind, role).
60
+ *
61
+ * @param {TreeModel} tree
62
+ * @param {object} opts
63
+ * @param {string} opts.id
64
+ * @param {boolean} [opts.includeCode] - Phase 10: extend the forward fan-out
65
+ * into Code Nodes.
66
+ * @returns {ImpactResult}
67
+ */
68
+ export function computeImpact(tree, { id, includeCode = false }) {
69
+ const pivotKind = kindOf(tree, id);
70
+ if (!pivotKind) return { pivot: id, found: false };
71
+
72
+ const back = computeTrace(tree, { id, direction: 'back' });
73
+ // expandFbsDependents: impact is the blast-radius question, so the FBS
74
+ // dependency fan-out is in scope here (D7: a dependent of an affected
75
+ // FBS is labelled re-execute). Plain trace keeps FBS as a cross-link
76
+ // leaf (AC-402-3).
77
+ const fwd = computeTrace(tree, {
78
+ id, direction: 'forward', includeCode, expandFbsDependents: true,
79
+ });
80
+
81
+ /** @type {ImpactNode[]} */
82
+ const nodes = [];
83
+ /** @type {ImpactEdge[]} */
84
+ const edges = [];
85
+ const seen = new Set();
86
+
87
+ const push = (n) => {
88
+ if (seen.has(n.id)) return;
89
+ seen.add(n.id);
90
+ nodes.push(n);
91
+ };
92
+
93
+ push({ id, kind: pivotKind, role: 'pivot', actionNeeded: null });
94
+
95
+ const backNodes = (back.nodes ?? []).filter((n) => n.id !== id);
96
+ for (const n of backNodes) {
97
+ push({
98
+ id: n.id,
99
+ kind: n.kind,
100
+ role: 'ancestor',
101
+ actionNeeded: labelFor(n.kind, 'ancestor'),
102
+ });
103
+ }
104
+ const fwdNodes = (fwd.nodes ?? []).filter((n) => n.id !== id);
105
+ for (const n of fwdNodes) {
106
+ push({
107
+ id: n.id,
108
+ kind: n.kind,
109
+ role: 'descendant',
110
+ actionNeeded: labelFor(n.kind, 'descendant'),
111
+ });
112
+ }
113
+
114
+ // Edges: combine both directions. De-dupe on (from, to) so the same
115
+ // edge doesn't appear twice when the forward and back traces share a
116
+ // segment (they don't, structurally, but keep the guard for safety).
117
+ const edgeKey = (e) => `${e.from}->${e.to}`;
118
+ const edgeSeen = new Set();
119
+ for (const e of (back.edges ?? []).concat(fwd.edges ?? [])) {
120
+ const k = edgeKey(e);
121
+ if (edgeSeen.has(k)) continue;
122
+ edgeSeen.add(k);
123
+ edges.push(e);
124
+ }
125
+
126
+ return { pivot: id, found: true, nodes, edges };
127
+ }
128
+
129
+ /**
130
+ * Map (kind, role) to a D7 action label. Returns null when no rule
131
+ * applies (either because it's the pivot or because the kind isn't
132
+ * in the D7 table for that role - a design defensive default).
133
+ *
134
+ * @param {string} kind
135
+ * @param {ImpactRole} role
136
+ * @returns {string | null}
137
+ */
138
+ export function labelFor(kind, role) {
139
+ if (role === 'pivot') return null;
140
+ if (role === 'ancestor') {
141
+ switch (kind) {
142
+ case 'prd': return 're-approve';
143
+ case 'tad': return 'review-arch';
144
+ case 'buildSequence': return 'review-plan';
145
+ case 'req': return 'review-scope';
146
+ case 'userStory': return 'review-scope';
147
+ case 'ac': return 're-approve';
148
+ case 'testSuite': return 're-verify';
149
+ case 'tac': return 'review-arch';
150
+ case 'adr': return 'review-arch';
151
+ case 'fbs': return 're-execute';
152
+ default: return null;
153
+ }
154
+ }
155
+ // descendant
156
+ switch (kind) {
157
+ case 'prd': return 're-approve';
158
+ case 'tad': return 'review-arch';
159
+ case 'buildSequence': return 'review-plan';
160
+ case 'req': return 'review-scope';
161
+ case 'userStory': return 'review-scope';
162
+ case 'ac': return 're-approve';
163
+ case 'testSuite': return 're-verify';
164
+ case 'tc': return 're-run';
165
+ case 'fbs': return 're-execute';
166
+ case 'tac': return 'review-context';
167
+ case 'adr': return 'review-context';
168
+ // Phase 10 (X2 CodeNode bridge): a code node reached forward from a
169
+ // spec change is source that may need re-implementation + re-test.
170
+ case 'codeNode': return 're-verify-code';
171
+ default: return null;
172
+ }
173
+ }
@@ -0,0 +1,13 @@
1
+ // Public surface for the query layer (Phase 5 §D16). Pure logic +
2
+ // formatters; handlers live under `src/cli/`.
3
+
4
+ export { computeCoverage, classifyCoverageScope } from './coverage.js';
5
+ export { computeTrace, kindOf } from './trace.js';
6
+ export { computeImpact, labelFor } from './impact.js';
7
+ export { formatTable } from './formatters/table.js';
8
+ export { formatJson } from './formatters/json.js';
9
+ export { formatMermaid } from './formatters/mermaid.js';
10
+ // 0.8.0 slug-train car 3: NV-BL-SR-03 addendum (ruling-sheet item 1)
11
+ // -- traceability / query tools share the refuse-first posture that
12
+ // gates rcf build. Callers wrap their query producer with this.
13
+ export { runWithAdmissibilityGate } from './refuse-on-admissibility.js';
@@ -0,0 +1,73 @@
1
+ // Traceability / query tool refuse-first wrapper (NV-BL-SR-03
2
+ // addendum on ruling-sheet item 1, ratified 2026-08-11).
3
+ //
4
+ // The ruleset's `toolScope` block declares:
5
+ // { chainAdmissibility: true, traceabilityAndQueryTools: true }
6
+ //
7
+ // meaning the same refusal semantics that gate `rcf build` also gate
8
+ // the traceability and query tools. A tool that hides an admissibility
9
+ // failure is the same class of defect as a build that hides one.
10
+ //
11
+ // This module wraps a query result so a REFUSE verdict from
12
+ // `enforceAdmissibility` short-circuits the tool's output. Callers
13
+ // pass the walker tree and the chain's declared ruleset version; on
14
+ // REFUSE the wrapper returns a refusal envelope naming the unresolved
15
+ // findings. On PASS or PASS-WITH-OVERRIDES the query's own result flows
16
+ // through unchanged.
17
+
18
+ import { enforceAdmissibility, getRulesetToolScope } from '#admissibility';
19
+
20
+ /**
21
+ * @typedef {import('../admissibility/enforce.js').AdmissibilityVerdict} AdmissibilityVerdict
22
+ * @typedef {import('../admissibility/enforce.js').AdmissibilityOverride} AdmissibilityOverride
23
+ */
24
+
25
+ /**
26
+ * @typedef {object} QueryResult
27
+ * @property {'ok' | 'refused-admissibility'} status
28
+ * @property {AdmissibilityVerdict} [admissibility] - always present so callers can log.
29
+ * @property {*} [payload] - the underlying query result on status 'ok'.
30
+ * @property {string} [refusal] - human-readable summary on 'refused-admissibility'.
31
+ */
32
+
33
+ /**
34
+ * Wrap a query producer with the refuse-first posture. The producer is
35
+ * only called when admissibility passes (or passes-with-overrides);
36
+ * on refusal, its produce function does NOT run and the wrapper
37
+ * returns a refusal envelope naming the unresolved rules.
38
+ *
39
+ * @param {object} args
40
+ * @param {object} args.tree
41
+ * @param {string|null} [args.chainRulesetVersion]
42
+ * @param {AdmissibilityOverride[]} [args.overrides]
43
+ * @param {() => (Promise<*> | *)} args.produce - the underlying query
44
+ * @param {object} [args.opts] - passed through to enforceAdmissibility
45
+ * @returns {Promise<QueryResult>}
46
+ */
47
+ export async function runWithAdmissibilityGate({
48
+ tree,
49
+ chainRulesetVersion = null,
50
+ overrides = [],
51
+ produce,
52
+ opts = {},
53
+ } = {}) {
54
+ const toolScope = await getRulesetToolScope();
55
+ if (!toolScope.traceabilityAndQueryTools) {
56
+ // Ruleset opted out of tool-scope gating (currently the artefact
57
+ // ships with this on -- item 1 addendum -- but the switch is
58
+ // read at runtime so a future ruleset revision can amend it).
59
+ const payload = await Promise.resolve(produce());
60
+ return { status: 'ok', payload };
61
+ }
62
+ const verdict = await enforceAdmissibility({ tree, chainRulesetVersion, overrides, opts });
63
+ if (verdict.verdict === 'refuse') {
64
+ const ruleIds = [...new Set(verdict.unresolved.map((f) => f.rule).filter(Boolean))].sort();
65
+ return {
66
+ status: 'refused-admissibility',
67
+ admissibility: verdict,
68
+ refusal: `traceability/query tool refused (NV-BL-SR-03 addendum): unresolved admissibility rules [${ruleIds.join(', ')}]. Fix or record a NV-BL-ADM-05 override before re-querying.`,
69
+ };
70
+ }
71
+ const payload = await Promise.resolve(produce());
72
+ return { status: 'ok', admissibility: verdict, payload };
73
+ }