@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
package/server.js DELETED
@@ -1,212 +0,0 @@
1
- const http = require('http');
2
- const path = require('path');
3
- const express = require('express');
4
- const { Server, matchMaker } = require('colyseus');
5
- const { WebSocketTransport } = require('@colyseus/ws-transport');
6
- const { isApiShapedPath, apiNotFoundBody } = require('./src/bongos/api-path-404');
7
- const uptimePoller = require('./src/bongos/uptime-poller');
8
- const moduleLoader = require('./src/module-loader/loader');
9
- const { mountInternalSurfaces, isStatusHost } = require('./src/bongos/serve-internal');
10
- const { isModuleEnabled } = require('./src/modules');
11
- const seams = require('./src/module-seams');
12
-
13
- const PORT = Number(process.env.PORT) || 3000;
14
- const HOST = process.env.HOST || '127.0.0.1';
15
-
16
- const app = express();
17
- app.disable('x-powered-by');
18
- // Behind Cloudflare → Caddy → Node. Trust the immediate proxy so
19
- // req.secure / x-forwarded-proto reflect reality for OAuth callbacks.
20
- app.set('trust proxy', 1);
21
-
22
- // All non-game surfaces — /healthz, /version, /api/gds, the Dev Box download
23
- // redirects, and the status + builders hosts — are served by the shared internal
24
- // surface module (ADR 0062 R51, task 1190). This also calls mountModuleRoutes,
25
- // which runs each enabled module's route factory — including the game module's,
26
- // which registers the game.staticRoot / game.registerRooms / game.precreateWorldRoom
27
- // seam ports before the code below resolves them (ADR 0083 / BV1.R49).
28
- // Mounted before the game tail below so /builders/* + the status host
29
- // short-circuit ahead of the game static middleware.
30
- mountInternalSurfaces(app);
31
-
32
- app.use((req, res, next) => {
33
- // From here down: only the game host. Skip if request was for the status
34
- // host (it should already be handled, but defense in depth).
35
- if (isStatusHost(req)) return res.status(404).type('text/plain').send('not found');
36
- next();
37
- });
38
-
39
- app.use(
40
- '/vendor/phaser.min.js',
41
- express.static(path.join(__dirname, 'node_modules/phaser/dist/phaser.min.js'), {
42
- maxAge: '1d',
43
- })
44
- );
45
-
46
- app.use(
47
- '/vendor/colyseus.js',
48
- express.static(path.join(__dirname, 'node_modules/colyseus.js/dist/colyseus.js'), {
49
- maxAge: '1d',
50
- })
51
- );
52
-
53
- // Game static files — served from the game module's public/ dir (modules/game/public/).
54
- // The path is provided by the game module via the game.staticRoot seam port, registered
55
- // by modules/game/routes/game.js when the loader runs mountModuleRoutes above.
56
- // If the game module is disabled (vanilla Cloud Bongos boot) these blocks are simply
57
- // skipped — no game assets served, no 404.html, no client JS. The platform API still
58
- // works via mountInternalSurfaces above.
59
- const _gameStaticRoot = seams.resolveOptional('game.staticRoot');
60
- if (_gameStaticRoot) {
61
- const gamePublic = _gameStaticRoot();
62
-
63
- // Tile-asset cache strategy (task #55 + #160 + #176).
64
- //
65
- // Three buckets:
66
- // 1. Content-hashed atlas (atlas-<sha8>.png + atlas-<sha8>.json if any)
67
- // Filename changes with content → safe to cache forever. Marked
68
- // immutable so browsers / CF skip revalidation entirely.
69
- // 2. Stable-URL files paired with bucket 1 (atlas-index.json, manifest.json,
70
- // atlas.json). URLs don't change across deploys but contents do, so
71
- // they get a short TTL — a deploy is visible within ~1 minute. atlas.json
72
- // lives here because tilePalette.js still hardcodes its URL; until the
73
- // build hashes it (idea_inbox follow-up), the short TTL is what prevents
74
- // the post-deploy "new PNG + stale frame coords" mismatch window.
75
- // 3. Legacy unhashed per-tile PNGs + everything else under /assets/tiles
76
- // Short TTL (5 min) until they migrate to content-hashed names. The
77
- // old 1-day TTL is what caused the 2026-05-10 stale-atlas incident.
78
- app.use(
79
- '/assets/tiles',
80
- express.static(path.join(gamePublic, 'assets/tiles'), {
81
- setHeaders: (res, filePath) => {
82
- const name = path.basename(filePath);
83
- if (/^atlas-[a-f0-9]{8,}\.(png|json)$/.test(name)) {
84
- res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
85
- } else if (
86
- name === 'atlas-index.json' ||
87
- name === 'manifest.json' ||
88
- name === 'atlas.json'
89
- ) {
90
- res.setHeader('Cache-Control', 'public, max-age=60');
91
- } else {
92
- res.setHeader('Cache-Control', 'public, max-age=300');
93
- }
94
- },
95
- })
96
- );
97
-
98
- // HTML + game JS — short TTL so deploys are visible quickly. .js files in
99
- // game/ are not content-hashed today (V1 shortcut), so a long TTL would
100
- // compound the stale-art class of bugs into stale-code bugs.
101
- app.use(
102
- express.static(gamePublic, {
103
- extensions: ['html'],
104
- setHeaders: (res, filePath) => {
105
- if (/\.(html|js|json)$/.test(filePath)) {
106
- res.setHeader('Cache-Control', 'public, max-age=60');
107
- } else {
108
- res.setHeader('Cache-Control', 'public, max-age=300');
109
- }
110
- },
111
- })
112
- );
113
- }
114
-
115
- // Themed 404 for the game host. Anything that didn't resolve to an API route,
116
- // the status/builders hosts, or a static file lands here.
117
- // The status host has its own plain-text 404 above; we don't poach it.
118
- // API-SHAPED paths get a JSON 404 with a corrective hint — never the HTML island
119
- // page — so a caller that missed the /api/gds mount (curl /tasks, /gds/*, an
120
- // /api/* typo) gets a self-describing error instead of HTML that JSON.parse-faults
121
- // on the client (V4.R14 / #952). The match is broader than the old `/api/` test:
122
- // it also catches the bare GDS resource roots a human hits without the prefix.
123
- app.use((req, res) => {
124
- if (isStatusHost(req)) return res.status(404).type('text/plain').send('not found');
125
- res.status(404);
126
- if (isApiShapedPath(req.path)) {
127
- return res.json(apiNotFoundBody(req.path));
128
- }
129
- if (req.method === 'GET' && req.accepts('html')) {
130
- res.setHeader('Cache-Control', 'public, max-age=60');
131
- const gameStaticRoot = seams.resolveOptional('game.staticRoot');
132
- if (gameStaticRoot) {
133
- return res.sendFile(path.join(gameStaticRoot(), '404.html'));
134
- }
135
- return res.type('text/plain').send('404 — not found');
136
- }
137
- return res.type('text/plain').send('404 — you have washed ashore on the wrong island');
138
- });
139
-
140
- const httpServer = http.createServer(app);
141
-
142
- const gameServer = new Server({
143
- transport: new WebSocketTransport({ server: httpServer }),
144
- });
145
-
146
- // The PRODUCT/game rooms (world + queue) — registered via the game module's seam
147
- // port (game.registerRooms), wired by modules/game/routes/game.js at loader mount
148
- // time. Only called when the game module is enabled (OTB does; vanilla skips).
149
- // The game-less platform entrypoint (src/platform-server.js) never constructs a
150
- // Colyseus server at all. The builders' hall live channel is NOT a Colyseus room —
151
- // it rides the SSE endpoint (src/bongos/routes/live.js, ADR 0069).
152
- const _registerRooms = seams.resolveOptional('game.registerRooms');
153
- if (_registerRooms) {
154
- _registerRooms(gameServer);
155
- } else {
156
- console.log('[game] game module disabled — no Colyseus rooms registered');
157
- }
158
-
159
- uptimePoller.start();
160
-
161
- // task 1134 — the server-owned publish reconciler (drives 'confirmed' tasks whose
162
- // branch has merged + deployed to 'shipped' on a timer, ADR 0055). CARVED into the
163
- // lifecycle module at BV1.R86 (ADR 0093 §1): it now starts via the loader's
164
- // startModulePollers (a declared poller — modules/lifecycle/pollers/publish-
165
- // reconciler.js), in the listen callback below alongside the Discord bot + routine
166
- // timers — so server.js no longer reaches into the lifecycle module to start it.
167
- // No-op without a push credential (local dev/tests); kill-switch via
168
- // PUBLISH_RECONCILER_DISABLED. Unref'd timers, best-effort — never takes the
169
- // game/API down.
170
-
171
- // task 1382 (ADR 0078 amendment) — server-internal routine timers (peer-vote-tally,
172
- // weekly, in-process; no model, no external cron). CARVED into the autonomy module
173
- // at BV1.R83 (ADR 0093 §1): it now starts via the loader's startModulePollers (a
174
- // declared poller — modules/autonomy/pollers/routine-timers.js), in the listen
175
- // callback below alongside the Discord bot poller. server.js is still the only
176
- // startModulePollers caller (not the game-less platform entrypoint), so a second
177
- // always-on instance can't double-run. Kill-switch ROUTINE_TIMERS_DISABLED stays.
178
-
179
- httpServer.listen(PORT, HOST, async () => {
180
- console.log(`Amazonprimea listening on ${HOST}:${PORT}`);
181
- // Pre-create the singleton WorldRoom so clients can use
182
- // `client.join('world')` (existing-only, no auto-create on overflow). The
183
- // precreate function is provided by the game module via the seam port
184
- // game.precreateWorldRoom; idempotent + failure-tolerant.
185
- const _precreate = seams.resolveOptional('game.precreateWorldRoom');
186
- if (_precreate) {
187
- await _precreate(matchMaker);
188
- }
189
-
190
- // Start each ENABLED module's web-process background pollers through the loader
191
- // — module-agnostic: it requires modules/<key>/pollers/<name>.js and fires
192
- // start(). The Discord bot (ADR 0033) is one such poller; a vanilla instance
193
- // with discord off simply has no poller to start, and never reads a webhook/bot
194
- // token. Non-blocking + failure-isolated by the loader (a poller problem must
195
- // never take the game/API down) — the module-agnostic successor to the
196
- // hardcoded discordBot.start() block (ADR 0083 / BV1.R47, task 1416).
197
- moduleLoader.startModulePollers({}, { isEnabled: isModuleEnabled });
198
- });
199
-
200
- // Graceful shutdown — stop the in-process pollers, then stop each module poller
201
- // (e.g. close the Discord gateway socket cleanly on a systemd restart / Ctrl-C so
202
- // we don't leave a zombie session for Discord to reap). stopModulePollers awaits
203
- // each stop and isolates errors, so a stuck poller never blocks the exit (BV1.R47).
204
- for (const sig of ['SIGTERM', 'SIGINT']) {
205
- process.on(sig, () => {
206
- // The publish reconciler + routine timers now stop via
207
- // moduleLoader.stopModulePollers() — the lifecycle module's publish-reconciler
208
- // poller (carved BV1.R86) + the autonomy module's routine-timers poller
209
- // (carved BV1.R83 / ADR 0093 §1).
210
- moduleLoader.stopModulePollers().finally(() => process.exit(0));
211
- });
212
- }
package/tests/art_key.mjs DELETED
@@ -1,121 +0,0 @@
1
- // tests/art_key.mjs
2
- //
3
- // Unit smoke for modules/art-pipeline/art-key.js (task 1281, moved BV1.R48) — the GDS-delivered SHARED Gemini
4
- // key for newcomers. Covers the pure entitlement decision (xenos in; thetes+ out;
5
- // unknown rank fails closed), the env/file key loader, and the describeArtKey
6
- // payload shape. The route (routes/me.js) + the local sync (fetch-art-key.js) and
7
- // the pipeline tier (gen_api.py) are exercised by the endpoint smoke + manual
8
- // resolution check; here we cover the logic without pg or network.
9
- //
10
- // Hermetic: the "not configured" cases unset SHARED_ART_GEMINI_KEY and repoint
11
- // HOME at an empty temp dir so the ~/.config/otb/gemini-shared.json fallback can't
12
- // resolve to a real operator file on the test machine.
13
- //
14
- // Run: node tests/art_key.mjs
15
-
16
- import { strict as assert } from 'node:assert';
17
- import { createRequire } from 'node:module';
18
- import { mkdtempSync } from 'node:fs';
19
- import { tmpdir } from 'node:os';
20
- import { join } from 'node:path';
21
-
22
- const require = createRequire(import.meta.url);
23
- const a = require('../modules/art-pipeline/art-key.js');
24
- const db = require('../modules/lifecycle/db.js');
25
-
26
- let passed = 0;
27
- let failed = 0;
28
- function t(name, fn) {
29
- try { fn(); passed++; console.log(` PASS ${name}`); }
30
- catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
31
- }
32
-
33
- // Run a thunk with SHARED_ART_GEMINI_KEY + HOME forced to known values, restoring
34
- // both afterward so cases don't leak into each other.
35
- function withEnv({ key, emptyHome }, fn) {
36
- const origKey = process.env.SHARED_ART_GEMINI_KEY;
37
- const origHome = process.env.HOME;
38
- if (key === null) delete process.env.SHARED_ART_GEMINI_KEY;
39
- else if (key !== undefined) process.env.SHARED_ART_GEMINI_KEY = key;
40
- if (emptyHome) process.env.HOME = mkdtempSync(join(tmpdir(), 'otb-artkey-'));
41
- try { fn(); }
42
- finally {
43
- if (origKey === undefined) delete process.env.SHARED_ART_GEMINI_KEY;
44
- else process.env.SHARED_ART_GEMINI_KEY = origKey;
45
- process.env.HOME = origHome;
46
- }
47
- }
48
-
49
- console.log('entitlement boundary:');
50
-
51
- t('SHARED_ART_KEY_MAX_TIER is the xenos tier (1)', () => {
52
- assert.equal(a.SHARED_ART_KEY_MAX_TIER, db.REQUIRES_RANK_TIER.xenos);
53
- assert.equal(a.SHARED_ART_KEY_MAX_TIER, 1);
54
- });
55
-
56
- t('xenos is entitled (reason newcomer)', () => {
57
- assert.deepEqual(a.entitlementFor('xenos'), { entitled: true, reason: 'newcomer' });
58
- });
59
-
60
- t('thetes / metic / archon are NOT entitled (reason rank_above_newcomer)', () => {
61
- for (const rank of ['thetes', 'metic', 'archon']) {
62
- assert.deepEqual(a.entitlementFor(rank), { entitled: false, reason: 'rank_above_newcomer' }, rank);
63
- }
64
- });
65
-
66
- t('unknown / absent rank fails closed (reason unknown_rank)', () => {
67
- for (const rank of [undefined, null, '', 'wizard', 'helot']) {
68
- assert.deepEqual(a.entitlementFor(rank), { entitled: false, reason: 'unknown_rank' }, String(rank));
69
- }
70
- });
71
-
72
- console.log('\nkey loader (env wins; trimmed):');
73
-
74
- t('loadSharedGeminiKey returns the env key, trimmed', () => {
75
- withEnv({ key: ' AIza-test-123 ' }, () => {
76
- assert.equal(a.loadSharedGeminiKey(), 'AIza-test-123');
77
- assert.equal(a.sharedGeminiKeyConfigured(), true);
78
- });
79
- });
80
-
81
- t('not configured when env unset and no operator file (empty HOME)', () => {
82
- withEnv({ key: null, emptyHome: true }, () => {
83
- assert.equal(a.loadSharedGeminiKey(), null);
84
- assert.equal(a.sharedGeminiKeyConfigured(), false);
85
- });
86
- });
87
-
88
- console.log('\ndescribeArtKey (the GET /me/art-key payload, sans secret):');
89
-
90
- t('xenos + configured: entitled, configured, shared_key placeholder is null', () => {
91
- withEnv({ key: 'AIza-shared' }, () => {
92
- const d = a.describeArtKey({ rank: 'xenos' });
93
- assert.equal(d.entitled, true);
94
- assert.equal(d.configured, true);
95
- assert.equal(d.reason, 'newcomer');
96
- assert.equal(d.rank, 'xenos');
97
- // The pure describe never embeds the secret — the route attaches it.
98
- assert.equal(d.shared_key, null);
99
- });
100
- });
101
-
102
- t('thetes: not entitled even when a key is configured (the withdraw signal)', () => {
103
- withEnv({ key: 'AIza-shared' }, () => {
104
- const d = a.describeArtKey({ rank: 'thetes' });
105
- assert.equal(d.entitled, false);
106
- assert.equal(d.reason, 'rank_above_newcomer');
107
- assert.equal(d.configured, true);
108
- assert.equal(d.shared_key, null);
109
- });
110
- });
111
-
112
- t('xenos but unconfigured: entitled yet configured=false (sync no-ops)', () => {
113
- withEnv({ key: null, emptyHome: true }, () => {
114
- const d = a.describeArtKey({ rank: 'xenos' });
115
- assert.equal(d.entitled, true);
116
- assert.equal(d.configured, false);
117
- });
118
- });
119
-
120
- console.log(`\n${passed} passed, ${failed} failed`);
121
- process.exit(failed === 0 ? 0 : 1);
@@ -1,197 +0,0 @@
1
- // tests/day_cycle.mjs
2
- //
3
- // The pure math behind the #639 time-of-day lighting. No Phaser, no DOM — the
4
- // fx/lighting.js glue is a thin shell over these functions, so getting the
5
- // math right here is most of the feature.
6
- //
7
- // Run: node tests/day_cycle.mjs
8
-
9
- import { strict as assert } from 'node:assert';
10
- import {
11
- DEFAULT_PERIOD_MS,
12
- KEYFRAMES,
13
- SKY_PHASES,
14
- wrap01,
15
- phaseFromClock,
16
- lerpColor,
17
- bracket,
18
- tintAt,
19
- parseSkyOverride,
20
- } from '../modules/game/public/game/fx/dayCycle.js';
21
-
22
- let passed = 0;
23
- function test(name, fn) {
24
- try {
25
- fn();
26
- console.log(` ok ${name}`);
27
- passed++;
28
- } catch (e) {
29
- console.error(` FAIL ${name}: ${e.message}`);
30
- process.exitCode = 1;
31
- }
32
- }
33
-
34
- const chan = (c) => [(c >> 16) & 0xff, (c >> 8) & 0xff, c & 0xff];
35
-
36
- // ---------- wrap01 -------------------------------------------------------
37
-
38
- test('wrap01 maps any real into [0,1)', () => {
39
- assert.equal(wrap01(0), 0);
40
- assert.equal(wrap01(0.5), 0.5);
41
- assert.equal(wrap01(1), 0);
42
- assert.ok(Math.abs(wrap01(1.25) - 0.25) < 1e-9);
43
- assert.ok(Math.abs(wrap01(-0.25) - 0.75) < 1e-9);
44
- assert.ok(Math.abs(wrap01(-2.1) - 0.9) < 1e-9);
45
- });
46
-
47
- // ---------- phaseFromClock ----------------------------------------------
48
-
49
- test('phaseFromClock is 0 at the epoch and 0.5 at half a period', () => {
50
- assert.equal(phaseFromClock(0), 0);
51
- assert.equal(phaseFromClock(DEFAULT_PERIOD_MS / 2), 0.5);
52
- });
53
-
54
- test('phaseFromClock wraps each period', () => {
55
- assert.equal(phaseFromClock(DEFAULT_PERIOD_MS), 0);
56
- assert.ok(Math.abs(phaseFromClock(DEFAULT_PERIOD_MS * 1.25) - 0.25) < 1e-9);
57
- });
58
-
59
- test('phaseFromClock is deterministic — shared across clients by construction', () => {
60
- const now = 1_700_000_123_456;
61
- assert.equal(phaseFromClock(now), phaseFromClock(now));
62
- // Two clients a few seconds apart see ~the same phase (period is minutes).
63
- const skew = phaseFromClock(now + 3000) - phaseFromClock(now);
64
- assert.ok(Math.abs(skew) < 0.01, `3s skew tiny: ${skew}`);
65
- });
66
-
67
- test('phaseFromClock guards a non-positive period', () => {
68
- assert.equal(phaseFromClock(1234, 0), 0);
69
- assert.equal(phaseFromClock(1234, -5), 0);
70
- });
71
-
72
- // ---------- lerpColor ----------------------------------------------------
73
-
74
- test('lerpColor returns the endpoints at f=0 and f=1', () => {
75
- assert.equal(lerpColor(0x000000, 0xffffff, 0), 0x000000);
76
- assert.equal(lerpColor(0x000000, 0xffffff, 1), 0xffffff);
77
- });
78
-
79
- test('lerpColor blends channel-by-channel at the midpoint', () => {
80
- // (0,128,255) is the per-channel midpoint of black and 0x00ffff... use a
81
- // clean case: midpoint of 0x000000 and 0x804020 is (64,32,16).
82
- assert.deepEqual(chan(lerpColor(0x000000, 0x804020, 0.5)), [64, 32, 16]);
83
- });
84
-
85
- test('lerpColor clamps f outside [0,1]', () => {
86
- assert.equal(lerpColor(0x102030, 0xa0b0c0, -1), 0x102030);
87
- assert.equal(lerpColor(0x102030, 0xa0b0c0, 2), 0xa0b0c0);
88
- });
89
-
90
- // ---------- bracket / tintAt --------------------------------------------
91
-
92
- test('tintAt hits each keyframe exactly at its phase', () => {
93
- for (const kf of KEYFRAMES) {
94
- const { color, alpha } = tintAt(kf.t);
95
- assert.equal(color, kf.color, `${kf.name}@${kf.t} color`);
96
- assert.ok(Math.abs(alpha - kf.alpha) < 1e-9, `${kf.name}@${kf.t} alpha`);
97
- }
98
- });
99
-
100
- test('high noon is fully transparent (base art at full strength)', () => {
101
- assert.equal(tintAt(SKY_PHASES.midday).alpha, 0);
102
- // The whole noon plateau holds at alpha 0.
103
- assert.equal(tintAt(0.45).alpha, 0);
104
- assert.equal(tintAt(0.50).alpha, 0);
105
- });
106
-
107
- test('golden hour is the warm hero state', () => {
108
- const t = tintAt(SKY_PHASES.golden);
109
- const [r, g, b] = chan(t.color);
110
- assert.ok(r > g && g > b, `amber: r>g>b got ${r},${g},${b}`);
111
- assert.ok(t.alpha > 0.3, `visible: ${t.alpha}`);
112
- assert.equal(t.name, 'golden');
113
- });
114
-
115
- test('night is a dark blue-dominant tint', () => {
116
- const [r, g, b] = chan(tintAt(0).color);
117
- assert.ok(b > r && b > g, `blue-dominant: ${r},${g},${b}`);
118
- assert.ok(tintAt(0).alpha > 0.5, 'strong');
119
- });
120
-
121
- test('the wrap segment interpolates nightfall → deep night across midnight', () => {
122
- // phase 0.94 sits between the 0.88 nightfall keyframe and the 0.00 deep-night
123
- // keyframe (read circularly). Alpha must land strictly between the two.
124
- const { alpha } = tintAt(0.94);
125
- assert.ok(alpha > 0.64 && alpha < 0.72, `between nightfall and deep night: ${alpha}`);
126
- });
127
-
128
- test('bracket picks the wrap segment for late-night phases', () => {
129
- const b = bracket(0.95);
130
- assert.equal(b.from.t, 0.88);
131
- assert.equal(b.to.t, 0.00);
132
- assert.ok(b.f > 0 && b.f < 1);
133
- });
134
-
135
- test('tintAt is continuous and total over the whole cycle (no gaps/throws)', () => {
136
- let prev = tintAt(0);
137
- for (let i = 1; i <= 1000; i++) {
138
- const p = i / 1000;
139
- const cur = tintAt(p);
140
- assert.ok(cur.alpha >= 0 && cur.alpha <= 1, `alpha in range @${p}`);
141
- // No discontinuous jump bigger than the largest keyframe-to-keyframe step.
142
- assert.ok(Math.abs(cur.alpha - prev.alpha) < 0.05, `smooth @${p}`);
143
- prev = cur;
144
- }
145
- });
146
-
147
- test('tintAt wraps: phase 1.0 equals phase 0.0', () => {
148
- assert.deepEqual(tintAt(1.0), tintAt(0.0));
149
- assert.deepEqual(tintAt(1.5), tintAt(0.5));
150
- });
151
-
152
- // ---------- parseSkyOverride --------------------------------------------
153
-
154
- test('parseSkyOverride: absent / blank => null (run the live clock)', () => {
155
- assert.equal(parseSkyOverride(null), null);
156
- assert.equal(parseSkyOverride(undefined), null);
157
- assert.equal(parseSkyOverride(''), null);
158
- assert.equal(parseSkyOverride(' '), null);
159
- });
160
-
161
- test('parseSkyOverride: mood names (case-insensitive) => their phase', () => {
162
- assert.equal(parseSkyOverride('golden'), SKY_PHASES.golden);
163
- assert.equal(parseSkyOverride('NIGHT'), SKY_PHASES.night);
164
- assert.equal(parseSkyOverride('Dusk'), SKY_PHASES.dusk);
165
- });
166
-
167
- test('parseSkyOverride: raw fractions, wrapped into [0,1)', () => {
168
- assert.equal(parseSkyOverride('0.68'), 0.68);
169
- assert.equal(parseSkyOverride('0'), 0);
170
- assert.ok(Math.abs(parseSkyOverride('1.25') - 0.25) < 1e-9);
171
- });
172
-
173
- test('parseSkyOverride: garbage => null', () => {
174
- assert.equal(parseSkyOverride('banana'), null);
175
- assert.equal(parseSkyOverride('NaN'), null);
176
- });
177
-
178
- // ---------- data sanity --------------------------------------------------
179
-
180
- test('KEYFRAMES are sorted, valid colours, alphas in [0,1]', () => {
181
- for (let i = 0; i < KEYFRAMES.length; i++) {
182
- const kf = KEYFRAMES[i];
183
- assert.ok(kf.t >= 0 && kf.t < 1, `t in [0,1): ${kf.t}`);
184
- assert.ok(kf.alpha >= 0 && kf.alpha <= 1, `alpha in [0,1]: ${kf.alpha}`);
185
- assert.ok(Number.isInteger(kf.color) && kf.color >= 0 && kf.color <= 0xffffff, 'color 24-bit');
186
- if (i > 0) assert.ok(kf.t > KEYFRAMES[i - 1].t, `strictly ascending @${i}`);
187
- }
188
- });
189
-
190
- test('SKY_PHASES all resolve into [0,1) and round-trip through tintAt', () => {
191
- for (const [name, p] of Object.entries(SKY_PHASES)) {
192
- assert.ok(p >= 0 && p < 1, `${name} phase in range`);
193
- assert.doesNotThrow(() => tintAt(p), `${name} tints`);
194
- }
195
- });
196
-
197
- console.log(`\nday_cycle: ${passed} passed`);