@bongos/core 1.17.1 → 1.17.3

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 (233) hide show
  1. package/.bongos-core.json +521 -281
  2. package/.claude/skills/blocker-review/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +2 -0
  4. package/.claude/skills/builder-exit/SKILL.md +2 -2
  5. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  6. package/.claude/skills/builder-setup/SKILL.md +2 -2
  7. package/.claude/skills/builder-start/SKILL.md +1 -1
  8. package/.claude/skills/dev/SKILL.md +1 -1
  9. package/.claude/skills/idea-triage/SKILL.md +2 -2
  10. package/.claude/skills/ideate/SKILL.md +1 -1
  11. package/.claude/skills/merge-mode/SKILL.md +1 -1
  12. package/.claude/skills/new-project/SKILL.md +3 -0
  13. package/.claude/skills/paint/SKILL.md +1 -1
  14. package/GOVERNANCE.md +1 -1
  15. package/README.md +1 -1
  16. package/clients/bongos-client/README.md +1 -1
  17. package/clients/bongos-client/bongos-client.global.js +2 -0
  18. package/clients/bongos-client/index.cjs +2 -0
  19. package/clients/bongos-client/index.d.ts +4 -2
  20. package/clients/bongos-client/index.mjs +2 -0
  21. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  22. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  23. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  24. package/docs/adr/0020-security-model-public-stub.md +1 -1
  25. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  26. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  27. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  28. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  29. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  30. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  31. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  32. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  33. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  34. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  35. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  36. package/docs/adr/0057-container-cost-ledger.md +3 -3
  37. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  38. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  39. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  40. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  41. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  42. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  43. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  44. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  45. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  46. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  47. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  48. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  49. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  50. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  51. package/docs/adr/README.md +9 -6
  52. package/docs/api/openapi.json +65 -26
  53. package/docs/api-reference.md +3 -2
  54. package/docs/architecture.md +2 -2
  55. package/docs/branding-contract.md +5 -4
  56. package/docs/canonical-permissions.md +1 -1
  57. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  58. package/docs/file-map.md +18 -17
  59. package/docs/handoff-template.md +2 -2
  60. package/docs/modules-contract.md +6 -1
  61. package/docs/onboarding/diagrams/README.md +7 -7
  62. package/docs/onboarding/drachmae-and-karma.md +18 -18
  63. package/docs/onboarding/primer.md +9 -9
  64. package/docs/recipes/audit-replay.md +2 -2
  65. package/docs/recipes/builder-box-lifecycle.md +2 -2
  66. package/docs/recipes/claude-design-loop.md +1 -1
  67. package/docs/recipes/ops-gotchas.md +1 -1
  68. package/docs/recipes/private-npm-distribution.md +17 -0
  69. package/docs/recipes/staging-environment.md +1 -1
  70. package/docs/recipes/upgrading-the-core.md +55 -0
  71. package/docs/research/cursor-competitive-analysis.md +1 -1
  72. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  73. package/modules/discord/module.json +2 -1
  74. package/modules/economy/achievements.js +9 -1
  75. package/modules/economy/credits.js +10 -1
  76. package/modules/hall-ui/public/builders.js +137 -620
  77. package/modules/hall-ui/public/goal-inbox.js +232 -0
  78. package/modules/hall-ui/public/goals-page.js +4 -1
  79. package/modules/hall-ui/public/goals.html +8 -0
  80. package/modules/hall-ui/public/hall-kit.js +474 -0
  81. package/modules/hall-ui/public/hall-widgets.js +14 -5
  82. package/modules/hall-ui/public/index.html +3 -0
  83. package/modules/hall-ui/public/instances-panel.js +24 -3
  84. package/modules/hall-ui/public/new-project-wizard.js +7 -5
  85. package/modules/hall-ui/public/style.css +168 -0
  86. package/modules/hall-ui/public/work.css +194 -0
  87. package/modules/hall-ui/public/work.html +46 -374
  88. package/modules/hall-ui/public/work.js +137 -102
  89. package/modules/lifecycle/criterion-suggest.js +80 -0
  90. package/modules/lifecycle/db.js +42 -5
  91. package/modules/lifecycle/routes/goals.js +26 -0
  92. package/modules/lifecycle/routes/tasks.js +21 -0
  93. package/modules/provisioning/CLAUDE.md +15 -0
  94. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  95. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  96. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  97. package/modules/provisioning/module.json +17 -0
  98. package/modules/provisioning/onboard-plan.js +37 -0
  99. package/modules/provisioning/provisioning.js +539 -0
  100. package/modules/provisioning/rate-limit.js +35 -0
  101. package/modules/provisioning/routes/provisioning.js +607 -0
  102. package/modules/provisioning/tests/provisioning.mjs +232 -0
  103. package/modules/provisioning/ui/.gitkeep +0 -0
  104. package/modules/security/secret-scrub.js +10 -0
  105. package/package-lock.json +2 -2
  106. package/package.json +7 -2
  107. package/scripts/discord/README.md +1 -1
  108. package/scripts/gds/claim.js +98 -48
  109. package/scripts/gds/claude-materialize.js +30 -9
  110. package/scripts/gds/cutover-codemod.js +241 -0
  111. package/scripts/gds/fitness.js +54 -0
  112. package/scripts/gds/init.js +59 -3
  113. package/scripts/gds/lint-infra-exec.js +70 -0
  114. package/scripts/gds/lint-workflow-exec.js +63 -0
  115. package/scripts/gds/mirror-lineage-check.js +14 -5
  116. package/scripts/gds/mirror-redact.js +8 -0
  117. package/scripts/gds/onboard.js +4 -0
  118. package/scripts/gds/package-core.js +18 -3
  119. package/scripts/gds/publish-manifest.js +60 -2
  120. package/scripts/gds/run-routine.js +51 -13
  121. package/scripts/gds/run-unit-tests.js +4 -0
  122. package/scripts/gds/update-channel.js +186 -0
  123. package/scripts/gds/upgrade.js +116 -9
  124. package/scripts/hall-preview/README.md +103 -0
  125. package/scripts/hall-preview/fixtures/access-requests.json +1 -0
  126. package/scripts/hall-preview/fixtures/achievements.json +76 -0
  127. package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
  128. package/scripts/hall-preview/fixtures/audit-log.json +1 -0
  129. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
  130. package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
  131. package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
  132. package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
  133. package/scripts/hall-preview/fixtures/goals.json +502 -0
  134. package/scripts/hall-preview/fixtures/goals__25.json +96 -0
  135. package/scripts/hall-preview/fixtures/goals__26.json +67 -0
  136. package/scripts/hall-preview/fixtures/goals__3.json +103 -0
  137. package/scripts/hall-preview/fixtures/goals__35.json +49 -0
  138. package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
  139. package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
  140. package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
  141. package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
  142. package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
  143. package/scripts/hall-preview/fixtures/inbox.json +330 -0
  144. package/scripts/hall-preview/fixtures/instance.json +124 -0
  145. package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
  146. package/scripts/hall-preview/fixtures/me.json +476 -0
  147. package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
  148. package/scripts/hall-preview/fixtures/override-requests.json +1 -0
  149. package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
  150. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
  151. package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
  152. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
  153. package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
  154. package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
  155. package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
  156. package/scripts/hall-preview/fixtures/public__grades.json +50 -0
  157. package/scripts/hall-preview/fixtures/public__progress.json +124 -0
  158. package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
  159. package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
  160. package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
  161. package/scripts/hall-preview/fixtures/security__docs.json +1 -0
  162. package/scripts/hall-preview/fixtures/security__reports.json +1 -0
  163. package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
  164. package/scripts/hall-preview/fixtures/tasks.json +2420 -0
  165. package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
  166. package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
  167. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
  168. package/scripts/hall-preview/fixtures/versions.json +89 -0
  169. package/scripts/hall-preview/refresh-fixtures.js +77 -0
  170. package/scripts/hall-preview/server.js +178 -0
  171. package/scripts/migrate.sh +29 -4
  172. package/src/bongos/auth.js +14 -1
  173. package/src/bongos/optimizer.js +37 -2
  174. package/src/bongos/serve-internal.js +10 -1
  175. package/src/branding.js +1 -0
  176. package/src/module-api.js +12 -1
  177. package/src/module-loader/loader.js +36 -4
  178. package/src/module-loader/manifest-schema.js +16 -1
  179. package/tests/branding.mjs +11 -0
  180. package/tests/claim_error_surface.mjs +107 -0
  181. package/tests/claude_materialize.mjs +20 -0
  182. package/tests/consumer_layout_boot.mjs +302 -0
  183. package/tests/criterion_suggest.mjs +106 -0
  184. package/tests/cutover_codemod.mjs +184 -0
  185. package/tests/device_flow_start.mjs +25 -2
  186. package/tests/hook_core_resolve.mjs +125 -0
  187. package/tests/infra_exec_paths.mjs +104 -0
  188. package/tests/init.mjs +47 -0
  189. package/tests/migrate_root_resolution.mjs +97 -0
  190. package/tests/mirror_lineage_proof.mjs +14 -0
  191. package/tests/module_loader.mjs +42 -0
  192. package/tests/module_manifest.mjs +19 -0
  193. package/tests/onboard.mjs +13 -0
  194. package/tests/package_core.mjs +8 -0
  195. package/tests/provisioning_github_app_routes.mjs +15 -0
  196. package/tests/publish_manifest.mjs +52 -1
  197. package/tests/run_routine.mjs +26 -1
  198. package/tests/run_routine_two_root.mjs +77 -0
  199. package/tests/scrubber_corpus.mjs +17 -0
  200. package/tests/update_channel.mjs +186 -0
  201. package/tests/upgrade.mjs +131 -0
  202. package/tests/workflow_exec_paths.mjs +98 -0
  203. package/docs/design/container-exterior-tiles.md +0 -49
  204. package/docs/design/container-exterior.md +0 -81
  205. package/docs/design/container-interior.md +0 -77
  206. package/docs/design/world-map-scroll.md +0 -41
  207. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  208. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  209. package/docs/recipes/cloudbongos-standup.md +0 -111
  210. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  211. package/docs/recipes/parallel-load-test.md +0 -32
  212. package/docs/recipes/pixel-art-pipeline.md +0 -411
  213. package/docs/recipes/staging-data.md +0 -50
  214. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  215. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  216. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  217. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  218. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  219. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  220. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  221. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  222. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  223. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  224. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  225. package/scripts/gds/art-stage.js +0 -152
  226. package/server.js +0 -212
  227. package/tests/art_key.mjs +0 -121
  228. package/tests/day_cycle.mjs +0 -197
  229. package/tests/snapshot_picker.mjs +0 -233
  230. package/tests/sprite_facing.mjs +0 -35
  231. package/tests/terrain_picker.mjs +0 -298
  232. package/tests/turn_in_place.mjs +0 -280
  233. package/tests/walkability.mjs +0 -188
@@ -0,0 +1,125 @@
1
+ // tests/hook_core_resolve.mjs
2
+ //
3
+ // Task 2139 — the materialized .claude/hooks resolve CORE modules (src/bongos/*,
4
+ // scripts/gds/*) in BOTH layouts: the repo root today, and the pinned @bongos/core
5
+ // package post-split (ADR 0108). Before this, conductor.js's UNGUARDED
6
+ // require('../../src/bongos/…') MODULE_NOT_FOUND'd on EVERY UserPromptSubmit once
7
+ // src/ left the instance root; the guarded hooks silently killed their feature.
8
+ //
9
+ // _core-resolve.coreModulePathFrom(instanceRoot, rel) is the whole mechanism, so
10
+ // it takes the root as an argument and this suite diverges it against synthetic
11
+ // trees (impossible to prove with the real single checkout, where root == core).
12
+ // Locks:
13
+ // 1. finds a core file inside node_modules/@bongos/core when it is NOT at the
14
+ // repo root — the post-split consumer layout, the point of the task;
15
+ // 2. repo root WINS when the file exists in both (today = byte-identical);
16
+ // 3. absent in every layout → null (fresh clone → callers degrade, never crash);
17
+ // 4. all three published core package names resolve;
18
+ // 5. requireCore loads a real core module today + throws a tagged error when absent;
19
+ // 6. conductor.js, loaded in a core-LESS instance root, does NOT crash and still
20
+ // emits its methodology nudges (the "conductor no longer hard-crashes" done-when).
21
+ //
22
+ // DB-free + no network → part of the unit gate. Run: node tests/hook_core_resolve.mjs
23
+
24
+ import { strict as assert } from 'node:assert';
25
+ import { execFileSync } from 'node:child_process';
26
+ import { mkdtempSync, writeFileSync, mkdirSync, cpSync } from 'node:fs';
27
+ import { tmpdir } from 'node:os';
28
+ import { join, dirname } from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+ import { createRequire } from 'node:module';
31
+
32
+ const require = createRequire(import.meta.url);
33
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
34
+ const resolver = require('../.claude/hooks/_core-resolve.js');
35
+
36
+ let passed = 0;
37
+ let failed = 0;
38
+ function t(name, fn) {
39
+ try { fn(); passed++; console.log(` PASS ${name}`); }
40
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
41
+ }
42
+
43
+ // Build a throwaway instance root from { relPath: contents }.
44
+ function makeTree(files) {
45
+ const root = mkdtempSync(join(tmpdir(), 'hook-core-'));
46
+ for (const [rel, body] of Object.entries(files)) {
47
+ const abs = join(root, rel);
48
+ mkdirSync(dirname(abs), { recursive: true });
49
+ writeFileSync(abs, body);
50
+ }
51
+ return root;
52
+ }
53
+
54
+ // 1. Post-split: the core file exists ONLY inside the installed package.
55
+ t('resolves a core module inside node_modules/@bongos/core (post-split layout)', () => {
56
+ const root = makeTree({ 'node_modules/@bongos/core/src/bongos/path-match.js': 'module.exports={};' });
57
+ const p = resolver.coreModulePathFrom(root, 'src/bongos/path-match');
58
+ assert.ok(p, 'expected a resolution');
59
+ assert.ok(p.includes(join('node_modules', '@bongos', 'core')), `expected a node_modules path, got ${p}`);
60
+ });
61
+
62
+ // 2. Priority: repo root wins when present in both (today's single checkout — unchanged).
63
+ t('prefers the repo root when a core file exists at both root and node_modules', () => {
64
+ const root = makeTree({
65
+ 'scripts/gds/cli-lib.js': 'module.exports={root:true};',
66
+ 'node_modules/@bongos/core/scripts/gds/cli-lib.js': 'module.exports={pkg:true};',
67
+ });
68
+ const p = resolver.coreModulePathFrom(root, 'scripts/gds/cli-lib');
69
+ assert.ok(p && !p.includes('node_modules'), `expected the root copy, got ${p}`);
70
+ });
71
+
72
+ // 3. Absent everywhere → null (fresh clone pre-`npm install`; callers degrade).
73
+ t('returns null when the core file exists in no layout', () => {
74
+ const root = makeTree({ 'README.md': '# empty instance' });
75
+ assert.equal(resolver.coreModulePathFrom(root, 'src/bongos/path-match'), null);
76
+ });
77
+
78
+ // 4. Every published core package name is honored (scoped-private, scoped-public, bare).
79
+ for (const pkg of resolver.CORE_PKG_NAMES) {
80
+ t(`resolves via published package name "${pkg}"`, () => {
81
+ const root = makeTree({ [`node_modules/${pkg}/scripts/gds/x.js`]: 'module.exports={};' });
82
+ const p = resolver.coreModulePathFrom(root, 'scripts/gds/x.js');
83
+ const frag = join('node_modules', ...pkg.split('/'));
84
+ assert.ok(p && p.includes(frag), `expected "${frag}" in ${p}`);
85
+ });
86
+ }
87
+
88
+ // 5. requireCore: loads a real core module at the repo root today; throws tagged when absent.
89
+ t('requireCore loads a real core module at the repo root today', () => {
90
+ const cli = resolver.requireCore('scripts/gds/cli-lib');
91
+ assert.equal(typeof cli.apiCall, 'function', 'expected cli-lib.apiCall');
92
+ });
93
+ t('requireCore throws CORE_MODULE_NOT_FOUND when the core module is absent', () => {
94
+ assert.throws(
95
+ () => resolver.requireCore('src/bongos/__does_not_exist_2139__'),
96
+ (e) => e && e.code === 'CORE_MODULE_NOT_FOUND',
97
+ );
98
+ });
99
+
100
+ // 6. The headline done-when: conductor.js in a core-LESS instance root does not crash
101
+ // and still emits methodology nudges (the sensitive-surface advisory degrades off).
102
+ // Runs in a child process with CLAUDE_PROJECT_DIR pointed at the core-less root so
103
+ // _core-resolve computes that root (this process's env points at the real repo).
104
+ t('conductor.js in a core-less instance root does not crash and still nudges', () => {
105
+ const root = mkdtempSync(join(tmpdir(), 'hook-coreless-'));
106
+ const hooks = join(root, '.claude', 'hooks');
107
+ mkdirSync(hooks, { recursive: true });
108
+ cpSync(join(ROOT, '.claude', 'hooks', 'conductor.js'), join(hooks, 'conductor.js'));
109
+ cpSync(join(ROOT, '.claude', 'hooks', '_core-resolve.js'), join(hooks, '_core-resolve.js'));
110
+ const snippet =
111
+ `const c = require(${JSON.stringify(join(hooks, 'conductor.js'))});` +
112
+ `const out = c.evaluateTriggers({ changedFiles: ['src/bongos/sensitive-surfaces.js'], prompt: 'ship it', activeClaims: [] });` +
113
+ `process.stdout.write(JSON.stringify(out.map((a) => a.id)));`;
114
+ const stdout = execFileSync(process.execPath, ['-e', snippet], {
115
+ encoding: 'utf8',
116
+ env: { ...process.env, CLAUDE_PROJECT_DIR: root },
117
+ });
118
+ const ids = JSON.parse(stdout);
119
+ assert.ok(ids.includes('no-claim'), 'still emits the no-claim methodology nudge without the core');
120
+ assert.ok(ids.includes('ship-intent'), 'still emits the ship-intent nudge without the core');
121
+ assert.ok(!ids.includes('sensitive-surface'), 'sensitive-surface degrades off (isSensitive fallback) when the core is absent');
122
+ });
123
+
124
+ console.log(`\n${passed} passed, ${failed} failed`);
125
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,104 @@
1
+ // tests/infra_exec_paths.mjs — task 2137 (ADR 0108 configurable-root).
2
+ //
3
+ // Two proofs that the host systemd units resolve their CORE scripts in a consumer layout after the
4
+ // R89 split:
5
+ // A. lint-infra-exec: every infra/*.service ExecStart that runs a core script goes through
6
+ // infra/core-node.sh AND names a PUBLISHABLE core file (ships in @bongos/core → present at
7
+ // node_modules/@bongos/core post-install) that also exists at the core root today. No unit
8
+ // still uses the bare repo-relative `node scripts/…` form.
9
+ // B. core-node.sh actually resolves both layouts: it prefers the instance root (monolith) and
10
+ // falls back to node_modules/@bongos/core (consumer), driven by $BONGOS_INSTANCE_ROOT.
11
+ //
12
+ // DB-free, no network. Run: node tests/infra_exec_paths.mjs
13
+
14
+ import { strict as assert } from 'node:assert';
15
+ import { execFileSync } from 'node:child_process';
16
+ import { createRequire } from 'node:module';
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const { lintInfraExec } = require('../scripts/gds/lint-infra-exec.js');
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const REPO = path.resolve(__dirname, '..');
26
+ const LAUNCHER = path.join(REPO, 'infra', 'core-node.sh');
27
+
28
+ let passed = 0, failed = 0;
29
+ function test(name, fn) {
30
+ try { fn(); console.log(` ok ${name}`); passed++; }
31
+ catch (err) { console.error(` FAIL ${name}`); console.error(' ', err.message); failed++; }
32
+ }
33
+
34
+ // ---- A. the lint is green on the real tree, and covers all 11 core-script units ----------------
35
+ test('lint-infra-exec: every core-script unit resolves in a consumer layout (publishable + present)', () => {
36
+ const r = lintInfraExec({ instanceRoot: REPO, coreRoot: REPO });
37
+ assert.deepEqual(r.violations, [], `expected no violations, got: ${JSON.stringify(r.violations)}`);
38
+ assert.ok(r.ok, 'lint should pass');
39
+ assert.equal(r.checked.length, 11, `expected all 11 core-script units repointed + resolving, got ${r.checked.length}`);
40
+ // The three core CLIs the units invoke are all covered.
41
+ const rels = new Set(r.checked.map((c) => c.rel));
42
+ for (const rel of ['scripts/gds/box.js', 'scripts/gds/provision.js', 'scripts/gds/search-ingest-db.js']) {
43
+ assert.ok(rels.has(rel), `expected a unit routing through ${rel}`);
44
+ }
45
+ });
46
+
47
+ test('lint-infra-exec: a bare repo-relative core ExecStart is flagged (counterfactual)', () => {
48
+ // Synthetic infra/ with the OLD form → must be reported as a violation.
49
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'infra-lint-'));
50
+ try {
51
+ fs.mkdirSync(path.join(tmp, 'infra'), { recursive: true });
52
+ fs.mkdirSync(path.join(tmp, 'scripts', 'gds'), { recursive: true });
53
+ fs.writeFileSync(path.join(tmp, 'scripts', 'gds', 'box.js'), '// probe\n');
54
+ fs.writeFileSync(path.join(tmp, 'infra', 'legacy.service'),
55
+ '[Service]\nExecStart=/usr/bin/node scripts/gds/box.js run-intents --apply\n');
56
+ const r = lintInfraExec({ instanceRoot: tmp, coreRoot: tmp });
57
+ assert.ok(!r.ok, 'a bare repo-relative core ExecStart must fail the lint');
58
+ assert.equal(r.violations.length, 1);
59
+ assert.match(r.violations[0].why, /core-node\.sh/);
60
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
61
+ });
62
+
63
+ // ---- B. the launcher resolves BOTH layouts ------------------------------------------------------
64
+ function runLauncher(rel, instanceRoot) {
65
+ return execFileSync('bash', [LAUNCHER, rel], {
66
+ encoding: 'utf8',
67
+ env: { ...process.env, BONGOS_INSTANCE_ROOT: instanceRoot },
68
+ }).trim();
69
+ }
70
+
71
+ test('core-node.sh resolves the monolith layout (script at the instance root)', () => {
72
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cn-mono-'));
73
+ try {
74
+ fs.mkdirSync(path.join(tmp, 'scripts', 'gds'), { recursive: true });
75
+ fs.writeFileSync(path.join(tmp, 'scripts', 'gds', 'probe.js'), 'console.log("MONOLITH");\n');
76
+ assert.equal(runLauncher('scripts/gds/probe.js', tmp), 'MONOLITH');
77
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
78
+ });
79
+
80
+ test('core-node.sh falls back to the consumer layout (script only under node_modules/@bongos/core)', () => {
81
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cn-cons-'));
82
+ try {
83
+ const pkgDir = path.join(tmp, 'node_modules', '@bongos', 'core', 'scripts', 'gds');
84
+ fs.mkdirSync(pkgDir, { recursive: true });
85
+ fs.writeFileSync(path.join(pkgDir, 'probe.js'), 'console.log("CONSUMER");\n');
86
+ // No scripts/gds/probe.js at the instance root → the resolver must reach node_modules.
87
+ assert.equal(runLauncher('scripts/gds/probe.js', tmp), 'CONSUMER');
88
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
89
+ });
90
+
91
+ test('core-node.sh exits non-zero when the script is in NO layout', () => {
92
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cn-none-'));
93
+ let threw = false;
94
+ try {
95
+ execFileSync('bash', [LAUNCHER, 'scripts/gds/nope.js'], {
96
+ encoding: 'utf8', stdio: 'pipe', env: { ...process.env, BONGOS_INSTANCE_ROOT: tmp },
97
+ });
98
+ } catch (_) { threw = true; }
99
+ finally { fs.rmSync(tmp, { recursive: true, force: true }); }
100
+ assert.ok(threw, 'expected a non-zero exit when the core script resolves in no layout');
101
+ });
102
+
103
+ console.log(`\ninfra_exec_paths: ${passed} passed, ${failed} failed`);
104
+ process.exit(failed ? 1 : 0);
package/tests/init.mjs CHANGED
@@ -358,6 +358,53 @@ test('buildInstancePackageJson: preserves existing fields + never clobbers a cus
358
358
  assert.equal(pkg.dependencies[init.CORE_PKG], 'file:vendor/bongos-core-1.17.0.tgz'); // dep (re)pinned
359
359
  });
360
360
 
361
+ // --- enabled-module dep composition (ADR 0138 / task 2160) --------------------
362
+
363
+ test('enabledModuleDeps: merges only the ENABLED modules’ declared deps', () => {
364
+ const readDeps = (k) => ({
365
+ discord: { 'discord.js': '^14.26.4' },
366
+ game: { colyseus: '^0.16.5', phaser: '^3.87.0' },
367
+ 'art-pipeline': { sharp: '^0.34.5' },
368
+ }[k] || {});
369
+ const merged = init.enabledModuleDeps(
370
+ { modules: { discord: true, game: false, 'art-pipeline': true, 'dev-box': false } },
371
+ { readDeps }
372
+ );
373
+ assert.deepEqual(merged, { 'discord.js': '^14.26.4', sharp: '^0.34.5' }); // game excluded (off)
374
+ });
375
+
376
+ test('enabledModuleDeps: no enabled modules → empty object', () => {
377
+ assert.deepEqual(init.enabledModuleDeps({ modules: { discord: false } }, { readDeps: () => ({ 'discord.js': '^14' }) }), {});
378
+ });
379
+
380
+ test('buildInstancePackageJson: composes moduleDeps (fills), but an existing pin WINS and core is always set', () => {
381
+ const existing = { dependencies: { 'discord.js': '^14.0.0', express: '^4' } }; // instance pinned discord.js older
382
+ const moduleDeps = { 'discord.js': '^14.26.4', sharp: '^0.34.5' };
383
+ const pkg = init.buildInstancePackageJson(existing, { name: 'demo', coreDep: 'file:x.tgz', moduleDeps });
384
+ assert.equal(pkg.dependencies['discord.js'], '^14.0.0'); // existing pin wins over the module range
385
+ assert.equal(pkg.dependencies.sharp, '^0.34.5'); // module dep fills what was missing
386
+ assert.equal(pkg.dependencies.express, '^4'); // untouched host dep preserved
387
+ assert.equal(pkg.dependencies[init.CORE_PKG], 'file:x.tgz'); // core always (re)set
388
+ });
389
+
390
+ test('buildInstancePackageJson: no moduleDeps → byte-identical to the pre-composition behavior', () => {
391
+ const existing = { dependencies: { express: '^4' } };
392
+ const a = init.buildInstancePackageJson(existing, { name: 'demo', coreDep: 'file:x.tgz' });
393
+ const b = init.buildInstancePackageJson(existing, { name: 'demo', coreDep: 'file:x.tgz', moduleDeps: {} });
394
+ assert.deepEqual(a.dependencies, b.dependencies);
395
+ assert.deepEqual(a.dependencies, { express: '^4', [init.CORE_PKG]: 'file:x.tgz' });
396
+ });
397
+
398
+ test('moduleDeclaredDeps: reads a module.json dependencies block; {} when absent', () => {
399
+ const readOk = (p) => JSON.stringify({ key: 'discord', dependencies: { 'discord.js': '^14.26.4' } });
400
+ assert.deepEqual(init.moduleDeclaredDeps('discord', { coreRoot: '/core', fsImpl: { readFileSync: readOk } }),
401
+ { 'discord.js': '^14.26.4' });
402
+ const readNone = () => JSON.stringify({ key: 'lifecycle', contributes: {} });
403
+ assert.deepEqual(init.moduleDeclaredDeps('lifecycle', { coreRoot: '/core', fsImpl: { readFileSync: readNone } }), {});
404
+ const readThrow = () => { throw new Error('ENOENT'); };
405
+ assert.deepEqual(init.moduleDeclaredDeps('ghost', { coreRoot: '/core', fsImpl: { readFileSync: readThrow } }), {});
406
+ });
407
+
361
408
  test('writeConfigs (greenfield): mints package.json with the caret core dep + start/migrate scripts', () => {
362
409
  const dir = mkdtempSync(join(tmpdir(), 'greenfield-'));
363
410
  try {
@@ -0,0 +1,97 @@
1
+ // tests/migrate_root_resolution.mjs
2
+ //
3
+ // R89 rehearsal F6 (task 2180): after the core→consumer split, scripts/migrate.sh
4
+ // must resolve the INSTANCE root from the INVOCATION cwd (OTB_INSTANCE_ROOT env,
5
+ // else the invocation cwd's bongos.config.json), NOT script-relative into the
6
+ // installed @bongos/core package. The old script-relative resolve `cd`'d into the
7
+ // package dir first — which has no bongos.config.json — so it silently picked the
8
+ // package as the "instance" and read the core's VANILLA module config, skipping
9
+ // every host + flag-gated module migration (rehearsal: `npm run migrate` applied
10
+ // 115 rows and created NO game schema; from the instance cwd it applied 121).
11
+ //
12
+ // This exercises migrate.sh's `--print-roots` seam (DB-free: it prints the resolved
13
+ // roots and exits before any DB work), so no Postgres is needed. CORE_ROOT must stay
14
+ // script-relative in every case; INSTANCE_ROOT must follow the invocation cwd.
15
+ //
16
+ // Run: node tests/migrate_root_resolution.mjs
17
+
18
+ import { strict as assert } from 'node:assert';
19
+ import { execFileSync } from 'node:child_process';
20
+ import { mkdtempSync, mkdirSync, writeFileSync, realpathSync } from 'node:fs';
21
+ import { tmpdir } from 'node:os';
22
+ import { fileURLToPath } from 'node:url';
23
+ import path from 'node:path';
24
+
25
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const MIGRATE = path.join(REPO_ROOT, 'scripts', 'migrate.sh');
27
+ const REPO_REAL = realpathSync(REPO_ROOT);
28
+
29
+ let passed = 0;
30
+ let failed = 0;
31
+ function t(name, fn) {
32
+ try { fn(); passed++; console.log(` PASS ${name}`); }
33
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
34
+ }
35
+
36
+ // Run `migrate.sh --print-roots` with a controlled cwd + INIT_CWD, and an env that
37
+ // carries NO *_INSTANCE_ROOT override, so we test cwd-based resolution alone.
38
+ function printRoots({ cwd, initCwd } = {}) {
39
+ const env = { ...process.env };
40
+ for (const k of Object.keys(env)) if (/_INSTANCE_ROOT$/.test(k)) delete env[k];
41
+ delete env.INIT_CWD;
42
+ if (initCwd) env.INIT_CWD = initCwd;
43
+ const out = execFileSync('bash', [MIGRATE, '--print-roots'], { cwd, env, encoding: 'utf8' });
44
+ const roots = {};
45
+ for (const line of out.split('\n')) {
46
+ const m = /^(INVOKE_CWD|CORE_ROOT|INSTANCE_ROOT)=(.*)$/.exec(line);
47
+ if (m) roots[m[1]] = m[2];
48
+ }
49
+ return roots;
50
+ }
51
+
52
+ // A synthetic consumer: a dir with a bongos.config.json pointing at itself, exactly
53
+ // as the post-split amazonprimea repo carries (bongos.config.json { instanceRoot: "." }).
54
+ function makeConsumer() {
55
+ const dir = realpathSync(mkdtempSync(path.join(tmpdir(), 'r89-consumer-')));
56
+ writeFileSync(path.join(dir, 'bongos.config.json'), JSON.stringify({ instanceRoot: '.' }));
57
+ return dir;
58
+ }
59
+ // A dir with NO bongos.config.json — stands in for the monolith / a stray cwd.
60
+ function makeBare() {
61
+ return realpathSync(mkdtempSync(path.join(tmpdir(), 'r89-bare-')));
62
+ }
63
+
64
+ // ---- the F6 regression: instance root follows the invocation cwd ----------
65
+ t('consumer run from its own dir: INSTANCE_ROOT resolves to the consumer (not the core)', () => {
66
+ const consumer = makeConsumer();
67
+ const r = printRoots({ cwd: consumer });
68
+ assert.equal(realpathSync(r.INSTANCE_ROOT), consumer, 'instance root must be the consumer dir');
69
+ assert.notEqual(realpathSync(r.INSTANCE_ROOT), REPO_REAL, 'must NOT fall back to the script-relative core root');
70
+ });
71
+
72
+ t('consumer via npm from elsewhere: INIT_CWD wins over the spawn cwd', () => {
73
+ const consumer = makeConsumer();
74
+ const elsewhere = makeBare();
75
+ // npm sets INIT_CWD to where `npm run` was invoked (the instance), even if a
76
+ // wrapper cd'd somewhere else first.
77
+ const r = printRoots({ cwd: elsewhere, initCwd: consumer });
78
+ assert.equal(realpathSync(r.INSTANCE_ROOT), consumer, 'INIT_CWD (the instance) must win');
79
+ });
80
+
81
+ // ---- the monolith / no-pointer fallback stays script-relative -------------
82
+ t('no bongos.config + no env: INSTANCE_ROOT falls back to the script-relative repo root', () => {
83
+ const bare = makeBare();
84
+ const r = printRoots({ cwd: bare });
85
+ assert.equal(realpathSync(r.INSTANCE_ROOT), REPO_REAL, 'a stray cwd must NOT masquerade as the instance');
86
+ });
87
+
88
+ // ---- CORE root is always script-relative, regardless of cwd ---------------
89
+ t('CORE_ROOT is always the script-relative repo root, independent of the invocation cwd', () => {
90
+ const consumer = makeConsumer();
91
+ const bare = makeBare();
92
+ assert.equal(realpathSync(printRoots({ cwd: consumer }).CORE_ROOT), REPO_REAL);
93
+ assert.equal(realpathSync(printRoots({ cwd: bare }).CORE_ROOT), REPO_REAL);
94
+ });
95
+
96
+ console.log(`\nmigrate_root_resolution: ${passed} passed, ${failed} failed`);
97
+ process.exit(failed ? 1 : 0);
@@ -22,6 +22,20 @@ test('migrationNumbersFromPaths picks CORE migrations only (not module/ADR numbe
22
22
  assert.deepEqual([...set].sort((a, b) => a - b), [3, 44, 60]);
23
23
  });
24
24
 
25
+ test('migrationNumbersFromPaths excludes migrations/instance/ seeds (task 2152)', () => {
26
+ // migrations/instance/*.sql are this instance's host seeds (ADR 0105), excluded from
27
+ // the publishable mirror (task 2140). Counting them as core put every instance seed in
28
+ // the PRIVATE set but not the MIRROR set → a false private⇄mirror divergence that
29
+ // red-lined the unit gate. Only the bare top-level core migration counts.
30
+ const set = migrationNumbersFromPaths([
31
+ 'migrations/080_core_thing.sql', // bare core → counted
32
+ 'migrations/instance/080_gds_v4_planning_seed.sql', // instance seed → NOT core
33
+ 'migrations/instance/186_governance_seed.sql', // instance seed → NOT core
34
+ 'migrations/core_192_namespaced.sql', // core_ prefix ≠ bare NNN_ form → not counted either
35
+ ]);
36
+ assert.deepEqual([...set].sort((a, b) => a - b), [80]);
37
+ });
38
+
25
39
  test('parseEnumChecks extracts the quoted value-lists from CHECK (... IN (...))', () => {
26
40
  const sql = `
27
41
  CREATE TABLE tasks (
@@ -84,6 +84,48 @@ test('discoverModules: a missing modules/ root is empty, not an error', () => {
84
84
  assert.deepEqual(r, { loaded: [], skipped: [], errors: [] });
85
85
  });
86
86
 
87
+ // --- task 2133 (R89-prereq): TWO-ROOT discovery. DEFAULT_ROOTS spans the core
88
+ // package's modules/ AND the consumer/instance's modules/, deduped by
89
+ // resolved path so a single checkout (roots coincide) never double-scans. ---
90
+
91
+ test('discoverModules: UNIONs modules across two diverged roots (the post-split shape)', () => {
92
+ const core = fixtureRoot({ 'core-mod': { manifest: validManifest('core-mod') } });
93
+ const instance = fixtureRoot({ 'inst-mod': { manifest: validManifest('inst-mod') } });
94
+ const { loaded, errors, skipped } = loader.discoverModules({ roots: [core, instance], core: '1.0.0' });
95
+ assert.deepEqual(loaded.map((m) => m.key).sort(), ['core-mod', 'inst-mod'], 'both roots contribute');
96
+ assert.deepEqual(errors, []);
97
+ assert.deepEqual(skipped, []);
98
+ });
99
+
100
+ test('discoverModules: scanning the SAME root twice makes every key a duplicate-key ERROR (why dedupeRoots exists)', () => {
101
+ const root = fixtureRoot({ 'only-mod': { manifest: validManifest('only-mod') } });
102
+ const { loaded, errors } = loader.discoverModules({ roots: [root, root], core: '1.0.0' });
103
+ assert.equal(loaded.length, 1, 'the first scan loads it');
104
+ assert.equal(errors.length, 1, 'the second scan of the identical root is a duplicate-key error');
105
+ assert.match(errors[0].errors[0], /duplicate module key/);
106
+ });
107
+
108
+ test('dedupeRoots: collapses roots that resolve to the same path, preserving order', () => {
109
+ assert.deepEqual(
110
+ loader.dedupeRoots(['/a/modules', '/a/modules', '/b/modules']),
111
+ ['/a/modules', '/b/modules'],
112
+ );
113
+ assert.deepEqual(loader.dedupeRoots(['/a/modules', '/a/./modules']), ['/a/modules'], 'normalizes before comparing');
114
+ assert.equal(loader.dedupeRoots(['/a/modules', '/b/modules']).length, 2, 'a genuinely diverged pair is kept');
115
+ });
116
+
117
+ test('DEFAULT_ROOTS is deduped → the single checkout scans no root twice (zero dup-key errors)', () => {
118
+ // In a single checkout resolveInstanceRoot() falls back to cwd === resolveCoreRoot(),
119
+ // so the two candidate roots are identical and MUST collapse to one (a diverged
120
+ // consumer repo keeps both). The invariant: no resolved path appears twice.
121
+ const resolved = loader.DEFAULT_ROOTS.map((r) => path.resolve(r));
122
+ assert.equal(new Set(resolved).size, resolved.length, 'DEFAULT_ROOTS has no duplicate resolved paths');
123
+ loader._reset();
124
+ const snap = loader.loaderSnapshot();
125
+ assert.deepEqual(snap.errors, [], 'default-root discovery yields zero errors (no duplicate-key)');
126
+ loader._reset();
127
+ });
128
+
87
129
  test('discoveredRegistry: shapes a loaded manifest like a MODULE_REGISTRY entry', () => {
88
130
  loader._reset();
89
131
  const root = fixtureRoot({ econ: { manifest: validManifest('econ', { default: true, provides: ['reward'], contributes: { disciplines: ['x'] } }) } });
@@ -192,3 +192,22 @@ test('an empty-string attribution field is rejected', () => {
192
192
  assert.equal(r.valid, false);
193
193
  assert.ok(r.errors.some((e) => e.startsWith('author')));
194
194
  });
195
+
196
+ // --- optional runtime dependencies (ADR 0138 / task 2160) ---
197
+
198
+ test('dependencies: a valid { name: range } object passes', () => {
199
+ const r = validateManifest({ ...baseManifest(), dependencies: { 'discord.js': '^14.26.4' } });
200
+ assert.equal(r.valid, true, r.errors.join('; '));
201
+ });
202
+
203
+ test('dependencies: omitted is fine (optional)', () => {
204
+ assert.equal(validateManifest(baseManifest()).valid, true);
205
+ });
206
+
207
+ test('dependencies: a non-object, or a non-string range, is rejected', () => {
208
+ for (const bad of [['discord.js'], 'discord.js@14', { 'discord.js': 14 }, { '': '^1' }]) {
209
+ const r = validateManifest({ ...baseManifest(), dependencies: bad });
210
+ assert.equal(r.valid, false, `should reject ${JSON.stringify(bad)}`);
211
+ assert.ok(r.errors.some((e) => e.startsWith('dependencies')), `error should mention dependencies: ${r.errors}`);
212
+ }
213
+ });
package/tests/onboard.mjs CHANGED
@@ -117,3 +117,16 @@ test('the web forms (/create + hall wizard) declare EXACTLY the canonical human
117
117
  assert.deepEqual(uniq, humanKeys, `${name} (${rel}) must tag exactly the canonical human steps [${humanKeys}], found [${uniq}]`);
118
118
  }
119
119
  });
120
+
121
+ test('the one-click GitHub-App manifest path surfaces the CLI-only Enable-Device-Flow step (task 2129)', () => {
122
+ // GitHub defaults Device Flow OFF and the manifest can't enable it, but `bongos login` needs
123
+ // it — so BOTH manifest surfaces must tell the owner to enable it after the one-click approval.
124
+ // Assert on a phrase DISTINCT to the one-click caveat: the manual OAuth path already mentions
125
+ // Device Flow (task 2127), so a bare "Device Flow" match wouldn't prove the manifest path carries it.
126
+ const { readFileSync } = require('node:fs');
127
+ const path = require('node:path');
128
+ const onboardSrc = readFileSync(path.join(import.meta.dirname, '..', 'scripts/gds/onboard.js'), 'utf8');
129
+ const wizardSrc = readFileSync(path.join(import.meta.dirname, '..', 'modules/hall-ui/public/new-project-wizard.js'), 'utf8');
130
+ assert.match(onboardSrc, /cannot enable Device Flow via the manifest/i, 'bongos onboard one-click caveat missing');
131
+ assert.match(wizardSrc, /Terminal CLI only: after approving, enable/i, 'hall wizard one-click caveat missing');
132
+ });
@@ -366,6 +366,14 @@ t('buildPackageJson: prunes to express+pg, keeps the source ranges + the undici
366
366
  assert.equal(pkg.exports['.'], './src/platform-server.js');
367
367
  assert.equal(pkg.exports['./serve-internal'], './src/bongos/serve-internal.js');
368
368
  assert.equal(pkg.exports['./module-api'], './src/module-api.js');
369
+ // rehearsal F3 / task 2161 — the game-host kit: the core internals a game-bearing
370
+ // instance's own entrypoint (server.js) wires. The cutover codemod rewrites server.js's
371
+ // relative ./src/* requires to exactly these subpaths, so they MUST be exported.
372
+ assert.equal(pkg.exports['./bongos/api-path-404'], './src/bongos/api-path-404.js');
373
+ assert.equal(pkg.exports['./bongos/uptime-poller'], './src/bongos/uptime-poller.js');
374
+ assert.equal(pkg.exports['./module-loader/loader'], './src/module-loader/loader.js');
375
+ assert.equal(pkg.exports['./modules'], './src/modules.js');
376
+ assert.equal(pkg.exports['./module-seams'], './src/module-seams.js');
369
377
  // task 2089 / ADR 0134 — the synthesized package publishes PRIVATE by default.
370
378
  assert.deepEqual(pkg.publishConfig, { access: 'restricted' });
371
379
  });
@@ -71,6 +71,21 @@ console.log('POST /provisioning/instances/:id/github-app (mint the one-click lin
71
71
  t('the attempt row was persisted with the minted state', !!(stored && stored.instanceId === 42 && stored.state === j.state && stored.requestedBy === 7));
72
72
  }
73
73
 
74
+ console.log('\nprovisioningOrigin override (task 2101 — customer App carries the PLATFORM brand, not the game domain):');
75
+ {
76
+ // A control plane that co-hosts a game (OTB = customer 0) has a GAME-branded publicOrigin.
77
+ // provisioningOrigin is the dedicated front door the customer's GitHub App must point at,
78
+ // so mothershipOrigin() must prefer it over publicOrigin. mothershipOrigin() calls
79
+ // api.branding() (module-api captured the fn ref at require-time), so override THAT.
80
+ const prevBranding = api.branding;
81
+ api.branding = () => ({ ...realBranding(), identity: { productName: 'Cloud Bongos' }, domains: { publicOrigin: 'https://amazonprimea.com', provisioningOrigin: 'https://get.cloudbongos.com' } });
82
+ const res = await fetch(`${base}/provisioning/instances/42/github-app`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
83
+ const j = await res.json();
84
+ t('redirect_url uses provisioningOrigin, not the game publicOrigin', j.manifest && j.manifest.redirect_url === 'https://get.cloudbongos.com/api/bongos/provisioning/github-app/callback', j.manifest && j.manifest.redirect_url);
85
+ t('webhook hook_attributes.url also uses provisioningOrigin', j.manifest && j.manifest.hook_attributes && j.manifest.hook_attributes.url === 'https://get.cloudbongos.com/api/bongos/provisioning/github-app/webhook', j.manifest && j.manifest.hook_attributes && j.manifest.hook_attributes.url);
86
+ api.branding = prevBranding;
87
+ }
88
+
74
89
  console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect):');
75
90
  {
76
91
  const res = await fetch(`${base}/provisioning/github-app/callback?code=GHCODE123&state=GOODSTATE`);
@@ -44,13 +44,13 @@ const PUBLISH = [
44
44
  'scripts/gds/ship.js', // the ship/grade pipeline publishes (sans secrets)
45
45
  'scripts/gds/publish-manifest.js',
46
46
  'scripts/migrate.sh',
47
- 'server.js',
48
47
  'migrations/182_some_change.sql',
49
48
  'tests/publish_manifest.mjs',
50
49
  'modules/lifecycle/db.js',
51
50
  'modules/grading/grader.js',
52
51
  'modules/hall-ui/public/work.js',
53
52
  'modules/dev-box/boxes.js',
53
+ 'modules/provisioning/provisioning.js', // instance-hosting control plane — CORE like its sibling dev-box (task 2135)
54
54
  // served-at-runtime surfaces a standalone must carry (task 2078) — their absence
55
55
  // from the packaged core 404'd the hall UI + /docs on demo.cloudbongos.com
56
56
  'clients/bongos-client/bongos-client.global.js', // /lib/bongos-client.js (window.BongosClient)
@@ -91,8 +91,19 @@ const EXCLUDE = [
91
91
  'src/world/terrain.js',
92
92
  'src/rooms/WorldRoom.js',
93
93
  'src/preview-server.js',
94
+ 'server.js', // repo-root game+platform entry (top-level require('colyseus')); the core boots via src/platform-server.js (task 2134)
94
95
  'scripts/art/orchestrate.js', // art-feature tooling under scripts/ (legacy location)
96
+ 'scripts/gds/art-stage.js', // art sandbox-staging CLI (requires modules/art-pipeline); excluded with the art feature (task 2135)
95
97
  'scripts/tile-add.js',
98
+ // host-feature tests that statically import an excluded module's source: shipped in the
99
+ // core package they'd MODULE_NOT_FOUND; excluded with the feature they cover (task 2147)
100
+ 'tests/walkability.mjs',
101
+ 'tests/terrain_picker.mjs',
102
+ 'tests/snapshot_picker.mjs',
103
+ 'tests/sprite_facing.mjs',
104
+ 'tests/turn_in_place.mjs',
105
+ 'tests/day_cycle.mjs',
106
+ 'tests/art_key.mjs',
96
107
  // instance identity + history
97
108
  'config/branding.json',
98
109
  'config/modules.json',
@@ -106,6 +117,7 @@ const EXCLUDE = [
106
117
  'blockers/blockers.md',
107
118
  'limitations/v1-shipped.md',
108
119
  'generative-ideas.md',
120
+ 'migrations/instance/018_pms_v3_planning_seed.sql', // instance seed migrations = host scope-truth (ADR 0105, task 2140)
109
121
  '.env.local',
110
122
  // topology / deploy / CI / local gate hooks / secrets — read-sensitive docs
111
123
  'infra/Caddyfile',
@@ -180,5 +192,44 @@ t('allowlist + denylist are non-empty arrays of strings', () => {
180
192
  for (const g of [...PUBLISH_ALLOWLIST, ...PUBLISH_DENYLIST]) assert.equal(typeof g, 'string');
181
193
  });
182
194
 
195
+ // ── R89 split guard: no PUBLISHABLE test may statically import an EXCLUDED module ─────
196
+ // tests/ is allowlisted, but a test that imports host-module source (game / art-pipeline)
197
+ // would ship inside the @bongos/core package WITHOUT that module → MODULE_NOT_FOUND when a
198
+ // core CI/consumer runs it, and the cutover would delete OTB's own host tests. Every such
199
+ // test must be host-excluded (see CONTENT_EXCLUDE). This DERIVES the set from disk so a NEW
200
+ // host-module test can't slip in unlisted (the enumerated assertions above would miss it).
201
+ // Static relative-literal imports only — temp-dir fixtures (modules/alpha, …) use path.join,
202
+ // so they never match; a module name appearing only as string DATA (this file's own arrays)
203
+ // isn't in a require()/import statement, so it never matches either. (task 2147)
204
+ t('no publishable test statically imports an excluded module (R89 boundary)', () => {
205
+ const fs = require('node:fs');
206
+ const path = require('node:path');
207
+ const { fileURLToPath } = require('node:url');
208
+ const testsDir = path.dirname(fileURLToPath(import.meta.url));
209
+ const IMPORT_RE = /(?:require|import)\s*\(\s*["'](\.\.?\/[^"']*?)modules\/([a-z0-9][a-z0-9-]*)\/[^"']*["']|from\s+["'](\.\.?\/[^"']*?)modules\/([a-z0-9][a-z0-9-]*)\/[^"']*["']/g;
210
+ const detected = []; // test files that import ANY excluded module (non-vacuous check)
211
+ const offenders = []; // …and are still publishable (the actual failure)
212
+ for (const name of fs.readdirSync(testsDir)) {
213
+ if (!/\.(mjs|js|cjs)$/.test(name)) continue;
214
+ const rel = `tests/${name}`;
215
+ const bad = new Set();
216
+ let m; IMPORT_RE.lastIndex = 0;
217
+ const src = fs.readFileSync(path.join(testsDir, name), 'utf8');
218
+ while ((m = IMPORT_RE.exec(src))) {
219
+ const key = m[2] || m[4];
220
+ if (key && !isPublishable(`modules/${key}/module.json`)) bad.add(key);
221
+ }
222
+ if (bad.size === 0) continue;
223
+ detected.push(rel);
224
+ if (isPublishable(rel)) {
225
+ offenders.push(`${rel} imports excluded module(s) [${[...bad].sort().join(', ')}] but is still PUBLISHABLE — add it to CONTENT_EXCLUDE (task 2147)`);
226
+ }
227
+ }
228
+ // non-vacuous: the scan must actually SEE the known host tests (a broken regex that
229
+ // matched nothing would otherwise pass silently). The 7 game/art tests are the floor.
230
+ assert.ok(detected.length >= 7, `expected ≥7 host-module tests detected, saw ${detected.length} — the import scan may be broken`);
231
+ assert.equal(offenders.length, 0, '\n ' + offenders.join('\n '));
232
+ });
233
+
183
234
  console.log(`\n${passed} passed, ${failed} failed`);
184
235
  process.exit(failed ? 1 : 0);