rcf-lite 0.0.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/LICENSE +202 -0
  3. package/README.md +92 -4
  4. package/bin/rcf-verify.js +122 -0
  5. package/bin/rcf.js +174 -0
  6. package/bin/view-supervisor-child.mjs +14 -0
  7. package/fixtures/canary-manifest.json +103 -0
  8. package/guidance/README.md +25 -0
  9. package/guidance/build-cycle-playbook.md +492 -0
  10. package/guidance/build-cycle.md +40 -0
  11. package/guidance/document-model.md +45 -0
  12. package/guidance/elicitation-playbook.md +442 -0
  13. package/guidance/harness-template.md +246 -0
  14. package/guidance/managed/README.md +63 -0
  15. package/guidance/managed/agent-instructions-block.hash +1 -0
  16. package/guidance/managed/agent-instructions-block.md +220 -0
  17. package/guidance/managed/legacy-fragment-hashes.json +22 -0
  18. package/guidance/manifest.json +21 -0
  19. package/guidance/overview.md +48 -0
  20. package/guidance/persona-programme.md +44 -0
  21. package/package.json +69 -8
  22. package/rcf/adrs/adr-001.json +25 -0
  23. package/rcf/adrs/adr-002.json +25 -0
  24. package/rcf/adrs/adr-003.json +25 -0
  25. package/rcf/adrs/adr-004.json +20 -0
  26. package/rcf/adrs/adr-005.json +20 -0
  27. package/rcf/adrs/adr-006.json +25 -0
  28. package/rcf/adrs/adr-007.json +25 -0
  29. package/rcf/adrs/adr-008.json +25 -0
  30. package/rcf/adrs/adr-009.json +25 -0
  31. package/rcf/build-sequence.json +11 -0
  32. package/rcf/code-nodes/cn-001.json +14 -0
  33. package/rcf/code-nodes/cn-002.json +14 -0
  34. package/rcf/code-nodes/cn-003.json +15 -0
  35. package/rcf/code-nodes/cn-004.json +17 -0
  36. package/rcf/code-nodes/cn-005.json +19 -0
  37. package/rcf/code-nodes/cn-006.json +17 -0
  38. package/rcf/code-nodes/cn-007.json +16 -0
  39. package/rcf/code-nodes/cn-008.json +19 -0
  40. package/rcf/code-nodes/cn-009.json +16 -0
  41. package/rcf/code-nodes/cn-010.json +18 -0
  42. package/rcf/code-nodes/cn-011.json +18 -0
  43. package/rcf/code-nodes/cn-012.json +18 -0
  44. package/rcf/code-nodes/cn-013.json +19 -0
  45. package/rcf/code-nodes/cn-014.json +16 -0
  46. package/rcf/code-nodes/cn-015.json +16 -0
  47. package/rcf/code-nodes/cn-016.json +16 -0
  48. package/rcf/code-nodes/cn-017.json +16 -0
  49. package/rcf/code-nodes/cn-018.json +15 -0
  50. package/rcf/code-nodes/cn-019.json +16 -0
  51. package/rcf/code-nodes/cn-020.json +16 -0
  52. package/rcf/code-nodes/cn-021.json +16 -0
  53. package/rcf/code-nodes/cn-022.json +16 -0
  54. package/rcf/code-nodes/cn-023.json +16 -0
  55. package/rcf/code-nodes/cn-024.json +16 -0
  56. package/rcf/code-nodes/cn-025.json +16 -0
  57. package/rcf/code-nodes/cn-026.json +16 -0
  58. package/rcf/code-nodes/cn-027.json +16 -0
  59. package/rcf/code-nodes/cn-028.json +16 -0
  60. package/rcf/code-nodes/cn-029.json +19 -0
  61. package/rcf/code-nodes/cn-030.json +14 -0
  62. package/rcf/code-nodes/cn-031.json +14 -0
  63. package/rcf/code-nodes/cn-032.json +14 -0
  64. package/rcf/code-nodes/cn-033.json +14 -0
  65. package/rcf/code-nodes/cn-034.json +14 -0
  66. package/rcf/code-nodes/cn-035.json +14 -0
  67. package/rcf/code-nodes/cn-036.json +14 -0
  68. package/rcf/code-nodes/cn-037.json +14 -0
  69. package/rcf/code-nodes/cn-038.json +14 -0
  70. package/rcf/code-nodes/cn-039.json +14 -0
  71. package/rcf/code-nodes/cn-040.json +14 -0
  72. package/rcf/code-nodes/cn-041.json +14 -0
  73. package/rcf/code-nodes/cn-042.json +14 -0
  74. package/rcf/code-nodes/cn-043.json +14 -0
  75. package/rcf/code-nodes/cn-044.json +14 -0
  76. package/rcf/code-nodes/cn-045.json +14 -0
  77. package/rcf/code-nodes/cn-046.json +14 -0
  78. package/rcf/code-nodes/cn-047.json +14 -0
  79. package/rcf/code-nodes/cn-048.json +14 -0
  80. package/rcf/code-nodes/cn-049.json +14 -0
  81. package/rcf/code-nodes/cn-050.json +14 -0
  82. package/rcf/code-nodes/cn-051.json +14 -0
  83. package/rcf/code-nodes/cn-052.json +14 -0
  84. package/rcf/code-nodes/cn-053.json +14 -0
  85. package/rcf/code-nodes/cn-054.json +14 -0
  86. package/rcf/code-nodes/cn-055.json +14 -0
  87. package/rcf/code-nodes/cn-056.json +14 -0
  88. package/rcf/code-nodes/cn-057.json +14 -0
  89. package/rcf/fbs/fbs-001.json +49 -0
  90. package/rcf/fbs/fbs-002.json +42 -0
  91. package/rcf/fbs/fbs-003.json +37 -0
  92. package/rcf/fbs/fbs-004.json +39 -0
  93. package/rcf/fbs/fbs-005.json +38 -0
  94. package/rcf/fbs/fbs-006.json +48 -0
  95. package/rcf/fbs/fbs-007.json +39 -0
  96. package/rcf/fbs/fbs-008.json +40 -0
  97. package/rcf/fbs/fbs-009.json +36 -0
  98. package/rcf/fbs/fbs-010.json +41 -0
  99. package/rcf/fbs/fbs-011.json +36 -0
  100. package/rcf/fbs/fbs-012.json +46 -0
  101. package/rcf/fbs/fbs-013.json +42 -0
  102. package/rcf/fbs/fbs-014.json +49 -0
  103. package/rcf/fbs/fbs-015.json +32 -0
  104. package/rcf/manifest.json +17 -0
  105. package/rcf/prd.json +47 -0
  106. package/rcf/requirements/req-001.json +19 -0
  107. package/rcf/requirements/req-002.json +19 -0
  108. package/rcf/requirements/req-003.json +19 -0
  109. package/rcf/requirements/req-004.json +19 -0
  110. package/rcf/requirements/req-005.json +19 -0
  111. package/rcf/requirements/req-006.json +19 -0
  112. package/rcf/requirements/req-007.json +19 -0
  113. package/rcf/requirements/req-008.json +19 -0
  114. package/rcf/requirements/req-009.json +19 -0
  115. package/rcf/tacs/tac-001.json +45 -0
  116. package/rcf/tacs/tac-002.json +109 -0
  117. package/rcf/tacs/tac-003.json +40 -0
  118. package/rcf/tacs/tac-004.json +51 -0
  119. package/rcf/tacs/tac-005.json +52 -0
  120. package/rcf/tacs/tac-006.json +104 -0
  121. package/rcf/tacs/tac-007.json +38 -0
  122. package/rcf/tacs/tac-008.json +51 -0
  123. package/rcf/tad.json +51 -0
  124. package/rcf/test-suites/PENDING.md +23 -0
  125. package/rcf/test-suites/ts-001.json +38 -0
  126. package/rcf/test-suites/ts-002.json +38 -0
  127. package/rcf/test-suites/ts-003.json +43 -0
  128. package/rcf/test-suites/ts-004.json +44 -0
  129. package/rcf/test-suites/ts-005.json +30 -0
  130. package/rcf/test-suites/ts-006.json +36 -0
  131. package/rcf/test-suites/ts-007.json +43 -0
  132. package/rcf/test-suites/ts-008.json +37 -0
  133. package/rcf/test-suites/ts-009.json +38 -0
  134. package/rcf/test-suites/ts-010.json +38 -0
  135. package/rcf/test-suites/ts-011.json +44 -0
  136. package/rcf/test-suites/ts-012.json +36 -0
  137. package/rcf/test-suites/ts-013.json +38 -0
  138. package/rcf/test-suites/ts-014.json +38 -0
  139. package/rcf/test-suites/ts-015.json +38 -0
  140. package/rcf/test-suites/ts-016.json +37 -0
  141. package/rcf/test-suites/ts-017.json +38 -0
  142. package/rcf/test-suites/ts-018.json +38 -0
  143. package/rcf/test-suites/ts-019.json +37 -0
  144. package/rcf/test-suites/ts-020.json +46 -0
  145. package/rcf/test-suites/ts-021.json +46 -0
  146. package/rcf/test-suites/ts-022.json +46 -0
  147. package/rcf/test-suites/ts-023.json +46 -0
  148. package/rcf/test-suites/ts-024.json +46 -0
  149. package/rcf/test-suites/ts-025.json +52 -0
  150. package/rcf/user-stories/us-101.json +40 -0
  151. package/rcf/user-stories/us-102.json +40 -0
  152. package/rcf/user-stories/us-201.json +40 -0
  153. package/rcf/user-stories/us-202.json +40 -0
  154. package/rcf/user-stories/us-203.json +32 -0
  155. package/rcf/user-stories/us-301.json +40 -0
  156. package/rcf/user-stories/us-302.json +40 -0
  157. package/rcf/user-stories/us-303.json +40 -0
  158. package/rcf/user-stories/us-304.json +40 -0
  159. package/rcf/user-stories/us-401.json +40 -0
  160. package/rcf/user-stories/us-402.json +40 -0
  161. package/rcf/user-stories/us-403.json +40 -0
  162. package/rcf/user-stories/us-501.json +40 -0
  163. package/rcf/user-stories/us-502.json +40 -0
  164. package/rcf/user-stories/us-503.json +40 -0
  165. package/rcf/user-stories/us-601.json +40 -0
  166. package/rcf/user-stories/us-602.json +40 -0
  167. package/rcf/user-stories/us-701.json +40 -0
  168. package/rcf/user-stories/us-702.json +40 -0
  169. package/rcf/user-stories/us-801.json +49 -0
  170. package/rcf/user-stories/us-802.json +49 -0
  171. package/rcf/user-stories/us-803.json +49 -0
  172. package/rcf/user-stories/us-804.json +49 -0
  173. package/rcf/user-stories/us-805.json +49 -0
  174. package/rcf/user-stories/us-901.json +40 -0
  175. package/src/.gitkeep +0 -0
  176. package/src/browser-verify/auth-smoke.js +109 -0
  177. package/src/browser-verify/index.js +29 -0
  178. package/src/browser-verify/invariants.js +336 -0
  179. package/src/browser-verify/manifest-writer.js +189 -0
  180. package/src/browser-verify/runner.js +171 -0
  181. package/src/build/bundle.js +198 -0
  182. package/src/build/formatters/json.js +18 -0
  183. package/src/build/formatters/markdown.js +406 -0
  184. package/src/build/index.js +14 -0
  185. package/src/build/mark.js +177 -0
  186. package/src/build/queue.js +285 -0
  187. package/src/cli/browser-verify.js +231 -0
  188. package/src/cli/build.js +584 -0
  189. package/src/cli/coverage.js +219 -0
  190. package/src/cli/create.js +355 -0
  191. package/src/cli/delete.js +127 -0
  192. package/src/cli/design.js +353 -0
  193. package/src/cli/doctor.js +548 -0
  194. package/src/cli/fbs.js +171 -0
  195. package/src/cli/finalise.js +377 -0
  196. package/src/cli/guidance.js +206 -0
  197. package/src/cli/help.js +156 -0
  198. package/src/cli/impact.js +119 -0
  199. package/src/cli/init.js +282 -0
  200. package/src/cli/intake.js +153 -0
  201. package/src/cli/link.js +128 -0
  202. package/src/cli/mcp.js +160 -0
  203. package/src/cli/preflight.js +220 -0
  204. package/src/cli/read.js +162 -0
  205. package/src/cli/req-baseline.js +269 -0
  206. package/src/cli/req-classify.js +135 -0
  207. package/src/cli/review.js +295 -0
  208. package/src/cli/test-suite.js +216 -0
  209. package/src/cli/trace.js +172 -0
  210. package/src/cli/ui-baseline.js +292 -0
  211. package/src/cli/ui-classify.js +108 -0
  212. package/src/cli/update.js +197 -0
  213. package/src/cli/validate.js +168 -0
  214. package/src/cli/view.js +452 -0
  215. package/src/core/baseline-catalog/data/auth.json +42 -0
  216. package/src/core/baseline-catalog/data/http-api.json +42 -0
  217. package/src/core/baseline-catalog/data/notifications.json +33 -0
  218. package/src/core/baseline-catalog/data/persistence.json +33 -0
  219. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  220. package/src/core/baseline-catalog/index.js +121 -0
  221. package/src/core/errors/index.js +167 -0
  222. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  223. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  224. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  225. package/src/core/isolation/index.js +60 -0
  226. package/src/core/mcp/framing.js +103 -0
  227. package/src/core/mcp/index.js +8 -0
  228. package/src/core/mcp/server.js +228 -0
  229. package/src/core/patterns/register-canary.js +209 -0
  230. package/src/core/patterns/req-shapes.js +158 -0
  231. package/src/core/patterns/services.js +358 -0
  232. package/src/core/patterns/ui-shapes.js +166 -0
  233. package/src/core/store/cn-resolve.js +134 -0
  234. package/src/core/store/derive-deps.js +93 -0
  235. package/src/core/store/ids.js +78 -0
  236. package/src/core/store/index.js +20 -0
  237. package/src/core/store/init.js +255 -0
  238. package/src/core/store/loader.js +211 -0
  239. package/src/core/store/tp-resolve.js +176 -0
  240. package/src/core/store/validator.js +191 -0
  241. package/src/core/store/walker.js +898 -0
  242. package/src/core/store/writer.js +1849 -0
  243. package/src/design/index.js +11 -0
  244. package/src/design/writer.js +271 -0
  245. package/src/finalise/detect.js +129 -0
  246. package/src/finalise/index.js +17 -0
  247. package/src/finalise/ingest.js +119 -0
  248. package/src/finalise/install.js +119 -0
  249. package/src/finalise/ship-without-verified.js +131 -0
  250. package/src/finalise/spawn.js +84 -0
  251. package/src/intake/fidelity.js +105 -0
  252. package/src/intake/index.js +6 -0
  253. package/src/intake/manifest-writer.js +100 -0
  254. package/src/intake/orchestrator.js +138 -0
  255. package/src/intake/validate.js +80 -0
  256. package/src/mcp/map-errors.js +131 -0
  257. package/src/mcp/prompts.js +49 -0
  258. package/src/mcp/resources.js +244 -0
  259. package/src/mcp/tools.js +1204 -0
  260. package/src/preflight/design-shapes.js +185 -0
  261. package/src/preflight/index.js +90 -0
  262. package/src/preflight/manifest-writer.js +264 -0
  263. package/src/preflight/scanner.js +206 -0
  264. package/src/preflight/secrets.js +134 -0
  265. package/src/preflight/session.js +246 -0
  266. package/src/query/attestation.js +285 -0
  267. package/src/query/coverage.js +308 -0
  268. package/src/query/formatters/json.js +21 -0
  269. package/src/query/formatters/mermaid.js +209 -0
  270. package/src/query/formatters/table.js +203 -0
  271. package/src/query/impact.js +173 -0
  272. package/src/query/index.js +9 -0
  273. package/src/query/trace.js +345 -0
  274. package/src/register-canary/fixture-loader.js +87 -0
  275. package/src/register-canary/index.js +10 -0
  276. package/src/register-canary/record-writer.js +132 -0
  277. package/src/register-canary/runner.js +156 -0
  278. package/src/req-baseline/gate.js +86 -0
  279. package/src/req-baseline/index.js +27 -0
  280. package/src/req-baseline/open-candidates.js +143 -0
  281. package/src/req-baseline/opt-out.js +195 -0
  282. package/src/req-baseline/sweep.js +230 -0
  283. package/src/req-detection/classifier.js +181 -0
  284. package/src/req-detection/index.js +9 -0
  285. package/src/req-detection/persist.js +55 -0
  286. package/src/review/index.js +325 -0
  287. package/src/review/mutation.js +117 -0
  288. package/src/review/ui-baseline-drift.js +138 -0
  289. package/src/server/index.js +178 -0
  290. package/src/server/routes.js +110 -0
  291. package/src/server/sse.js +118 -0
  292. package/src/setup/agent-setup.js +362 -0
  293. package/src/setup/identity-seed.js +104 -0
  294. package/src/setup/knowledge-seed.js +123 -0
  295. package/src/setup/managed-block.js +193 -0
  296. package/src/setup/managed-gitignore.js +166 -0
  297. package/src/setup/managed-markers.js +49 -0
  298. package/src/ui-baseline/defaults.js +119 -0
  299. package/src/ui-baseline/index.js +25 -0
  300. package/src/ui-baseline/manifest-writer.js +282 -0
  301. package/src/ui-baseline/session.js +178 -0
  302. package/src/ui-detection/classifier.js +192 -0
  303. package/src/verify/chain/index.js +190 -0
  304. package/src/verify/cli/cleanup.js +61 -0
  305. package/src/verify/cli/help.js +56 -0
  306. package/src/verify/cli/mcp.js +98 -0
  307. package/src/verify/cli/provision.js +71 -0
  308. package/src/verify/cli/report.js +71 -0
  309. package/src/verify/cli/run.js +155 -0
  310. package/src/verify/engine/brief.js +87 -0
  311. package/src/verify/engine/index.js +177 -0
  312. package/src/verify/engine/launcher.js +307 -0
  313. package/src/verify/mcp/tools.js +107 -0
  314. package/src/verify/profile/index.js +146 -0
  315. package/src/verify/provision/index.js +256 -0
  316. package/src/verify/report/index.js +139 -0
  317. package/src/verify/report/renderer.js +118 -0
  318. package/src/verify/verdict/index.js +246 -0
  319. package/src/view/doc-renderers/adr.js +44 -0
  320. package/src/view/doc-renderers/build-sequence.js +40 -0
  321. package/src/view/doc-renderers/fbs.js +128 -0
  322. package/src/view/doc-renderers/helpers.js +159 -0
  323. package/src/view/doc-renderers/index.js +12 -0
  324. package/src/view/doc-renderers/prd.js +45 -0
  325. package/src/view/doc-renderers/req.js +43 -0
  326. package/src/view/doc-renderers/tac.js +38 -0
  327. package/src/view/doc-renderers/tad.js +74 -0
  328. package/src/view/doc-renderers/test-suite.js +45 -0
  329. package/src/view/doc-renderers/user-story.js +63 -0
  330. package/src/view/html-page.js +462 -0
  331. package/src/view/index.js +63 -0
  332. package/src/view/live-client.js +338 -0
  333. package/src/view/mermaid-diagram.js +178 -0
  334. package/src/view/style.css +735 -0
  335. package/src/view/tree-model.js +152 -0
  336. package/src/view/vendored/mermaid.min.js +2607 -0
  337. package/src/view-supervisor/index.js +26 -0
  338. package/src/view-supervisor/logs.js +32 -0
  339. package/src/view-supervisor/manifest-writer.js +178 -0
  340. package/src/view-supervisor/persist-until.js +85 -0
  341. package/src/view-supervisor/supervisor.js +276 -0
  342. package/src/watch/index.js +152 -0
@@ -0,0 +1,325 @@
1
+ // REVIEW-stage test-theatre audit + mutation-sampling
2
+ // (verification-integrity-cluster-spec §5.5, §6).
3
+ //
4
+ // Two surfaces composed into ONE Review pass, emitting ONE
5
+ // reviewAudit record per FBS:
6
+ //
7
+ // 1. test-theatre audit (deterministic): five finding categories from
8
+ // §5.5 - mockOnlyIntegrationClaim, testPointerBroken,
9
+ // assertionStrengthWeak, acIdsCoverageDrift, otherDeclared. This
10
+ // module implements the four deterministic ones; assertion strength
11
+ // needs a code-reading agent, so v1 raises it only via the
12
+ // mutation-sampling side, not the deterministic sweep (the sweep is
13
+ // the belt, the mutation-sampling agent is the braces).
14
+ //
15
+ // 2. mutation-sampling (agent-driven, injectable): the runner accepts
16
+ // a `mutationRunner` dependency conforming to the spec §6 shape.
17
+ // The runner takes the FBS diff and TS/TC list and returns the
18
+ // mutation-sampling record. When no runner is wired, the audit
19
+ // emits a `notes` entry explaining that the sampler was not run;
20
+ // the reviewAudit record is still valid schema.
21
+
22
+ import { classifyAttestationProfile, aggregateAttestationsByAc } from '../query/attestation.js';
23
+
24
+ /**
25
+ * @typedef {import('#core/store/walker.js').TreeModel} TreeModel
26
+ * @typedef {'mockOnlyIntegrationClaim'|'testPointerBroken'|'assertionStrengthWeak'|'acIdsCoverageDrift'|'uiBaselineDrift'|'otherDeclared'} TestTheatreFindingKind
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} TestTheatreFinding
31
+ * @property {string} [tsId] required for test-theatre kinds; omitted on kind=uiBaselineDrift (rcf-schemas 0.4.2)
32
+ * @property {string} [tcId]
33
+ * @property {string} [anchorId] free-form anchor for kinds without a test suite (for example, uiBaselineDrift, anchored on an FBS id or file path)
34
+ * @property {TestTheatreFindingKind} kind
35
+ * @property {string} detail
36
+ * @property {'advisory'|'warn'|'block'} severity
37
+ * @property {string} [kindDescription]
38
+ */
39
+
40
+ /**
41
+ * @typedef {object} MutationSurvivor
42
+ * @property {string} mutationId
43
+ * @property {string} targetFile
44
+ * @property {string} [targetSymbol]
45
+ * @property {string} mutationSummary
46
+ * @property {string[]} acIds
47
+ * @property {string[]} [tsIdsShouldHaveCaught]
48
+ * @property {string[]} [tcIdsShouldHaveCaught]
49
+ */
50
+
51
+ /**
52
+ * @typedef {object} MutationSamplingRecord
53
+ * @property {string} mode
54
+ * @property {number} mutantsGenerated
55
+ * @property {number} mutantsRun
56
+ * @property {number} killed
57
+ * @property {number} survived
58
+ * @property {number} [durationMs]
59
+ * @property {MutationSurvivor[]} [survivors]
60
+ * @property {string} [notes]
61
+ */
62
+
63
+ const INTEGRATION_LEVELS = new Set(['integration', 'e2e', 'contract']);
64
+ const MOCK_PROFILES = new Set(['mock', 'stub', 'fixture']);
65
+
66
+ /**
67
+ * Detect `mockOnlyIntegrationClaim`: a TS whose testLevel is
68
+ * integration-or-higher and every TC records `runtimeProvenance.profile`
69
+ * in {mock, stub, fixture} AND at least one bound AC's aggregated
70
+ * attestation is live or sandboxed. This is the exact d-142 failure
71
+ * mode.
72
+ *
73
+ * @param {TreeModel} tree
74
+ * @param {object} fbs
75
+ * @returns {TestTheatreFinding[]}
76
+ */
77
+ function detectMockOnlyIntegrationClaim(tree, fbs) {
78
+ const findings = [];
79
+ const attByAc = aggregateAttestationsByAc(tree);
80
+ const fbsAcIds = new Set(fbs.acIds ?? []);
81
+ // Every TS that covers one of the FBS's ACs.
82
+ const relevantTs = (tree.testSuites ?? []).filter((ts) => (ts.acIds ?? []).some((a) => fbsAcIds.has(a)));
83
+ for (const ts of relevantTs) {
84
+ if (!INTEGRATION_LEVELS.has(ts.testLevel)) continue;
85
+ const tcs = ts.testCases ?? [];
86
+ if (tcs.length === 0) continue;
87
+ const allMockShaped = tcs.every((tc) => MOCK_PROFILES.has(tc?.runtimeProvenance?.profile ?? ''));
88
+ if (!allMockShaped) continue;
89
+ const bindingsOnFbsAcs = (ts.acIds ?? [])
90
+ .filter((a) => fbsAcIds.has(a))
91
+ .flatMap((a) => attByAc.get(a) ?? []);
92
+ const liveish = bindingsOnFbsAcs.find((b) => b.attestationMode === 'live' || b.attestationMode === 'sandboxed');
93
+ if (liveish) {
94
+ findings.push({
95
+ tsId: ts.id,
96
+ kind: 'mockOnlyIntegrationClaim',
97
+ detail: `${ts.id} testLevel=${ts.testLevel} but every TC's runtimeProvenance.profile is in {mock,stub,fixture}, while AC bindings include service ${liveish.serviceId} attested ${liveish.attestationMode}.`,
98
+ severity: 'block',
99
+ });
100
+ }
101
+ }
102
+ return findings;
103
+ }
104
+
105
+ /**
106
+ * Detect `testPointerBroken`: any TC whose `testPointer` is missing or
107
+ * fails resolution. Uses the pre-computed testPointers map (a resolved
108
+ * pointer's `.resolved === true`). When no map is supplied, we skip
109
+ * this check (the caller must pass one for the audit to police it).
110
+ *
111
+ * @param {TreeModel} tree
112
+ * @param {object} fbs
113
+ * @param {Map<string, { resolved?: boolean, testPointer?: string|null, reason?: string }>} [testPointers]
114
+ * @returns {TestTheatreFinding[]}
115
+ */
116
+ function detectTestPointerBroken(tree, fbs, testPointers) {
117
+ if (!testPointers || testPointers.size === 0) return [];
118
+ const findings = [];
119
+ const fbsAcIds = new Set(fbs.acIds ?? []);
120
+ for (const ts of tree.testSuites ?? []) {
121
+ for (const tc of ts.testCases ?? []) {
122
+ if (!fbsAcIds.has(tc.acId)) continue;
123
+ const key = `${ts.id}::${tc.id}`;
124
+ const resolution = testPointers.get(key);
125
+ if (!resolution || resolution.resolved !== true) {
126
+ findings.push({
127
+ tsId: ts.id,
128
+ tcId: tc.id,
129
+ kind: 'testPointerBroken',
130
+ detail: `TC ${tc.id} testPointer '${resolution?.testPointer ?? tc.testPointer ?? '(missing)'}' does not resolve: ${resolution?.reason ?? 'missing-pointer'}.`,
131
+ severity: 'block',
132
+ });
133
+ }
134
+ }
135
+ }
136
+ return findings;
137
+ }
138
+
139
+ /**
140
+ * Detect `acIdsCoverageDrift`: an FBS acId that no TS covers, OR a
141
+ * covering TS acId that this FBS does not claim.
142
+ *
143
+ * @param {TreeModel} tree
144
+ * @param {object} fbs
145
+ * @returns {TestTheatreFinding[]}
146
+ */
147
+ function detectAcIdsCoverageDrift(tree, fbs) {
148
+ const findings = [];
149
+ const fbsAcIds = new Set(fbs.acIds ?? []);
150
+ for (const ts of tree.testSuites ?? []) {
151
+ const covers = (ts.acIds ?? []).some((a) => fbsAcIds.has(a));
152
+ if (!covers) continue;
153
+ for (const a of ts.acIds ?? []) {
154
+ if (!fbsAcIds.has(a)) {
155
+ findings.push({
156
+ tsId: ts.id,
157
+ kind: 'acIdsCoverageDrift',
158
+ detail: `${ts.id} covers ${a}, but ${fbs.fbsId} does not claim ${a} in acIds[].`,
159
+ severity: 'warn',
160
+ });
161
+ }
162
+ }
163
+ }
164
+ // The reverse ("FBS claims an AC no TS covers") is intentionally
165
+ // deferred to `coverage --strict`, which already raises exit 4 on
166
+ // uncovered ACs and needs no tsId to do so (the reviewAudit schema
167
+ // requires a valid tsId on every testTheatreFinding, and the "no TS
168
+ // covers" case has none to name). Belt and braces: coverage is the
169
+ // belt, this audit is the braces.
170
+ return findings;
171
+ }
172
+
173
+ /**
174
+ * Detect `attestationDrift` findings as test-theatre entries. This is
175
+ * the passWithWarn class from §3.5 (declaredMockOnly × live) plus the
176
+ * refuse class (already blocking coverage --strict, but re-raised here
177
+ * so the Review record surfaces it too).
178
+ *
179
+ * The kind used is `mockOnlyIntegrationClaim` when the drift refuses
180
+ * live-attested ACs on mock-profile TCs; the standalone `attestationDrift`
181
+ * kind is not in the schema enum (spec §3.4 fixed enum), so v1 folds
182
+ * these into the closest matching kind (`mockOnlyIntegrationClaim` for
183
+ * live-attested drift, `otherDeclared` with a kindDescription for the
184
+ * passWithWarn drift). This is spec-faithful — §5.5 defines only the
185
+ * enumerated `kind` set, and downstream consumers key on that set.
186
+ *
187
+ * @param {TreeModel} tree
188
+ * @param {object} fbs
189
+ * @returns {TestTheatreFinding[]}
190
+ */
191
+ function detectAttestationDrift(tree, fbs) {
192
+ const findings = [];
193
+ const attByAc = aggregateAttestationsByAc(tree);
194
+ const fbsAcIds = new Set(fbs.acIds ?? []);
195
+ for (const ts of tree.testSuites ?? []) {
196
+ for (const tc of ts.testCases ?? []) {
197
+ if (!fbsAcIds.has(tc.acId)) continue;
198
+ const profile = tc?.runtimeProvenance?.profile;
199
+ if (typeof profile !== 'string') continue;
200
+ const bindings = attByAc.get(tc.acId) ?? [];
201
+ for (const { serviceId, attestationMode } of bindings) {
202
+ const cell = classifyAttestationProfile(attestationMode, profile);
203
+ if (cell.verdict === 'passWithWarn') {
204
+ findings.push({
205
+ tsId: ts.id,
206
+ tcId: tc.id,
207
+ kind: 'otherDeclared',
208
+ kindDescription: 'attestationDrift',
209
+ detail: `${ts.id}/${tc.id} on ${tc.acId} (service ${serviceId}): ${cell.reason}`,
210
+ severity: 'warn',
211
+ });
212
+ }
213
+ }
214
+ }
215
+ }
216
+ return findings;
217
+ }
218
+
219
+ /**
220
+ * Run the deterministic test-theatre audit on one FBS. Returns the
221
+ * findings list (aggregated across the four detectors).
222
+ *
223
+ * @param {object} args
224
+ * @param {TreeModel} args.tree
225
+ * @param {object} args.fbs
226
+ * @param {Map<string, object>} [args.testPointers]
227
+ * @returns {TestTheatreFinding[]}
228
+ */
229
+ export function auditTestTheatre({ tree, fbs, testPointers }) {
230
+ return [
231
+ ...detectMockOnlyIntegrationClaim(tree, fbs),
232
+ ...detectTestPointerBroken(tree, fbs, testPointers),
233
+ ...detectAcIdsCoverageDrift(tree, fbs),
234
+ ...detectAttestationDrift(tree, fbs),
235
+ ];
236
+ }
237
+
238
+ /**
239
+ * @typedef {object} ReviewAuditRecord
240
+ * @property {string} id `ra-<fbsId>-<n>`
241
+ * @property {string} fbsId
242
+ * @property {string} createdAt
243
+ * @property {TestTheatreFinding[]} testTheatreFindings
244
+ * @property {MutationSamplingRecord} [mutationSampling]
245
+ * @property {'pass'|'warn'|'block'} verdict
246
+ */
247
+
248
+ /**
249
+ * Aggregate a verdict from the findings + mutation sampling per spec
250
+ * §5.5: block if any finding severity is block or any mutation
251
+ * survivor traces to an in-scope AC; warn if any warn and no block;
252
+ * pass otherwise.
253
+ *
254
+ * Review N-2 (non-blocking): an unwired mutation runner
255
+ * (`mode: 'agent-v1-not-wired'`) is promoted to `warn` even on an
256
+ * otherwise-clean audit. Without this, an unwired runner is
257
+ * indistinguishable from a wired runner that killed every mutant:
258
+ * both emit `verdict: pass`, so the exit-code layer conflates "audit
259
+ * clean" with "audit did not run". Warn forces the operator to wire a
260
+ * runner or pass `--skip-mutation` (mode: 'skipped'), which remains
261
+ * pass as an explicit operator choice.
262
+ *
263
+ * @param {TestTheatreFinding[]} findings
264
+ * @param {MutationSamplingRecord} [mutationSampling]
265
+ * @returns {'pass'|'warn'|'block'}
266
+ */
267
+ export function aggregateVerdict(findings, mutationSampling) {
268
+ const severities = findings.map((f) => f.severity);
269
+ if (severities.includes('block')) return 'block';
270
+ if ((mutationSampling?.survivors?.length ?? 0) > 0) return 'block';
271
+ if (severities.includes('warn')) return 'warn';
272
+ if (mutationSampling?.mode === 'agent-v1-not-wired') return 'warn';
273
+ return 'pass';
274
+ }
275
+
276
+ /**
277
+ * Monotonic id allocator for the reviewAudit array.
278
+ *
279
+ * @param {object|null} manifest
280
+ * @param {string} fbsId
281
+ * @returns {string}
282
+ */
283
+ export function nextReviewAuditId(manifest, fbsId) {
284
+ const prefix = `ra-${fbsId}-`;
285
+ const existing = Array.isArray(manifest?.reviewAudit) ? manifest.reviewAudit : [];
286
+ let maxN = 0;
287
+ for (const rec of existing) {
288
+ if (typeof rec?.id !== 'string' || !rec.id.startsWith(prefix)) continue;
289
+ const n = Number.parseInt(rec.id.slice(prefix.length), 10);
290
+ if (Number.isFinite(n) && n > maxN) maxN = n;
291
+ }
292
+ return `${prefix}${maxN + 1}`;
293
+ }
294
+
295
+ /**
296
+ * Compose the record. Timestamps at UTC ISO.
297
+ *
298
+ * @param {object} args
299
+ * @param {TreeModel} args.tree
300
+ * @param {object} args.fbs
301
+ * @param {TestTheatreFinding[]} args.findings
302
+ * @param {MutationSamplingRecord} [args.mutationSampling]
303
+ * @param {Date} [args.now]
304
+ * @returns {ReviewAuditRecord}
305
+ */
306
+ export function composeReviewAuditRecord({ tree, fbs, findings, mutationSampling, now = new Date() }) {
307
+ const id = nextReviewAuditId(tree.manifest, fbs.fbsId);
308
+ const verdict = aggregateVerdict(findings, mutationSampling);
309
+ const record = {
310
+ id,
311
+ fbsId: fbs.fbsId,
312
+ createdAt: now.toISOString(),
313
+ testTheatreFindings: findings.map((f) => normaliseFinding(f)),
314
+ verdict,
315
+ };
316
+ if (mutationSampling) record.mutationSampling = mutationSampling;
317
+ return record;
318
+ }
319
+
320
+ function normaliseFinding(f) {
321
+ const out = { tsId: f.tsId, kind: f.kind, detail: f.detail, severity: f.severity };
322
+ if (typeof f.tcId === 'string' && f.tcId.length > 0) out.tcId = f.tcId;
323
+ if (typeof f.kindDescription === 'string' && f.kindDescription.length > 0) out.kindDescription = f.kindDescription;
324
+ return out;
325
+ }
@@ -0,0 +1,117 @@
1
+ // Agent-driven mutation-sampling coordinator
2
+ // (verification-integrity-cluster-spec §6).
3
+ //
4
+ // This module owns the ORCHESTRATION contract, not the agent dispatch.
5
+ // The actual mutation-sampling is done by a Review-stage subagent (§6.5,
6
+ // §6.6) that runs in a worktree, applies each mutation, runs the test
7
+ // command, and records kill / survive. The subagent lives outside the
8
+ // build package (it is a Claude Code dispatch orchestrated by the
9
+ // harness, spec §6.7 note that native tool adapters are v2). Build
10
+ // provides:
11
+ // - the CONTRACT the runner must satisfy (function shape, input, output);
12
+ // - a DEFAULT runner that no-ops with a clear note when no wiring is
13
+ // supplied (the record still validates against the schema); this
14
+ // lets `rcf review` return a valid reviewAudit record on any repo,
15
+ // even before the harness is set up;
16
+ // - the SIZING policy (§6.3) and TEST-COMMAND resolver (§6.4).
17
+ //
18
+ // The wire-up: `rcf review` accepts `--mutation-runner <name>` (in a
19
+ // future iteration) or a dep-injected runner (this iteration). In tests,
20
+ // injection is direct. In production, the harness passes a runner that
21
+ // dispatches an Opus 4.7 subagent per the estate ladder.
22
+
23
+ /**
24
+ * @typedef {object} MutationRunnerInput
25
+ * @property {string} fbsId
26
+ * @property {string[]} acIds the FBS's in-scope ACs
27
+ * @property {object[]} testSuites the TSes that cover any of the ACs
28
+ * @property {object} sizing min/max/timeBudgetMs
29
+ * @property {string} [testCommand] resolved test command (§6.4)
30
+ * @property {string} projectRoot
31
+ */
32
+
33
+ /**
34
+ * @typedef {object} MutationRunnerOutput
35
+ * @property {import('./index.js').MutationSamplingRecord} record
36
+ */
37
+
38
+ /**
39
+ * @typedef {(input: MutationRunnerInput) => Promise<MutationRunnerOutput>} MutationRunner
40
+ */
41
+
42
+ /**
43
+ * Resolve the sizing policy for a mutation-sampling pass. Spec §6.3:
44
+ * 10 min, 30 max, ~3 mutants per 50 LOC of changed function bodies,
45
+ * default 10-minute time budget. LOC counting is the runner's job;
46
+ * this returns the plain envelope.
47
+ *
48
+ * @param {object} [opts]
49
+ * @param {number} [opts.timeBudgetMs]
50
+ * @returns {{ min: number, max: number, timeBudgetMs: number }}
51
+ */
52
+ export function defaultSizing({ timeBudgetMs = 10 * 60 * 1000 } = {}) {
53
+ return { min: 10, max: 30, timeBudgetMs };
54
+ }
55
+
56
+ /**
57
+ * Test command resolution (§6.4): manifest.testCommand -> flag ->
58
+ * fallback. Returns the resolved command string OR the reason a
59
+ * fallback was reached. Never throws — the caller records the reason
60
+ * on the mutation-sampling record when applicable.
61
+ *
62
+ * @param {object} args
63
+ * @param {object|null} args.manifest
64
+ * @param {string} [args.cliFlag]
65
+ * @param {boolean} args.hasPackageJson
66
+ * @returns {{ command: string|null, source: 'manifest'|'flag'|'fallback'|'unresolved' }}
67
+ */
68
+ export function resolveTestCommand({ manifest, cliFlag, hasPackageJson }) {
69
+ if (typeof cliFlag === 'string' && cliFlag.length > 0) {
70
+ return { command: cliFlag, source: 'flag' };
71
+ }
72
+ if (typeof manifest?.testCommand === 'string' && manifest.testCommand.length > 0) {
73
+ return { command: manifest.testCommand, source: 'manifest' };
74
+ }
75
+ if (hasPackageJson) return { command: 'npm test', source: 'fallback' };
76
+ return { command: null, source: 'unresolved' };
77
+ }
78
+
79
+ /**
80
+ * Default runner: emits a valid mutation-sampling record with zero
81
+ * mutants and a `notes` explaining that no runner was wired. This is
82
+ * the "no-op with honesty" path — the reviewAudit record stays valid,
83
+ * the audit's verdict is not spoofed, and the operator sees an
84
+ * explicit message rather than a silent green.
85
+ *
86
+ * @type {MutationRunner}
87
+ */
88
+ export async function defaultMutationRunner() {
89
+ return {
90
+ record: {
91
+ mode: 'agent-v1-not-wired',
92
+ mutantsGenerated: 0,
93
+ mutantsRun: 0,
94
+ killed: 0,
95
+ survived: 0,
96
+ notes: 'No mutation-sampling runner supplied. Wire one via the harness (spec §6.5-§6.7) or run --skip-mutation to acknowledge the omission on the record.',
97
+ },
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Explicit skip: same shape as the default runner but says so.
103
+ *
104
+ * @type {MutationRunner}
105
+ */
106
+ export async function skippedMutationRunner() {
107
+ return {
108
+ record: {
109
+ mode: 'skipped',
110
+ mutantsGenerated: 0,
111
+ mutantsRun: 0,
112
+ killed: 0,
113
+ survived: 0,
114
+ notes: 'Mutation sampling skipped by --skip-mutation.',
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,138 @@
1
+ // Stage-3 UI baseline drift audit
2
+ // (ui-design-gate-0.7.0-spec §3.4, §7 mandate 1, mandate 3).
3
+ //
4
+ // Runs alongside the existing Track A test-theatre audit and emits
5
+ // findings on the same reviewAudit record under the `uiBaselineDrift`
6
+ // kind (schema-registered on `reviewAudit.testTheatreFindings[].kind`
7
+ // per rcf-schemas@0.4.0).
8
+ //
9
+ // Two deterministic checks in v1:
10
+ // 1. `noHexInViewFiles` (§7 mandate 1): any view file matches
11
+ // /#[0-9a-fA-F]{3,8}\b/ while the baseline sets
12
+ // `defaults.noHexInViewFiles: true`. Severity: block. Scope is
13
+ // configurable via `uiBaseline.defaults.viewFileGlobs` (default
14
+ // ["src/ui/**"], minus the themeTokensModule path).
15
+ // 2. `sharedLayoutImport` (§7 mandate 3): route files (default heuristic
16
+ // src/routes/**) do NOT import the sharedLayoutModule declared on
17
+ // the baseline. Severity: block.
18
+ //
19
+ // Both checks are opt-out-aware via `uiBaseline.operatorOptOuts[]`; the
20
+ // finding is still recorded (so the operator can see the drift), but
21
+ // severity demotes to `advisory` when the opt-out matches.
22
+ //
23
+ // The audit is a pure function of (fbs, uiBaseline, file listing +
24
+ // contents). Callers supply the file listing so tests can stub without
25
+ // touching disk.
26
+
27
+ import { readFile } from 'node:fs/promises';
28
+ import { join } from 'node:path';
29
+
30
+ /**
31
+ * @typedef {import('./index.js').TestTheatreFinding} TestTheatreFinding
32
+ */
33
+
34
+ const HEX_RE = /#[0-9a-fA-F]{3,8}\b/;
35
+
36
+ /**
37
+ * Run the UI-baseline drift audit on one FBS. Requires `uiBaseline`
38
+ * present on the manifest; when absent, returns an empty list (the
39
+ * baseline is the ruling, and no baseline means nothing to drift from
40
+ * for this audit - coverage of that state lives elsewhere).
41
+ *
42
+ * @param {object} args
43
+ * @param {string} args.projectRoot
44
+ * @param {object} args.fbs
45
+ * @param {object|null} args.uiBaseline
46
+ * @param {(patterns: string[]) => Promise<string[]>} args.listFiles glob helper (injectable; project-root-relative paths returned)
47
+ * @returns {Promise<TestTheatreFinding[]>}
48
+ */
49
+ export async function auditUiBaselineDrift({ projectRoot, fbs, uiBaseline, listFiles }) {
50
+ /** @type {TestTheatreFinding[]} */
51
+ const findings = [];
52
+ if (!uiBaseline || fbs?.uiBearing !== true) return findings;
53
+
54
+ const defaults = uiBaseline.defaults ?? {};
55
+ const optOuts = new Set((uiBaseline.operatorOptOuts ?? []).map((o) => o.field));
56
+
57
+ // 1. noHexInViewFiles
58
+ if (defaults.noHexInViewFiles === true) {
59
+ const globs = Array.isArray(defaults.viewFileGlobs) && defaults.viewFileGlobs.length > 0
60
+ ? defaults.viewFileGlobs
61
+ : ['src/ui/**'];
62
+ const tokensModule = typeof defaults.designTokensModule === 'string' ? defaults.designTokensModule : null;
63
+ const files = await safeListFiles(listFiles, globs);
64
+ for (const rel of files) {
65
+ if (tokensModule && rel === tokensModule) continue;
66
+ const abs = join(projectRoot, rel);
67
+ let contents;
68
+ try { contents = await readFile(abs, 'utf8'); } catch { continue; }
69
+ const match = HEX_RE.exec(contents);
70
+ if (match) {
71
+ const severity = optOuts.has('noHexInViewFiles') ? 'advisory' : 'block';
72
+ findings.push({
73
+ // Track B review N-5 (@stravica-ai/rcf-schemas 0.4.2): the
74
+ // finding anchors on the FBS id via the dedicated anchorId
75
+ // field. Prior versions smuggled fbs.fbsId through the tsId
76
+ // slot to satisfy the pre-0.4.2 blanket required rule; the
77
+ // slot mismatch was a real category-vs-slot defect (a
78
+ // downstream reader assuming tsId names a TS would follow a
79
+ // broken pointer). 0.4.2 keeps tsId required for the
80
+ // test-theatre kinds only.
81
+ anchorId: fbs.fbsId,
82
+ kind: 'uiBaselineDrift',
83
+ detail: `hex literal ${match[0]} detected in ${rel} (baseline defaults.noHexInViewFiles: true). Move colours into ${tokensModule ?? 'the design tokens module'}.`,
84
+ severity,
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ // 2. sharedLayoutImport
91
+ if (typeof defaults.sharedLayoutModule === 'string' && defaults.sharedLayoutModule.length > 0) {
92
+ const routeGlobs = Array.isArray(defaults.routeFileGlobs) && defaults.routeFileGlobs.length > 0
93
+ ? defaults.routeFileGlobs
94
+ : ['src/routes/**'];
95
+ const layoutModule = defaults.sharedLayoutModule;
96
+ const layoutBase = basename(layoutModule).replace(/\.[jt]sx?$/, '');
97
+ const files = await safeListFiles(listFiles, routeGlobs);
98
+ for (const rel of files) {
99
+ const abs = join(projectRoot, rel);
100
+ let contents;
101
+ try { contents = await readFile(abs, 'utf8'); } catch { continue; }
102
+ // A route file is expected to import the shared layout module,
103
+ // either by the module path or by the module basename. Both are
104
+ // recognised; the check is a coarse heuristic, not a formal AST.
105
+ const hasImport = contents.includes(layoutModule)
106
+ || new RegExp(`\\b${escapeForRegExp(layoutBase)}\\b`).test(contents);
107
+ if (!hasImport) {
108
+ const severity = optOuts.has('sharedLayoutModule') ? 'advisory' : 'block';
109
+ findings.push({
110
+ // See the noHexInViewFiles comment above for the anchorId
111
+ // rationale (rcf-schemas 0.4.2, Track B review N-5).
112
+ anchorId: fbs.fbsId,
113
+ kind: 'uiBaselineDrift',
114
+ detail: `${rel} does not import the shared layout module (${layoutModule}); baseline mandate 3 says every route uses one layout.`,
115
+ severity,
116
+ });
117
+ }
118
+ }
119
+ }
120
+ return findings;
121
+ }
122
+
123
+ async function safeListFiles(listFiles, patterns) {
124
+ if (typeof listFiles !== 'function') return [];
125
+ try {
126
+ const raw = await listFiles(patterns);
127
+ return Array.isArray(raw) ? raw : [];
128
+ } catch { return []; }
129
+ }
130
+
131
+ function basename(path) {
132
+ const idx = String(path).lastIndexOf('/');
133
+ return idx >= 0 ? String(path).slice(idx + 1) : String(path);
134
+ }
135
+
136
+ function escapeForRegExp(s) {
137
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
138
+ }