@bongos/core 1.19.1073 → 1.19.1074

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 (297) hide show
  1. package/.bongos-core.json +320 -465
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -60
  5. package/clients/bongos-client/index.cjs +4 -60
  6. package/clients/bongos-client/index.d.ts +5 -90
  7. package/clients/bongos-client/index.mjs +4 -60
  8. package/config/branding.neutral.json +1 -5
  9. package/config/modules.neutral.json +2 -3
  10. package/config/scheduled-routines.json +0 -2
  11. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  12. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  13. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  14. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  15. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +2 -0
  16. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  17. package/docs/adr/README.md +1 -0
  18. package/docs/api/openapi.json +171 -1661
  19. package/docs/api-reference.md +9 -41
  20. package/docs/architecture.md +16 -1
  21. package/docs/branding-contract.md +0 -3
  22. package/docs/copy-inventory.md +454 -479
  23. package/docs/copy-registry.json +746 -978
  24. package/docs/file-map.md +5 -34
  25. package/docs/module-api-changelog.md +2 -0
  26. package/docs/modules-contract.md +13 -1
  27. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  28. package/docs/onboarding/diagrams/README.md +3 -3
  29. package/docs/onboarding/diagrams/assertions.json +2 -22
  30. package/docs/onboarding/primer.md +9 -28
  31. package/docs/page-inventory.json +4 -1
  32. package/docs/page-readings.json +1032 -1077
  33. package/docs/recipes/render-pilot.md +29 -10
  34. package/migrations/core_257_module_store_registry.sql +87 -0
  35. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  36. package/modules/agents/module.json +1 -0
  37. package/modules/autonomy/module.json +1 -0
  38. package/modules/builder-settings/module.json +1 -0
  39. package/modules/builder-settings/render-prefs.js +4 -5
  40. package/modules/copy-desk/docx.js +78 -5
  41. package/modules/copy-desk/module.json +1 -0
  42. package/modules/copy-desk/routes/copy-desk.js +9 -9
  43. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  44. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  45. package/modules/discord/module.json +1 -0
  46. package/modules/economy/module.json +1 -0
  47. package/modules/economy/routes/credits.js +1 -2
  48. package/modules/government/catalog.js +13 -17
  49. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  50. package/modules/government/module.json +1 -0
  51. package/modules/government/protected-surfaces.json +3 -11
  52. package/modules/government/resolver.js +3 -46
  53. package/modules/government/routes/government.js +0 -2
  54. package/modules/government/session-scopes.js +43 -83
  55. package/modules/government/session-scopes.json +8 -18
  56. package/modules/grading/module.json +1 -0
  57. package/modules/hall-ui/module.json +1 -0
  58. package/modules/hall-ui/public/board-lib.js +2 -2
  59. package/modules/hall-ui/public/builders.js +29 -40
  60. package/modules/hall-ui/public/collab.html +1 -1
  61. package/modules/hall-ui/public/collab.js +1 -1
  62. package/modules/hall-ui/public/diagrams.html +3 -3
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/gate.js +2 -2
  65. package/modules/hall-ui/public/gate.states.json +1 -1
  66. package/modules/hall-ui/public/hall-render.js +43 -79
  67. package/modules/hall-ui/public/index.html +5 -1
  68. package/modules/hall-ui/public/modules.js +1 -0
  69. package/modules/hall-ui/public/oversight.css +5 -5
  70. package/modules/hall-ui/public/palette.js +0 -11
  71. package/modules/hall-ui/public/primer.js +1 -1
  72. package/modules/hall-ui/public/sessions.html +1 -1
  73. package/modules/hall-ui/public/settings-sessions.js +1 -1
  74. package/modules/hall-ui/public/settings.css +0 -1
  75. package/modules/hall-ui/public/settings.html +4 -33
  76. package/modules/hall-ui/public/settings.js +3 -33
  77. package/modules/hall-ui/public/settings.states.json +2 -2
  78. package/modules/hall-ui/public/studio.html +1 -1
  79. package/modules/hall-ui/public/style.css +10 -13
  80. package/modules/hall-ui/public/task.html +4 -1
  81. package/modules/hall-ui/public/task.js +36 -1
  82. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  83. package/modules/hall-ui/public/tweak-editor.css +16 -0
  84. package/modules/hall-ui/public/tweak-editor.html +26 -3
  85. package/modules/hall-ui/public/tweak-editor.js +32 -1
  86. package/modules/hall-ui/public/work.js +1 -1
  87. package/modules/hall-ui/records/tweak-editor.md +1 -0
  88. package/modules/ideas/module.json +1 -0
  89. package/modules/lifecycle/db-claim-reads.js +1 -58
  90. package/modules/lifecycle/db-ship.js +0 -5
  91. package/modules/lifecycle/db.js +0 -4
  92. package/modules/lifecycle/kickoff-checklist.js +0 -4
  93. package/modules/lifecycle/lifecycle.js +0 -8
  94. package/modules/lifecycle/module.json +1 -0
  95. package/modules/lifecycle/routes/tasks.js +5 -7
  96. package/modules/memory/module.json +1 -0
  97. package/modules/npm-release/module.json +1 -0
  98. package/modules/onboarding/module.json +1 -0
  99. package/modules/onboarding/onboarding-state.js +17 -36
  100. package/modules/onboarding/routes/access-requests.js +3 -3
  101. package/modules/platform-identity/module.json +1 -0
  102. package/modules/platform-identity/platform-identity.js +13 -5
  103. package/modules/platform-identity/routes/sso.js +2 -2
  104. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  105. package/modules/provisioning/module.json +1 -0
  106. package/modules/provisioning/provisioning.js +2 -2
  107. package/modules/provisioning/starter-bundles.js +5 -25
  108. package/modules/public-landing/module.json +1 -0
  109. package/modules/public-landing/public/assets/cosmos.css +1 -1
  110. package/modules/public-landing/public/contact.html +1 -1
  111. package/modules/public-landing/public/privacy.html +1 -1
  112. package/modules/public-landing/public/projects.html +101 -32
  113. package/modules/public-landing/public/projects.probes.json +2 -2
  114. package/modules/public-landing/public/projects.states.json +4 -3
  115. package/modules/public-landing/public/terms.html +1 -1
  116. package/modules/security/module.json +1 -0
  117. package/modules/sessions/module.json +1 -0
  118. package/modules/specialities/module.json +1 -0
  119. package/modules/status-ui/module.json +1 -0
  120. package/modules/ui-design/module.json +1 -0
  121. package/package-lock.json +2 -2
  122. package/package.json +1 -1
  123. package/release-notes.json +78 -0
  124. package/scripts/gds/artifact-format.js +203 -0
  125. package/scripts/gds/audit-authorship.sh +1 -13
  126. package/scripts/gds/claim.js +0 -15
  127. package/scripts/gds/cli-lib.js +8 -8
  128. package/scripts/gds/diagram-facts.js +12 -23
  129. package/scripts/gds/do-api.js +13 -80
  130. package/scripts/gds/doc-cli-guard.js +2 -0
  131. package/scripts/gds/doctor.js +1 -1
  132. package/scripts/gds/feedback-latest.js +139 -9
  133. package/scripts/gds/fitness-checks-identity.js +2 -7
  134. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  135. package/scripts/gds/fitness-ratchets.js +4 -4
  136. package/scripts/gds/fitness.js +10 -10
  137. package/scripts/gds/gen-diagrams.js +49 -71
  138. package/scripts/gds/http-api-client.js +12 -19
  139. package/scripts/gds/init.js +1 -1
  140. package/scripts/gds/lint-infra-exec.js +1 -1
  141. package/scripts/gds/local-preview-lib.js +8 -39
  142. package/scripts/gds/local-preview.js +2 -15
  143. package/scripts/gds/migration-namespace.js +3 -3
  144. package/scripts/gds/module-artifact.js +212 -0
  145. package/scripts/gds/module.js +95 -6
  146. package/scripts/gds/package-core.js +5 -116
  147. package/scripts/gds/provision-config.js +2 -2
  148. package/scripts/gds/provision-net.js +2 -3
  149. package/scripts/gds/provision.js +9 -9
  150. package/scripts/gds/publish-manifest.js +1 -2
  151. package/scripts/gds/regen-instance-docs.js +1 -1
  152. package/scripts/gds/run-unit-tests.js +2 -16
  153. package/scripts/gds/sandbox-stage.js +39 -341
  154. package/scripts/gds/ship-flow.js +6 -6
  155. package/scripts/gds/ship-land.js +3 -4
  156. package/scripts/gds/ship-regen.js +10 -41
  157. package/scripts/gds/ship.js +4 -24
  158. package/scripts/gds/start.js +0 -15
  159. package/scripts/hall-preview/README.md +5 -5
  160. package/scripts/hall-preview/server.js +6 -6
  161. package/scripts/render-diagrams.sh +1 -1
  162. package/src/bongos/api-errors.js +1 -1
  163. package/src/bongos/auth-github.js +2 -3
  164. package/src/bongos/auth.js +51 -126
  165. package/src/bongos/db-kernel.js +2 -3
  166. package/src/bongos/db.js +5 -28
  167. package/src/bongos/module-scope-map.js +14 -29
  168. package/src/bongos/module-store.js +141 -0
  169. package/src/bongos/route-rank-check.js +3 -16
  170. package/src/bongos/routes/auth.js +0 -116
  171. package/src/bongos/routes/builders.js +5 -15
  172. package/src/bongos/routes/instance.js +8 -14
  173. package/src/bongos/routes/me.js +6 -22
  174. package/src/bongos/routes/modules.js +83 -0
  175. package/src/bongos/routes.js +5 -0
  176. package/src/module-api.js +13 -6
  177. package/src/module-loader/catalog.js +3 -0
  178. package/src/module-loader/loader.js +4 -4
  179. package/src/module-loader/manifest-schema.js +15 -7
  180. package/src/module-seams.js +2 -3
  181. package/src/modules.js +42 -14
  182. package/tests/agents_spend_guard.mjs +1 -1
  183. package/tests/api_client.mjs +0 -15
  184. package/tests/auth_page_gate.mjs +10 -10
  185. package/tests/canonical_profile_url.mjs +1 -3
  186. package/tests/claim_action.mjs +4 -1
  187. package/tests/claim_from_task_and_home.mjs +127 -0
  188. package/tests/consumer_layout_boot.mjs +2 -0
  189. package/tests/copy_desk_page_docx.mjs +33 -2
  190. package/tests/core_upgrade_runner.mjs +6 -6
  191. package/tests/deploy_divergence_line.mjs +11 -11
  192. package/tests/diagram_facts_offset.mjs +1 -1
  193. package/tests/do_api.mjs +106 -0
  194. package/tests/feedback_latest.mjs +122 -0
  195. package/tests/fitness.mjs +44 -42
  196. package/tests/gate_approvals.mjs +0 -1
  197. package/tests/go_live.mjs +3 -3
  198. package/tests/government_abuse_matrix.mjs +20 -19
  199. package/tests/government_ownership_scope.mjs +33 -35
  200. package/tests/government_parity.mjs +2 -2
  201. package/tests/government_protected_surfaces.mjs +56 -37
  202. package/tests/government_require_permission.mjs +3 -3
  203. package/tests/government_seed.mjs +37 -4
  204. package/tests/government_session_scope.mjs +92 -366
  205. package/tests/hall_error_envelope.mjs +4 -0
  206. package/tests/hall_landing_boot.mjs +4 -1
  207. package/tests/hall_palette.mjs +1 -16
  208. package/tests/hall_record_world.mjs +6 -0
  209. package/tests/hall_tweak_editor.mjs +52 -1
  210. package/tests/host_topology_skips.mjs +1 -2
  211. package/tests/html_comment_nesting.mjs +137 -0
  212. package/tests/infra_exec_paths.mjs +9 -10
  213. package/tests/init.mjs +4 -5
  214. package/tests/instance_manifest.mjs +18 -25
  215. package/tests/kickoff_checklist.mjs +1 -1
  216. package/tests/lib_sh_resolution.mjs +0 -221
  217. package/tests/local_preview.mjs +0 -12
  218. package/tests/module-scope-map.mjs +9 -9
  219. package/tests/module_catalog.mjs +1 -1
  220. package/tests/module_cli.mjs +15 -15
  221. package/tests/module_contributions.mjs +6 -6
  222. package/tests/module_loader.mjs +6 -6
  223. package/tests/module_manifest.mjs +13 -4
  224. package/tests/module_store_publish.mjs +307 -0
  225. package/tests/module_store_publish_route.mjs +121 -0
  226. package/tests/module_store_registry_migration.mjs +70 -0
  227. package/tests/modules.mjs +56 -26
  228. package/tests/onboarding_route_signals.mjs +21 -31
  229. package/tests/onboarding_state.mjs +35 -49
  230. package/tests/permission_path.mjs +0 -3
  231. package/tests/platform_boot.mjs +1 -2
  232. package/tests/profile_route.mjs +1 -6
  233. package/tests/projects_hub.mjs +5 -5
  234. package/tests/projects_hub_app_step.mjs +138 -0
  235. package/tests/projects_hub_module_picker.mjs +2 -2
  236. package/tests/projects_hub_pre_uat.mjs +4 -5
  237. package/tests/provision.mjs +7 -7
  238. package/tests/provisioning_starter_bundles.mjs +0 -22
  239. package/tests/publish_branch_route.mjs +10 -18
  240. package/tests/publish_manifest.mjs +1 -2
  241. package/tests/rank_tier_single_source.mjs +1 -13
  242. package/tests/repo_map.mjs +3 -4
  243. package/tests/runner_drift.mjs +3 -21
  244. package/tests/sandbox_stage.mjs +57 -390
  245. package/tests/session_rename_fallback.mjs +0 -21
  246. package/tests/ship_error_shape.mjs +1 -1
  247. package/tests/task_visual_slots.mjs +31 -0
  248. package/tests/terms_acceptance.mjs +1 -1
  249. package/tests/upgrade.mjs +4 -4
  250. package/tests/watch_sealed_floor.mjs +0 -1
  251. package/tests/wizard_draft_resume.mjs +2 -2
  252. package/tests/wizard_intent_resume.mjs +21 -10
  253. package/tests/wizard_preselect_why.mjs +18 -18
  254. package/docs/onboarding/browser-terminal-guide.md +0 -73
  255. package/docs/recipes/managed-settings-remote-control.md +0 -416
  256. package/modules/dev-box/CLAUDE.md +0 -15
  257. package/modules/dev-box/box-access.js +0 -623
  258. package/modules/dev-box/box-credential.js +0 -94
  259. package/modules/dev-box/box-onboard.js +0 -486
  260. package/modules/dev-box/boxes.js +0 -1038
  261. package/modules/dev-box/db.js +0 -39
  262. package/modules/dev-box/module.json +0 -19
  263. package/modules/dev-box/routes/box.js +0 -1223
  264. package/scripts/gds/box-auth-check.js +0 -160
  265. package/scripts/gds/box-infra.js +0 -321
  266. package/scripts/gds/box-sync.js +0 -216
  267. package/scripts/gds/box.js +0 -1332
  268. package/scripts/gds/cf-tunnel.js +0 -558
  269. package/scripts/gds/smoke-box.sh +0 -150
  270. package/scripts/gds/tree-preflight.js +0 -241
  271. package/src/bongos/app-pair.js +0 -172
  272. package/tests/app_pair.mjs +0 -168
  273. package/tests/box_access.mjs +0 -578
  274. package/tests/box_auth_check.mjs +0 -114
  275. package/tests/box_code_staleness.mjs +0 -129
  276. package/tests/box_connect_e2e.mjs +0 -196
  277. package/tests/box_cost.mjs +0 -70
  278. package/tests/box_credential.mjs +0 -125
  279. package/tests/box_dns_repoint.mjs +0 -153
  280. package/tests/box_env.mjs +0 -64
  281. package/tests/box_host_keys.mjs +0 -126
  282. package/tests/box_infra_resolve.mjs +0 -246
  283. package/tests/box_onboard.mjs +0 -317
  284. package/tests/box_scope_session.mjs +0 -119
  285. package/tests/box_sweep_docs.mjs +0 -119
  286. package/tests/box_sync.mjs +0 -55
  287. package/tests/box_sync_scope_report.mjs +0 -126
  288. package/tests/box_task_scope.mjs +0 -169
  289. package/tests/box_task_scope_pause.mjs +0 -66
  290. package/tests/box_terminal_ssrf.mjs +0 -96
  291. package/tests/boxes.mjs +0 -1585
  292. package/tests/cf_ruleset_preflight.mjs +0 -142
  293. package/tests/cf_tunnel.mjs +0 -496
  294. package/tests/docs_scrubber_damage.mjs +0 -111
  295. package/tests/module_scope_active_claims.mjs +0 -94
  296. package/tests/ship_api_client_pathspec.mjs +0 -100
  297. package/tests/tree_preflight.mjs +0 -243
package/tests/modules.mjs CHANGED
@@ -14,12 +14,11 @@ const {
14
14
  } = modulesMod;
15
15
 
16
16
  const VANILLA = {}; // no config at all
17
- const OTB = { modules: { game: true, 'art-pipeline': true, discord: true, 'dev-box': true } };
18
- // The post-extraction registry: built-ins (game, art-pipeline, discord) + every
19
- // discovered module. dev-box now lives in modules/dev-box/ (BV1.R43), so it is a
20
- // DISCOVERED key, not a built-in MODULE_REGISTRY one. The pure resolver tests
21
- // below pass this effective registry so 'dev-box' is a known key — exactly what
22
- // loadModules() does in production. Membership across the move is preserved here.
17
+ const OTB = { modules: { game: true, 'art-pipeline': true, discord: true, 'npm-release': true } };
18
+ // The post-extraction registry: every discovered module (npm-release lives in
19
+ // modules/npm-release/, so it is a DISCOVERED key, not a built-in MODULE_REGISTRY
20
+ // one). The pure resolver tests below pass this effective registry so 'npm-release'
21
+ // is a known key — exactly what loadModules() does in production.
23
22
  const EFFECTIVE = modulesMod.effectiveRegistry();
24
23
 
25
24
  // Sample feature-module keys for the PURE resolver tests below. `game` and
@@ -43,7 +42,7 @@ test('every registry module has an explicit default of false', () => {
43
42
  test('instance config turns modules on', () => {
44
43
  const m = resolveModules({ neutral: VANILLA, instance: OTB, registry: SAMPLE_REG });
45
44
  assert.equal(m.game, true);
46
- assert.equal(m['dev-box'], true);
45
+ assert.equal(m['npm-release'], true);
47
46
  assert.equal(isModuleEnabled('discord', m), true);
48
47
  // memory (BV1.R72), grading (BV1.R73), economy (BV1.R77), ideas (BV1.R78),
49
48
  // security (BV1.R79), builder-settings (BV1.R80), onboarding (BV1.R81),
@@ -70,7 +69,7 @@ test('instance config turns modules on', () => {
70
69
  });
71
70
 
72
71
  test('instance config overrides the neutral starter per-flag (deep merge of the modules map)', () => {
73
- const neutral = { modules: { game: false, discord: false, 'dev-box': false, 'art-pipeline': false } };
72
+ const neutral = { modules: { game: false, discord: false, 'npm-release': false, 'art-pipeline': false } };
74
73
  const instance = { modules: { discord: true } }; // only flip one
75
74
  const m = resolveModules({ neutral, instance, registry: SAMPLE_REG });
76
75
  assert.equal(m.discord, true);
@@ -78,8 +77,8 @@ test('instance config overrides the neutral starter per-flag (deep merge of the
78
77
  });
79
78
 
80
79
  test('env override beats instance config (precedence), with prefix + snake-casing', () => {
81
- const m = resolveModules({ instance: OTB, env: { OTB_MODULE_DEV_BOX: '0' }, envPrefix: 'OTB', registry: SAMPLE_REG });
82
- assert.equal(m['dev-box'], false); // env off beat instance on
80
+ const m = resolveModules({ instance: OTB, env: { OTB_MODULE_NPM_RELEASE: '0' }, envPrefix: 'OTB', registry: SAMPLE_REG });
81
+ assert.equal(m['npm-release'], false); // env off beat instance on
83
82
  assert.equal(m.game, true); // others untouched
84
83
  });
85
84
 
@@ -106,14 +105,14 @@ test('an unknown module key in config is rejected when ENABLED (typo guard)', ()
106
105
  assert.throws(() => resolveModules({ instance: { modules: { gaem: true } } }), /unknown module "gaem"/);
107
106
  });
108
107
 
109
- // task 1824 — a stripped CORE has no game/art-pipeline/discord/dev-box, but the
108
+ // task 1824 — a stripped CORE has no game/art-pipeline/discord, but the
110
109
  // config/modules.neutral.json that SHIPS with it names them (so the same file is valid
111
110
  // in the full repo). The loader must TOLERATE an unknown module when it is DISABLED, or
112
111
  // a vanilla self-host crashes on `unknown module "game"`.
113
112
  test('an unknown module key that is DISABLED is tolerated (a config may name a not-yet-installed module)', () => {
114
113
  const reg = { memory: { default: true }, grading: { default: true } }; // core-only registry (no feature modules)
115
114
  // The exact vanilla-core scenario: neutral names four absent feature modules, all off.
116
- const neutral = { modules: { game: false, 'art-pipeline': false, discord: false, 'dev-box': false } };
115
+ const neutral = { modules: { game: false, 'art-pipeline': false, discord: false, 'npm-release': false } };
117
116
  let m;
118
117
  assert.doesNotThrow(() => { m = resolveModules({ neutral, registry: reg }); });
119
118
  assert.deepEqual(m, { memory: true, grading: true }); // core modules resolve; absent-disabled dropped
@@ -202,7 +201,38 @@ test('the committed config files resolve: OTB enables all four, on disk', () =>
202
201
  assert.equal(m.game, true);
203
202
  assert.equal(m['art-pipeline'], true);
204
203
  assert.equal(m.discord, true);
205
- assert.equal(m['dev-box'], true);
204
+ assert.equal(m['npm-release'], true);
205
+ });
206
+
207
+ // task 1003892 — a RETIRED key (the module was deleted from the core) that a
208
+ // committed instance pack still switches ON must NOT stop the process booting.
209
+ // cloudbongos.com carried `"dev-box": true` when the module went; the typo guard
210
+ // would have thrown `cannot enable unknown module` at the next restart.
211
+ test('1003892: a retired module enabled in the instance pack is dropped with a warning, not thrown on', () => {
212
+ const warned = [];
213
+ const m = resolveModules({
214
+ instance: { modules: { 'dev-box': true, discord: true } },
215
+ registry: SAMPLE_REG,
216
+ warnRetired: (key, source) => warned.push(`${key}@${source}`),
217
+ });
218
+ assert.equal('dev-box' in m, false, 'a retired key never enters the resolved set');
219
+ assert.equal(m.discord, true, 'the rest of the pack still resolves');
220
+ assert.deepEqual(warned, ['dev-box@instance'], 'the stale line is named once, with its layer');
221
+ // disabled is the quiet no-op it always was
222
+ const quiet = [];
223
+ resolveModules({ neutral: { modules: { 'dev-box': false } }, registry: SAMPLE_REG, warnRetired: (k) => quiet.push(k) });
224
+ assert.deepEqual(quiet, [], 'a retired key switched OFF says nothing');
225
+ });
226
+
227
+ test('1003892: the retired list does not weaken the typo guard for any other unknown key', () => {
228
+ assert.throws(
229
+ () => resolveModules({ instance: { modules: { 'dev-boxes': true } }, registry: SAMPLE_REG, warnRetired: () => {} }),
230
+ /cannot enable unknown module "dev-boxes"/,
231
+ );
232
+ for (const [key, why] of Object.entries(modulesMod.RETIRED_MODULE_KEYS)) {
233
+ assert.ok(!(key in modulesMod.effectiveRegistry()), `${key} is retired, so no installed module may carry it`);
234
+ assert.ok(why && why.length > 20, `${key} must say why it left`);
235
+ }
206
236
  });
207
237
 
208
238
  test('GDS_MODULES_FILE selects the instance modules pack on disk (mirrors GDS_BRANDING_FILE)', () => {
@@ -235,8 +265,8 @@ test('a branded sibling (GDS_BRANDING_FILE set, no GDS_MODULES_FILE) defaults to
235
265
  test('1002857: an override outranks the committed instance pack', () => {
236
266
  // The whole point: the pack is what a deploy restores, so an override that sat
237
267
  // below it would be erased by the very event it exists to survive.
238
- const m = resolveModules({ instance: OTB, override: { 'dev-box': false }, registry: SAMPLE_REG });
239
- assert.equal(m['dev-box'], false, 'the override wins over the pack');
268
+ const m = resolveModules({ instance: OTB, override: { 'npm-release': false }, registry: SAMPLE_REG });
269
+ assert.equal(m['npm-release'], false, 'the override wins over the pack');
240
270
  assert.equal(m.discord, true, 'a module with no override still reads the pack');
241
271
  });
242
272
 
@@ -248,12 +278,12 @@ test('1002857: an override turns a module ON as well as off', () => {
248
278
  test('1002857: env still outranks an override — the incident knob needs no database', () => {
249
279
  const m = resolveModules({
250
280
  instance: VANILLA,
251
- override: { 'dev-box': true },
252
- env: { OTB_MODULE_DEV_BOX: '0' },
281
+ override: { 'npm-release': true },
282
+ env: { OTB_MODULE_NPM_RELEASE: '0' },
253
283
  envPrefix: 'OTB',
254
284
  registry: SAMPLE_REG,
255
285
  });
256
- assert.equal(m['dev-box'], false, 'env must be able to kill a module the DB turned on');
286
+ assert.equal(m['npm-release'], false, 'env must be able to kill a module the DB turned on');
257
287
  });
258
288
 
259
289
  test('1002857: an override for a module this build does not carry is inert, not fatal', () => {
@@ -279,12 +309,12 @@ test('1002857: each module reports the layer that produced its state', () => {
279
309
  const m = resolveModules({
280
310
  neutral: { modules: { discord: false } },
281
311
  instance: OTB,
282
- override: { 'dev-box': false },
312
+ override: { 'npm-release': false },
283
313
  env: { OTB_MODULE_GAME: '0' },
284
314
  envPrefix: 'OTB',
285
315
  registry: SAMPLE_REG,
286
316
  });
287
- assert.equal(m.sources['dev-box'], 'override');
317
+ assert.equal(m.sources['npm-release'], 'override');
288
318
  assert.equal(m.sources.game, 'env');
289
319
  assert.equal(m.sources.discord, 'instance', 'the instance pack outranks the neutral one');
290
320
  assert.equal(m.sources['art-pipeline'], 'instance');
@@ -316,18 +346,18 @@ test('1002857: setModuleOverrides installs the snapshot AND invalidates the cach
316
346
  // the exact class of bug this feature exists to remove.
317
347
  try {
318
348
  const before = modulesMod.modules();
319
- assert.equal(before['dev-box'], false, 'precondition: off on a vanilla resolve');
320
- const after = modulesMod.setModuleOverrides({ 'dev-box': true });
321
- assert.equal(after['dev-box'], true, 'the returned set reflects the override');
322
- assert.equal(modulesMod.modules()['dev-box'], true, 'and so does the live singleton');
323
- assert.equal(moduleOverrides()['dev-box'], true);
349
+ assert.equal(before['npm-release'], false, 'precondition: off on a vanilla resolve');
350
+ const after = modulesMod.setModuleOverrides({ 'npm-release': true });
351
+ assert.equal(after['npm-release'], true, 'the returned set reflects the override');
352
+ assert.equal(modulesMod.modules()['npm-release'], true, 'and so does the live singleton');
353
+ assert.equal(moduleOverrides()['npm-release'], true);
324
354
  } finally {
325
355
  modulesMod._resetForTests();
326
356
  }
327
357
  });
328
358
 
329
359
  test('1002857: _resetForTests clears the override snapshot too', () => {
330
- modulesMod.setModuleOverrides({ 'dev-box': true });
360
+ modulesMod.setModuleOverrides({ 'npm-release': true });
331
361
  modulesMod._resetForTests();
332
362
  assert.deepEqual(moduleOverrides(), {}, 'a leaked snapshot would cross-contaminate tests');
333
363
  });
@@ -3,16 +3,15 @@
3
3
  // Regression for task 1444 (scootermack's bug).
4
4
  //
5
5
  // GET /builders/:id/onboarding shapes the SAME displayed checklist as GET /me,
6
- // but three of its steps clear from LIVE signals rather than the onboarding_state
7
- // row: box_connect (boxes.hasEverConnected), join_discord (db.getDiscordLink),
8
- // cli_setup (db.hasCliSession). The route originally called
9
- // shapeForApi(state, { shipCount }) and omitted those signals — so they defaulted
10
- // to false and the route rendered all three steps perpetually UNCHECKED even after
11
- // the builder cleared them. This locks in that the route passes the live signals
12
- // through, exactly as /me does.
6
+ // but two of its steps clear from LIVE signals rather than the onboarding_state
7
+ // row: join_discord (the discord.isLinked port) and cli_setup (db.hasCliSession).
8
+ // The route originally called shapeForApi(state, { shipCount }) and omitted those
9
+ // signals — so they defaulted to false and the route rendered the steps
10
+ // perpetually UNCHECKED even after the builder cleared them. This locks in that
11
+ // the route passes the live signals through, exactly as /me does.
13
12
  //
14
- // We stub auth + db and patch onboarding-state.getState / boxes.hasEverConnected,
15
- // but exercise the REAL shapeForApi. No network, no Postgres.
13
+ // We stub auth + db and patch onboarding-state.getState, but exercise the REAL
14
+ // shapeForApi. No network, no Postgres.
16
15
  //
17
16
  // Run: node tests/onboarding_route_signals.mjs
18
17
 
@@ -66,16 +65,12 @@ const onboardingState = require('../modules/onboarding/onboarding-state.js');
66
65
  let fakeState = null;
67
66
  onboardingState.getState = async () => fakeState;
68
67
 
69
- // box_connect's live signal now reaches builders.js through the kernel seam (the
70
- // `box.hasEverConnected` port the dev-box module provides, BV1.R43) — not a direct
71
- // boxes import. join_discord's signal reaches it the same way through the
72
- // `discord.isLinked` port the discord module provides (BV1.R47) — not db.getDiscordLink.
73
- // So we register providers instead of monkey-patching; the route resolves them via
74
- // resolveOptional exactly as in production.
68
+ // join_discord's live signal reaches builders.js through the kernel seam (the
69
+ // `discord.isLinked` port the discord module provides, BV1.R47) — not
70
+ // db.getDiscordLink. So we register a provider instead of monkey-patching; the
71
+ // route resolves it via resolveOptional exactly as in production.
75
72
  const seams = require('../src/module-seams.js');
76
- let fakeBoxConnected = false;
77
73
  seams._reset();
78
- seams.registerProvider('box.hasEverConnected', async () => fakeBoxConnected);
79
74
  seams.registerProvider('discord.isLinked', async () => dbSignals.discordLinked);
80
75
 
81
76
  // The onboarding state machine carved into modules/onboarding/ (BV1.R81): the
@@ -83,7 +78,7 @@ seams.registerProvider('discord.isLinked', async () => dbSignals.discordLinked);
83
78
  // (resolveOptional('onboarding')), not a direct require. Register the real port
84
79
  // provider — its getState delegates to the (patched-above) onboarding-state, so
85
80
  // the genuine shapeForApi is still exercised. (Mirrors how this test already
86
- // registers the box/discord providers instead of monkey-patching.)
81
+ // registers the discord provider instead of monkey-patching.)
87
82
  seams.registerProvider('onboarding', require('../modules/onboarding/port.js'));
88
83
 
89
84
  // The lifecycle (tasks · claims · ship) carved to modules/lifecycle/ (BV1.R86):
@@ -129,45 +124,40 @@ const stepCleared = (body, id) => {
129
124
  };
130
125
 
131
126
  // A mid-onboarding row (mirrors a real newcomer who's signed in, picked a craft,
132
- // and run /builder-start) — so the box/discord/cli steps are decided purely by
127
+ // and run /builder-start) — so the discord/cli steps are decided purely by
133
128
  // the LIVE signals, which is exactly what regressed.
134
129
  const MID_STATE = { current_stage: 'first_claim', completed_stages: ['auth_complete', 'pick_disciplines', 'first_start'] };
135
130
 
136
131
  const { server, port } = await listen(buildApp());
137
132
 
138
- await t('no live signals → box/discord/cli all uncleared', async () => {
133
+ await t('no live signals → discord/cli both uncleared, and no retired box step is rendered', async () => {
139
134
  fakeState = MID_STATE;
140
- fakeBoxConnected = false;
141
135
  dbSignals.discordLinked = false; dbSignals.cliAuthed = false;
142
136
  const r = await get(port, '/api/gds/builders/94/onboarding');
143
137
  assert.equal(r.status, 200);
144
- assert.equal(stepCleared(r.json, 'box_connect'), false);
145
138
  assert.equal(stepCleared(r.json, 'join_discord'), false);
146
139
  assert.equal(stepCleared(r.json, 'cli_setup'), false);
140
+ assert.equal(stepCleared(r.json, 'box_connect'), undefined, 'the dev-box step left the checklist (task 1004310)');
147
141
  });
148
142
 
149
- await t('box connected (first_connected_at set) → box_connect clears [the scootermack regression]', async () => {
143
+ await t('discord linked → join_discord clears from ITS OWN signal [the scootermack regression]', async () => {
150
144
  fakeState = MID_STATE;
151
- fakeBoxConnected = true;
152
- dbSignals.discordLinked = false; dbSignals.cliAuthed = false;
145
+ dbSignals.discordLinked = true; dbSignals.cliAuthed = false;
153
146
  const r = await get(port, '/api/gds/builders/94/onboarding');
154
147
  assert.equal(r.status, 200);
155
- assert.equal(stepCleared(r.json, 'box_connect'), true, 'box_connect must clear once the box has connected');
156
- // The other two stay false — proving box_connect cleared from ITS OWN signal,
148
+ assert.equal(stepCleared(r.json, 'join_discord'), true, 'join_discord must clear once the account is linked');
149
+ // cli_setup stays false — proving join_discord cleared from its own signal,
157
150
  // not because every step got force-cleared.
158
- assert.equal(stepCleared(r.json, 'join_discord'), false);
159
151
  assert.equal(stepCleared(r.json, 'cli_setup'), false);
160
152
  });
161
153
 
162
- await t('discord linked + cli authed → those two clear independently', async () => {
154
+ await t('discord linked + cli authed → both clear', async () => {
163
155
  fakeState = MID_STATE;
164
- fakeBoxConnected = false;
165
156
  dbSignals.discordLinked = true; dbSignals.cliAuthed = true;
166
157
  const r = await get(port, '/api/gds/builders/94/onboarding');
167
158
  assert.equal(r.status, 200);
168
159
  assert.equal(stepCleared(r.json, 'join_discord'), true);
169
160
  assert.equal(stepCleared(r.json, 'cli_setup'), true);
170
- assert.equal(stepCleared(r.json, 'box_connect'), false);
171
161
  });
172
162
 
173
163
  server.close();
@@ -77,29 +77,27 @@ test('isKnownStage: typo rejected', () => {
77
77
 
78
78
  // ---- shapeForApi (displayed checklist, #777) ------------------------------
79
79
  //
80
- // shapeForApi returns the DISPLAYED 5-step "Path into the City"
81
- // (box_connect, join_discord, pick_disciplines, cli_setup, ship_three) — split
82
- // from the engine STAGES so NONE of the displayed steps affect graduation.
83
- // `graduated` and `current_stage` still reflect the engine. box_connect/
84
- // join_discord/cli_setup are auto-derived from `boxConnected`/`discordLinked`/
85
- // `cliAuthed` (no manual ticks, #823/#777).
80
+ // shapeForApi returns the DISPLAYED 4-step "Path into the City"
81
+ // (join_discord, pick_disciplines, cli_setup, ship_three) — split from the
82
+ // engine STAGES so NONE of the displayed steps affect graduation. `graduated`
83
+ // and `current_stage` still reflect the engine. join_discord/cli_setup are
84
+ // auto-derived from `discordLinked`/`cliAuthed` (no manual ticks, #777).
86
85
 
87
- const DISPLAY_ORDER = ['box_connect', 'join_discord', 'pick_disciplines', 'cli_setup', 'ship_three'];
86
+ const DISPLAY_ORDER = ['join_discord', 'pick_disciplines', 'cli_setup', 'ship_three'];
88
87
 
89
- test('shapeForApi: null row → 5-step displayed shape in order', () => {
88
+ test('shapeForApi: null row → 4-step displayed shape in order', () => {
90
89
  const out = onboarding.shapeForApi(null);
91
90
  assert.equal(out.current_stage, 'auth_complete'); // engine stage still surfaced
92
91
  assert.deepEqual(out.completed_stages, []);
93
92
  assert.equal(out.graduated, false);
94
93
  assert.equal(out.progress.cleared, 0);
95
- assert.equal(out.progress.total, 5);
94
+ assert.equal(out.progress.total, 4);
96
95
  assert.deepEqual(out.stages.map((s) => s.id), DISPLAY_ORDER);
97
- assert.equal(out.stages[0].is_current, true); // box_connect, first gap
96
+ assert.equal(out.stages[0].is_current, true); // join_discord, first gap
98
97
  assert.equal(out.stages[0].cleared, false);
99
- assert.equal(out.stages[0].source, 'box');
100
- assert.equal(out.stages[1].source, 'discord'); // join_discord
101
- assert.equal(out.stages[2].source, 'stage'); // pick_disciplines is a real stage
102
- assert.equal(out.stages[3].source, 'cli'); // cli_setup
98
+ assert.equal(out.stages[0].source, 'discord'); // join_discord
99
+ assert.equal(out.stages[1].source, 'stage'); // pick_disciplines is a real stage
100
+ assert.equal(out.stages[2].source, 'cli'); // cli_setup
103
101
  });
104
102
 
105
103
  test('shapeForApi: partial row → cleared flags + is_current points at first displayed gap', () => {
@@ -113,62 +111,50 @@ test('shapeForApi: partial row → cleared flags + is_current points at first di
113
111
  const byId = Object.fromEntries(out.stages.map((s) => [s.id, s]));
114
112
  // pick_disciplines is the only displayed step the engine has cleared.
115
113
  assert.equal(byId.pick_disciplines.cleared, true);
116
- assert.equal(byId.box_connect.cleared, false); // no box connection
117
114
  assert.equal(byId.cli_setup.cleared, false); // no cli auth
118
115
  assert.equal(byId.join_discord.cleared, false); // no link
119
116
  assert.equal(byId.ship_three.cleared, false);
120
- // First displayed gap is box_connect (it sorts before the cleared craft step).
121
- assert.equal(byId.box_connect.is_current, true);
117
+ // First displayed gap is join_discord (it sorts before the cleared craft step).
118
+ assert.equal(byId.join_discord.is_current, true);
122
119
  assert.equal(byId.pick_disciplines.is_current, false);
123
120
  assert.equal(out.progress.cleared, 1);
124
121
  });
125
122
 
126
- test('shapeForApi: box_connect clears from boxConnected; is_current advances to join_discord', () => {
123
+ test('shapeForApi: a legacy caller passing boxConnected changes nothing (the step is gone)', () => {
124
+ // Removed with the dev box (task 1004310): an unknown option must be inert,
125
+ // never a fifth step or a thrown error.
127
126
  const out = onboarding.shapeForApi(
128
127
  { current_stage: 'auth_complete', completed_stages: ['auth_complete'] },
129
128
  { boxConnected: true }
130
129
  );
131
- const byId = Object.fromEntries(out.stages.map((s) => [s.id, s]));
132
- assert.equal(byId.box_connect.cleared, true);
133
- // Next gap is now join_discord (no link supplied).
134
- assert.equal(byId.join_discord.is_current, true);
130
+ assert.deepEqual(out.stages.map((s) => s.id), DISPLAY_ORDER);
131
+ assert.equal(out.progress.cleared, 0);
135
132
  });
136
133
 
137
- test('shapeForApi: cli_setup (step 4) clears from cliAuthed; box_connect stays first gap without a connection', () => {
134
+ test('shapeForApi: cli_setup (step 3) clears from cliAuthed; join_discord stays first gap without a link', () => {
138
135
  const out = onboarding.shapeForApi(
139
136
  { current_stage: 'auth_complete', completed_stages: ['auth_complete'] },
140
137
  { cliAuthed: true }
141
138
  );
142
139
  const byId = Object.fromEntries(out.stages.map((s) => [s.id, s]));
143
140
  assert.equal(byId.cli_setup.cleared, true);
144
- assert.equal(byId.box_connect.cleared, false);
145
- // box_connect is still the first gap (it precedes the cleared cli step).
146
- assert.equal(byId.box_connect.is_current, true);
147
- });
148
-
149
- test('shapeForApi: box_connect and cli_setup are independent signals', () => {
150
- // Connected to the box but CLI not yet authenticated — only box_connect ticks.
151
- const out = onboarding.shapeForApi(
152
- { current_stage: 'auth_complete', completed_stages: ['auth_complete'] },
153
- { boxConnected: true, cliAuthed: false }
154
- );
155
- const byId = Object.fromEntries(out.stages.map((s) => [s.id, s]));
156
- assert.equal(byId.box_connect.cleared, true);
157
- assert.equal(byId.cli_setup.cleared, false);
158
- // box_connect clears, but cli_setup (step 4) stays open on its own signal.
159
- assert.equal(byId.box_connect.is_current, false);
141
+ assert.equal(byId.join_discord.cleared, false);
142
+ // join_discord is still the first gap (it precedes the cleared cli step).
143
+ assert.equal(byId.join_discord.is_current, true);
160
144
  });
161
145
 
162
- test('shapeForApi: join_discord clears from a live Discord link; box_connect stays first gap without a connection', () => {
146
+ test('shapeForApi: join_discord and cli_setup are independent signals', () => {
147
+ // Discord linked but CLI not yet authenticated — only join_discord ticks.
163
148
  const out = onboarding.shapeForApi(
164
149
  { current_stage: 'auth_complete', completed_stages: ['auth_complete'] },
165
- { discordLinked: true }
150
+ { discordLinked: true, cliAuthed: false }
166
151
  );
167
152
  const byId = Object.fromEntries(out.stages.map((s) => [s.id, s]));
168
153
  assert.equal(byId.join_discord.cleared, true);
169
- assert.equal(byId.box_connect.cleared, false);
170
- // box_connect is still the first gap (it precedes the cleared discord step).
171
- assert.equal(byId.box_connect.is_current, true);
154
+ assert.equal(byId.cli_setup.cleared, false);
155
+ // join_discord clears, and the next gap is the craft picker.
156
+ assert.equal(byId.join_discord.is_current, false);
157
+ assert.equal(byId.pick_disciplines.is_current, true);
172
158
  });
173
159
 
174
160
  test('shapeForApi: graduated row → all displayed steps cleared, none current (regardless of derived signals)', () => {
@@ -177,10 +163,10 @@ test('shapeForApi: graduated row → all displayed steps cleared, none current (
177
163
  completed_stages: onboarding.STAGES,
178
164
  started_at: '2026-05-01T00:00:00Z',
179
165
  completed_at: '2026-05-05T00:00:00Z',
180
- }, { discordLinked: false, boxConnected: false, cliAuthed: false }); // proves display steps don't gate graduation
166
+ }, { discordLinked: false, cliAuthed: false }); // proves display steps don't gate graduation
181
167
  assert.equal(out.graduated, true);
182
- assert.equal(out.progress.cleared, 5);
183
- assert.equal(out.progress.total, 5);
168
+ assert.equal(out.progress.cleared, 4);
169
+ assert.equal(out.progress.total, 4);
184
170
  assert.ok(out.stages.every((s) => s.cleared === true));
185
171
  assert.ok(out.stages.every((s) => s.is_current === false));
186
172
  });
@@ -329,9 +315,9 @@ test('graduation is engine-only (display signals never gate it)', async () => {
329
315
  }
330
316
  const state = await onboarding.getState(c, 60);
331
317
  assert.equal(state.current_stage, 'graduated');
332
- // graduated:true even with discordLinked/boxConnected/cliAuthed false — proves
318
+ // graduated:true even with discordLinked/cliAuthed false — proves
333
319
  // the display-only steps never gate the xenos→thetes promotion (#692).
334
- const shaped = onboarding.shapeForApi(state, { discordLinked: false, boxConnected: false, cliAuthed: false });
320
+ const shaped = onboarding.shapeForApi(state, { discordLinked: false, cliAuthed: false });
335
321
  assert.equal(shaped.graduated, true);
336
322
  assert.ok(shaped.stages.every((s) => s.cleared === true));
337
323
  });
@@ -73,9 +73,6 @@ t('#863-followup: newly-added protected surfaces are covered (findings #3/#9/#10
73
73
  '.husky/pre-push',
74
74
  '.husky/pre-commit',
75
75
  'modules/grading/grader-workers/hacker.js',
76
- 'modules/dev-box/boxes.js',
77
- 'modules/dev-box/box-access.js',
78
- 'modules/dev-box/box-credential.js',
79
76
  ];
80
77
  for (const f of newlyProtected) {
81
78
  const hits = ppc.matchProtected([f]);
@@ -178,7 +178,7 @@ test('R56: the served hall injects window.__BRANDING__ synchronously', async ()
178
178
  });
179
179
 
180
180
  test('R60: the served hall injects window.__MODULES__ (feature-module flags) synchronously', async () => {
181
- // The hall's dev-box/discord UI gating (R60, task 1199) reads
181
+ // The hall's discord UI gating (R60, task 1199) reads
182
182
  // window.__MODULES__.enabled[...]; serve-internal must inject it the same
183
183
  // synchronous way as __BRANDING__, or the frontend gate has nothing to read.
184
184
  const res = await request('/', 'builders.test.local');
@@ -189,7 +189,6 @@ test('R60: the served hall injects window.__MODULES__ (feature-module flags) syn
189
189
  assert.ok(m, 'a __MODULES__ assignment is present');
190
190
  const parsed = JSON.parse(m[1]);
191
191
  assert.equal(typeof parsed.enabled, 'object');
192
- assert.equal(typeof parsed.enabled['dev-box'], 'boolean');
193
192
  assert.equal(typeof parsed.enabled.discord, 'boolean');
194
193
  });
195
194
 
@@ -4,7 +4,7 @@
4
4
  // goal 25 "bx-identity"). The route is intentionally UNAUTHENTICATED, so its one
5
5
  // load-bearing invariant is that it must NEVER leak the private columns that
6
6
  // db.getBuilderById also returns (anthropic_email / anthropic_user_id /
7
- // monthly_budget_usd / system_role / box_blocked* / deactivated_reason). It
7
+ // monthly_budget_usd / system_role / deactivated_reason). It
8
8
  // whitelists a public subset off the row; this test proves the whitelist holds,
9
9
  // that the shape is right, and that unknown/invalid ids answer 404/400.
10
10
  //
@@ -74,17 +74,12 @@ const FULL_ROW = {
74
74
  anthropic_email: SECRET_EMAIL,
75
75
  anthropic_user_id: SECRET_USER,
76
76
  monthly_budget_usd: 500,
77
- box_blocked: true,
78
- box_blocked_reason: SECRET_REASON,
79
- box_blocked_at: '2026-01-01T00:00:00.000Z',
80
- box_blocked_by: 7,
81
77
  };
82
78
 
83
79
  // The columns that must NEVER appear on the returned builder object.
84
80
  const PRIVATE_KEYS = [
85
81
  'github_id', 'status', 'deactivated_at', 'reactivated_at', 'deactivated_reason',
86
82
  'system_role', 'anthropic_email', 'anthropic_user_id', 'monthly_budget_usd',
87
- 'box_blocked', 'box_blocked_reason', 'box_blocked_at', 'box_blocked_by',
88
83
  ];
89
84
  // The public subset the route DOES return.
90
85
  const PUBLIC_KEYS = [
@@ -707,14 +707,14 @@ test('the About answers travel on the create request, and an empty one is OMITTE
707
707
 
708
708
  test('a renumbering of the wizard invalidates old drafts — the key carries the version', () => {
709
709
  // task 1002772 renumbered once and bumped to -v2; task 1002334 renumbered again;
710
- // task 1002339 (the module picker) is the third. A draft replayed under a
710
+ // task 1002339 (the module picker) is the third; task 1004351 (the app step) the fourth. A draft replayed under a
711
711
  // different numbering lands on the wrong panel with the rail already marking
712
712
  // the skipped steps done.
713
713
  const key = (SCRIPT.match(/var DRAFT_KEY = '([^']+)'/) || [, ''])[1];
714
714
  assert.match(key, /-v\d+$/, 'the draft key is versioned');
715
715
  const panels = (SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1];
716
716
  const count = [...panels.matchAll(/\d+:\s*'panel-/g)].length;
717
- assert.equal(count, 9, 'gate + seven numbered steps + done — bump DRAFT_KEY when this changes');
717
+ assert.equal(count, 10, 'gate + eight numbered steps + done — bump DRAFT_KEY when this changes');
718
718
  });
719
719
 
720
720
  test('every row lays the same five parts — the ledger has no ragged row', () => {
@@ -2133,9 +2133,9 @@ test('the wizard takes the world by scope: no card ground, the six-step strip as
2133
2133
  assert.ok(at > -1, 'the #view-new block exists in cosmos.css');
2134
2134
  const block = COSMOS.slice(at);
2135
2135
  assert.match(block, /#view-new \.card\{background:none;border-radius:0;box-shadow:none;/, 'no card ground');
2136
- // seven tracks since task 1002339 added the Modules step; the count is the
2136
+ // eight tracks since task 1004351 added the App step (seven since 1002339); the count is the
2137
2137
  // rail's own, so it moves with the panels map rather than being a magic number
2138
- assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(7,1fr\);/, 'the strip lays a track per numbered step');
2138
+ assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(8,1fr\);/, 'the strip lays a track per numbered step');
2139
2139
  assert.match(block, /#view-new \.steps li::before\{content:"";position:absolute;top:-4px;left:0;right:auto;width:7px;height:7px;/, 'the connector becomes the 7px dot');
2140
2140
  assert.match(block, /#view-new \.steps li\.now::before\{background:var\(--accent\);border-color:var\(--accent\);outline:4px solid/, 'the lit step carries the accent and an outline halo');
2141
2141
  assert.match(block, /#view-new \.panel h1\{font-weight:300;[^}]*text-transform:lowercase;/, 'the step title in the display voice, lowercase');
@@ -2182,7 +2182,7 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
2182
2182
  assert.match(block, /\.addrBig code\{display:block;font-family:var\(--font-display\);/, 'the address stands in the display voice');
2183
2183
  assert.doesNotMatch(block, /linear-gradient/, 'no tinted plates');
2184
2184
  assert.doesNotMatch(block, /box-shadow:(?!none)/, 'no box shadow in the wizard block');
2185
- assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="6">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
2185
+ assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="7">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
2186
2186
  assert.match(HUB, /<h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play<\/h2>/, 'the steps heading is the label face by class, not an inline style (the id is where a skipped invite step hands focus on, task 1002346)');
2187
2187
  // what is enqueued is unchanged (ADR 0111): the filing body the control plane reads
2188
2188
  assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');