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
package/src/cli/fbs.js ADDED
@@ -0,0 +1,171 @@
1
+ // `rcf fbs <fbs-id> <verb> [options]` subcommand handler
2
+ // (verification-integrity-cluster-spec §3.1, §5.2).
3
+ //
4
+ // One verb in Track A:
5
+ // depends-on append or replace a dependsOnServices[] entry
6
+ //
7
+ // Written at Define stage (build-cycle §3, spec §5.1) with the
8
+ // attestation mode drawn from the pre-flight config record. Never
9
+ // invented mid-Test to justify a green suite. The verb refuses to
10
+ // write an entry whose acIds are not on the FBS.
11
+
12
+ import { parseArgs } from 'node:util';
13
+
14
+ import { isRcfError, writeUnexpectedFailure } from '#core/errors';
15
+ import { updateDocument, walkTree } from '#core/store';
16
+
17
+ import { findProjectRoot } from '../view/index.js';
18
+
19
+ const VALID_MODES = new Set(['live', 'sandboxed', 'mocked', 'declaredMockOnly', 'notShipped']);
20
+
21
+ const DEPENDS_ON_OPTION_SPEC = {
22
+ service: { type: 'string' },
23
+ mode: { type: 'string' },
24
+ acs: { type: 'string' },
25
+ display: { type: 'string' },
26
+ purpose: { type: 'string' },
27
+ preflight: { type: 'string' },
28
+ force: { type: 'boolean' },
29
+ quiet: { type: 'boolean' },
30
+ help: { type: 'boolean' },
31
+ };
32
+
33
+ export const HELP = `Usage: rcf fbs <fbs-id> <verb> [options]
34
+
35
+ Verbs:
36
+ depends-on Record a third-party service dependency on this FBS
37
+
38
+ rcf fbs <fbs-id> depends-on --service <id> --mode <live|sandboxed|mocked|declaredMockOnly|notShipped>
39
+ --acs <acId>[,<acId>...] [--display "..."] [--purpose "..."] [--preflight <pfc-id>]
40
+
41
+ Writes a dependsOnServices[] entry keyed by --service. Idempotent by
42
+ service id: an existing entry with the same id is replaced (silent
43
+ overwrite is intentional here — the entry is derived from the same
44
+ pre-flight ruling every time). --acs must reference ACs the FBS
45
+ already binds via its acIds.
46
+
47
+ Options common:
48
+ --force Bypass the AC-in-FBS check (for repair only)
49
+ --quiet Suppress non-error stdout
50
+ --help Print this help
51
+ `;
52
+
53
+ /**
54
+ * @param {string[]} argv - argv slice after `fbs`
55
+ * @param {object} [deps]
56
+ * @returns {Promise<number>}
57
+ */
58
+ export async function main(argv, deps = {}) {
59
+ const stdout = deps.stdout ?? process.stdout;
60
+ const stderr = deps.stderr ?? process.stderr;
61
+ const cwd = deps.cwd ?? process.cwd();
62
+
63
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
64
+ stdout.write(HELP);
65
+ return argv[0] ? 0 : 2;
66
+ }
67
+
68
+ const fbsId = argv[0];
69
+ const verb = argv[1];
70
+ const rest = argv.slice(2);
71
+ if (!verb) {
72
+ stderr.write('[error] usage fbs: verb required (depends-on)\n');
73
+ return 2;
74
+ }
75
+ if (!/^FBS-\d{3,}$/.test(fbsId)) {
76
+ stderr.write(`[error] usage fbs: expected an FBS id like FBS-011, got '${fbsId}'\n`);
77
+ return 2;
78
+ }
79
+
80
+ const projectRoot = await findProjectRoot(cwd);
81
+ if (!projectRoot) {
82
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor).\n');
83
+ return 2;
84
+ }
85
+ const walkResult = await walkTree({ projectRoot });
86
+ const tree = walkResult.tree;
87
+ const fbs = tree.byId.get(fbsId);
88
+ if (!fbs || tree.kindById.get(fbsId) !== 'fbs') {
89
+ stderr.write(`[error] usage fbs: ${fbsId} not found or not an FBS\n`);
90
+ return 2;
91
+ }
92
+
93
+ if (verb === 'depends-on') return runDependsOn({ fbsId, fbs, rest, projectRoot, tree, walkResult, stdout, stderr });
94
+ stderr.write(`[error] usage fbs: unknown verb '${verb}' (expected depends-on)\n`);
95
+ return 2;
96
+ }
97
+
98
+ async function runDependsOn({ fbsId, fbs, rest, projectRoot, tree, walkResult, stdout, stderr }) {
99
+ let parsed;
100
+ try {
101
+ parsed = parseArgs({ args: rest, options: DEPENDS_ON_OPTION_SPEC, allowPositionals: false, strict: true });
102
+ } catch (err) {
103
+ stderr.write(`[error] usage ${err.message}\n`);
104
+ return 2;
105
+ }
106
+ const flags = parsed.values;
107
+ if (flags.help) { stdout.write(HELP); return 0; }
108
+
109
+ if (!flags.service) return usageErr(stderr, 'fbs depends-on: --service required');
110
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(flags.service)) {
111
+ return usageErr(stderr, `fbs depends-on: --service must be camelCase (got '${flags.service}')`);
112
+ }
113
+ if (!flags.mode) return usageErr(stderr, 'fbs depends-on: --mode required');
114
+ if (!VALID_MODES.has(flags.mode)) {
115
+ return usageErr(stderr, `fbs depends-on: unknown --mode '${flags.mode}' (expected ${[...VALID_MODES].join(' | ')})`);
116
+ }
117
+ if (!flags.acs) return usageErr(stderr, 'fbs depends-on: --acs required (comma-separated AC ids)');
118
+ const acIds = flags.acs.split(',').map((s) => s.trim()).filter(Boolean);
119
+ if (acIds.length === 0) return usageErr(stderr, 'fbs depends-on: --acs is empty');
120
+
121
+ if (!flags.force) {
122
+ const fbsAcIds = new Set(fbs.acIds ?? []);
123
+ const outside = acIds.filter((ac) => !fbsAcIds.has(ac));
124
+ if (outside.length > 0) {
125
+ stderr.write(`[error] refused fbs depends-on: ${fbsId} does not bind AC(s): ${outside.join(', ')}; add them to the FBS first (or pass --force for a repair path).\n`);
126
+ return 4;
127
+ }
128
+ }
129
+
130
+ const entry = {
131
+ id: flags.service,
132
+ displayName: flags.display ?? flags.service,
133
+ purpose: flags.purpose ?? `${flags.service} dependency`,
134
+ attestationMode: flags.mode,
135
+ acIds,
136
+ };
137
+ if (typeof flags.preflight === 'string' && flags.preflight.length > 0) {
138
+ // If the operator passed a bare pfc id, expand to the full ref; if
139
+ // they passed the composite (`pfc-...#services.<id>`) keep it.
140
+ entry.preFlightRef = flags.preflight.includes('#')
141
+ ? flags.preflight
142
+ : `${flags.preflight}#services.${flags.service}`;
143
+ }
144
+
145
+ const existing = Array.isArray(fbs.dependsOnServices) ? fbs.dependsOnServices : [];
146
+ const nextEntries = existing.filter((e) => e.id !== flags.service);
147
+ nextEntries.push(entry);
148
+
149
+ const sets = [{ path: 'dependsOnServices', value: nextEntries }];
150
+ const result = await updateDocument({
151
+ projectRoot, tree, id: fbsId, patch: null, sets, options: {}, walkErrors: walkResult.errors,
152
+ });
153
+ if (isRcfError(result)) return handleWriterError(result, stderr);
154
+ if (!flags.quiet) {
155
+ stdout.write(`fbs ${fbsId} depends-on: ${flags.service} (${flags.mode}) covers ${acIds.length} AC(s).\n`);
156
+ }
157
+ return 0;
158
+ }
159
+
160
+ function usageErr(stderr, message) {
161
+ stderr.write(`[error] usage ${message}\n`);
162
+ return 2;
163
+ }
164
+
165
+ function handleWriterError(err, stderr) {
166
+ if (err.kind === 'ioFailure') { writeUnexpectedFailure(err, stderr); return 1; }
167
+ stderr.write(`[error] ${err.kind} ${err.message}\n`);
168
+ if (err.kind === 'usage') return 2;
169
+ if (err.kind === 'validation' || err.kind === 'brokenReference') return 3;
170
+ return 1;
171
+ }
@@ -0,0 +1,377 @@
1
+ // `rcf finalise` subcommand handler (spec §8, amendments 4 + 5). The build-side
2
+ // home of the finalise-gate integration contract: the ship gate that promotes
3
+ // an FBS from `complete` to `verified` ONLY when an independent rcf-verify run
4
+ // against the deployed app passes.
5
+ //
6
+ // rcf finalise <fbs-id> --url <deploy-url> [options]
7
+ //
8
+ // The spec (§8.2) offers two homes for this: overloading `rcf build --mark
9
+ // complete`, or "a dedicated finalise verb". This is the dedicated verb.
10
+ // `rcf build --mark` is a deterministic-only, pure transition path that runs
11
+ // PRE-deploy; the finalise gate spawns a subprocess, hits the network, and may
12
+ // prompt - it does not belong on the mark path. The `complete -> verified`
13
+ // transition is the exact semantic home: `verified` means "independently
14
+ // verified against the running deploy", which is precisely what rcf-verify
15
+ // stamps.
16
+ //
17
+ // Hard contract points (spec §8.2 / §9):
18
+ // - verify is a FRESH SUBPROCESS with the isolation env, never an in-process
19
+ // import (§8.2 / §9: the verifier agent must start cold, zero build context);
20
+ // - the gate is the subprocess exit code AND the report's ship authority
21
+ // (spec §4): exit 0 with verdictAuthority 'ship' -> promote to verified;
22
+ // exit 0 with correctness-only authority -> HOLD (stay put, no promotion);
23
+ // non-zero -> stay put, surface findings;
24
+ // - findings flow via the --out report file, not stdout scraping;
25
+ // - if rcf-verify is absent, PROMPT to install (or accept an explicit
26
+ // --install-verify flag) - NEVER silently skip the gate (§8.3).
27
+
28
+ import { resolve as resolvePath } from 'node:path';
29
+ import process from 'node:process';
30
+ import { parseArgs } from 'node:util';
31
+
32
+ import { isRcfError, rcfError, writeUnexpectedFailure } from '#core/errors';
33
+ import { updateDocument, walkTree } from '#core/store';
34
+
35
+ import { findProjectRoot } from '../view/index.js';
36
+ import { kindOf } from '../query/index.js';
37
+ import {
38
+ buildVerifyArgs,
39
+ composeShipWithoutVerifiedRecord,
40
+ detectVerify,
41
+ findMockOnlyDeclaredAcs,
42
+ loadReport,
43
+ reportHasMockOnlyDeclared,
44
+ resolveAbsentVerify,
45
+ spawnVerify,
46
+ summariseReport,
47
+ writeShipWithoutVerifiedRecord,
48
+ } from '../finalise/index.js';
49
+
50
+ // Kept in sync with verify's own sets by contract (build never imports verify -
51
+ // that would break the §9 independence guarantee). Profiles: spec §4.
52
+ // Severities: spec §5.1 finding taxonomy.
53
+ const PROFILES = new Set(['deployed', 'ci', 'local-dev']);
54
+ const SEVERITIES = new Set(['PASS', 'COSMETIC', 'DEGRADED', 'BROKEN']);
55
+ const DEFAULT_PROFILE = 'deployed';
56
+ const DEFAULT_GATE = 'BROKEN';
57
+ const DEFAULT_REPORT = '.rcf-verify-report.json';
58
+ // The finalise gate only makes sense on a built item; verified re-verify is
59
+ // allowed (the §5.4 loop re-runs the gate on a previously-verified item).
60
+ const FINALISABLE_STATUSES = new Set(['complete', 'verified']);
61
+
62
+ const OPTION_SPEC = {
63
+ url: { type: 'string' },
64
+ profile: { type: 'string' },
65
+ 'parity-env': { type: 'boolean' },
66
+ provision: { type: 'string' },
67
+ 'severity-gate': { type: 'string' },
68
+ out: { type: 'string' },
69
+ chain: { type: 'string' },
70
+ persona: { type: 'string' },
71
+ 'install-verify': { type: 'boolean' },
72
+ // 0.7.0 verification-integrity: acknowledge MOCK-ONLY-DECLARED /
73
+ // BLOCKED-BY-DECLARATION per-AC verdicts on the verify report and
74
+ // ship the FBS 'complete' without promoting to 'verified'. Records
75
+ // the operator's ack on the manifest.
76
+ 'ship-without-verified': { type: 'boolean' },
77
+ quiet: { type: 'boolean' },
78
+ help: { type: 'boolean' },
79
+ };
80
+
81
+ export const HELP = `Usage: rcf finalise <fbs-id> --url <deploy-url> [options]
82
+
83
+ The ship gate. Promotes an FBS at status complete to verified ONLY when an
84
+ independent rcf-verify run passes AND carries ship authority.
85
+
86
+ rcf-verify is launched as a FRESH SUBPROCESS (never in-process) so the
87
+ verifier agent starts cold, with zero build context - its only inputs are the
88
+ RCF chain (the acceptance contract) and the live URL. The gate is the
89
+ subprocess exit code AND the report's ship authority: a run must both pass and
90
+ carry SHIP authority (a 'deployed'-profile run, or a 'ci'/'local-dev' run with
91
+ --parity-env) to promote. A correctness-only PASS holds without promoting.
92
+ Findings flow back via the --out report file.
93
+
94
+ If rcf-verify is not installed, finalise PROMPTS to install it (install-together
95
+ posture) - it never silently skips the gate. Off a TTY, pass --install-verify.
96
+
97
+ Required:
98
+ <fbs-id> The FBS item to finalise (must be 'complete')
99
+ --url <deploy-url> The running deployed app to verify against
100
+
101
+ Options:
102
+ --profile <p> deployed | ci | local-dev (default: deployed)
103
+ --parity-env Assert a ci/local-dev runtime is edge-identical to
104
+ prod - the only path to a SHIP verdict off 'deployed'
105
+ --provision <file> Provisioning / prerequisite-credentials FILE
106
+ (never inline; secrets never echoed)
107
+ --severity-gate <sev> Block at/above this severity:
108
+ PASS | COSMETIC | DEGRADED | BROKEN (default: BROKEN)
109
+ --out <path> Where verify writes its report artifact
110
+ (default: ${DEFAULT_REPORT})
111
+ --chain <PRD/ref> Which PRD/chain to verify against (default: repo's)
112
+ --persona <name> Adversarial persona flavour
113
+ --install-verify If rcf-verify is absent, install it without
114
+ prompting (the sanctioned non-interactive path)
115
+ --ship-without-verified Acknowledge MOCK-ONLY-DECLARED /
116
+ BLOCKED-BY-DECLARATION per-AC verdicts on
117
+ the verify report and ship the FBS
118
+ 'complete' without promoting to 'verified'
119
+ (verification-integrity 0.7.0 §5.2). Reads
120
+ the disclosed verdicts and leaves the FBS at
121
+ 'complete'. Without this flag, the gate
122
+ refuses to promote when any AC lands in
123
+ those verdicts.
124
+ --quiet Suppress non-error confirmations
125
+ --help Print this help
126
+
127
+ Exit codes:
128
+ 0 gate passed - FBS marked verified
129
+ 1 IO / unexpected runtime failure
130
+ 2 usage error (bad flags, unknown / non-FBS id)
131
+ 3 schema validation or broken references (tree unreadable, or write refused)
132
+ 4 gate NOT passed - verify blocked ship, OR verify passed but the run lacks
133
+ ship authority (a correctness-only HOLD), OR rcf-verify absent and install
134
+ declined/unavailable - the FBS is left unchanged, findings surfaced
135
+ `;
136
+
137
+ /**
138
+ * @param {string[]} argv - argv slice after `finalise`
139
+ * @param {object} [deps] - injectable seams (detect / spawn / prompt / install /
140
+ * loadReport) for testing the gate without a real rcf-verify + live app.
141
+ * @returns {Promise<number>}
142
+ */
143
+ export async function main(argv, deps = {}) {
144
+ const stdout = deps.stdout ?? process.stdout;
145
+ const stderr = deps.stderr ?? process.stderr;
146
+ const cwd = deps.cwd ?? process.cwd();
147
+
148
+ let parsed;
149
+ try {
150
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true });
151
+ } catch (err) {
152
+ stderr.write(`[error] usage ${err.message}\n`);
153
+ stderr.write(HELP);
154
+ return 2;
155
+ }
156
+ const flags = parsed.values;
157
+ const positionals = parsed.positionals;
158
+ if (flags.help) { stdout.write(HELP); return 0; }
159
+
160
+ const usage = (message) => {
161
+ stderr.write(`[error] usage finalise: ${message}\n`);
162
+ return 2;
163
+ };
164
+
165
+ if (positionals.length === 0) return usage('an <fbs-id> positional is required');
166
+ if (positionals.length > 1) return usage('multiple positional ids are not supported');
167
+ const fbsId = positionals[0];
168
+ if (fbsId.includes('*') || fbsId.includes('?')) {
169
+ return usage('wildcard / glob positional not supported');
170
+ }
171
+
172
+ if (!flags.url) return usage('--url <deploy-url> is required');
173
+
174
+ const profile = flags.profile ?? DEFAULT_PROFILE;
175
+ if (!PROFILES.has(profile)) {
176
+ return usage(`unknown --profile ${profile} (expected deployed | ci | local-dev)`);
177
+ }
178
+ const gate = flags['severity-gate'] ?? DEFAULT_GATE;
179
+ if (!SEVERITIES.has(gate)) {
180
+ return usage(`unknown --severity-gate ${gate} (expected PASS | COSMETIC | DEGRADED | BROKEN)`);
181
+ }
182
+ // Dash-footgun (mirrors build's --body-file / verify's --provision discipline):
183
+ // a --provision that looks like a flag is a swallowed next option or an inline
184
+ // credential. Credentials are FILE-only, never inline, never echoed.
185
+ if (typeof flags.provision === 'string' && flags.provision.startsWith('-')) {
186
+ return usage('--provision takes a file path, not a flag or inline value (credentials are never accepted inline)');
187
+ }
188
+
189
+ const projectRoot = await findProjectRoot(cwd);
190
+ if (!projectRoot) {
191
+ stderr.write('[error] usage no project root found (no rcf/manifest.json in this directory or any ancestor). Run `npx rcf init` to create and wire a project.\n');
192
+ return 2;
193
+ }
194
+
195
+ // Walker errors block finalise entirely - no gate runs against a tree that
196
+ // fails validation, and no status write lands (mirrors build --mark, §D6).
197
+ const { tree, errors } = await walkTree({ projectRoot });
198
+ if (errors.length > 0) {
199
+ stderr.write('[error] validation the RCF tree does not validate; fix it before finalising.\n');
200
+ return 3;
201
+ }
202
+
203
+ // The positional must be an FBS at a finalisable status.
204
+ const kind = kindOf(tree, fbsId);
205
+ if (kind !== 'fbs') {
206
+ if (!kind) return usage(`id ${fbsId} not found`);
207
+ return usage(`${fbsId} is a ${kind} id; finalise addresses FBS items only`);
208
+ }
209
+ const fbs = tree.byId.get(fbsId);
210
+ const currentStatus = fbs.executionStatus;
211
+ if (!FINALISABLE_STATUSES.has(currentStatus)) {
212
+ stderr.write(`[error] refused finalise: ${fbsId} is '${currentStatus}'; the finalise gate promotes 'complete' -> 'verified'. `
213
+ + `Mark it complete first: rcf build ${fbsId} --mark complete\n`);
214
+ return 4;
215
+ }
216
+
217
+ const quiet = Boolean(flags.quiet);
218
+ const io = { stdout, stderr, input: deps.input ?? process.stdin, output: deps.output ?? process.stdout };
219
+
220
+ // --- rcf-verify install detection (§8.3) --------------------------------
221
+ let detection = deps.detectVerify
222
+ ? await deps.detectVerify({ cwd: projectRoot })
223
+ : await detectVerify({ cwd: projectRoot });
224
+ if (!detection.installed) {
225
+ const isTty = deps.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
226
+ const outcome = await resolveAbsentVerify(
227
+ { installFlag: Boolean(flags['install-verify']), isTty },
228
+ io,
229
+ deps,
230
+ );
231
+ if (outcome.action === 'abort') {
232
+ stderr.write(`[error] refused ${outcome.reason}\n`);
233
+ return outcome.code;
234
+ }
235
+ // Installed - re-detect to get a concrete invocation.
236
+ detection = deps.detectVerify
237
+ ? await deps.detectVerify({ cwd: projectRoot })
238
+ : await detectVerify({ cwd: projectRoot });
239
+ if (!detection.installed) {
240
+ stderr.write('[error] unexpected rcf-verify still not resolvable after install; install it manually and re-run.\n');
241
+ return 1;
242
+ }
243
+ }
244
+
245
+ // --- spawn the fresh verify subprocess (§8.2) ---------------------------
246
+ const outPath = resolvePath(projectRoot, flags.out ?? DEFAULT_REPORT);
247
+ const verifyArgs = buildVerifyArgs({
248
+ repo: projectRoot,
249
+ url: flags.url,
250
+ profile,
251
+ out: outPath,
252
+ severityGate: gate,
253
+ parityEnv: Boolean(flags['parity-env']),
254
+ provision: flags.provision,
255
+ chain: flags.chain,
256
+ persona: flags.persona,
257
+ });
258
+
259
+ if (!quiet) {
260
+ stdout.write(`[finalise] launching rcf-verify (fresh subprocess) against ${flags.url} [profile=${profile}]...\n`);
261
+ }
262
+
263
+ let spawnResult;
264
+ try {
265
+ spawnResult = deps.spawnVerify
266
+ ? await deps.spawnVerify({ invocation: detection.invocation, verifyArgs, cwd: projectRoot }, deps)
267
+ : await spawnVerify({ invocation: detection.invocation, verifyArgs, cwd: projectRoot }, deps);
268
+ } catch (err) {
269
+ writeUnexpectedFailure(
270
+ rcfError({ kind: 'ioFailure', message: `finalise: could not launch rcf-verify: ${err.message}`, stack: err.stack }),
271
+ stderr,
272
+ );
273
+ return 1;
274
+ }
275
+
276
+ // --- gate on the subprocess exit code (§8.2) + ship authority (§4) ------
277
+ if (spawnResult.code === 0) {
278
+ // Gate passed on exit code. Promote to verified (idempotent if a re-verify
279
+ // of an already-verified item).
280
+ if (currentStatus === 'verified') {
281
+ if (!quiet) stdout.write(`[finalise] gate passed; ${fbsId} already verified (re-verify) -> ${outPath}\n`);
282
+ return 0;
283
+ }
284
+ // Ship-authority gate (spec §4, w-2026-07-22-004): exit 0 is necessary but
285
+ // not sufficient to write `verified`. Only a run that carries SHIP authority
286
+ // may promote - a `deployed`-profile run, or a `ci`/`local-dev` run with an
287
+ // explicit --parity-env parity assertion. A correctness-only PASS (e.g. a
288
+ // bare `--profile ci` run) is a real regression pass but carries no ship
289
+ // authority, so it must HOLD rather than promote. We read the authority from
290
+ // the report verify stamped (never re-derive it here - that would break the
291
+ // §9 independence guarantee). An unreadable report on a pass is treated as a
292
+ // HOLD, never a silent promotion (mirrors ingest's "unreadable is not a pass").
293
+ const passLoaded = await (deps.loadReport ? deps.loadReport(outPath, deps) : loadReport(outPath, deps));
294
+ const authority = passLoaded.ok ? passLoaded.report.verdictAuthority : undefined;
295
+ if (authority !== 'ship') {
296
+ stderr.write(`[finalise] HOLD: rcf-verify passed but this run carries '${authority ?? 'unknown'}' authority, `
297
+ + `not 'ship'; ${fbsId} left '${currentStatus}'.\n`);
298
+ stderr.write('Only a \'deployed\'-profile run - or a \'ci\'/\'local-dev\' run with --parity-env asserting the '
299
+ + 'runtime is edge-identical to prod - carries ship authority and can promote to verified. '
300
+ + `Report: ${outPath}\n`);
301
+ if (!passLoaded.ok) stderr.write(`[finalise] (the verify report could not be read: ${passLoaded.reason})\n`);
302
+ return 4;
303
+ }
304
+ // 0.7.0 verification-integrity §5.2: the finalise gate reads the
305
+ // verify report's per-AC verdicts and refuses to promote to
306
+ // 'verified' on MOCK-ONLY-DECLARED / BLOCKED-BY-DECLARATION unless
307
+ // the operator has explicitly shipped-without-verified. Graceful
308
+ // when the report carries no perAcVerdicts field (verify's train
309
+ // car is later; older reports are silent on this axis).
310
+ if (passLoaded.ok && reportHasMockOnlyDeclared(passLoaded.report)) {
311
+ const declared = findMockOnlyDeclaredAcs(passLoaded.report);
312
+ if (flags['ship-without-verified']) {
313
+ // B-1 (spec section 5.2, review 2026-07-31): the ack is
314
+ // recorded on the manifest so it is durable and greppable at
315
+ // ship time. Stdout alone is neither. Written BEFORE the
316
+ // success confirmation so a manifest-write failure surfaces
317
+ // to the operator as a hard failure rather than a silent
318
+ // stdout-only ack.
319
+ const ackRecord = composeShipWithoutVerifiedRecord({
320
+ manifest: tree.manifest,
321
+ fbsId,
322
+ declaredAcs: declared,
323
+ reportPath: outPath,
324
+ });
325
+ const ackResult = await writeShipWithoutVerifiedRecord({
326
+ projectRoot, tree, record: ackRecord,
327
+ });
328
+ if (isRcfError(ackResult)) {
329
+ if (ackResult.kind === 'ioFailure') { writeUnexpectedFailure(ackResult, stderr); return 1; }
330
+ stderr.write(`[error] ${ackResult.kind} ${ackResult.message}\n`);
331
+ if (ackResult.kind === 'validation' || ackResult.kind === 'brokenReference') return 3;
332
+ return 1;
333
+ }
334
+ if (!quiet) {
335
+ stdout.write(`[finalise] gate passed and ${declared.length} AC(s) came back with declared mock-only verdicts; --ship-without-verified acknowledged (${ackRecord.id} on rcf/manifest.json); ${fbsId} left '${currentStatus}'. Report: ${outPath}\n`);
336
+ stdout.write(summariseReport(passLoaded.report));
337
+ }
338
+ return 0;
339
+ }
340
+ stderr.write(`[finalise] gate NOT promoted: ${declared.length} AC(s) came back MOCK-ONLY-DECLARED / BLOCKED-BY-DECLARATION on the report; ${fbsId} left '${currentStatus}'.\n`);
341
+ stderr.write('Only a run where every service-bound AC verifies with a live-attested profile promotes to verified. To ship the FBS at complete anyway, re-run with --ship-without-verified (the ack lands on the record).\n');
342
+ stderr.write(summariseReport(passLoaded.report));
343
+ stderr.write(`Report: ${outPath}\n`);
344
+ return 4;
345
+ }
346
+ const result = await updateDocument({
347
+ projectRoot, tree, id: fbsId, sets: [{ path: 'executionStatus', value: 'verified' }], options: {},
348
+ });
349
+ if (isRcfError(result)) {
350
+ if (result.kind === 'ioFailure') { writeUnexpectedFailure(result, stderr); return 1; }
351
+ stderr.write(`[error] ${result.kind} ${result.message}\n`);
352
+ if (result.kind === 'usage') return 2;
353
+ if (result.kind === 'validation' || result.kind === 'brokenReference') return 3;
354
+ return 1;
355
+ }
356
+ if (!quiet) stdout.write(`[finalise] gate passed; marked ${fbsId} complete -> verified. Report: ${outPath}\n`);
357
+ return 0;
358
+ }
359
+
360
+ // Gate NOT passed. The FBS stays put; surface the findings from the report
361
+ // (the §5.4 verify -> fix -> re-verify seam), then fail non-zero.
362
+ stderr.write(`[finalise] gate NOT passed (rcf-verify exit ${spawnResult.code}); ${fbsId} left '${currentStatus}'.\n`);
363
+ const loaded = await (deps.loadReport ? deps.loadReport(outPath, deps) : loadReport(outPath, deps));
364
+ if (loaded.ok) {
365
+ stderr.write(summariseReport(loaded.report));
366
+ stderr.write(`Full report (build-lite ingestible, chain-node-addressed): ${outPath}\n`);
367
+ } else {
368
+ stderr.write(`[finalise] could not read the verify report: ${loaded.reason}\n`);
369
+ }
370
+ // Map verify's exit-code convention to finalise's. Verify: 2 usage, 3 chain
371
+ // load, 5 gate/NOT-DEPLOYED/BLOCKED/LAUNCH-FAILURE, 1 io. A tripped ship gate
372
+ // is a refusal to finalise -> 4; a verify usage/chain error surfaces as-is.
373
+ if (spawnResult.code === 2) return 2;
374
+ if (spawnResult.code === 3) return 3;
375
+ if (spawnResult.code === 5) return 4;
376
+ return 1;
377
+ }