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,256 @@
1
+ // Prerequisite provisioning (spec §6, amendment 2). Adversarial verification
2
+ // usually can't start from nothing. One mechanism, three kinds of
3
+ // prerequisite the chain's ACs may imply: auth accounts (common), third-party
4
+ // service sandboxes/keys, and seeded test data.
5
+ //
6
+ // Hard rules this module enforces (each a §10 acceptance criterion):
7
+ // - the `zzverify-` prefix on ALL provisioned artefacts, so they are
8
+ // greppable and sweepable;
9
+ // - credentials/keys go ONLY to the --provision file, NEVER inline, NEVER
10
+ // echoed to logs or the report body (redactSecrets is the guard);
11
+ // - what cannot be provisioned is BLOCKED (naming the missing prerequisite)
12
+ // and its dependent ACs are marked BLOCKED — never silently skipped;
13
+ // - cleanup removes provisioned artefacts and the report states what it did.
14
+
15
+ import { writeFile, readFile } from 'node:fs/promises';
16
+
17
+ import { rcfError } from '#core/errors';
18
+ import { matchServiceSignals } from '#core/patterns/services';
19
+
20
+ /** Greppable prefix on every provisioned artefact (accounts + data alike). */
21
+ export const ZZVERIFY_PREFIX = 'zzverify-';
22
+
23
+ /** v1 auth provisioning stands up at least this many accounts (multi-user isolation journeys). */
24
+ export const MIN_AUTH_ACCOUNTS = 2;
25
+
26
+ /** Field names whose VALUES must never reach a log or the report body. */
27
+ export const SECRET_FIELDS = Object.freeze(['password', 'token', 'secret', 'key', 'apiKey', 'credential', 'cookie']);
28
+
29
+ const AUTH_PATTERNS = /\b(sign[\s-]?in|sign[\s-]?up|log[\s-]?in|logout|register|registration|authenticat|account|password|credential|session|onboard)/i;
30
+ const SEEDDATA_PATTERNS = /\b(admin[\s-]?(created|seeded)|seeded|pre[\s-]?populated|existing (record|row|dataset)|another user|user b\b|other user)/i;
31
+
32
+ /**
33
+ * Classify the prerequisite kind an AC implies (or null if none). Matches on
34
+ * the AC's given/when/then/description text. Heuristic — the honest hard part
35
+ * (§6): where the route can't be derived, the caller BLOCKS rather than skips.
36
+ *
37
+ * The `serviceSandbox` branch is delegated to core's shared pattern set
38
+ * (`#core/patterns/services`), the same seed data
39
+ * consumed by build's pre-flight scanner (verification-integrity-cluster-spec
40
+ * §5.3 / §8.4). Verify's original inline SERVICE_PATTERNS regex is
41
+ * superseded — a single shared source removes the class of drift that
42
+ * shipped the d-2026-07-30-142 "email channel" miss.
43
+ *
44
+ * @param {object} ac - a flattened AC ({acId, description, given, when, then})
45
+ * @returns {'authAccount'|'serviceSandbox'|'seedData'|null}
46
+ */
47
+ export function classifyPrerequisite(ac) {
48
+ const text = [ac.description, ac.given, ac.when, ac.then, ac.title].filter(Boolean).join(' ');
49
+ if (matchServiceSignals(text).length > 0) return 'serviceSandbox';
50
+ if (SEEDDATA_PATTERNS.test(text)) return 'seedData';
51
+ if (AUTH_PATTERNS.test(text)) return 'authAccount';
52
+ return null;
53
+ }
54
+
55
+ /**
56
+ * Derive the provisioning plan from the chain's ACs: which prerequisite kinds
57
+ * are required, and which ACs depend on each. v1 automates authAccount against
58
+ * signup-exposing apps; serviceSandbox / seedData routes are honestly BLOCKED
59
+ * unless an out-of-band route is supplied (§6 v1 scope note).
60
+ *
61
+ * @param {Array<object>} acs
62
+ * @returns {{ required: string[], acsByKind: Record<string, string[]> }}
63
+ */
64
+ export function deriveProvisioningPlan(acs = []) {
65
+ const acsByKind = {};
66
+ for (const ac of acs) {
67
+ const kind = classifyPrerequisite(ac);
68
+ if (!kind) continue;
69
+ (acsByKind[kind] ??= []).push(ac.acId);
70
+ }
71
+ return { required: Object.keys(acsByKind), acsByKind };
72
+ }
73
+
74
+ /**
75
+ * Deep-redact secret values for anything bound for a log or the report body.
76
+ * Returns a new object; input untouched. Any key in SECRET_FIELDS (case-
77
+ * insensitive) has its value replaced with '[redacted]'.
78
+ *
79
+ * @param {unknown} value
80
+ * @returns {unknown}
81
+ */
82
+ export function redactSecrets(value) {
83
+ if (Array.isArray(value)) return value.map(redactSecrets);
84
+ if (value && typeof value === 'object') {
85
+ const out = {};
86
+ for (const [k, v] of Object.entries(value)) {
87
+ out[k] = SECRET_FIELDS.some((f) => f.toLowerCase() === k.toLowerCase())
88
+ ? '[redacted]'
89
+ : redactSecrets(v);
90
+ }
91
+ return out;
92
+ }
93
+ return value;
94
+ }
95
+
96
+ /**
97
+ * Write provisioned credentials to the --provision file (spec §3 rule 3, §6).
98
+ * This is the ONLY sink for secrets. Never returns the secrets to the caller
99
+ * for logging.
100
+ *
101
+ * @param {string} provisionPath
102
+ * @param {object} data - full provisioning record including credentials
103
+ * @returns {Promise<import('#core/errors').RcfError | null>}
104
+ */
105
+ export async function writeProvisionFile(provisionPath, data) {
106
+ if (typeof provisionPath !== 'string' || provisionPath.length === 0) {
107
+ return rcfError({ kind: 'usage', message: '--provision requires a file path', field: 'provision' });
108
+ }
109
+ try {
110
+ await writeFile(provisionPath, JSON.stringify(data, null, 2), 'utf8');
111
+ return null;
112
+ } catch (err) {
113
+ return rcfError({ kind: 'ioFailure', message: `failed to write provision file: ${err.message}`, filePath: provisionPath, stack: err.stack });
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Read a provisioning file (credentials/fixtures) back for a run/cleanup.
119
+ *
120
+ * @param {string} provisionPath
121
+ * @returns {Promise<object | import('#core/errors').RcfError>}
122
+ */
123
+ export async function readProvisionFile(provisionPath) {
124
+ try {
125
+ return JSON.parse(await readFile(provisionPath, 'utf8'));
126
+ } catch (err) {
127
+ return rcfError({ kind: 'ioFailure', message: `failed to read provision file: ${err.message}`, filePath: provisionPath, stack: err.stack });
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Provision auth accounts against a signup-exposing app. The actual signup is
133
+ * app-specific and agent-driven — supplied via the injected `signup` function
134
+ * (the seam; §6 "the provisioning route is app-specific"). Without a signup
135
+ * route the whole kind is BLOCKED, honestly.
136
+ *
137
+ * @param {object} opts
138
+ * @param {string} opts.url
139
+ * @param {number} [opts.count]
140
+ * @param {((ctx: {url: string, username: string}) => Promise<object>)} [opts.signup]
141
+ * - returns account credentials; may throw to signal an unprovisionable app
142
+ * @returns {Promise<{ provisioned: object[], blocked: object[], credentials: object[] }>}
143
+ */
144
+ export async function provisionAuth({ url, count = MIN_AUTH_ACCOUNTS, signup } = {}) {
145
+ const provisioned = [];
146
+ const blocked = [];
147
+ const credentials = [];
148
+ if (typeof signup !== 'function') {
149
+ blocked.push({ kind: 'authAccount', reason: 'cannot provision: app exposes no derivable signup route (invite-only / admin-seeded / out-of-band). See §6 deferred.' });
150
+ return { provisioned, blocked, credentials };
151
+ }
152
+ const suffixes = 'abcdefghijklmnopqrstuvwxyz';
153
+ for (let i = 0; i < Math.max(count, MIN_AUTH_ACCOUNTS); i += 1) {
154
+ const ref = `${ZZVERIFY_PREFIX}${suffixes[i] ?? String(i)}`;
155
+ try {
156
+ const creds = await signup({ url, username: ref });
157
+ provisioned.push({ kind: 'authAccount', ref });
158
+ credentials.push({ ref, ...creds });
159
+ } catch (err) {
160
+ blocked.push({ kind: 'authAccount', reason: `cannot provision ${ref}: ${err.message}` });
161
+ }
162
+ }
163
+ return { provisioned, blocked, credentials };
164
+ }
165
+
166
+ /**
167
+ * Run provisioning end-to-end for a `run` (spec §6, run-internalised). Derives
168
+ * the plan, provisions auth where a signup route exists, BLOCKS every other
169
+ * required kind (naming the missing prerequisite), marks dependent ACs as
170
+ * BLOCKED, and writes credentials to the --provision file only.
171
+ *
172
+ * @param {object} opts
173
+ * @param {Array<object>} opts.acs
174
+ * @param {string} opts.url
175
+ * @param {string} [opts.provisionPath]
176
+ * @param {'run'|'skip'} [opts.mode]
177
+ * @param {((ctx: object) => Promise<object>)} [opts.signup] - injected signup route
178
+ * @returns {Promise<{ provisioning: object, blockedAcs: object[] }>}
179
+ */
180
+ export async function runProvisioning({ acs = [], url, provisionPath, mode = 'run', signup } = {}) {
181
+ const provisioning = { provisioned: [], blocked: [], cleanupRan: false, cleanupRemoved: [] };
182
+ const blockedAcs = [];
183
+
184
+ if (mode === 'skip') return { provisioning, blockedAcs };
185
+
186
+ const plan = deriveProvisioningPlan(acs);
187
+ const allCredentials = [];
188
+
189
+ for (const kind of plan.required) {
190
+ const dependentAcIds = plan.acsByKind[kind] ?? [];
191
+ if (kind === 'authAccount') {
192
+ const { provisioned, blocked, credentials } = await provisionAuth({ url, signup });
193
+ provisioning.provisioned.push(...provisioned);
194
+ provisioning.blocked.push(...blocked);
195
+ allCredentials.push(...credentials);
196
+ // If auth could not be provisioned at all, its dependent ACs are BLOCKED.
197
+ if (provisioned.length === 0) {
198
+ const reason = blocked[0]?.reason ?? 'cannot provision: authAccount';
199
+ for (const acId of dependentAcIds) blockedAcs.push({ acId, reason });
200
+ }
201
+ } else {
202
+ // serviceSandbox / seedData — v1 honest BLOCK unless an out-of-band route exists.
203
+ const reason = `cannot provision: ${kind} (out-of-band route required — §6 v1 deferred)`;
204
+ provisioning.blocked.push({ kind, reason });
205
+ for (const acId of dependentAcIds) blockedAcs.push({ acId, reason });
206
+ }
207
+ }
208
+
209
+ // Credentials go ONLY to the provision file — never into `provisioning`
210
+ // (which lands in the report body) and never returned for logging.
211
+ if (allCredentials.length > 0 && provisionPath) {
212
+ const writeErr = await writeProvisionFile(provisionPath, {
213
+ schemaVersion: '1',
214
+ url,
215
+ credentials: allCredentials,
216
+ });
217
+ // Surface the write failure as data instead of discarding it: a failed
218
+ // credential write means the run's provisioned accounts are unusable
219
+ // downstream, and silently swallowing it hid a real fault (w-2026-07-21-006).
220
+ // Only the message + path are recorded — never the credentials themselves.
221
+ if (writeErr) {
222
+ provisioning.provisionWriteError = { message: writeErr.message, filePath: writeErr.filePath ?? provisionPath };
223
+ }
224
+ }
225
+
226
+ return { provisioning, blockedAcs };
227
+ }
228
+
229
+ /**
230
+ * Tear down provisioned artefacts (spec §6 cleanup contract). The actual
231
+ * teardown is app-specific — supplied via the injected `teardown` function.
232
+ * Records what it removed for the report.
233
+ *
234
+ * @param {object} opts
235
+ * @param {object[]} [opts.provisioned] - [{kind, ref}]
236
+ * @param {((ref: string) => Promise<void>)} [opts.teardown]
237
+ * @returns {Promise<{ cleanupRan: boolean, cleanupRemoved: string[], cleanupBlocked: object[] }>}
238
+ */
239
+ export async function cleanup({ provisioned = [], teardown } = {}) {
240
+ const cleanupRemoved = [];
241
+ const cleanupBlocked = [];
242
+ if (typeof teardown !== 'function') {
243
+ // No teardown route — report honestly rather than claim a clean sweep.
244
+ for (const p of provisioned) cleanupBlocked.push({ ref: p.ref, reason: 'no teardown route supplied' });
245
+ return { cleanupRan: false, cleanupRemoved, cleanupBlocked };
246
+ }
247
+ for (const p of provisioned) {
248
+ try {
249
+ await teardown(p.ref);
250
+ cleanupRemoved.push(p.ref);
251
+ } catch (err) {
252
+ cleanupBlocked.push({ ref: p.ref, reason: err.message });
253
+ }
254
+ }
255
+ return { cleanupRan: true, cleanupRemoved, cleanupBlocked };
256
+ }
@@ -0,0 +1,139 @@
1
+ // Report artifact (spec §5.3). The report is NOT a dead-end artifact — it is
2
+ // build-lite's next input (§5.4), so it is emitted chain-node-addressed and
3
+ // camelCase, with `schemaVersion` from day one so build-lite's ingest can
4
+ // version-gate. Credentials never appear in the report body (§10) — the
5
+ // builder passes an already-clean provisioning record, and redactSecrets is a
6
+ // defence-in-depth guard on top.
7
+
8
+ import { rcfError } from '#core/errors';
9
+
10
+ import { redactSecrets } from '../provision/index.js';
11
+ import { PER_AC_VERDICTS, VERDICTS } from '../verdict/index.js';
12
+
13
+ /** The report schema version. Present from day one for build-lite's version-gated ingest. */
14
+ export const SCHEMA_VERSION = '1';
15
+
16
+ const DEFAULT_PERSONA = 'generic-sceptic';
17
+
18
+ /**
19
+ * Assemble the §5.3 report object. Pure — all runtime facts are passed in.
20
+ *
21
+ * @param {object} p
22
+ * @returns {object} the report artifact
23
+ */
24
+ export function buildReport(p) {
25
+ const report = {
26
+ schemaVersion: SCHEMA_VERSION,
27
+ run: {
28
+ profile: p.profile,
29
+ url: p.url,
30
+ parityEnv: Boolean(p.parityEnv),
31
+ reachability: p.reachability ?? null,
32
+ chainRef: p.chainRef ?? null,
33
+ repo: p.repo ?? null,
34
+ persona: p.persona ?? DEFAULT_PERSONA,
35
+ startedAt: p.startedAt ?? null,
36
+ finishedAt: p.finishedAt ?? null,
37
+ verifierIsolation: p.verifierIsolation ?? { autoMemory: false, nonEssentialTraffic: false },
38
+ // Agent usage/timing from the --output-format json envelope (§5.3, additive).
39
+ // Omit-not-fake: null when the launcher could not report it.
40
+ runStats: p.runStats ?? null,
41
+ },
42
+ verdict: p.verdict,
43
+ verdictAuthority: p.verdictAuthority,
44
+ findings: Array.isArray(p.findings) ? p.findings : [],
45
+ blockedAcs: Array.isArray(p.blockedAcs) ? p.blockedAcs : [],
46
+ provisioning: p.provisioning ?? null,
47
+ // Present (non-null) only on a LAUNCH-FAILURE verdict: the agent could not
48
+ // run or its output could not be ingested. Carries the error + the path to
49
+ // the preserved raw transcript so the §5.4 fix loop has something to ingest.
50
+ launchFailure: p.launchFailure ?? null,
51
+ };
52
+ // 0.7.0 per-AC verdicts (verification-integrity-cluster-spec §5.2,
53
+ // ui-design-gate §8.7). Present-only when at least one entry has been
54
+ // derived from the chain — an older chain with no 0.7.0 fields produces
55
+ // no per-AC verdicts and the field is omitted so backward-compatible
56
+ // consumers (rcf finalise pre-0.7.0) do not see an unexpected field.
57
+ if (Array.isArray(p.perAcVerdicts) && p.perAcVerdicts.length > 0) {
58
+ report.perAcVerdicts = p.perAcVerdicts.map((v) => ({
59
+ acId: v.acId,
60
+ verdict: v.verdict,
61
+ ...(v.reason !== undefined ? { reason: v.reason } : {}),
62
+ }));
63
+ }
64
+ // Defence-in-depth: never let a secret reach the report body (§10).
65
+ return redactSecrets(report);
66
+ }
67
+
68
+ /**
69
+ * Serialise a report to the on-disk artifact string (camelCase JSON).
70
+ *
71
+ * @param {object} report
72
+ * @returns {string}
73
+ */
74
+ export function serialiseReport(report) {
75
+ return `${JSON.stringify(report, null, 2)}\n`;
76
+ }
77
+
78
+ /**
79
+ * Parse a report artifact back (for `rcf-verify report`). Errors as data.
80
+ *
81
+ * @param {string} raw
82
+ * @returns {object | import('#core/errors').RcfError}
83
+ */
84
+ export function parseReport(raw) {
85
+ let doc;
86
+ try {
87
+ doc = JSON.parse(raw);
88
+ } catch (err) {
89
+ return rcfError({ kind: 'parseFailure', message: `report is not valid JSON: ${err.message}` });
90
+ }
91
+ const shapeErr = validateReportShape(doc);
92
+ if (shapeErr) return shapeErr;
93
+ return doc;
94
+ }
95
+
96
+ /**
97
+ * Minimal shape check on a report artifact.
98
+ *
99
+ * @param {object} doc
100
+ * @returns {import('#core/errors').RcfError | null}
101
+ */
102
+ export function validateReportShape(doc) {
103
+ if (!doc || typeof doc !== 'object') {
104
+ return rcfError({ kind: 'validation', message: 'report must be an object' });
105
+ }
106
+ if (doc.schemaVersion !== SCHEMA_VERSION) {
107
+ return rcfError({ kind: 'validation', message: `unsupported report schemaVersion: ${doc.schemaVersion ?? '(missing)'}`, field: 'schemaVersion' });
108
+ }
109
+ if (!VERDICTS.includes(doc.verdict)) {
110
+ return rcfError({ kind: 'validation', message: `unknown verdict: ${doc.verdict}`, field: 'verdict' });
111
+ }
112
+ if (doc.verdictAuthority !== 'ship' && doc.verdictAuthority !== 'correctness') {
113
+ return rcfError({ kind: 'validation', message: `verdictAuthority must be ship|correctness (got ${doc.verdictAuthority})`, field: 'verdictAuthority' });
114
+ }
115
+ if (!doc.run || typeof doc.run !== 'object') {
116
+ return rcfError({ kind: 'validation', message: 'report.run is required', field: 'run' });
117
+ }
118
+ // perAcVerdicts is optional (0.7.0 addition); when present every entry
119
+ // must carry an acId and a known per-AC verdict class. Unknown verdicts
120
+ // are rejected as data so a consumer never has to guess the class of an
121
+ // unfamiliar string.
122
+ if (doc.perAcVerdicts !== undefined) {
123
+ if (!Array.isArray(doc.perAcVerdicts)) {
124
+ return rcfError({ kind: 'validation', message: 'report.perAcVerdicts must be an array when present', field: 'perAcVerdicts' });
125
+ }
126
+ for (const entry of doc.perAcVerdicts) {
127
+ if (!entry || typeof entry !== 'object') {
128
+ return rcfError({ kind: 'validation', message: 'report.perAcVerdicts entries must be objects', field: 'perAcVerdicts' });
129
+ }
130
+ if (typeof entry.acId !== 'string' || entry.acId.length === 0) {
131
+ return rcfError({ kind: 'validation', message: 'report.perAcVerdicts[].acId is required', field: 'perAcVerdicts' });
132
+ }
133
+ if (!PER_AC_VERDICTS.includes(entry.verdict)) {
134
+ return rcfError({ kind: 'validation', message: `report.perAcVerdicts[].verdict must be one of ${PER_AC_VERDICTS.join('/')} (got ${entry.verdict})`, field: 'perAcVerdicts' });
135
+ }
136
+ }
137
+ }
138
+ return null;
139
+ }
@@ -0,0 +1,118 @@
1
+ // Self-contained finding-renderer (spec §5.3, §7.2). v1 core is store + errors
2
+ // + mcp-shell ONLY: the build `view/` layer is NOT a clean shell and is
3
+ // firmly OUT of v1 core. So verify ships its OWN minimal, dependency-free human
4
+ // render for `rcf-verify report`. Plain text, British English, ASCII hyphens,
5
+ // mirroring build's help-surface conventions without importing any core view code.
6
+ //
7
+ // Honest-limit language (§9): the render NEVER says "fully verified" / "safe".
8
+
9
+ /** One-line human summary of a verdict, including its ship implication. */
10
+ const VERDICT_LINE = {
11
+ PASS: 'PASS: every AC verified against the running app; no defect above COSMETIC.',
12
+ BROKEN: 'BROKEN: one or more ACs fail on the running app. Blocks ship.',
13
+ DEGRADED: 'DEGRADED: app works but a criterion is materially weakened. Reported; may block per gate.',
14
+ COSMETIC: 'COSMETIC: hygiene only; no AC touched. Does not block.',
15
+ 'NOT-DEPLOYED': 'NOT-DEPLOYED: deployed profile declared but no real deploy reachable. A refusal to issue a verdict, not a pass.',
16
+ BLOCKED: 'BLOCKED: a prerequisite could not be provisioned; dependent ACs were not exercisable.',
17
+ 'LAUNCH-FAILURE': 'LAUNCH-FAILURE: the verifier agent could not run or its output could not be ingested. A refusal to issue a verdict, not a pass.',
18
+ };
19
+
20
+ /**
21
+ * Render a report artifact to a human-readable string.
22
+ *
23
+ * @param {object} report
24
+ * @returns {string}
25
+ */
26
+ export function renderReport(report) {
27
+ const lines = [];
28
+ const run = report.run ?? {};
29
+ lines.push('RCF Verify: verification report');
30
+ lines.push('='.repeat(40));
31
+ lines.push(`Verdict: ${report.verdict} [authority: ${report.verdictAuthority}]`);
32
+ lines.push(` ${VERDICT_LINE[report.verdict] ?? ''}`.trimEnd());
33
+ lines.push('');
34
+ lines.push('Runtime provenance');
35
+ lines.push(` profile: ${run.profile}`);
36
+ lines.push(` url: ${run.url}`);
37
+ lines.push(` parityEnv: ${run.parityEnv === true}`);
38
+ if (run.reachability) {
39
+ lines.push(` reachable: ${run.reachability.reachable} looksLocal: ${run.reachability.looksLocal}`);
40
+ }
41
+ lines.push(` chainRef: ${run.chainRef ?? '(none)'}`);
42
+ lines.push(` persona: ${run.persona ?? '(default)'}`);
43
+ if (run.verifierIsolation) {
44
+ lines.push(` isolation: autoMemory=${run.verifierIsolation.autoMemory} nonEssentialTraffic=${run.verifierIsolation.nonEssentialTraffic}`);
45
+ }
46
+ lines.push('');
47
+
48
+ const findings = report.findings ?? [];
49
+ lines.push(`Findings (${findings.length})`);
50
+ if (findings.length === 0) {
51
+ lines.push(' (none)');
52
+ } else {
53
+ for (const f of findings) {
54
+ lines.push(` [${f.severity}] ${f.acId}: ${f.journey}`);
55
+ for (const step of f.reproSteps ?? []) lines.push(` - ${step}`);
56
+ if (f.evidence) {
57
+ const detail = f.evidence.detail ?? f.evidence.kind ?? JSON.stringify(f.evidence);
58
+ lines.push(` evidence: ${detail}`);
59
+ }
60
+ }
61
+ }
62
+ lines.push('');
63
+
64
+ const blocked = report.blockedAcs ?? [];
65
+ if (blocked.length > 0) {
66
+ lines.push(`Blocked ACs (${blocked.length}), NOT exercisable, not silently skipped`);
67
+ for (const b of blocked) lines.push(` ${b.acId}: ${b.reason}`);
68
+ lines.push('');
69
+ }
70
+
71
+ // 0.7.0 per-AC verdicts. Rendered as a dedicated section so an operator
72
+ // reading the human render sees the honest picture on any AC that came
73
+ // back mock-only / declared / UI-baseline-unmet, even when the top-level
74
+ // verdict is PASS. Present-only; pre-0.7.0 reports skip this block.
75
+ const perAc = Array.isArray(report.perAcVerdicts) ? report.perAcVerdicts : [];
76
+ if (perAc.length > 0) {
77
+ lines.push(`Per-AC verdicts (${perAc.length}), chain-derived alongside the run verdict`);
78
+ for (const v of perAc) {
79
+ lines.push(` [${v.verdict}] ${v.acId}`);
80
+ if (v.reason) lines.push(` ${v.reason}`);
81
+ }
82
+ lines.push('');
83
+ }
84
+
85
+ if (report.launchFailure) {
86
+ lines.push('Launch failure');
87
+ lines.push(` ${report.launchFailure.message}`);
88
+ if (report.launchFailure.rawOutputPath) lines.push(` raw transcript: ${report.launchFailure.rawOutputPath}`);
89
+ lines.push('');
90
+ }
91
+
92
+ if (run.runStats) {
93
+ const s = run.runStats;
94
+ const bits = [];
95
+ if (typeof s.durationMs === 'number') bits.push(`duration=${s.durationMs}ms`);
96
+ if (typeof s.numTurns === 'number') bits.push(`turns=${s.numTurns}`);
97
+ if (s.tokens) bits.push(`tokens in/out=${s.tokens.inputTokens ?? '?'}/${s.tokens.outputTokens ?? '?'}`);
98
+ if (typeof s.totalCostUsd === 'number') bits.push(`cost=$${s.totalCostUsd.toFixed(4)}`);
99
+ if (bits.length) {
100
+ lines.push(`Run stats: ${bits.join(' ')}`);
101
+ lines.push('');
102
+ }
103
+ }
104
+
105
+ const prov = report.provisioning;
106
+ if (prov) {
107
+ lines.push('Provisioning');
108
+ lines.push(` provisioned: ${(prov.provisioned ?? []).map((p) => p.ref).join(', ') || '(none)'}`);
109
+ if ((prov.blocked ?? []).length) {
110
+ lines.push(` blocked: ${prov.blocked.map((b) => `${b.kind} (${b.reason})`).join('; ')}`);
111
+ }
112
+ lines.push(` cleanupRan: ${prov.cleanupRan === true} removed: ${(prov.cleanupRemoved ?? []).join(', ') || '(none)'}`);
113
+ lines.push('');
114
+ }
115
+
116
+ lines.push('Note: this is an independent ship-readiness signal, not a correctness guarantee.');
117
+ return `${lines.join('\n')}\n`;
118
+ }