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,11 @@
1
+ // Public surface for the design substage module
2
+ // (ui-design-gate-0.7.0-spec §5). CLI handler at `src/cli/design.js`.
3
+
4
+ export {
5
+ firstBaselineDisagreement,
6
+ missingDesignStageArtefacts,
7
+ writeJourneyAdd,
8
+ writeMarkComplete,
9
+ writeNavSet,
10
+ writeThemeA11ySet,
11
+ } from './writer.js';
@@ -0,0 +1,271 @@
1
+ // Design substage writer (ui-design-gate-0.7.0-spec §5, §6.2).
2
+ //
3
+ // Persistence primitives for the three artefacts (journeys, navModel,
4
+ // themeAndA11y) plus the `designStageComplete` boolean. Each verb
5
+ // writes via the core store's `updateDocument` path so the writer
6
+ // bumps `updatedAt` and schema-validates the FBS document.
7
+ //
8
+ // Grammar constraint (spec §5.5): the sub-verb parser lives in the CLI
9
+ // handler (`src/cli/design.js`); this module accepts already-parsed
10
+ // arguments and does not need to defend against string positional
11
+ // confusion at this layer.
12
+
13
+ import { rcfError } from '#core/errors';
14
+ import { updateDocument } from '#core/store';
15
+
16
+ import { baselineDesignDisagreement } from '../ui-baseline/manifest-writer.js';
17
+
18
+ /**
19
+ * @typedef {object} DesignJourney
20
+ * @property {string} id
21
+ * @property {string} actor
22
+ * @property {string} goal
23
+ * @property {string[]} steps
24
+ */
25
+
26
+ /**
27
+ * @typedef {object} DesignNavRoute
28
+ * @property {string} path
29
+ * @property {string} label
30
+ * @property {boolean} authRequired
31
+ */
32
+
33
+ const NAV_SHAPES = new Set([
34
+ 'shared-persistent',
35
+ 'shared-per-section',
36
+ 'none-single-page',
37
+ 'operator-declared-other',
38
+ ]);
39
+ const THEME_MODES = new Set([
40
+ 'light-default-with-toggle',
41
+ 'dark-default-with-toggle',
42
+ 'single-theme-declared',
43
+ ]);
44
+
45
+ /**
46
+ * Append a journey to `fbs.designStage.journeys[]`. Writes via
47
+ * `updateDocument`.
48
+ *
49
+ * @param {object} args
50
+ * @param {string} args.projectRoot
51
+ * @param {import('#core/store/walker.js').TreeModel} args.tree
52
+ * @param {string} args.fbsId
53
+ * @param {DesignJourney} args.journey
54
+ * @param {string} [args.authoredBy]
55
+ * @param {Date} [args.now]
56
+ */
57
+ export async function writeJourneyAdd({ projectRoot, tree, fbsId, journey, authoredBy = 'operator', now = new Date() }) {
58
+ const fbs = tree.byId?.get(fbsId);
59
+ if (!fbs || tree.kindById?.get(fbsId) !== 'fbs') {
60
+ return rcfError({ kind: 'usage', message: `design journeys add: ${fbsId} not found or not an FBS`, documentId: fbsId });
61
+ }
62
+ const validation = validateJourneyShape(journey);
63
+ if (validation) return validation;
64
+ const existing = fbs.designStage?.journeys ?? [];
65
+ if (existing.some((j) => j.id === journey.id)) {
66
+ return rcfError({ kind: 'usage', message: `design journeys add: journey id '${journey.id}' already exists on ${fbsId}`, documentId: fbsId });
67
+ }
68
+ const nextStage = {
69
+ ...(fbs.designStage ?? {}),
70
+ journeys: [...existing, { ...journey }],
71
+ authoredAt: now.toISOString(),
72
+ authoredBy,
73
+ };
74
+ return await updateDocument({
75
+ projectRoot, tree, id: fbsId,
76
+ sets: [{ path: 'designStage', value: nextStage }],
77
+ options: {},
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Overwrite `fbs.designStage.navModel`. Writes via `updateDocument`.
83
+ *
84
+ * @param {object} args
85
+ */
86
+ export async function writeNavSet({ projectRoot, tree, fbsId, shape, routes, signedInAsAffordance, notes, authoredBy = 'operator', now = new Date() }) {
87
+ const fbs = tree.byId?.get(fbsId);
88
+ if (!fbs || tree.kindById?.get(fbsId) !== 'fbs') {
89
+ return rcfError({ kind: 'usage', message: `design nav set: ${fbsId} not found or not an FBS`, documentId: fbsId });
90
+ }
91
+ if (!NAV_SHAPES.has(shape)) {
92
+ return rcfError({ kind: 'usage', message: `design nav set: unknown --shape '${shape}' (expected ${[...NAV_SHAPES].join(' | ')})` });
93
+ }
94
+ if (!Array.isArray(routes) || routes.length === 0) {
95
+ return rcfError({ kind: 'usage', message: 'design nav set: at least one --route is required' });
96
+ }
97
+ for (const r of routes) {
98
+ if (!r || typeof r.path !== 'string' || typeof r.label !== 'string' || typeof r.authRequired !== 'boolean') {
99
+ return rcfError({ kind: 'usage', message: `design nav set: bad route '${JSON.stringify(r)}'; expected path=label:authRequired` });
100
+ }
101
+ }
102
+ const navModel = { shape, routes: routes.map((r) => ({ path: r.path, label: r.label, authRequired: r.authRequired })) };
103
+ if (typeof signedInAsAffordance === 'boolean') navModel.signedInAsAffordance = signedInAsAffordance;
104
+ if (typeof notes === 'string' && notes.length > 0) navModel.notes = notes;
105
+ const nextStage = {
106
+ ...(fbs.designStage ?? {}),
107
+ navModel,
108
+ authoredAt: now.toISOString(),
109
+ authoredBy,
110
+ };
111
+ return await updateDocument({
112
+ projectRoot, tree, id: fbsId,
113
+ sets: [{ path: 'designStage', value: nextStage }],
114
+ options: {},
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Overwrite `fbs.designStage.themeAndA11y`. Writes via `updateDocument`.
120
+ *
121
+ * @param {object} args
122
+ */
123
+ export async function writeThemeA11ySet({
124
+ projectRoot, tree, fbsId, themeMode, themeTokensModule, contrastTargets,
125
+ contrastTestPath, contrastTestAuthoredBeforePalette, authoredBy = 'operator', now = new Date(),
126
+ }) {
127
+ const fbs = tree.byId?.get(fbsId);
128
+ if (!fbs || tree.kindById?.get(fbsId) !== 'fbs') {
129
+ return rcfError({ kind: 'usage', message: `design theme-a11y set: ${fbsId} not found or not an FBS`, documentId: fbsId });
130
+ }
131
+ if (!THEME_MODES.has(themeMode)) {
132
+ return rcfError({ kind: 'usage', message: `design theme-a11y set: unknown --mode '${themeMode}' (expected ${[...THEME_MODES].join(' | ')})` });
133
+ }
134
+ if (typeof themeTokensModule !== 'string' || themeTokensModule.length === 0) {
135
+ return rcfError({ kind: 'usage', message: 'design theme-a11y set: --tokens is required' });
136
+ }
137
+ if (typeof contrastTestPath !== 'string' || contrastTestPath.length === 0) {
138
+ return rcfError({ kind: 'usage', message: 'design theme-a11y set: --contrast-test is required' });
139
+ }
140
+ if (typeof contrastTestAuthoredBeforePalette !== 'boolean') {
141
+ return rcfError({ kind: 'usage', message: 'design theme-a11y set: --contrast-before-palette (true|false) is required' });
142
+ }
143
+ const themeAndA11y = {
144
+ themeMode,
145
+ themeTokensModule,
146
+ contrastTargets: contrastTargets || 'WCAG AA',
147
+ contrastTestPath,
148
+ contrastTestAuthoredBeforePalette,
149
+ };
150
+ const nextStage = {
151
+ ...(fbs.designStage ?? {}),
152
+ themeAndA11y,
153
+ authoredAt: now.toISOString(),
154
+ authoredBy,
155
+ };
156
+ return await updateDocument({
157
+ projectRoot, tree, id: fbsId,
158
+ sets: [{ path: 'designStage', value: nextStage }],
159
+ options: {},
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Set `designStageComplete: true`. Refuses when any of the three
165
+ * artefacts is absent or empty, and when the baseline disagrees with
166
+ * the designStage without an opt-out (spec §6.2 second refusal shape).
167
+ *
168
+ * @param {object} args
169
+ * @returns {Promise<{ ok: true } | import('#core/errors').RcfError>}
170
+ */
171
+ export async function writeMarkComplete({
172
+ projectRoot, tree, fbsId, authoredBy = 'operator', now = new Date(),
173
+ }) {
174
+ const fbs = tree.byId?.get(fbsId);
175
+ if (!fbs || tree.kindById?.get(fbsId) !== 'fbs') {
176
+ return rcfError({ kind: 'usage', message: `design --mark-complete: ${fbsId} not found or not an FBS`, documentId: fbsId });
177
+ }
178
+ const missing = missingDesignStageArtefacts(fbs);
179
+ if (missing.length > 0) {
180
+ return rcfError({
181
+ kind: 'usage',
182
+ message: `design --mark-complete: refused - ${fbsId} designStage is missing: ${missing.join(', ')}. `
183
+ + 'Author the missing artefacts first via `rcf design ' + fbsId + ' journeys add ...`, `nav set ...`, or `theme-a11y set ...`.',
184
+ documentId: fbsId,
185
+ });
186
+ }
187
+ const disagreement = firstBaselineDisagreement(tree, fbs);
188
+ if (disagreement) {
189
+ return rcfError({
190
+ kind: 'usage',
191
+ message: `design --mark-complete: refused - ${fbsId} designStage.${disagreement.designStagePath} = ${JSON.stringify(disagreement.designValue)} conflicts with uiBaseline.defaults.${disagreement.path} = ${JSON.stringify(disagreement.baselineValue)} and there is no operatorOptOuts entry. `
192
+ + 'Options: (1) change designStage to match, (2) rcf ui-baseline opt-out --field '
193
+ + disagreement.path + ' --reason "..." (project override), (3) rcf update '
194
+ + fbsId + ' --set designStage.' + disagreement.designStagePath + '=... (per-FBS override).',
195
+ documentId: fbsId,
196
+ });
197
+ }
198
+ const nextStage = {
199
+ ...(fbs.designStage ?? {}),
200
+ authoredAt: now.toISOString(),
201
+ authoredBy,
202
+ };
203
+ const result = await updateDocument({
204
+ projectRoot, tree, id: fbsId,
205
+ sets: [
206
+ { path: 'designStage', value: nextStage },
207
+ { path: 'designStageComplete', value: true },
208
+ ],
209
+ options: {},
210
+ });
211
+ if (result && 'kind' in result && 'message' in result) return result;
212
+ return { ok: true };
213
+ }
214
+
215
+ /**
216
+ * Which of the three required artefacts is missing or empty on the FBS.
217
+ *
218
+ * @param {object} fbs
219
+ * @returns {string[]}
220
+ */
221
+ export function missingDesignStageArtefacts(fbs) {
222
+ const missing = [];
223
+ const stage = fbs?.designStage ?? {};
224
+ if (!Array.isArray(stage.journeys) || stage.journeys.length === 0) missing.push('journeys');
225
+ if (!stage.navModel || !Array.isArray(stage.navModel.routes) || stage.navModel.routes.length === 0) missing.push('navModel');
226
+ if (!stage.themeAndA11y || typeof stage.themeAndA11y.themeMode !== 'string') missing.push('themeAndA11y');
227
+ return missing;
228
+ }
229
+
230
+ /**
231
+ * First baseline-vs-designStage disagreement (per spec §6.2). Returns
232
+ * null on agreement or when no baseline / designStage exists.
233
+ *
234
+ * @param {import('#core/store/walker.js').TreeModel} tree
235
+ * @param {object} fbs
236
+ * @returns {{ path: string, designStagePath: string, baselineValue: *, designValue: * }|null}
237
+ */
238
+ export function firstBaselineDisagreement(tree, fbs) {
239
+ const baseline = tree?.manifest?.uiBaseline;
240
+ const stage = fbs?.designStage;
241
+ if (!baseline || !stage) return null;
242
+ // Fields that pair between baseline defaults and designStage.
243
+ const pairs = [
244
+ { defaults: 'themeMode', designStage: 'themeAndA11y.themeMode' },
245
+ { defaults: 'designTokensModule', designStage: 'themeAndA11y.themeTokensModule' },
246
+ { defaults: 'contrastTestBeforePalette', designStage: 'themeAndA11y.contrastTestAuthoredBeforePalette' },
247
+ ];
248
+ for (const p of pairs) {
249
+ const disagreement = baselineDesignDisagreement(baseline, stage, p.defaults, p.designStage);
250
+ if (disagreement) {
251
+ return { ...disagreement, designStagePath: p.designStage };
252
+ }
253
+ }
254
+ return null;
255
+ }
256
+
257
+ function validateJourneyShape(j) {
258
+ if (!j || typeof j !== 'object') return rcfError({ kind: 'usage', message: 'design journeys add: journey object required' });
259
+ if (typeof j.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(j.id)) {
260
+ return rcfError({ kind: 'usage', message: `design journeys add: --id must be a lowercase slug matching /^[a-z][a-z0-9-]*$/, got '${j.id}'` });
261
+ }
262
+ if (typeof j.actor !== 'string' || j.actor.length === 0) return rcfError({ kind: 'usage', message: 'design journeys add: --actor is required' });
263
+ if (typeof j.goal !== 'string' || j.goal.length === 0) return rcfError({ kind: 'usage', message: 'design journeys add: --goal is required' });
264
+ if (!Array.isArray(j.steps) || j.steps.length < 2 || j.steps.length > 8) {
265
+ return rcfError({ kind: 'usage', message: `design journeys add: --step required 2 to 8 times, got ${Array.isArray(j.steps) ? j.steps.length : 0}` });
266
+ }
267
+ for (const s of j.steps) if (typeof s !== 'string' || s.length === 0) {
268
+ return rcfError({ kind: 'usage', message: 'design journeys add: each --step must be a non-empty string' });
269
+ }
270
+ return null;
271
+ }
@@ -0,0 +1,151 @@
1
+ // rcf-verify install detection (spec §8.3, amendment 5 - install-together
2
+ // posture). The finalise gate MUST detect whether `rcf-verify` is resolvable
3
+ // and, when it is absent, prompt to install it - NEVER silently skip the ship
4
+ // gate (the one behaviour §8.3 explicitly forbids).
5
+ //
6
+ // Two detection routes, in order, matching how the CLI is actually installed:
7
+ // 1. The `rcf-verify` bin on PATH - the umbrella install exposes both `rcf`
8
+ // and `rcf-verify` as global bins (`npm i -g rcf-lite`).
9
+ // 2. Package resolution from the project dir - the local-project install
10
+ // (`npm i rcf-lite` in a repo's node_modules). Two package specifiers
11
+ // are tried in order: the current umbrella `rcf-lite`, then the legacy
12
+ // `@stravica-ai/rcf-verify-lite` name (deprecated at 0.7.1 but still
13
+ // resolvable in lockfiles that pinned it). Either resolves to the same
14
+ // `bin/rcf-verify.js` entry point.
15
+ // Either route yields a concrete invocation the finalise spawn (spawn.js)
16
+ // uses verbatim. A miss returns { installed:false } and the caller enters
17
+ // the prompt-or-explicit-flag path.
18
+
19
+ import { access, constants } from 'node:fs/promises';
20
+ import { createRequire } from 'node:module';
21
+ import { delimiter, dirname, join, resolve } from 'node:path';
22
+
23
+ // VERIFY_PACKAGE is the current install target and the name shown to
24
+ // operators in absent-verify messaging. VERIFY_PACKAGE_LEGACY is only used
25
+ // as a resolution fallback for lockfiles still pinning the pre-0.7.1 scoped
26
+ // name; it is never displayed as an install target.
27
+ const VERIFY_PACKAGE = 'rcf-lite';
28
+ const VERIFY_PACKAGE_LEGACY = '@stravica-ai/rcf-verify-lite';
29
+ const VERIFY_PACKAGE_CANDIDATES = [VERIFY_PACKAGE, VERIFY_PACKAGE_LEGACY];
30
+ const VERIFY_BIN = 'rcf-verify';
31
+
32
+ /**
33
+ * The concrete way to launch rcf-verify as a fresh subprocess.
34
+ * - `{ command: '<abs-bin>', prefixArgs: [] }` for a PATH / shim bin
35
+ * (its own shebang runs it).
36
+ * - `{ command: process.execPath, prefixArgs: ['<abs-entry.js>'] }` for a
37
+ * package-resolved entry (run it under the current node).
38
+ * @typedef {{ command: string, prefixArgs: string[], source: 'path'|'package' }} VerifyInvocation
39
+ */
40
+
41
+ /**
42
+ * @typedef {{ installed: boolean, invocation: VerifyInvocation | null }} VerifyDetection
43
+ */
44
+
45
+ /**
46
+ * Scan PATH for an executable named `rcf-verify` (plus the Windows
47
+ * `.cmd`/`.exe` shim variants). Returns the first absolute path that exists
48
+ * and is executable, or null.
49
+ *
50
+ * @param {string} name
51
+ * @param {object} [io]
52
+ * @param {NodeJS.ProcessEnv} [io.env]
53
+ * @returns {Promise<string|null>}
54
+ */
55
+ export async function findOnPath(name, { env = process.env } = {}) {
56
+ const rawPath = env.PATH ?? env.Path ?? '';
57
+ if (!rawPath) return null;
58
+ const dirs = rawPath.split(delimiter).filter(Boolean);
59
+ // On Windows a bare name resolves via PATHEXT-style shims; probe the common
60
+ // npm shim names. On POSIX only the bare name matters.
61
+ const candidates = process.platform === 'win32'
62
+ ? [`${name}.cmd`, `${name}.exe`, name]
63
+ : [name];
64
+ for (const dir of dirs) {
65
+ for (const candidate of candidates) {
66
+ const full = resolve(dir, candidate);
67
+ try {
68
+ // X_OK is meaningless for the .cmd/.exe shims on Windows; existence is
69
+ // enough there. On POSIX require the execute bit.
70
+ await access(full, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
71
+ return full;
72
+ } catch {
73
+ // not here; keep scanning
74
+ }
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Resolve the rcf-verify bin entry point from a starting directory, following
82
+ * the normal node_modules resolution the caller's project sees. Returns the
83
+ * absolute path to `bin/rcf-verify.js`, or null if no candidate package is
84
+ * installed / resolvable from there.
85
+ *
86
+ * Candidates are tried in the order defined by VERIFY_PACKAGE_CANDIDATES: the
87
+ * current umbrella (`rcf-lite`) first, then the deprecated scoped name
88
+ * (`@stravica-ai/rcf-verify-lite`) as a fallback for lockfiles pinned to it.
89
+ * The first candidate whose bin resolves and exists on disk wins.
90
+ *
91
+ * @param {string} fromDir - directory to resolve from (the project root / cwd)
92
+ * @returns {Promise<string|null>}
93
+ */
94
+ export async function resolvePackageBin(fromDir) {
95
+ // Resolve from a synthetic module living in fromDir so node walks that
96
+ // project's node_modules chain, not this package's own.
97
+ let req;
98
+ try {
99
+ req = createRequire(join(fromDir, 'noop.js'));
100
+ } catch {
101
+ return null;
102
+ }
103
+ for (const candidate of VERIFY_PACKAGE_CANDIDATES) {
104
+ try {
105
+ const pkgJsonPath = req.resolve(`${candidate}/package.json`);
106
+ const req2 = createRequire(pkgJsonPath);
107
+ const pkg = req2(`${candidate}/package.json`);
108
+ const binField = pkg.bin;
109
+ const rel = typeof binField === 'string' ? binField : binField?.[VERIFY_BIN];
110
+ if (!rel) continue;
111
+ const abs = resolve(dirname(pkgJsonPath), rel);
112
+ await access(abs, constants.F_OK);
113
+ return abs;
114
+ } catch {
115
+ // Candidate not resolvable from here; try the next one.
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Detect whether rcf-verify is resolvable and, if so, how to launch it.
123
+ * Deps are injectable so the finalise gate can be exercised without a real
124
+ * rcf-verify on the test machine.
125
+ *
126
+ * @param {object} [deps]
127
+ * @param {string} [deps.cwd] - project dir to resolve a local install from
128
+ * @param {typeof findOnPath} [deps.findOnPath]
129
+ * @param {typeof resolvePackageBin} [deps.resolvePackageBin]
130
+ * @param {NodeJS.ProcessEnv} [deps.env]
131
+ * @returns {Promise<VerifyDetection>}
132
+ */
133
+ export async function detectVerify(deps = {}) {
134
+ const cwd = deps.cwd ?? process.cwd();
135
+ const onPath = deps.findOnPath ? await deps.findOnPath(VERIFY_BIN, { env: deps.env })
136
+ : await findOnPath(VERIFY_BIN, { env: deps.env });
137
+ if (onPath) {
138
+ return { installed: true, invocation: { command: onPath, prefixArgs: [], source: 'path' } };
139
+ }
140
+ const resolved = deps.resolvePackageBin ? await deps.resolvePackageBin(cwd)
141
+ : await resolvePackageBin(cwd);
142
+ if (resolved) {
143
+ return {
144
+ installed: true,
145
+ invocation: { command: process.execPath, prefixArgs: [resolved], source: 'package' },
146
+ };
147
+ }
148
+ return { installed: false, invocation: null };
149
+ }
150
+
151
+ export { VERIFY_PACKAGE, VERIFY_PACKAGE_LEGACY, VERIFY_PACKAGE_CANDIDATES, VERIFY_BIN };
@@ -0,0 +1,31 @@
1
+ // Finalise-gate barrel (spec §8). build-lite's finalise step invokes rcf-verify
2
+ // as a fresh subprocess with the isolation env (spawn.js), gates the
3
+ // complete -> verified transition on the subprocess exit code AND the report's
4
+ // ship authority (spec §4 - a correctness-only pass holds without promoting),
5
+ // ingests findings from the --out report (ingest.js), and - when rcf-verify is
6
+ // absent - prompts to install rather than silently skipping the gate (detect.js
7
+ // + install.js).
8
+
9
+ export {
10
+ detectVerify,
11
+ findOnPath,
12
+ resolvePackageBin,
13
+ VERIFY_PACKAGE,
14
+ VERIFY_PACKAGE_LEGACY,
15
+ VERIFY_PACKAGE_CANDIDATES,
16
+ VERIFY_BIN,
17
+ } from './detect.js';
18
+ export { buildVerifyArgs, spawnVerify } from './spawn.js';
19
+ export { promptYesNo, installVerify, resolveAbsentVerify } from './install.js';
20
+ export {
21
+ loadReport, summariseReport,
22
+ findMockOnlyDeclaredAcs, reportHasMockOnlyDeclared,
23
+ // 0.8.0 slug-train car 4: NV-BL-GATE-01 pull-in of the profile-vs-AC
24
+ // scope-mismatch check into REVIEW.
25
+ findScopeMismatchAcs, reportHasScopeMismatch,
26
+ } from './ingest.js';
27
+ export {
28
+ composeShipWithoutVerifiedRecord,
29
+ nextShipWithoutVerifiedId,
30
+ writeShipWithoutVerifiedRecord,
31
+ } from './ship-without-verified.js';
@@ -0,0 +1,160 @@
1
+ // Report ingestion (spec §8.2 + §5.4). Findings flow from verify to build-lite
2
+ // via the --out report file (chain-node-addressed camelCase JSON), NEVER via
3
+ // stdout scraping. build-lite reads the artifact and, on a blocked gate,
4
+ // surfaces the findings mapped to their contract lines (acId / chain node) so
5
+ // the operator can drive the §5.4 verify -> fix -> re-verify loop.
6
+ //
7
+ // This is deliberately a READ of verify's artifact - build never re-derives a
8
+ // verdict, it consumes the one verify stamped.
9
+
10
+ import { readFile } from 'node:fs/promises';
11
+
12
+ /**
13
+ * Load and parse a verify report artifact. Returns the parsed report, or a
14
+ * shape describing why it could not be read (missing / unparseable) so the
15
+ * caller can degrade gracefully - a gate failure with an unreadable report is
16
+ * still a gate failure, never a pass.
17
+ *
18
+ * @param {string} reportPath
19
+ * @param {object} [deps]
20
+ * @param {typeof readFile} [deps.readFile]
21
+ * @returns {Promise<{ ok: true, report: object } | { ok: false, reason: string }>}
22
+ */
23
+ export async function loadReport(reportPath, deps = {}) {
24
+ const read = deps.readFile ?? readFile;
25
+ let raw;
26
+ try {
27
+ raw = await read(reportPath, 'utf8');
28
+ } catch (err) {
29
+ return { ok: false, reason: `report not found at ${reportPath}: ${err.message}` };
30
+ }
31
+ try {
32
+ return { ok: true, report: JSON.parse(raw) };
33
+ } catch (err) {
34
+ return { ok: false, reason: `report at ${reportPath} is not valid JSON: ${err.message}` };
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Render a compact, human-readable finalise summary of a verify report. Maps
40
+ * each finding to its contract line (acId) - the RCF payoff. Pure so it is
41
+ * directly testable; the caller writes the returned string to its sink.
42
+ *
43
+ * The verification-integrity 0.7.0 additions (spec §5.2 / §4.5 finalise
44
+ * text): if the report carries per-AC verdicts in
45
+ * {MOCK-ONLY-DECLARED, BLOCKED-BY-DECLARATION}, they are surfaced as a
46
+ * dedicated section so the operator sees an honest picture of what
47
+ * shipped mock-only. The section renders even on a passing verdict
48
+ * (it is disclosure, not a refusal).
49
+ *
50
+ * @param {object} report - a parsed verify report (§5.3 schema)
51
+ * @returns {string}
52
+ */
53
+ export function summariseReport(report) {
54
+ const lines = [];
55
+ const authority = report.verdictAuthority ? ` [${report.verdictAuthority}]` : '';
56
+ lines.push(`verdict: ${report.verdict}${authority}`);
57
+ const run = report.run ?? {};
58
+ if (run.profile || run.url) {
59
+ lines.push(`runtime: profile=${run.profile ?? '?'} url=${run.url ?? '?'}`
60
+ + (run.parityEnv ? ' parity-env' : ''));
61
+ }
62
+ const findings = Array.isArray(report.findings) ? report.findings : [];
63
+ if (findings.length > 0) {
64
+ lines.push(`findings (${findings.length}):`);
65
+ for (const f of findings) {
66
+ const ac = f.acId ? ` ${f.acId}` : '';
67
+ const journey = f.journey ? ` (${f.journey})` : '';
68
+ lines.push(` - ${f.severity ?? '?'}${ac}${journey}`);
69
+ }
70
+ }
71
+ const blocked = Array.isArray(report.blockedAcs) ? report.blockedAcs : [];
72
+ if (blocked.length > 0) {
73
+ lines.push(`blocked ACs (${blocked.length}):`);
74
+ for (const b of blocked) {
75
+ lines.push(` - ${b.acId ?? '?'}: ${b.reason ?? 'unprovisionable'}`);
76
+ }
77
+ }
78
+ const declared = findMockOnlyDeclaredAcs(report);
79
+ if (declared.length > 0) {
80
+ lines.push(`mock-only declared (${declared.length}):`);
81
+ for (const d of declared) {
82
+ lines.push(` - ${d.acId ?? '?'} (${d.verdict}): ${d.reason ?? 'declaredMockOnly at pre-flight; verify emitted the honest verdict rather than a false PASS.'}`);
83
+ }
84
+ }
85
+ // 0.8.0 slug-train car 4: NV-BL-GATE-01 pulls verify's profile-vs-AC
86
+ // scope-mismatch check into REVIEW. When verify emits SCOPE-MISMATCH
87
+ // on perAcVerdicts[] the summary surfaces it so REVIEW / finalise see
88
+ // it. Zero-mismatch reports render nothing (no false-flag noise).
89
+ const scopeMismatches = findScopeMismatchAcs(report);
90
+ if (scopeMismatches.length > 0) {
91
+ lines.push(`scope mismatches (${scopeMismatches.length}):`);
92
+ for (const s of scopeMismatches) {
93
+ lines.push(` - ${s.acId ?? '?'} (${s.verdict}): ${s.reason ?? 'a bound TC is narrower than the AC scope; NV-BL-ADM-03 / NV-BL-GATE-01.'}`);
94
+ }
95
+ }
96
+ if (report.launchFailure?.message) {
97
+ lines.push(`launch failure: ${report.launchFailure.message}`);
98
+ }
99
+ return `${lines.join('\n')}\n`;
100
+ }
101
+
102
+ /**
103
+ * Extract per-AC verdicts in {MOCK-ONLY-DECLARED, BLOCKED-BY-DECLARATION}
104
+ * from a verify report. Verify authors these under `perAcVerdicts[]` in
105
+ * the 0.7.0 extension; earlier reports carry no field, in which case
106
+ * this returns an empty array (graceful when no verify-side extension
107
+ * has landed — verify's train car is later).
108
+ *
109
+ * @param {object} report
110
+ * @returns {Array<{ acId: string, verdict: string, reason?: string }>}
111
+ */
112
+ export function findMockOnlyDeclaredAcs(report) {
113
+ const perAc = Array.isArray(report?.perAcVerdicts) ? report.perAcVerdicts : [];
114
+ return perAc
115
+ .filter((e) => e && (e.verdict === 'MOCK-ONLY-DECLARED' || e.verdict === 'BLOCKED-BY-DECLARATION'))
116
+ .map((e) => ({ acId: e.acId, verdict: e.verdict, reason: e.reason }));
117
+ }
118
+
119
+ /**
120
+ * True when a verify report carries at least one MOCK-ONLY-DECLARED or
121
+ * BLOCKED-BY-DECLARATION verdict. Used by the finalise gate to refuse
122
+ * promotion to `verified` on such reports unless the operator has
123
+ * explicitly shipped-without-verified (spec §5.2 finalise gate rule).
124
+ *
125
+ * @param {object} report
126
+ * @returns {boolean}
127
+ */
128
+ export function reportHasMockOnlyDeclared(report) {
129
+ return findMockOnlyDeclaredAcs(report).length > 0;
130
+ }
131
+
132
+ /**
133
+ * Extract SCOPE-MISMATCH per-AC verdicts from a verify report. Introduced
134
+ * in the 0.8.0 slug-train (car 4) so REVIEW consumes the same shape via
135
+ * NV-BL-GATE-01. Earlier reports carry no such entries; this returns an
136
+ * empty array on those.
137
+ *
138
+ * @param {object} report
139
+ * @returns {Array<{ acId: string, verdict: string, reason?: string }>}
140
+ */
141
+ export function findScopeMismatchAcs(report) {
142
+ const perAc = Array.isArray(report?.perAcVerdicts) ? report.perAcVerdicts : [];
143
+ return perAc
144
+ .filter((e) => e && e.verdict === 'SCOPE-MISMATCH')
145
+ .map((e) => ({ acId: e.acId, verdict: e.verdict, reason: e.reason }));
146
+ }
147
+
148
+ /**
149
+ * True when a verify report carries at least one SCOPE-MISMATCH per-AC
150
+ * verdict. NV-BL-GATE-01 (0.8.0 slug-train car 4): the REVIEW gate
151
+ * consumes this so a scope mismatch caught at REVIEW-time fails the
152
+ * gate and returns the FBS to BUILD; the finalise gate reads the same
153
+ * shape as a last-mile refusal.
154
+ *
155
+ * @param {object} report
156
+ * @returns {boolean}
157
+ */
158
+ export function reportHasScopeMismatch(report) {
159
+ return findScopeMismatchAcs(report).length > 0;
160
+ }