@bongos/core 1.19.1073 → 1.19.1075

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 (377) hide show
  1. package/.bongos-core.json +404 -549
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/bin/bongos.js +1 -3
  4. package/clients/bongos-client/README.md +1 -1
  5. package/clients/bongos-client/bongos-client.global.js +4 -60
  6. package/clients/bongos-client/index.cjs +4 -60
  7. package/clients/bongos-client/index.d.ts +5 -90
  8. package/clients/bongos-client/index.mjs +4 -60
  9. package/config/branding.neutral.json +1 -5
  10. package/config/modules.neutral.json +2 -3
  11. package/config/scheduled-routines.json +0 -2
  12. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  13. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  14. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  15. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  16. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +2 -0
  17. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  18. package/docs/adr/README.md +1 -0
  19. package/docs/api/openapi.json +175 -1665
  20. package/docs/api-reference.md +9 -41
  21. package/docs/architecture.md +16 -1
  22. package/docs/branding-contract.md +0 -3
  23. package/docs/copy-inventory.md +454 -479
  24. package/docs/copy-registry.json +746 -978
  25. package/docs/file-map.md +5 -34
  26. package/docs/module-api-changelog.md +4 -0
  27. package/docs/modules-contract.md +13 -1
  28. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  29. package/docs/onboarding/diagrams/README.md +3 -3
  30. package/docs/onboarding/diagrams/assertions.json +2 -22
  31. package/docs/onboarding/primer.md +9 -28
  32. package/docs/page-inventory.json +4 -1
  33. package/docs/page-readings.json +1032 -1077
  34. package/docs/recipes/render-pilot.md +29 -10
  35. package/migrations/core_257_module_store_registry.sql +87 -0
  36. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  37. package/modules/agents/module.json +1 -0
  38. package/modules/autonomy/module.json +1 -0
  39. package/modules/builder-settings/module.json +1 -0
  40. package/modules/builder-settings/render-prefs.js +4 -5
  41. package/modules/copy-desk/docx.js +78 -5
  42. package/modules/copy-desk/module.json +1 -0
  43. package/modules/copy-desk/routes/copy-desk.js +10 -10
  44. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  45. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  46. package/modules/discord/module.json +1 -0
  47. package/modules/discord/ship-broadcast.js +3 -3
  48. package/modules/economy/credits.js +2 -2
  49. package/modules/economy/module.json +1 -0
  50. package/modules/economy/routes/credits.js +1 -2
  51. package/modules/government/catalog.js +13 -17
  52. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  53. package/modules/government/module.json +1 -0
  54. package/modules/government/protected-surfaces.json +3 -11
  55. package/modules/government/resolver.js +3 -46
  56. package/modules/government/routes/government.js +0 -2
  57. package/modules/government/session-scopes.js +43 -83
  58. package/modules/government/session-scopes.json +8 -18
  59. package/modules/grading/module.json +1 -0
  60. package/modules/hall-ui/module.json +1 -0
  61. package/modules/hall-ui/public/board-lib.js +2 -2
  62. package/modules/hall-ui/public/builders.js +29 -40
  63. package/modules/hall-ui/public/collab.html +1 -1
  64. package/modules/hall-ui/public/collab.js +1 -1
  65. package/modules/hall-ui/public/diagrams.html +3 -3
  66. package/modules/hall-ui/public/dom-utils.js +1 -1
  67. package/modules/hall-ui/public/gate.js +2 -2
  68. package/modules/hall-ui/public/gate.states.json +1 -1
  69. package/modules/hall-ui/public/hall-render.js +43 -79
  70. package/modules/hall-ui/public/index.html +5 -1
  71. package/modules/hall-ui/public/modules.js +1 -0
  72. package/modules/hall-ui/public/oversight.css +5 -5
  73. package/modules/hall-ui/public/palette.js +0 -11
  74. package/modules/hall-ui/public/primer.js +1 -1
  75. package/modules/hall-ui/public/sessions.html +1 -1
  76. package/modules/hall-ui/public/settings-sessions.js +1 -1
  77. package/modules/hall-ui/public/settings.css +0 -1
  78. package/modules/hall-ui/public/settings.html +4 -33
  79. package/modules/hall-ui/public/settings.js +3 -33
  80. package/modules/hall-ui/public/settings.states.json +2 -2
  81. package/modules/hall-ui/public/studio.html +1 -1
  82. package/modules/hall-ui/public/style.css +10 -13
  83. package/modules/hall-ui/public/task.html +4 -1
  84. package/modules/hall-ui/public/task.js +36 -1
  85. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  86. package/modules/hall-ui/public/tweak-editor.css +16 -0
  87. package/modules/hall-ui/public/tweak-editor.html +26 -3
  88. package/modules/hall-ui/public/tweak-editor.js +32 -1
  89. package/modules/hall-ui/public/work.js +1 -1
  90. package/modules/hall-ui/records/tweak-editor.md +1 -0
  91. package/modules/ideas/module.json +1 -0
  92. package/modules/ideas/projection.js +1 -1
  93. package/modules/lifecycle/db-claim-reads.js +1 -58
  94. package/modules/lifecycle/db-ship.js +4 -9
  95. package/modules/lifecycle/db.js +0 -4
  96. package/modules/lifecycle/github-push.js +12 -14
  97. package/modules/lifecycle/kickoff-checklist.js +0 -4
  98. package/modules/lifecycle/lifecycle.js +0 -8
  99. package/modules/lifecycle/module.json +1 -0
  100. package/modules/lifecycle/publish-reconciler.js +10 -10
  101. package/modules/lifecycle/routes/tasks.js +13 -15
  102. package/modules/lifecycle/ship-card.js +1 -1
  103. package/modules/lifecycle/task-visuals.js +2 -2
  104. package/modules/memory/module.json +1 -0
  105. package/modules/memory/routes/memory.js +2 -2
  106. package/modules/npm-release/module.json +1 -0
  107. package/modules/onboarding/module.json +1 -0
  108. package/modules/onboarding/onboarding-state.js +17 -36
  109. package/modules/onboarding/routes/access-requests.js +3 -3
  110. package/modules/platform-identity/module.json +1 -0
  111. package/modules/platform-identity/platform-identity.js +13 -5
  112. package/modules/platform-identity/routes/sso.js +5 -4
  113. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  114. package/modules/provisioning/capacity.js +1 -1
  115. package/modules/provisioning/module.json +1 -0
  116. package/modules/provisioning/provisioning.js +6 -5
  117. package/modules/provisioning/routes/provisioning.js +4 -4
  118. package/modules/provisioning/starter-bundles.js +5 -25
  119. package/modules/public-landing/module.json +1 -0
  120. package/modules/public-landing/public/assets/cosmos.css +1 -1
  121. package/modules/public-landing/public/contact.html +1 -1
  122. package/modules/public-landing/public/privacy.html +1 -1
  123. package/modules/public-landing/public/projects.html +101 -32
  124. package/modules/public-landing/public/projects.probes.json +2 -2
  125. package/modules/public-landing/public/projects.states.json +4 -3
  126. package/modules/public-landing/public/terms.html +1 -1
  127. package/modules/security/module.json +1 -0
  128. package/modules/sessions/db.js +1 -1
  129. package/modules/sessions/module.json +1 -0
  130. package/modules/specialities/module.json +1 -0
  131. package/modules/status-ui/module.json +1 -0
  132. package/modules/ui-design/module.json +1 -0
  133. package/package-lock.json +2 -2
  134. package/package.json +1 -1
  135. package/release-notes.json +84 -0
  136. package/scripts/gds/artifact-format.js +203 -0
  137. package/scripts/gds/artifact-staleness.js +2 -2
  138. package/scripts/gds/audit-authorship.sh +1 -13
  139. package/scripts/gds/backfill-task-rewards.js +5 -5
  140. package/scripts/gds/claim.js +0 -15
  141. package/scripts/gds/claude-materialize.js +2 -2
  142. package/scripts/gds/cli-lib.js +15 -15
  143. package/scripts/gds/codemod-rename-src-gds.js +1 -1
  144. package/scripts/gds/context-pack.js +5 -7
  145. package/scripts/gds/diagram-facts.js +12 -23
  146. package/scripts/gds/do-api.js +13 -80
  147. package/scripts/gds/doc-cli-guard.js +2 -0
  148. package/scripts/gds/doctor.js +4 -4
  149. package/scripts/gds/feedback-latest.js +139 -9
  150. package/scripts/gds/fitness-checks-identity.js +2 -7
  151. package/scripts/gds/fitness-checks-packaging.js +4 -4
  152. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  153. package/scripts/gds/fitness-ratchets.js +5 -5
  154. package/scripts/gds/fitness.js +10 -10
  155. package/scripts/gds/gen-api-client.js +4 -4
  156. package/scripts/gds/gen-api-docs.js +1 -1
  157. package/scripts/gds/gen-diagrams.js +49 -71
  158. package/scripts/gds/gen-repo-map.js +13 -14
  159. package/scripts/gds/gen-session-index.js +8 -8
  160. package/scripts/gds/http-api-client.js +12 -19
  161. package/scripts/gds/init.js +1 -1
  162. package/scripts/gds/leak-scan-allowlist.js +1 -1
  163. package/scripts/gds/lint-infra-exec.js +1 -1
  164. package/scripts/gds/local-preview-lib.js +8 -39
  165. package/scripts/gds/local-preview.js +2 -15
  166. package/scripts/gds/migration-namespace.js +3 -3
  167. package/scripts/gds/module-artifact.js +212 -0
  168. package/scripts/gds/module.js +95 -6
  169. package/scripts/gds/package-core.js +5 -116
  170. package/scripts/gds/plain-cards.js +1 -1
  171. package/scripts/gds/provision-config.js +2 -2
  172. package/scripts/gds/provision-net.js +2 -3
  173. package/scripts/gds/provision.js +9 -9
  174. package/scripts/gds/publish-manifest.js +1 -2
  175. package/scripts/gds/push-path-brief.js +10 -10
  176. package/scripts/gds/regen-instance-docs.js +1 -1
  177. package/scripts/gds/rename-history-check.js +1 -1
  178. package/scripts/gds/run-unit-tests.js +2 -16
  179. package/scripts/gds/sandbox-stage.js +39 -341
  180. package/scripts/gds/session-digest-build.js +2 -2
  181. package/scripts/gds/ship-deploy-target.js +13 -13
  182. package/scripts/gds/ship-flow.js +7 -7
  183. package/scripts/gds/ship-land.js +9 -10
  184. package/scripts/gds/ship-merge.js +4 -4
  185. package/scripts/gds/ship-preflight-steps.js +4 -4
  186. package/scripts/gds/ship-regen.js +15 -49
  187. package/scripts/gds/ship.js +13 -33
  188. package/scripts/gds/skill-preflight.js +9 -8
  189. package/scripts/gds/start.js +0 -15
  190. package/scripts/hall-preview/README.md +5 -5
  191. package/scripts/hall-preview/server.js +6 -6
  192. package/scripts/render-diagrams.sh +1 -1
  193. package/src/bongos/api-errors.js +1 -1
  194. package/src/bongos/api-prefix.js +4 -4
  195. package/src/bongos/auth-github.js +2 -3
  196. package/src/bongos/auth.js +51 -126
  197. package/src/bongos/db-kernel.js +2 -3
  198. package/src/bongos/db.js +5 -28
  199. package/src/bongos/module-scope-map.js +14 -29
  200. package/src/bongos/module-store.js +141 -0
  201. package/src/bongos/platform-visibility-gate.js +1 -1
  202. package/src/bongos/route-rank-check.js +3 -16
  203. package/src/bongos/routes/auth.js +0 -116
  204. package/src/bongos/routes/builders.js +5 -15
  205. package/src/bongos/routes/instance.js +8 -14
  206. package/src/bongos/routes/me.js +7 -23
  207. package/src/bongos/routes/modules.js +83 -0
  208. package/src/bongos/routes/security.js +3 -3
  209. package/src/bongos/routes.js +7 -2
  210. package/src/bongos/serve-internal.js +5 -6
  211. package/src/bongos-downloads.js +1 -6
  212. package/src/branding.js +2 -2
  213. package/src/build-info.js +4 -6
  214. package/src/instance-config.js +2 -2
  215. package/src/module-api.js +13 -6
  216. package/src/module-loader/catalog.js +3 -0
  217. package/src/module-loader/loader.js +4 -4
  218. package/src/module-loader/manifest-schema.js +16 -8
  219. package/src/module-seams.js +2 -3
  220. package/src/modules.js +42 -14
  221. package/tests/agents_spend_guard.mjs +1 -1
  222. package/tests/api_alias_caller_ratchet.mjs +1 -1
  223. package/tests/api_alias_exceptions.mjs +1 -1
  224. package/tests/api_client.mjs +1 -16
  225. package/tests/api_path_404.mjs +1 -1
  226. package/tests/auth_page_gate.mjs +10 -10
  227. package/tests/backfill_task_rewards.mjs +1 -1
  228. package/tests/canonical_profile_url.mjs +1 -3
  229. package/tests/claim_action.mjs +4 -1
  230. package/tests/claim_from_task_and_home.mjs +127 -0
  231. package/tests/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
  232. package/tests/cli_sessions.mjs +1 -1
  233. package/tests/consumer_layout_boot.mjs +2 -0
  234. package/tests/context_pack.mjs +13 -13
  235. package/tests/copy_desk_page_docx.mjs +33 -2
  236. package/tests/copy_inventory.mjs +7 -7
  237. package/tests/core_upgrade_runner.mjs +6 -6
  238. package/tests/credit_grant.mjs +2 -2
  239. package/tests/criterion_suggest.mjs +1 -1
  240. package/tests/deploy_divergence_line.mjs +11 -11
  241. package/tests/design_tokens_sync.mjs +3 -2
  242. package/tests/diagram_facts_offset.mjs +1 -1
  243. package/tests/do_api.mjs +106 -0
  244. package/tests/feedback_latest.mjs +122 -0
  245. package/tests/fitness.mjs +44 -42
  246. package/tests/fitness_ratchets.mjs +2 -2
  247. package/tests/gate_approvals.mjs +0 -1
  248. package/tests/github_push_land_proof.mjs +1 -1
  249. package/tests/go_live.mjs +3 -3
  250. package/tests/goal_advisory.mjs +3 -3
  251. package/tests/goal_suggest.mjs +3 -3
  252. package/tests/government_abuse_matrix.mjs +21 -19
  253. package/tests/government_ownership_scope.mjs +33 -35
  254. package/tests/government_parity.mjs +2 -2
  255. package/tests/government_protected_surfaces.mjs +56 -37
  256. package/tests/government_require_permission.mjs +3 -3
  257. package/tests/government_seed.mjs +37 -4
  258. package/tests/government_session_scope.mjs +92 -366
  259. package/tests/hall_audit.mjs +1 -1
  260. package/tests/hall_error_envelope.mjs +4 -0
  261. package/tests/hall_landing_boot.mjs +4 -1
  262. package/tests/hall_palette.mjs +1 -16
  263. package/tests/hall_record_world.mjs +6 -0
  264. package/tests/hall_tweak_editor.mjs +52 -1
  265. package/tests/helpers.mjs +1 -1
  266. package/tests/host_topology_skips.mjs +3 -4
  267. package/tests/html_comment_nesting.mjs +137 -0
  268. package/tests/infra_exec_paths.mjs +9 -10
  269. package/tests/init.mjs +4 -5
  270. package/tests/instance_manifest.mjs +18 -25
  271. package/tests/kickoff_checklist.mjs +1 -1
  272. package/tests/lib_sh_resolution.mjs +0 -221
  273. package/tests/local_preview.mjs +0 -12
  274. package/tests/migration_namespace.mjs +6 -6
  275. package/tests/module-scope-map.mjs +9 -9
  276. package/tests/module_api.mjs +2 -2
  277. package/tests/module_catalog.mjs +1 -1
  278. package/tests/module_cli.mjs +20 -20
  279. package/tests/module_contributions.mjs +6 -6
  280. package/tests/module_loader.mjs +6 -6
  281. package/tests/module_manifest.mjs +26 -17
  282. package/tests/module_route_rank.mjs +2 -2
  283. package/tests/module_store_publish.mjs +307 -0
  284. package/tests/module_store_publish_route.mjs +121 -0
  285. package/tests/module_store_registry_migration.mjs +70 -0
  286. package/tests/modules.mjs +56 -26
  287. package/tests/modules_route_wiring.mjs +1 -1
  288. package/tests/onboarding_route_signals.mjs +21 -31
  289. package/tests/onboarding_state.mjs +35 -49
  290. package/tests/paste_token_session_store.mjs +1 -1
  291. package/tests/permission_path.mjs +0 -3
  292. package/tests/platform_boot.mjs +1 -2
  293. package/tests/platform_visibility_gate.mjs +1 -1
  294. package/tests/profile_route.mjs +1 -6
  295. package/tests/project_modules_ui.mjs +17 -17
  296. package/tests/projects_hub.mjs +5 -5
  297. package/tests/projects_hub_app_step.mjs +138 -0
  298. package/tests/projects_hub_module_picker.mjs +77 -76
  299. package/tests/projects_hub_pre_uat.mjs +4 -5
  300. package/tests/provision.mjs +7 -7
  301. package/tests/provisioning_capacity.mjs +1 -1
  302. package/tests/provisioning_recommendations.mjs +2 -1
  303. package/tests/provisioning_starter_bundles.mjs +0 -22
  304. package/tests/publish_branch_route.mjs +11 -19
  305. package/tests/publish_manifest.mjs +2 -3
  306. package/tests/publish_reconciler.mjs +8 -8
  307. package/tests/push_path_brief.mjs +10 -9
  308. package/tests/rank_tier_single_source.mjs +1 -13
  309. package/tests/regrade_eligibility.mjs +1 -1
  310. package/tests/rename_history_restraint.mjs +1 -1
  311. package/tests/repo_map.mjs +12 -13
  312. package/tests/runner_drift.mjs +3 -21
  313. package/tests/sandbox_stage.mjs +57 -390
  314. package/tests/seam_wiring_guard.mjs +11 -11
  315. package/tests/search_isolation.mjs +3 -3
  316. package/tests/session_records.mjs +1 -1
  317. package/tests/session_rename_fallback.mjs +1 -22
  318. package/tests/session_start_freshness.mjs +3 -3
  319. package/tests/session_token_hash_db.mjs +1 -1
  320. package/tests/ship_card.mjs +1 -1
  321. package/tests/ship_ci_deploy.mjs +17 -17
  322. package/tests/ship_error_shape.mjs +1 -1
  323. package/tests/ship_premerge.mjs +1 -1
  324. package/tests/ship_resume.mjs +1 -1
  325. package/tests/skill_preflight.mjs +2 -2
  326. package/tests/skip_is_not_pass.mjs +1 -1
  327. package/tests/task_visual_slots.mjs +31 -0
  328. package/tests/terms_acceptance.mjs +2 -2
  329. package/tests/upgrade.mjs +4 -4
  330. package/tests/watch_sealed_floor.mjs +0 -1
  331. package/tests/wizard_draft_resume.mjs +2 -2
  332. package/tests/wizard_intent_resume.mjs +21 -10
  333. package/tests/wizard_preselect_why.mjs +18 -18
  334. package/docs/onboarding/browser-terminal-guide.md +0 -73
  335. package/docs/recipes/managed-settings-remote-control.md +0 -416
  336. package/modules/dev-box/CLAUDE.md +0 -15
  337. package/modules/dev-box/box-access.js +0 -623
  338. package/modules/dev-box/box-credential.js +0 -94
  339. package/modules/dev-box/box-onboard.js +0 -486
  340. package/modules/dev-box/boxes.js +0 -1038
  341. package/modules/dev-box/db.js +0 -39
  342. package/modules/dev-box/module.json +0 -19
  343. package/modules/dev-box/routes/box.js +0 -1223
  344. package/scripts/gds/box-auth-check.js +0 -160
  345. package/scripts/gds/box-infra.js +0 -321
  346. package/scripts/gds/box-sync.js +0 -216
  347. package/scripts/gds/box.js +0 -1332
  348. package/scripts/gds/cf-tunnel.js +0 -558
  349. package/scripts/gds/smoke-box.sh +0 -150
  350. package/scripts/gds/tree-preflight.js +0 -241
  351. package/src/bongos/app-pair.js +0 -172
  352. package/tests/app_pair.mjs +0 -168
  353. package/tests/box_access.mjs +0 -578
  354. package/tests/box_auth_check.mjs +0 -114
  355. package/tests/box_code_staleness.mjs +0 -129
  356. package/tests/box_connect_e2e.mjs +0 -196
  357. package/tests/box_cost.mjs +0 -70
  358. package/tests/box_credential.mjs +0 -125
  359. package/tests/box_dns_repoint.mjs +0 -153
  360. package/tests/box_env.mjs +0 -64
  361. package/tests/box_host_keys.mjs +0 -126
  362. package/tests/box_infra_resolve.mjs +0 -246
  363. package/tests/box_onboard.mjs +0 -317
  364. package/tests/box_scope_session.mjs +0 -119
  365. package/tests/box_sweep_docs.mjs +0 -119
  366. package/tests/box_sync.mjs +0 -55
  367. package/tests/box_sync_scope_report.mjs +0 -126
  368. package/tests/box_task_scope.mjs +0 -169
  369. package/tests/box_task_scope_pause.mjs +0 -66
  370. package/tests/box_terminal_ssrf.mjs +0 -96
  371. package/tests/boxes.mjs +0 -1585
  372. package/tests/cf_ruleset_preflight.mjs +0 -142
  373. package/tests/cf_tunnel.mjs +0 -496
  374. package/tests/docs_scrubber_damage.mjs +0 -111
  375. package/tests/module_scope_active_claims.mjs +0 -94
  376. package/tests/ship_api_client_pathspec.mjs +0 -100
  377. package/tests/tree_preflight.mjs +0 -243
@@ -1,578 +0,0 @@
1
- // tests/box_access.mjs
2
- //
3
- // Hermetic unit smoke for the rank-gated SOURCE-access decision core of
4
- // modules/dev-box/box-access.js (ADR 0031 §6 + §9.2, task #600; extended for the
5
- // sandbox-for-everyone starter scope, ADR 0052 / #1001). Covers the PURE
6
- // functions the GET /box/source-access route and `box.js reconcile` both consume
7
- // — with NO database, NO DigitalOcean, NO network. The DB helpers
8
- // (listBoxesForReconcile / recordSourceEvent / flagBoxReconcileForBuilder) are
9
- // thin wrappers exercised on the control plane; the DECISIONS they rely on are
10
- // pinned here so the route and the reconciler can never drift from one rule.
11
- //
12
- // Re-added under #674 (the test was written + passing in #600 but deferred by
13
- // parallel-claim conflicts on tests/ and docs/file-map.md). Wired into
14
- // scripts/gds/smoke-box.sh step 1b.
15
- //
16
- // Run: node tests/box_access.mjs
17
-
18
- import { strict as assert } from 'node:assert';
19
- import { createRequire } from 'node:module';
20
-
21
- const require = createRequire(import.meta.url);
22
- const access = require('../modules/dev-box/box-access.js');
23
- const boxes = require('../modules/dev-box/boxes.js');
24
-
25
- let passed = 0;
26
- let failed = 0;
27
- function t(name, fn) {
28
- try { fn(); passed++; console.log(` PASS ${name}`); }
29
- catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
30
- }
31
-
32
- const FLOOR = boxes.DEFAULT_PROVISION_FLOOR; // 'xenos' — everyone meets the default
33
-
34
- console.log('boxCanHoldSource — only a live/becoming-live droplet holds a clone:');
35
-
36
- t('boxCanHoldSource: true for active / provisioning / waking', () => {
37
- assert.equal(access.boxCanHoldSource('active'), true);
38
- assert.equal(access.boxCanHoldSource('provisioning'), true);
39
- assert.equal(access.boxCanHoldSource('waking'), true);
40
- });
41
-
42
- t('boxCanHoldSource: false for parked / destroyed / none / idle / unknown', () => {
43
- assert.equal(access.boxCanHoldSource('parked'), false);
44
- assert.equal(access.boxCanHoldSource('destroyed'), false);
45
- assert.equal(access.boxCanHoldSource('none'), false);
46
- assert.equal(access.boxCanHoldSource('idle'), false);
47
- assert.equal(access.boxCanHoldSource(undefined), false);
48
- });
49
-
50
- console.log('\ncloneSpecForScope — full vs starter, fail-closed to least privilege:');
51
-
52
- t("cloneSpecForScope('full') → full clone, no sparse paths", () => {
53
- assert.deepEqual(access.cloneSpecForScope('full'), { scope: 'full', mode: 'full', sparsePaths: null });
54
- });
55
-
56
- t("cloneSpecForScope('starter') → sparse clone of the starter surface", () => {
57
- const spec = access.cloneSpecForScope('starter');
58
- assert.equal(spec.scope, 'starter');
59
- assert.equal(spec.mode, 'sparse');
60
- assert.deepEqual(spec.sparsePaths, access.STARTER_SPARSE_PATHS);
61
- });
62
-
63
- t('cloneSpecForScope: unknown/missing scope FAILS CLOSED to starter (never full)', () => {
64
- for (const bad of ['admin', '', undefined, null, 'FULL']) {
65
- assert.equal(access.cloneSpecForScope(bad).scope, 'starter', `scope ${JSON.stringify(bad)} must collapse to starter`);
66
- }
67
- });
68
-
69
- t('cloneSpecForScope: starter sparsePaths is a COPY (mutating it cannot poison the catalog)', () => {
70
- const spec = access.cloneSpecForScope('starter');
71
- spec.sparsePaths.push('SHOULD_NOT_PERSIST');
72
- assert.ok(!access.STARTER_SPARSE_PATHS.includes('SHOULD_NOT_PERSIST'),
73
- 'the returned sparsePaths must not alias the shared STARTER_SPARSE_PATHS array');
74
- });
75
-
76
- console.log('\nSTARTER_SPARSE_PATHS — the scoped surface (ADR 0052 §9.2):');
77
-
78
- t('starter surface includes the build tooling + game runtime', () => {
79
- for (const p of ['.claude', 'scripts', 'src/bongos', 'src/world', 'src/rooms', 'public', '.devcontainer', 'infra', 'tests']) {
80
- assert.ok(access.STARTER_SPARSE_PATHS.includes(p), `starter scope must include ${p}`);
81
- }
82
- });
83
-
84
- t('starter surface EXCLUDES migrations/ (degraded-DB preview needs no schema) and the hall', () => {
85
- assert.ok(!access.STARTER_SPARSE_PATHS.includes('migrations'), 'migrations/ must stay out of the starter scope');
86
- // the hall carved to modules/hall-ui/ in BV1.R85; it stays out of a starter box
87
- assert.ok(!access.STARTER_SPARSE_PATHS.includes('modules/hall-ui'), 'modules/hall-ui (the hall) must stay out of the starter scope');
88
- assert.ok(!access.STARTER_SPARSE_PATHS.includes('public-builders'), 'the old public-builders/ path must stay out of the starter scope');
89
- });
90
-
91
- console.log('\ndecideSourceAccess — the live per-fetch gate:');
92
-
93
- t('decideSourceAccess: active Xenos at the default floor → allowed, starter scope', () => {
94
- const d = access.decideSourceAccess({ rank: 'xenos', status: 'active' });
95
- assert.equal(d.allowed, true);
96
- assert.equal(d.scope, 'starter');
97
- assert.equal(d.spec.mode, 'sparse');
98
- });
99
-
100
- t('decideSourceAccess: active Metic → allowed, FULL scope (live rank, not stored scope)', () => {
101
- const d = access.decideSourceAccess({ rank: 'metic', status: 'active' });
102
- assert.equal(d.allowed, true);
103
- assert.equal(d.scope, 'full');
104
- assert.equal(d.spec.sparsePaths, null);
105
- });
106
-
107
- t('decideSourceAccess: Archon → full; Thetes → starter', () => {
108
- assert.equal(access.decideSourceAccess({ rank: 'archon', status: 'active' }).scope, 'full');
109
- assert.equal(access.decideSourceAccess({ rank: 'thetes', status: 'active' }).scope, 'starter');
110
- });
111
-
112
- t('decideSourceAccess: inactive builder → BUILDER_INACTIVE (checked BEFORE rank)', () => {
113
- // A deactivated builder keeps a rank but loses access — and status is checked
114
- // first, so even a Metic gets denied for inactivity, not a rank reason.
115
- const d = access.decideSourceAccess({ rank: 'metic', status: 'inactive' });
116
- assert.deepEqual(d, { allowed: false, reason: 'BUILDER_INACTIVE' });
117
- });
118
-
119
- t('decideSourceAccess: rank below an explicit floor → INSUFFICIENT_RANK with required+actual', () => {
120
- const d = access.decideSourceAccess({ rank: 'xenos', status: 'active', floor: 'metic' });
121
- assert.deepEqual(d, { allowed: false, reason: 'INSUFFICIENT_RANK', required: 'metic', actual: 'xenos' });
122
- });
123
-
124
- t('decideSourceAccess: unknown rank → INSUFFICIENT_RANK (fail closed), actual echoes the bad rank', () => {
125
- const d = access.decideSourceAccess({ rank: 'wizard', status: 'active', floor: FLOOR });
126
- assert.equal(d.allowed, false);
127
- assert.equal(d.reason, 'INSUFFICIENT_RANK');
128
- assert.equal(d.actual, 'wizard');
129
- });
130
-
131
- t('decideSourceAccess: missing rank → actual null (not undefined)', () => {
132
- const d = access.decideSourceAccess({ status: 'active', floor: FLOOR });
133
- assert.equal(d.allowed, false);
134
- assert.equal(d.actual, null);
135
- });
136
-
137
- t('decideSourceAccess: status omitted (falsy) is treated as active — rank alone decides', () => {
138
- const d = access.decideSourceAccess({ rank: 'metic' });
139
- assert.equal(d.allowed, true);
140
- assert.equal(d.scope, 'full');
141
- });
142
-
143
- console.log('\nreconcileSourceAccess — the slower droplet-level clawback/rescope:');
144
-
145
- t('reconcileSourceAccess: no box (none/destroyed/missing) → none/no-box', () => {
146
- assert.deepEqual(access.reconcileSourceAccess({ boxState: 'none', rank: 'metic', status: 'active' }), { action: 'none', reason: 'no-box' });
147
- assert.deepEqual(access.reconcileSourceAccess({ boxState: 'destroyed', rank: 'metic', status: 'active' }), { action: 'none', reason: 'no-box' });
148
- assert.deepEqual(access.reconcileSourceAccess({ rank: 'metic', status: 'active' }), { action: 'none', reason: 'no-box' });
149
- });
150
-
151
- t('reconcileSourceAccess: inactive builder with a live box → deprovision (the clawback)', () => {
152
- const d = access.reconcileSourceAccess({ boxScope: 'starter', boxState: 'active', rank: 'xenos', status: 'inactive' });
153
- assert.deepEqual(d, { action: 'deprovision', reason: 'builder-inactive' });
154
- });
155
-
156
- t('reconcileSourceAccess: rank below floor → deprovision/below-floor', () => {
157
- const d = access.reconcileSourceAccess({ boxScope: 'starter', boxState: 'active', rank: 'xenos', status: 'active', floor: 'metic' });
158
- assert.deepEqual(d, { action: 'deprovision', reason: 'below-floor' });
159
- });
160
-
161
- t('reconcileSourceAccess: stored FULL box but live rank no longer Metic+ → deprovision (fresh box needed)', () => {
162
- // A full box already holds the whole repo on disk; it cannot be safely
163
- // downgraded in place, so the reconciler destroys it.
164
- const d = access.reconcileSourceAccess({ boxScope: 'full', boxState: 'active', rank: 'thetes', status: 'active' });
165
- assert.deepEqual(d, { action: 'deprovision', reason: 'scope-downgrade-needs-fresh-box' });
166
- });
167
-
168
- t('reconcileSourceAccess: stored STARTER box but live rank now Metic+ → rescope to full (graduation)', () => {
169
- const d = access.reconcileSourceAccess({ boxScope: 'starter', boxState: 'active', rank: 'metic', status: 'active' });
170
- assert.deepEqual(d, { action: 'rescope', reason: 'graduation', newScope: 'full' });
171
- });
172
-
173
- t('reconcileSourceAccess: scope already matches live rank → none/in-sync', () => {
174
- assert.deepEqual(access.reconcileSourceAccess({ boxScope: 'starter', boxState: 'active', rank: 'xenos', status: 'active' }), { action: 'none', reason: 'in-sync' });
175
- assert.deepEqual(access.reconcileSourceAccess({ boxScope: 'full', boxState: 'active', rank: 'metic', status: 'active' }), { action: 'none', reason: 'in-sync' });
176
- });
177
-
178
- t('reconcileSourceAccess: inactivity outranks an otherwise in-sync full Metic box', () => {
179
- // status is checked before the scope comparison, so a deactivated Metic with a
180
- // matching full box is still torn down.
181
- const d = access.reconcileSourceAccess({ boxScope: 'full', boxState: 'active', rank: 'metic', status: 'inactive' });
182
- assert.deepEqual(d, { action: 'deprovision', reason: 'builder-inactive' });
183
- });
184
-
185
- // ===========================================================================
186
- // TASK-SCOPED source access (ADR 0148 / goal 1000051, R93). BASE_SPARSE_PATHS,
187
- // the pure sparsePathsForScopeKeys(scopeKeys, moduleScopeMap) cone-filter, and
188
- // the decideSourceAccess three-way branch. Uses the REAL module-scope-map (a pure
189
- // core module) as the map the route (R94) will pass in.
190
- // ===========================================================================
191
- const scopeMap = require('../src/bongos/module-scope-map.js');
192
- const BASE = access.BASE_SPARSE_PATHS;
193
-
194
- console.log('\nBASE_SPARSE_PATHS — the non-product base a task-scoped box layers on:');
195
-
196
- t('BASE carries the methodology + tooling, NOT the product/game', () => {
197
- // clients/ is load-bearing: cli-lib.js hard-imports clients/bongos-client, so a
198
- // cone without it has a fully dead builder CLI on the box (task 1002745).
199
- // modules/lifecycle is load-bearing for the same reason (task 1003085):
200
- // scripts/gds/context-pack.js reads the task enums from its task-classifier,
201
- // and context-pack sits on BOTH /builder-start and /builder-claim. A box with
202
- // no active claim put nothing in scope, so both commands died at import — and
203
- // neither could fix it, because a CLAIM is what fetches the module.
204
- for (const p of ['.devcontainer', '.claude', 'scripts', 'src/bongos', 'docs', 'infra', 'tests', 'clients', 'modules/lifecycle']) {
205
- assert.ok(BASE.includes(p), `BASE must include ${p}`);
206
- }
207
- for (const p of ['src/world', 'src/rooms', 'public', 'art', 'modules/status-ui']) {
208
- assert.ok(!BASE.includes(p), `BASE must NOT include the product surface ${p}`);
209
- }
210
- });
211
-
212
- console.log('\nsparsePathsForScopeKeys — BASE ∪ the claims’ module DIRS (cone-mode):');
213
-
214
- t('no keys (empty array) → BASE only, in declared order', () => {
215
- assert.deepEqual(access.sparsePathsForScopeKeys([], scopeMap), [...BASE]);
216
- });
217
-
218
- t('one key → BASE + that module’s directory (status-ui → modules/status-ui)', () => {
219
- const out = access.sparsePathsForScopeKeys(['status-ui'], scopeMap);
220
- assert.deepEqual(out, [...BASE, 'modules/status-ui']);
221
- });
222
-
223
- t('cone-filter: dev-box keeps its DIRS, DROPS its file globs (scripts/gds/box.js)', () => {
224
- const out = access.sparsePathsForScopeKeys(['dev-box'], scopeMap);
225
- assert.ok(out.includes('modules/dev-box'), 'the module dir is kept');
226
- assert.ok(out.includes('devbox-app'), 'the app dir is kept');
227
- assert.ok(!out.some((p) => p.endsWith('.js') || p.endsWith('.sh')), 'no file globs survive the cone filter');
228
- assert.ok(!out.includes('scripts/gds/box.js'), 'the box CLI file is dropped (BASE scripts covers it)');
229
- // …and its PARENT is not added either: BASE's `scripts` already covers it, so
230
- // the ancestor check must stop `scripts/gds` from appearing. This is the half
231
- // that keeps the file-glob rescue below from over-sharing.
232
- assert.ok(!out.includes('scripts/gds'), 'a covered parent is not re-emitted');
233
- });
234
-
235
- // A file glob whose parent is NOT already covered must contribute that parent.
236
- // Regression for the live failure that released task 1003018 (R05 of goal
237
- // 1000069): the `government` key's config/government*.json globs were dropped on
238
- // the reasoning that every file glob sits under a BASE directory — true when
239
- // written, false once ADR 0174 added them, since `config` is not in BASE. The box
240
- // could not see two files inside its own scope wall.
241
- t('file-glob rescue: government contributes `config` for its config/*.json globs', () => {
242
- const out = access.sparsePathsForScopeKeys(['government'], scopeMap);
243
- assert.ok(out.includes('modules/government'), 'the module dir is kept');
244
- assert.ok(out.includes('config'), 'config/government.neutral.json must reach the box via its parent');
245
- assert.ok(!out.some((p) => p.endsWith('.json')), 'still no file paths — cone mode would reject one');
246
- });
247
-
248
- t('file-glob rescue holds for EVERY key in the map — no module can silently lose one', () => {
249
- // Enumerated, never sampled: a module that gains a file glob under a new parent
250
- // is covered the day it lands, which is exactly how this bug escaped before.
251
- for (const key of scopeMap.moduleKeys()) {
252
- const out = access.sparsePathsForScopeKeys([key], scopeMap);
253
- for (const glob of scopeMap.scopeForModule(key).globs) {
254
- if (glob.endsWith('/')) continue;
255
- const cut = glob.lastIndexOf('/');
256
- if (cut < 0) continue; // a root file — cone mode includes it automatically
257
- const parent = glob.slice(0, cut);
258
- const reachable = out.some((p) => parent === p || parent.startsWith(`${p}/`));
259
- assert.ok(reachable, `${key}: no cone path materializes ${glob} (parent ${parent})`);
260
- }
261
- }
262
- });
263
-
264
- t('the emitted set stays directories only, for every key', () => {
265
- // A leading dot is a DIRECTORY here ('.devcontainer', '.claude'), so the
266
- // extension test must require a non-dot character before the dot — otherwise
267
- // every BASE dotfile-dir reads as a file and this assertion fires on healthy
268
- // input. (It did, on first write.)
269
- const looksLikeFile = (seg) => /[^.]\.[a-z0-9]+$/i.test(seg);
270
- for (const key of scopeMap.moduleKeys()) {
271
- for (const p of access.sparsePathsForScopeKeys([key], scopeMap)) {
272
- assert.ok(!looksLikeFile(p.split('/').pop()),
273
- `${key}: ${p} looks like a file — git sparse-checkout set would abort on it`);
274
- }
275
- }
276
- });
277
-
278
- t('cone-filter: the kernel key materializes its real DIR globs (config, src/module-loader)', () => {
279
- const out = access.sparsePathsForScopeKeys(['kernel'], scopeMap);
280
- // kernel is mostly individual src/bongos/*.js FILES (dropped — BASE src/bongos
281
- // covers them via cone ancestor-inclusion) + a few genuine DIR globs.
282
- assert.ok(out.includes('config'), 'config/ is a real dir the task needs, not in BASE');
283
- // src/module-loader/ is a real dir not in BASE, but it is NOT emitted verbatim:
284
- // kernel also carries the file globs src/instance-config.js, src/branding.js,
285
- // src/modules.js and src/module-api.js, whose rescued parent is `src` — and once
286
- // `src` is emitted the ancestor check absorbs every path beneath it, this one
287
- // included. Asserting literal membership would be asserting emission ORDER, not
288
- // reachability. What the box must actually get is the DIRECTORY's contents, so
289
- // assert that, using the same idiom as the enumerated rescue test above.
290
- const reaches = (dir) => out.some((p) => dir === p || dir.startsWith(`${p}/`));
291
- assert.ok(reaches('src/module-loader'), 'src/module-loader must be materialized (here via the rescued `src`)');
292
- assert.ok(!out.includes('src/bongos/db.js'), 'the kernel FILE globs are dropped');
293
- assert.ok(!out.includes('server.js'), 'top-level files never enter the sparse set (cone auto-includes them)');
294
- });
295
-
296
- t('N keys → union, deduped + sorted; unknown keys contribute nothing', () => {
297
- const out = access.sparsePathsForScopeKeys(['status-ui', 'hall-ui'], scopeMap);
298
- assert.deepEqual(out, [...BASE, 'modules/hall-ui', 'modules/status-ui'], 'union of both dirs, sorted');
299
- assert.deepEqual(access.sparsePathsForScopeKeys(['status-ui', 'status-ui'], scopeMap), [...BASE, 'modules/status-ui'], 'dedup');
300
- assert.deepEqual(access.sparsePathsForScopeKeys(['bogus'], scopeMap), [...BASE], 'unknown key → no extra paths');
301
- });
302
-
303
- t('missing/!scopeForModule moduleScopeMap → BASE only (guards the missing map)', () => {
304
- assert.deepEqual(access.sparsePathsForScopeKeys(['dev-box'], null), [...BASE]);
305
- assert.deepEqual(access.sparsePathsForScopeKeys(['dev-box'], {}), [...BASE]);
306
- });
307
-
308
- console.log('\ndecideSourceAccess — the ADR 0148 three-way (rank still gates ALLOW/DENY):');
309
-
310
- t('scopeKeys present + non-empty → TASK scope, BASE ∪ the module dirs (rank no longer caps breadth)', () => {
311
- const d = access.decideSourceAccess({ rank: 'xenos', status: 'active', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
312
- assert.equal(d.allowed, true);
313
- assert.equal(d.scope, 'task');
314
- assert.equal(d.spec.mode, 'sparse');
315
- assert.deepEqual(d.spec.sparsePaths, [...BASE, 'modules/status-ui'], 'even a Xenos with a claim gets the claim’s module scope');
316
- });
317
-
318
- t('scopeKeys present + EMPTY → BASE-only (a live box with no active claim)', () => {
319
- const d = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: [], moduleScopeMap: scopeMap });
320
- assert.equal(d.allowed, true);
321
- assert.equal(d.scope, 'base');
322
- assert.deepEqual(d.spec.sparsePaths, [...BASE], 'idle Metic+ now gets BASE-only, not the whole repo (ADR 0148 flagged change)');
323
- });
324
-
325
- t('scopeKeys ABSENT → the pre-ADR-0148 RANK fallback is preserved EXACTLY (R93-before-R94 safety)', () => {
326
- assert.equal(access.decideSourceAccess({ rank: 'metic', status: 'active' }).scope, 'full', 'no claim signal → rank fallback (full)');
327
- assert.equal(access.decideSourceAccess({ rank: 'xenos', status: 'active' }).scope, 'starter', 'no claim signal → rank fallback (starter)');
328
- });
329
-
330
- t('the ALLOW/DENY gates run BEFORE scope selection', () => {
331
- const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
332
- assert.equal(inactive.allowed, false);
333
- assert.equal(inactive.reason, 'BUILDER_INACTIVE');
334
- const belowFloor = access.decideSourceAccess({ rank: 'xenos', status: 'active', floor: 'metic', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
335
- assert.equal(belowFloor.allowed, false);
336
- assert.equal(belowFloor.reason, 'INSUFFICIENT_RANK');
337
- });
338
-
339
- console.log('\nscopeMetadata — the credential-FREE projection for GET /box/scope (BV1.R100):');
340
-
341
- t('task scope → modules deduped+sorted, sparse_paths carried, NO credential fields', () => {
342
- const scopeKeys = ['dev-box', 'onboarding', 'dev-box']; // dup on purpose
343
- const decision = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys, moduleScopeMap: scopeMap });
344
- const meta = access.scopeMetadata({ decision, scopeKeys });
345
- assert.equal(meta.scope, 'task');
346
- assert.equal(meta.mode, 'sparse');
347
- assert.deepEqual(meta.modules, ['dev-box', 'onboarding'], 'deduped + sorted');
348
- assert.ok(Array.isArray(meta.sparse_paths) && meta.sparse_paths.includes('modules/dev-box'));
349
- assert.ok(!('credential' in meta) && !('repo_url' in meta), 'never carries credential fields');
350
- });
351
-
352
- t('empty claims → base scope, modules is [] (not null) — a live box with no active claim', () => {
353
- const decision = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: [], moduleScopeMap: scopeMap });
354
- const meta = access.scopeMetadata({ decision, scopeKeys: [] });
355
- assert.equal(meta.scope, 'base');
356
- assert.deepEqual(meta.modules, []);
357
- assert.deepEqual(meta.sparse_paths, [...BASE]);
358
- });
359
-
360
- t('rank fallback (scopeKeys absent) → modules is null (breadth is rank-driven, not per-module)', () => {
361
- const decision = access.decideSourceAccess({ rank: 'metic', status: 'active' });
362
- const meta = access.scopeMetadata({ decision, scopeKeys: undefined });
363
- assert.equal(meta.scope, 'full');
364
- assert.equal(meta.mode, 'full');
365
- assert.equal(meta.modules, null);
366
- });
367
-
368
- t('a DENIED decision projects to null (the route returns 403, never a scope body)', () => {
369
- const denied = access.decideSourceAccess({ rank: 'xenos', status: 'inactive', scopeKeys: [], moduleScopeMap: scopeMap });
370
- assert.equal(denied.allowed, false);
371
- assert.equal(access.scopeMetadata({ decision: denied, scopeKeys: [] }), null);
372
- assert.equal(access.scopeMetadata({}), null, 'no decision → null');
373
- assert.equal(access.scopeMetadata(), null, 'no args → null');
374
- });
375
-
376
- // ---------- task 1003227: config/ must be in the box's BASE slice ----------
377
- //
378
- // Without it a box cannot run its own quality gate: fitness.js compares against
379
- // config/fitness-baselines.json, and with the file absent the ratchet used to report
380
- // PASS (a planted 5,000-line + 94-cycle regression sailed through). So a boxed builder
381
- // shipped believing they were clean and learned otherwise from red CI, with nothing on
382
- // the box to reproduce it. Precedent for the same class: ADR 0174 put
383
- // config/government.json in the `government` globs, config/ was not in BASE, and task
384
- // 1003018 was RELEASED because its box could never see its own deliverable.
385
-
386
- t('1003227 BASE includes config/ so a box can read its own fitness baselines', () => {
387
- const spec = access.decideSourceAccess({
388
- rank: 'archon', status: 'active', scopeKeys: [], hasActiveClaims: false, moduleScopeMap: {},
389
- });
390
- assert.equal(spec.scope, 'base');
391
- assert.ok(spec.spec.sparsePaths.includes('config'),
392
- `base scope must carry config/; got ${spec.spec.sparsePaths.join(',')}`);
393
- });
394
-
395
- t('1003227 a TASK-scoped box carries config/ too (the common case — a claim is held)', () => {
396
- // The REAL scope map, not a hand-rolled object — the shape matters here.
397
- const scopeMap = require('../src/bongos/module-scope-map.js');
398
- const spec = access.decideSourceAccess({
399
- rank: 'xenos', status: 'active', scopeKeys: ['economy'], hasActiveClaims: true,
400
- moduleScopeMap: scopeMap,
401
- });
402
- assert.equal(spec.scope, 'task');
403
- assert.ok(spec.spec.sparsePaths.includes('config'), 'task scope must carry config/ as well');
404
- assert.ok(spec.spec.sparsePaths.includes('modules/economy'),
405
- `and still layer the claimed module on top; got ${spec.spec.sparsePaths.join(',')}`);
406
- });
407
-
408
- t('1003227 config/ is a cone-mode DIRECTORY entry (a file path aborts the whole set)', () => {
409
- // ADR 0148: `git sparse-checkout set` rejects a file path, which kills the entire
410
- // checkout — so every BASE entry must be a bare directory, no trailing slash.
411
- for (const p of access.BASE_SPARSE_PATHS) {
412
- assert.ok(!p.endsWith('/'), `${p} must not carry a trailing slash`);
413
- // A dotted DIRECTORY (.claude, .devcontainer) is fine; what cone mode rejects is
414
- // a file, so only test for an extension on the FINAL segment of a non-dotfile.
415
- const last = p.split('/').pop();
416
- if (!last.startsWith('.')) {
417
- assert.ok(!/\.[a-z0-9]+$/i.test(last), `${p} looks like a FILE — cone mode would abort the set`);
418
- }
419
- }
420
- });
421
-
422
- // ---------------------------------------------------------------------------
423
- // task 1003087 / idea 1000793 — builder widening that SURVIVES the */10 tick
424
- //
425
- // The bug: the cron re-applies the server-computed sparse set, so a builder's
426
- // own `git sparse-checkout add` is deleted — sometimes mid-command (a live suite
427
- // died with MODULE_NOT_FOUND that read exactly like a code bug). The widen set is
428
- // therefore recorded server-side and unioned in HERE, on the same path the cron
429
- // asks every tick. These pin the two halves that matter: it actually widens, and
430
- // it cannot be used to climb past the rank scope.
431
- // ---------------------------------------------------------------------------
432
-
433
- console.log('\nnormalizeWidenPath — a repo-relative directory, or nothing:');
434
-
435
- t('accepts a plain repo-relative dir and strips the noise around it', () => {
436
- assert.equal(access.normalizeWidenPath('docs'), 'docs');
437
- assert.equal(access.normalizeWidenPath(' docs '), 'docs');
438
- assert.equal(access.normalizeWidenPath('./docs/'), 'docs');
439
- assert.equal(access.normalizeWidenPath('docs//adr'), 'docs/adr');
440
- });
441
-
442
- t('folds a Windows separator rather than judging the path on it', () => {
443
- const BS = String.fromCharCode(92);
444
- assert.equal(access.normalizeWidenPath(`modules${BS}game`), 'modules/game');
445
- });
446
-
447
- t('REFUSES traversal, absolute paths and drive letters outright', () => {
448
- // Refused, never "cleaned up into something that still works" — this string
449
- // reaches `git sparse-checkout set` on a real box.
450
- for (const bad of ['../etc', 'a/../../b', '/etc/passwd', 'C:/Windows', '..', '.', '']) {
451
- assert.equal(access.normalizeWidenPath(bad), null, `${JSON.stringify(bad)} must be refused`);
452
- }
453
- });
454
-
455
- t('refuses a non-string, an over-long path, and odd characters', () => {
456
- assert.equal(access.normalizeWidenPath(null), null);
457
- assert.equal(access.normalizeWidenPath(42), null);
458
- assert.equal(access.normalizeWidenPath({}), null);
459
- assert.equal(access.normalizeWidenPath('a'.repeat(500)), null);
460
- assert.equal(access.normalizeWidenPath('modules/ga me'), null);
461
- assert.equal(access.normalizeWidenPath('modules/rm -rf'), null);
462
- });
463
-
464
- console.log('\nadmissibleWidenPaths — rank is the ceiling, and it is the whole point:');
465
-
466
- t('a Metic (full rank scope) may widen anywhere', () => {
467
- assert.deepEqual(access.admissibleWidenPaths(['modules/game'], 'metic'), ['modules/game']);
468
- assert.deepEqual(access.admissibleWidenPaths(['migrations'], 'archon'), ['migrations']);
469
- });
470
-
471
- t('a Xenos may NOT widen past the starter surface — that would be rank escalation', () => {
472
- // The security property of this feature stated as a test: if an arbitrary widen
473
- // were honored, a convenience flag would hand a starter-scoped builder the whole
474
- // repo, which is the rank gate defeated rather than a checkout preference.
475
- assert.deepEqual(access.admissibleWidenPaths(['modules/game'], 'xenos'), []);
476
- assert.deepEqual(access.admissibleWidenPaths(['migrations'], 'xenos'), []);
477
- assert.deepEqual(access.admissibleWidenPaths(['modules/lifecycle'], 'thetes'), []);
478
- });
479
-
480
- t('a Xenos MAY widen INSIDE starter — a task box is narrower than their rank allows', () => {
481
- // Not a null grant: task scope is BASE union modules, which is narrower than
482
- // starter, so this is the room to climb back to what rank already permitted.
483
- assert.deepEqual(access.admissibleWidenPaths(['docs/adr'], 'xenos'), ['docs/adr']);
484
- assert.deepEqual(access.admissibleWidenPaths(['src/world'], 'xenos'), ['src/world']);
485
- });
486
-
487
- t('normalizes, dedupes and sorts, and drops the invalid without failing the rest', () => {
488
- const got = access.admissibleWidenPaths(['./docs/', 'docs', '../etc', 'art'], 'metic');
489
- assert.deepEqual(got, ['art', 'docs'], 'one invalid entry must not discard the valid ones');
490
- });
491
-
492
- t('is capped, so a widen set can never become a second scope system', () => {
493
- const many = Array.from({ length: access.MAX_WIDEN_PATHS + 20 }, (_, i) => `docs/d${i}`);
494
- assert.equal(access.admissibleWidenPaths(many, 'metic').length, access.MAX_WIDEN_PATHS);
495
- });
496
-
497
- t('a missing / non-array widen set is simply no widening', () => {
498
- for (const v of [undefined, null, 'docs', 42, {}]) {
499
- assert.deepEqual(access.admissibleWidenPaths(v, 'metic'), []);
500
- }
501
- });
502
-
503
- console.log('\nwithWidenPaths — union into the computed set, without redundancy:');
504
-
505
- t('adds a dir the set does not already cover', () => {
506
- assert.deepEqual(access.withWidenPaths(['src/bongos'], ['docs']), ['src/bongos', 'docs']);
507
- });
508
-
509
- t('skips a dir an ANCESTOR already covers — cone mode pulls the whole subtree', () => {
510
- assert.deepEqual(access.withWidenPaths(['scripts'], ['scripts/gds']), ['scripts'],
511
- 'adding scripts/gds under scripts is noise in the git command and in the scope readout');
512
- });
513
-
514
- t('passes a null sparse set (the full clone) straight through', () => {
515
- assert.equal(access.withWidenPaths(null, ['docs']), null,
516
- 'a full clone already holds everything — there is nothing to widen INTO');
517
- });
518
-
519
- t('an empty widen set returns the computed set untouched', () => {
520
- const base = ['src/bongos', 'scripts'];
521
- assert.deepEqual(access.withWidenPaths(base, []), base);
522
- });
523
-
524
- console.log('\ndecideSourceAccess — the widen reaches the spec the box actually fetches:');
525
-
526
- t('a task-scoped box gets its widen unioned into the sparse set', () => {
527
- const d = access.decideSourceAccess({
528
- rank: 'metic', status: 'active', scopeKeys: [], hasActiveClaims: false,
529
- moduleScopeMap: {}, widenPaths: ['modules/game'],
530
- });
531
- assert.equal(d.allowed, true);
532
- assert.ok(d.spec.sparsePaths.includes('modules/game'),
533
- 'the widen must reach the SPEC — this is the value the cron writes into sparse-checkout');
534
- });
535
-
536
- t('an out-of-scope widen never reaches the spec for a starter-scoped builder', () => {
537
- const d = access.decideSourceAccess({
538
- rank: 'xenos', status: 'active', scopeKeys: [], hasActiveClaims: false,
539
- moduleScopeMap: {}, widenPaths: ['modules/game'],
540
- });
541
- assert.ok(!d.spec.sparsePaths.includes('modules/game'));
542
- });
543
-
544
- t('no widenPaths leaves every scope byte-identical to before the feature', () => {
545
- const withArg = access.decideSourceAccess({
546
- rank: 'metic', status: 'active', scopeKeys: [], hasActiveClaims: false, moduleScopeMap: {}, widenPaths: [],
547
- });
548
- const without = access.decideSourceAccess({
549
- rank: 'metic', status: 'active', scopeKeys: [], hasActiveClaims: false, moduleScopeMap: {},
550
- });
551
- assert.deepEqual(withArg.spec, without.spec, 'a box that asked for nothing must fetch exactly what it always did');
552
- });
553
-
554
- t('a full-clone builder is unaffected — sparsePaths stays null', () => {
555
- const d = access.decideSourceAccess({ rank: 'metic', status: 'active', widenPaths: ['docs'] });
556
- assert.equal(d.spec.sparsePaths, null);
557
- assert.equal(d.spec.mode, 'full');
558
- });
559
-
560
- t('widening cannot resurrect access for an inactive or below-floor builder', () => {
561
- // The widen set is read from the box row, so it must not become a way around
562
- // the ALLOW/DENY gate that runs before any scope is computed.
563
- const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', widenPaths: ['docs'] });
564
- assert.equal(inactive.allowed, false);
565
- assert.equal(inactive.reason, 'BUILDER_INACTIVE');
566
- assert.equal(inactive.spec, undefined);
567
- });
568
-
569
- t('every admitted widen path is still a DIRECTORY — cone mode aborts on a file', () => {
570
- for (const p of access.admissibleWidenPaths(['docs/adr', 'src/world'], 'metic')) {
571
- assert.ok(!p.endsWith('/'), `${p} must not carry a trailing slash`);
572
- const last = p.split('/').pop();
573
- assert.ok(!/\.[a-z0-9]+$/i.test(last), `${p} looks like a FILE — cone mode would abort the set`);
574
- }
575
- });
576
-
577
- console.log(`\nbox_access.mjs: ${passed} passed, ${failed} failed`);
578
- process.exit(failed === 0 ? 0 : 1);