@bongos/core 1.18.3 → 1.18.19

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 (471) hide show
  1. package/.bongos-core.json +1576 -286
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +4 -2
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +6 -1
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  27. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  28. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  29. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  30. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  31. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  32. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  33. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  34. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  35. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  36. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  37. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  38. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  39. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  40. package/docs/adr/README.md +10 -0
  41. package/docs/api/openapi.json +843 -17
  42. package/docs/api-reference.md +23 -10
  43. package/docs/architecture.md +4 -2
  44. package/docs/branding-contract.md +3 -1
  45. package/docs/canonical-permissions.md +21 -8
  46. package/docs/file-map.md +8 -0
  47. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  48. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  49. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  50. package/docs/onboarding/diagrams/assertions.json +4 -7
  51. package/docs/onboarding/drachmae-and-karma.md +3 -3
  52. package/docs/onboarding/primer.md +17 -16
  53. package/docs/onboarding/slash-commands.md +124 -0
  54. package/docs/recipes/builder-box-lifecycle.md +23 -0
  55. package/docs/recipes/devbox-app-release.md +14 -4
  56. package/docs/recipes/instance-first-run.md +16 -0
  57. package/docs/recipes/local-dev.md +81 -0
  58. package/docs/recipes/ops-gotchas.md +19 -1
  59. package/docs/recipes/overnight-builder.md +1 -1
  60. package/docs/recipes/parallel-goal-waves.md +174 -0
  61. package/docs/recipes/ship-and-regrade.md +2 -2
  62. package/docs/recipes/standalone-live-docs.md +48 -0
  63. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  64. package/migrations/003_pms.sql +6 -7
  65. package/migrations/006_pms_v2.sql +3 -31
  66. package/migrations/core_193_task_needs_rebase.sql +28 -0
  67. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  68. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  69. package/migrations/core_196_session_reward_topup.sql +69 -0
  70. package/modules/autonomy/pollers/routine-timers.js +9 -8
  71. package/modules/builder-settings/wandering-prefs.js +61 -18
  72. package/modules/dev-box/app/README.md +73 -0
  73. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  74. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  75. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  76. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  77. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  78. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  79. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  80. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  81. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  82. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  83. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  84. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  85. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  86. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  87. package/modules/dev-box/app/build-resources/icon.png +0 -0
  88. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  89. package/modules/dev-box/app/electron-builder.yml +95 -0
  90. package/modules/dev-box/app/package-lock.json +5291 -0
  91. package/modules/dev-box/app/package.json +30 -0
  92. package/modules/dev-box/app/renderer/app.js +319 -0
  93. package/modules/dev-box/app/renderer/index.html +45 -0
  94. package/modules/dev-box/app/renderer/styles.css +246 -0
  95. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  96. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  97. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  98. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  99. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  100. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  101. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  102. package/modules/dev-box/app/src/box-controller.js +287 -0
  103. package/modules/dev-box/app/src/brand.generated.json +13 -0
  104. package/modules/dev-box/app/src/brand.js +16 -0
  105. package/modules/dev-box/app/src/config.js +19 -0
  106. package/modules/dev-box/app/src/gds-client.js +90 -0
  107. package/modules/dev-box/app/src/main.js +610 -0
  108. package/modules/dev-box/app/src/pairing.js +57 -0
  109. package/modules/dev-box/app/src/preload.js +29 -0
  110. package/modules/dev-box/app/src/session-store.js +58 -0
  111. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  112. package/modules/dev-box/app/src/task-picker.js +70 -0
  113. package/modules/dev-box/app/src/tray.js +99 -0
  114. package/modules/dev-box/app/src/updater.js +55 -0
  115. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  116. package/modules/dev-box/app/test/unit.mjs +526 -0
  117. package/modules/dev-box/box-access.js +121 -4
  118. package/modules/dev-box/boxes.js +20 -0
  119. package/modules/dev-box/module.json +1 -1
  120. package/modules/dev-box/routes/box.js +87 -3
  121. package/modules/economy/credits.js +116 -25
  122. package/modules/economy/reward.js +8 -0
  123. package/modules/economy/streak.js +2 -1
  124. package/modules/governance/CLAUDE.md +15 -0
  125. package/modules/governance/catalog.js +419 -0
  126. package/modules/governance/config.js +174 -0
  127. package/modules/governance/db.js +125 -0
  128. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  129. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  130. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  131. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  132. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  133. package/modules/governance/module.json +10 -0
  134. package/modules/governance/protected-surfaces.js +88 -0
  135. package/modules/governance/protected-surfaces.json +80 -0
  136. package/modules/governance/rank-roles.js +83 -0
  137. package/modules/governance/resolver.js +221 -0
  138. package/modules/governance/routes/governance.js +89 -0
  139. package/modules/governance/routes/role-sync.js +49 -0
  140. package/modules/governance/session-scopes.js +127 -0
  141. package/modules/governance/session-scopes.json +38 -0
  142. package/modules/hall-ui/public/atlas.css +117 -0
  143. package/modules/hall-ui/public/atlas.html +3 -114
  144. package/modules/hall-ui/public/brand-apply.js +34 -6
  145. package/modules/hall-ui/public/builders.js +187 -219
  146. package/modules/hall-ui/public/diagrams.html +2 -0
  147. package/modules/hall-ui/public/dom-utils.js +27 -0
  148. package/modules/hall-ui/public/drachmae.html +2 -0
  149. package/modules/hall-ui/public/drachmae.js +54 -1
  150. package/modules/hall-ui/public/gate.css +98 -0
  151. package/modules/hall-ui/public/gate.html +3 -95
  152. package/modules/hall-ui/public/goal-inbox.js +4 -1
  153. package/modules/hall-ui/public/goals-page.js +626 -114
  154. package/modules/hall-ui/public/goals.css +79 -1
  155. package/modules/hall-ui/public/goals.html +38 -11
  156. package/modules/hall-ui/public/hall-widgets.js +62 -52
  157. package/modules/hall-ui/public/harbor.css +123 -0
  158. package/modules/hall-ui/public/harbor.html +3 -120
  159. package/modules/hall-ui/public/index.html +11 -4
  160. package/modules/hall-ui/public/instances-panel.js +4 -1
  161. package/modules/hall-ui/public/modules.css +63 -0
  162. package/modules/hall-ui/public/modules.html +3 -60
  163. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  164. package/modules/hall-ui/public/not-ready.html +2 -0
  165. package/modules/hall-ui/public/pair.css +73 -0
  166. package/modules/hall-ui/public/pair.html +3 -70
  167. package/modules/hall-ui/public/palette.js +302 -0
  168. package/modules/hall-ui/public/primer.css +13 -0
  169. package/modules/hall-ui/public/primer.html +2 -0
  170. package/modules/hall-ui/public/profile.html +2 -0
  171. package/modules/hall-ui/public/ranks.html +2 -0
  172. package/modules/hall-ui/public/roadmap.css +179 -0
  173. package/modules/hall-ui/public/roadmap.html +9 -172
  174. package/modules/hall-ui/public/roadmap.js +329 -316
  175. package/modules/hall-ui/public/sessions.css +16 -0
  176. package/modules/hall-ui/public/sessions.html +3 -13
  177. package/modules/hall-ui/public/settings.html +2 -0
  178. package/modules/hall-ui/public/settings.js +4 -4
  179. package/modules/hall-ui/public/shell.js +143 -18
  180. package/modules/hall-ui/public/style.css +231 -39
  181. package/modules/hall-ui/public/task-detail.js +347 -26
  182. package/modules/hall-ui/public/task.html +6 -1
  183. package/modules/hall-ui/public/watch.html +2 -0
  184. package/modules/hall-ui/public/work.html +8 -6
  185. package/modules/hall-ui/public/work.js +1 -1
  186. package/modules/ideas/inbox.js +61 -11
  187. package/modules/ideas/routes/inbox.js +23 -0
  188. package/modules/lifecycle/claim-eligibility.js +72 -0
  189. package/modules/lifecycle/db.js +519 -139
  190. package/modules/lifecycle/done-when.js +18 -3
  191. package/modules/lifecycle/github-push.js +19 -2
  192. package/modules/lifecycle/goal-advisory.js +114 -0
  193. package/modules/lifecycle/goal-authz.js +62 -0
  194. package/modules/lifecycle/lifecycle.js +9 -0
  195. package/modules/lifecycle/publish-reconciler.js +109 -6
  196. package/modules/lifecycle/routes/claims.js +68 -23
  197. package/modules/lifecycle/routes/done-when.js +110 -15
  198. package/modules/lifecycle/routes/goals.js +8 -38
  199. package/modules/lifecycle/routes/tasks.js +38 -6
  200. package/modules/lifecycle/ship-card.js +6 -0
  201. package/modules/onboarding/onboarding-state.js +1 -1
  202. package/modules/platform-identity/CLAUDE.md +15 -0
  203. package/modules/platform-identity/hub-device.js +147 -0
  204. package/modules/platform-identity/hub-keys.js +151 -0
  205. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  206. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  207. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  208. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  209. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  210. package/modules/platform-identity/module.json +18 -0
  211. package/modules/platform-identity/platform-identity.js +531 -0
  212. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  213. package/modules/platform-identity/routes/my-projects.js +140 -0
  214. package/modules/platform-identity/routes/projects.js +107 -0
  215. package/modules/platform-identity/routes/scouting.js +35 -0
  216. package/modules/platform-identity/routes/sso.js +402 -0
  217. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  218. package/modules/platform-identity/ui/.gitkeep +0 -0
  219. package/modules/security/routes/reports.js +10 -0
  220. package/modules/sessions/db.js +166 -21
  221. package/modules/sessions/module.json +1 -1
  222. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  223. package/modules/sessions/routes/sessions.js +33 -0
  224. package/package-lock.json +2 -2
  225. package/package.json +1 -1
  226. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  227. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  228. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  229. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  230. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  231. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  232. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  233. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  234. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  235. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  236. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  237. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  238. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  239. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  270. package/public-landing/create.html +51 -58
  271. package/public-landing/gate/README.md +137 -0
  272. package/public-landing/gate/dispmaps/bar.png +0 -0
  273. package/public-landing/gate/dispmaps/flat.png +0 -0
  274. package/public-landing/gate/dispmaps/key.png +0 -0
  275. package/public-landing/gate/index.html +587 -0
  276. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  277. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  278. package/public-landing/gate/space.css +799 -0
  279. package/public-landing/gate/space.js +1826 -0
  280. package/public-landing/gate/sprites/enter.png +0 -0
  281. package/public-landing/gate/sprites/search.png +0 -0
  282. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  283. package/public-landing/gate/sprites/tile.png +0 -0
  284. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  285. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  286. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  287. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  288. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  289. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  290. package/scripts/gds/abandon.js +6 -4
  291. package/scripts/gds/api.js +10 -5
  292. package/scripts/gds/api.ps1 +26 -0
  293. package/scripts/gds/api.sh +6 -0
  294. package/scripts/gds/audit-ships.js +7 -1
  295. package/scripts/gds/backfill-sessions.js +78 -18
  296. package/scripts/gds/backup.js +23 -1
  297. package/scripts/gds/box-connect-lib.js +124 -31
  298. package/scripts/gds/box-sync.js +66 -0
  299. package/scripts/gds/box.js +225 -53
  300. package/scripts/gds/cf-tunnel.js +145 -6
  301. package/scripts/gds/claim.js +34 -3
  302. package/scripts/gds/cli-lib.js +161 -27
  303. package/scripts/gds/code.js +6 -2
  304. package/scripts/gds/deploy-divergence.js +209 -0
  305. package/scripts/gds/dev-lib.js +309 -0
  306. package/scripts/gds/dev.js +322 -0
  307. package/scripts/gds/doctor.js +90 -61
  308. package/scripts/gds/fetch-art-key.js +1 -1
  309. package/scripts/gds/fetch-sound-prefs.js +2 -2
  310. package/scripts/gds/fitness.js +228 -12
  311. package/scripts/gds/gds-db-backup.service +16 -1
  312. package/scripts/gds/gds-db-offsite.service +9 -1
  313. package/scripts/gds/gen-api-client.js +1 -1
  314. package/scripts/gds/gen-api-docs.js +36 -14
  315. package/scripts/gds/gen-diagrams.js +45 -28
  316. package/scripts/gds/gen-file-map.js +2 -1
  317. package/scripts/gds/gen-freshness.js +32 -0
  318. package/scripts/gds/gen-repo-map.js +3 -2
  319. package/scripts/gds/gen-session-index.js +14 -5
  320. package/scripts/gds/hooks/pre-push.js +2 -2
  321. package/scripts/gds/init.js +60 -3
  322. package/scripts/gds/install-git-hooks.js +1 -1
  323. package/scripts/gds/key-card.js +15 -6
  324. package/scripts/gds/key-status.js +1 -1
  325. package/scripts/gds/linkify-refs.js +35 -9
  326. package/scripts/gds/login.js +16 -1
  327. package/scripts/gds/paste-token.js +7 -1
  328. package/scripts/gds/preflight.js +126 -0
  329. package/scripts/gds/provision.js +123 -12
  330. package/scripts/gds/publish-manifest.js +7 -1
  331. package/scripts/gds/reauth.js +7 -3
  332. package/scripts/gds/regen-instance-docs.js +47 -0
  333. package/scripts/gds/run-unit-tests.js +4 -2
  334. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  335. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  336. package/scripts/gds/sequence.js +425 -0
  337. package/scripts/gds/session-cost.js +10 -1
  338. package/scripts/gds/setup.js +31 -5
  339. package/scripts/gds/shell.js +5 -3
  340. package/scripts/gds/ship.js +319 -29
  341. package/scripts/gds/smoke-gds.js +139 -8
  342. package/scripts/gds/start.js +171 -22
  343. package/scripts/gds/status.js +1 -1
  344. package/scripts/gds/triage.js +10 -9
  345. package/scripts/gds/upgrade.js +124 -2
  346. package/scripts/migrate.sh +99 -14
  347. package/scripts/status-mirror-sync.js +114 -84
  348. package/src/bongos/api-errors.js +1 -0
  349. package/src/bongos/auth.js +447 -102
  350. package/src/bongos/db-kernel.js +27 -0
  351. package/src/bongos/db.js +39 -0
  352. package/src/bongos/module-scope-map.js +27 -1
  353. package/src/bongos/permission-path-check.js +136 -102
  354. package/src/bongos/repo-info.js +47 -11
  355. package/src/bongos/route-rank-check.js +429 -46
  356. package/src/bongos/routes/_helpers.js +12 -1
  357. package/src/bongos/routes/auth.js +219 -2
  358. package/src/bongos/routes/backup.js +58 -2
  359. package/src/bongos/routes/me.js +35 -1
  360. package/src/bongos/serve-internal.js +84 -15
  361. package/src/branding.js +32 -0
  362. package/src/instance-config.js +30 -0
  363. package/src/module-api.js +124 -1
  364. package/src/platform-server.js +86 -9
  365. package/tests/abandon_task.mjs +59 -3
  366. package/tests/api_cli_flags.mjs +53 -0
  367. package/tests/auth_page_gate.mjs +13 -0
  368. package/tests/backfill_sessions.mjs +127 -1
  369. package/tests/backup_cli.mjs +54 -0
  370. package/tests/bongos_box_connect.mjs +77 -0
  371. package/tests/box_access.mjs +130 -1
  372. package/tests/box_credential.mjs +125 -0
  373. package/tests/box_host_keys.mjs +24 -0
  374. package/tests/box_infra_resolve.mjs +149 -0
  375. package/tests/box_scope_session.mjs +6 -2
  376. package/tests/box_sync.mjs +55 -0
  377. package/tests/box_task_scope.mjs +99 -0
  378. package/tests/branding.mjs +12 -0
  379. package/tests/cf_ruleset_preflight.mjs +142 -0
  380. package/tests/cf_tunnel.mjs +37 -0
  381. package/tests/claim_gate_rebase.mjs +98 -0
  382. package/tests/clawback_confirmed_backlog.mjs +103 -0
  383. package/tests/cli_lib_formatters.mjs +20 -0
  384. package/tests/confirm_attribution.mjs +30 -46
  385. package/tests/credit_grant.mjs +198 -10
  386. package/tests/criterion_authoring_gate.mjs +272 -0
  387. package/tests/criterion_progress.mjs +33 -1
  388. package/tests/dev_lib.mjs +253 -0
  389. package/tests/devbox_artifact_gate.mjs +75 -0
  390. package/tests/devbox_dmg_backdrop.mjs +137 -0
  391. package/tests/devbox_icons.mjs +304 -0
  392. package/tests/devbox_installer_brand.mjs +232 -0
  393. package/tests/devbox_key_register.mjs +234 -0
  394. package/tests/docs_openapi_route.mjs +110 -0
  395. package/tests/doctor_hooks.mjs +141 -0
  396. package/tests/extract_json.mjs +140 -0
  397. package/tests/fitness.mjs +89 -0
  398. package/tests/gen_freshness.mjs +96 -0
  399. package/tests/goal_advisory.mjs +251 -0
  400. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  401. package/tests/goal_routes.mjs +59 -4
  402. package/tests/goal_tier.mjs +12 -3
  403. package/tests/governance_catalog.mjs +129 -0
  404. package/tests/governance_claim_eligibility.mjs +262 -0
  405. package/tests/governance_config.mjs +187 -0
  406. package/tests/governance_ownership_scope.mjs +410 -0
  407. package/tests/governance_page_gates.mjs +324 -0
  408. package/tests/governance_parity.mjs +336 -0
  409. package/tests/governance_principals.mjs +232 -0
  410. package/tests/governance_protected_surfaces.mjs +208 -0
  411. package/tests/governance_require_permission.mjs +223 -0
  412. package/tests/governance_resolver.mjs +84 -0
  413. package/tests/governance_role_sync.mjs +231 -0
  414. package/tests/governance_seed.mjs +127 -0
  415. package/tests/governance_session_scope.mjs +434 -0
  416. package/tests/governance_vanilla_boot.mjs +203 -0
  417. package/tests/grade_attribution.mjs +7 -7
  418. package/tests/hall_audit.mjs +223 -0
  419. package/tests/hall_nav.mjs +191 -0
  420. package/tests/hall_palette.mjs +206 -0
  421. package/tests/hall_signout.mjs +124 -0
  422. package/tests/hook_user_agents.mjs +10 -0
  423. package/tests/idea_triage.mjs +162 -0
  424. package/tests/idp_device_exchange.mjs +93 -0
  425. package/tests/init.mjs +51 -0
  426. package/tests/instance_config.mjs +65 -0
  427. package/tests/instance_mismatch_guard.mjs +23 -6
  428. package/tests/instance_release_config.mjs +104 -0
  429. package/tests/linkify_refs.mjs +15 -2
  430. package/tests/migrate_root_resolution.mjs +124 -1
  431. package/tests/module_contributions.mjs +55 -0
  432. package/tests/module_loader.mjs +10 -5
  433. package/tests/module_scope_active_claims.mjs +94 -0
  434. package/tests/needs_rebase.mjs +92 -0
  435. package/tests/one_active_claim_per_session.mjs +76 -0
  436. package/tests/override_attribution.mjs +10 -8
  437. package/tests/override_request_ownership.mjs +8 -4
  438. package/tests/parse_cookie_decode.mjs +42 -0
  439. package/tests/pay_on_land.mjs +129 -0
  440. package/tests/platform_boot.mjs +100 -1
  441. package/tests/post_login_landing.mjs +42 -0
  442. package/tests/preflight_checks.mjs +72 -0
  443. package/tests/provision.mjs +132 -7
  444. package/tests/publish_branch_route.mjs +53 -0
  445. package/tests/publish_reconciler.mjs +187 -2
  446. package/tests/repo_info_binding.mjs +248 -0
  447. package/tests/reward_gate.mjs +11 -2
  448. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  449. package/tests/reward_mode_surfacing.mjs +168 -0
  450. package/tests/route_permission_check.mjs +301 -0
  451. package/tests/route_rank_check.mjs +254 -12
  452. package/tests/route_validation.mjs +30 -0
  453. package/tests/sequence.mjs +267 -0
  454. package/tests/session_digest.mjs +44 -0
  455. package/tests/session_index.mjs +18 -6
  456. package/tests/session_records.mjs +39 -1
  457. package/tests/session_reward_on_land.mjs +381 -0
  458. package/tests/session_reward_reconcile.mjs +229 -0
  459. package/tests/ship_api_client_pathspec.mjs +100 -0
  460. package/tests/ship_base_flag.mjs +93 -0
  461. package/tests/ship_ci_deploy.mjs +80 -3
  462. package/tests/ship_upstream_deletion.mjs +57 -0
  463. package/tests/smoke_target.mjs +233 -0
  464. package/tests/start_env_guard.mjs +87 -0
  465. package/tests/start_rebase_warning.mjs +103 -0
  466. package/tests/status_mirror_sync.mjs +122 -0
  467. package/tests/stop_refcheck.mjs +19 -1
  468. package/tests/task_goal_link.mjs +15 -6
  469. package/tests/upgrade.mjs +139 -0
  470. package/tests/web_admission_status.mjs +162 -0
  471. package/tests/write_validation_gaps.mjs +110 -0
@@ -194,6 +194,115 @@ function regenerateApiArtifacts({ instanceDir, coreRoot }, run = spawnSync, fsIm
194
194
  return { ok: true, ran };
195
195
  }
196
196
 
197
+ // The instance's GITIGNORED WHOLE-FILE navigation docs (ADR 0110). On the monorepo these
198
+ // regenerate at deploy (deploy-prod.sh) + box-fetch and are gitignored so no branch ever diffs
199
+ // them. STANDALONE instances (ADR 0108) never got that step: their bespoke deploy regenerated
200
+ // nothing and init/upgrade only did the API docs, so a fresh instance shipped with an empty docs/
201
+ // and nothing kept the index live (task 1002314, the owner's "docs didn't transfer from primea").
202
+ // This is the standalone sibling of deploy-prod.sh's regen: run the generator(s) against the
203
+ // INSTANCE root, best-effort + LOUD — a broken generator must surface (::ERROR::) but must NEVER
204
+ // fail the deploy/bump (a stale nav doc is tolerable; a failed deploy is not — ADR 0110 §2
205
+ // "accepted staleness").
206
+ //
207
+ // SCOPE — deliberately just session-index (the one gitignored whole-file doc that is INSTANCE
208
+ // content). Excluded on purpose:
209
+ // • repo-map.md — CORE-scoped: gen-repo-map writes to resolveCoreRoot() (the platform code map),
210
+ // not the instance. A pure standalone instance runs core from node_modules and has ~no code of
211
+ // its own to map, so an instance-level repo-map is empty/meaningless; the core ships its own.
212
+ // (An instance-scoped map for BROWNFIELD/adopted repos is a separate enhancement — task 1001655.)
213
+ // • file-map.md + API docs — TRACKED/committed (seeded at init, refreshed at ship/upgrade);
214
+ // regenerating them on the box would perpetually dirty the tree that deploy.sh `git reset
215
+ // --hard`es. file-map is also HYBRID (hand-written prose + generated sections, ADR 0110) so it
216
+ // cannot leave git; seeding it at standup is a follow-up (needs a scaffolded file-map.md).
217
+ // • diagrams + atlas.json — NOT instance content: they are CORE-scoped generated assets that
218
+ // live under resolveCoreRoot(). They are handled separately by healDocAssets() below, because
219
+ // an upgrade is precisely what destroys them (task 1002441).
220
+ const NAV_WHOLE_FILE_GENERATORS = ['gen-session-index.js'];
221
+ function regenerateNavDocs({ instanceDir, coreRoot, generators = NAV_WHOLE_FILE_GENERATORS }, run = spawnSync, fsImpl = fs) {
222
+ const env = { ...process.env, CLOUDBONGOS_INSTANCE_ROOT: instanceDir };
223
+ const ran = [];
224
+ const failed = [];
225
+ for (const gen of generators) {
226
+ const script = path.join(coreRoot, 'scripts', 'gds', gen);
227
+ if (!fsImpl.existsSync(script)) { failed.push({ gen, reason: `not found in core (${coreRoot})` }); continue; }
228
+ const r = run(process.execPath, [script], { cwd: instanceDir, env, stdio: 'inherit' });
229
+ if (!r || r.error || r.status !== 0) {
230
+ // LOUD per ADR 0110 §2 — journald greps ::ERROR:: so a broken generator alarms.
231
+ failed.push({ gen, reason: r && r.error ? r.error.message : `exited ${r && r.status}` });
232
+ continue;
233
+ }
234
+ ran.push(gen);
235
+ }
236
+ return { ok: failed.length === 0, ran, failed };
237
+ }
238
+
239
+ // The CORE-scoped generated doc assets that back two builders'-hall surfaces:
240
+ //
241
+ // modules/hall-ui/public/atlas.json → the Repo Atlas (/builders/atlas)
242
+ // docs/onboarding/diagrams/<n>.svg+.png → the system maps (/builders/diagrams)
243
+ //
244
+ // Both are GITIGNORED (.gitignore: `modules/hall-ui/public/atlas.json`,
245
+ // `docs/onboarding/diagrams/*.svg|*.png` — ADR 0122 / task 1002027) and both are generated INTO
246
+ // the vendored core root. So an upgrade DESTROYS them: npmInstall replaces node_modules wholesale
247
+ // and the new tarball carries only the .mmd sources. Nothing else puts them back —
248
+ // • packaging does not render (package-core.js has no render step, and the renders are
249
+ // gitignored so a git checkout has none to copy), and
250
+ // • a standalone instance's pull-deploy typically short-circuits on an unchanged git SHA, which
251
+ // an upgrade does not move — so its regen hooks never fire.
252
+ // Result before this healer: /builders/atlas 404'd and the diagrams page showed broken images
253
+ // after every `bongos upgrade`, until some unrelated commit happened to land (task 1002441).
254
+ //
255
+ // Both generators resolve their own output dir from __dirname, so running the CORE's copy always
256
+ // writes into coreRoot regardless of cwd. Idempotent by design — each asset is regenerated only
257
+ // when actually absent, so this is also safe to call from a deploy loop.
258
+ //
259
+ // Best-effort + LOUD, per ADR 0110 §2 "accepted staleness": a stale diagram is tolerable, a failed
260
+ // upgrade is not. render-diagrams.sh additionally needs a system Chromium (ADR 0122); where none is
261
+ // installed it fails and we say so via ::ERROR:: rather than aborting the bump.
262
+ const DOC_ASSET_GENERATORS = {
263
+ atlas: { rel: path.join('scripts', 'gds', 'gen-atlas.js'), kind: 'node' },
264
+ diagrams: { rel: path.join('scripts', 'render-diagrams.sh'), kind: 'bash' },
265
+ };
266
+
267
+ // Which of the two asset groups are missing under coreRoot. Exported for testing.
268
+ function missingDocAssets(coreRoot, fsImpl = fs) {
269
+ const missing = [];
270
+ if (!fsImpl.existsSync(path.join(coreRoot, 'modules', 'hall-ui', 'public', 'atlas.json'))) {
271
+ missing.push('atlas');
272
+ }
273
+ const diagramDir = path.join(coreRoot, 'docs', 'onboarding', 'diagrams');
274
+ let sources = [];
275
+ try {
276
+ sources = fsImpl.readdirSync(diagramDir).filter((f) => f.endsWith('.mmd'));
277
+ } catch { sources = []; } // no diagram dir in this core → nothing to render
278
+ const unrendered = sources.some((src) => {
279
+ const base = path.join(diagramDir, src.replace(/\.mmd$/, ''));
280
+ return !fsImpl.existsSync(`${base}.svg`) || !fsImpl.existsSync(`${base}.png`);
281
+ });
282
+ if (unrendered) missing.push('diagrams');
283
+ return missing;
284
+ }
285
+
286
+ function healDocAssets({ coreRoot }, run = spawnSync, fsImpl = fs) {
287
+ const missing = missingDocAssets(coreRoot, fsImpl);
288
+ if (missing.length === 0) return { ok: true, ran: [], failed: [], missing: [] };
289
+ const ran = [];
290
+ const failed = [];
291
+ for (const group of missing) {
292
+ const gen = DOC_ASSET_GENERATORS[group];
293
+ const script = path.join(coreRoot, gen.rel);
294
+ if (!fsImpl.existsSync(script)) { failed.push({ group, reason: `not found in core (${gen.rel})` }); continue; }
295
+ const argv = gen.kind === 'bash' ? ['bash', [script]] : [process.execPath, [script]];
296
+ const r = run(argv[0], argv[1], { cwd: coreRoot, stdio: 'inherit' });
297
+ if (!r || r.error || r.status !== 0) {
298
+ failed.push({ group, reason: r && r.error ? r.error.message : `exited ${r && r.status}` });
299
+ continue;
300
+ }
301
+ ran.push(group);
302
+ }
303
+ return { ok: failed.length === 0, ran, failed, missing };
304
+ }
305
+
197
306
  function restartService({ service, restartCmd }, run = spawnSync) {
198
307
  if (restartCmd) {
199
308
  const parts = restartCmd.split(/\s+/);
@@ -587,7 +696,20 @@ async function runUpgrade(opts, deps = {}) {
587
696
  const regen = (deps.regenerateApiArtifacts || regenerateApiArtifacts)({ instanceDir, coreRoot }, run);
588
697
  if (regen.ok) log(` ✓ regenerated API docs + typed client from core ${targetVersion}`);
589
698
  else err(` ! API-artifact regen incomplete (${regen.reason}) — ran ${regen.ran.join(', ') || 'nothing'}; the next ship's fitness gate refreshes them.`);
590
- } else log(' • API-artifact regen skipped (--skip-regen-docs)');
699
+ // 5c. regenerate the whole-file navigation docs (repo-map + session-index) from the new
700
+ // core against the instance root (ADR 0110 / task 1002314). Best-effort + LOUD: a broken
701
+ // generator alarms via ::ERROR:: but never fails the bump.
702
+ const nav = (deps.regenerateNavDocs || regenerateNavDocs)({ instanceDir, coreRoot }, run);
703
+ if (nav.ok) log(` ✓ regenerated navigation docs (${nav.ran.join(', ')}) from core ${targetVersion}`);
704
+ else err(` ! ::ERROR:: navigation-doc regen incomplete — ran ${nav.ran.join(', ') || 'nothing'}; failed ${nav.failed.map((f) => `${f.gen} (${f.reason})`).join(', ')}. The next deploy retries.`);
705
+ // 5d. re-generate the CORE-scoped doc assets this very upgrade just wiped — the Repo Atlas
706
+ // data file + the onboarding diagram renders (task 1002441). See healDocAssets() for why
707
+ // nothing else puts them back. Best-effort + LOUD, same contract as 5c.
708
+ const heal = (deps.healDocAssets || healDocAssets)({ coreRoot }, run);
709
+ if (heal.missing.length === 0) log(' • doc assets intact (atlas + diagram renders present)');
710
+ else if (heal.ok) log(` ✓ regenerated doc assets wiped by the core swap (${heal.ran.join(', ')})`);
711
+ else err(` ! ::ERROR:: doc-asset regen incomplete — ran ${heal.ran.join(', ') || 'nothing'}; failed ${heal.failed.map((f) => `${f.group} (${f.reason})`).join(', ')}. /builders/atlas or /builders/diagrams may be broken until the next deploy retries (diagram renders need a system Chromium — ADR 0122).`);
712
+ } else log(' • doc regen skipped (--skip-regen-docs)');
591
713
 
592
714
  // 6. restart.
593
715
  const rs = restartService({ service: opts.service, restartCmd: opts.restartCmd }, run);
@@ -700,7 +822,7 @@ async function main(argv = process.argv.slice(2)) {
700
822
  module.exports = {
701
823
  runUpgrade, parseArgs, main, rollback,
702
824
  versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
703
- vendorTarball, gitTreeClean, preflightModules, npmInstall, runMigrate, regenerateApiArtifacts, restartService, recordLedger, pollHealth,
825
+ vendorTarball, gitTreeClean, preflightModules, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth,
704
826
  readManifest, resolveReferenceManifest, verifyInstalledPin, // task 1002216 (audit H8) — integrity-pin verification
705
827
  CORE_PKG, ARTIFACT,
706
828
  };
@@ -51,6 +51,15 @@ fi
51
51
 
52
52
  DB="${PGDATABASE:-amazonprimea}"
53
53
 
54
+ # Test/ops seam (task 1002611): --print-migrations walks the REAL core-migration loop but reports
55
+ # each path instead of applying it, and touches NO DB. tests/migrate_root_resolution.mjs uses it to
56
+ # assert that a checkout whose path contains a space still yields whole, readable paths. Reporting
57
+ # through the real loop (rather than a copy of it in the test) is the point: if the loop ever reverts
58
+ # to an unquoted `for f in $MIGRATIONS`, this seam fragments the paths and the test fails. Also handy
59
+ # in the field — `npm run migrate -- --print-migrations` shows exactly what migrate would apply.
60
+ DRY_RUN=0
61
+ if [ "${1:-}" = "--print-migrations" ]; then DRY_RUN=1; fi
62
+
54
63
  # --- Instance-seed policy (task 1704 / ADR 0105) --------------------------
55
64
  # OTB/product scope-truth (versions, goals, done_when_criteria) lives in seed
56
65
  # migrations under migrations/instance/ (plus a few guarded blocks embedded in the
@@ -81,22 +90,40 @@ echo "instance-seed policy: app.seed_instance_data=$SEED_INSTANCE (migrations/in
81
90
 
82
91
  # Make sure schema_migrations exists before we query it. (001 creates
83
92
  # it; other migrations rely on it.)
84
- psql -d "$DB" -v ON_ERROR_STOP=1 -tAc "
85
- CREATE TABLE IF NOT EXISTS schema_migrations (
86
- version text PRIMARY KEY,
87
- applied_at timestamptz NOT NULL DEFAULT now()
88
- );
89
- " >/dev/null
93
+ if [ "$DRY_RUN" = 0 ]; then
94
+ psql -d "$DB" -v ON_ERROR_STOP=1 -tAc "
95
+ CREATE TABLE IF NOT EXISTS schema_migrations (
96
+ version text PRIMARY KEY,
97
+ applied_at timestamptz NOT NULL DEFAULT now()
98
+ );
99
+ " >/dev/null
90
100
 
91
- APPLIED=$(psql -d "$DB" -tA -c "SELECT version FROM schema_migrations ORDER BY version")
101
+ APPLIED=$(psql -d "$DB" -tA -c "SELECT version FROM schema_migrations ORDER BY version")
102
+ else
103
+ APPLIED=""
104
+ fi
92
105
 
93
106
  # Apply one migration file unless its filename stem is already recorded. Stem (not path) is the
94
107
  # key, so moving a file between migrations/ and migrations/game/ does NOT re-run it on prod.
108
+ #
109
+ # The two counters are what the no-op guard below reads: a run that LISTED core migrations but
110
+ # neither applied nor recognized a single one is the space-in-path failure (task 1002611) and must
111
+ # be fatal, not silent. Both loops that call this run in the CURRENT shell (herestring / plain glob,
112
+ # never a `... | while` pipeline), so these increments are visible to the caller.
113
+ MIGRATIONS_APPLIED=0
114
+ MIGRATIONS_ALREADY=0
95
115
  apply_migration() {
96
116
  local f="$1" version
97
117
  version="$(basename "$f" .sql)"
118
+ if [ "$DRY_RUN" = 1 ]; then
119
+ # Counted as applied so the no-op guard below is exercised by the seam too.
120
+ printf 'would-apply\t%s\n' "$f"
121
+ MIGRATIONS_APPLIED=$((MIGRATIONS_APPLIED + 1))
122
+ return
123
+ fi
98
124
  if echo "$APPLIED" | grep -q "^${version}$"; then
99
125
  echo "skip $version (already applied)"
126
+ MIGRATIONS_ALREADY=$((MIGRATIONS_ALREADY + 1))
100
127
  return
101
128
  fi
102
129
  echo "apply $version"
@@ -108,6 +135,7 @@ apply_migration() {
108
135
  psql -d "$DB" -v ON_ERROR_STOP=1 -tAc \
109
136
  "INSERT INTO schema_migrations (version) VALUES ('${version}') ON CONFLICT DO NOTHING" \
110
137
  >/dev/null
138
+ MIGRATIONS_APPLIED=$((MIGRATIONS_APPLIED + 1))
111
139
  }
112
140
 
113
141
  # --- Forward-safety notice (task 1002223 / audit H10) ---------------------
@@ -116,7 +144,7 @@ apply_migration() {
116
144
  # UNACKNOWLEDGED forward-unsafe core migration so an operator knows a rollback past it would strand
117
145
  # old code on new schema. Fail-closed enforcement is the unit gate (tests/migrations_forward_safe.mjs);
118
146
  # here it is advisory (`|| true`) so a lint hiccup can NEVER break a deploy.
119
- if [ -f "$CORE_ROOT/scripts/gds/lint-migrations.js" ]; then
147
+ if [ "$DRY_RUN" = 0 ] && [ -f "$CORE_ROOT/scripts/gds/lint-migrations.js" ]; then
120
148
  node "$CORE_ROOT/scripts/gds/lint-migrations.js" --warn --dir "$CORE_ROOT/migrations" || true
121
149
  fi
122
150
 
@@ -131,14 +159,66 @@ fi
131
159
  # are included only when SEED_INSTANCE=on; stem-keyed tracking means a file's directory never
132
160
  # affects whether it re-runs on OTB prod. Today CORE_ROOT == INSTANCE_ROOT (single checkout);
133
161
  # once the core installs from node_modules the two roots diverge and this still merges correctly.
162
+ #
163
+ # PATHS WITH SPACES (task 1002611): this list is NEWLINE-separated and must be read a line at a
164
+ # time. The old `for f in $MIGRATIONS` word-split it on IFS — which includes space — so any checkout
165
+ # whose absolute path contains a space ("/Users/.../Hermeslines Marketing/...") fragmented every
166
+ # path, the `[ -e ]` guard below silently dropped all of them, and ZERO core migrations ran.
167
+ # Reproduced on 1.18.18: 117 files listed, 234 loop iterations, 234 -e failures, schema_migrations
168
+ # empty. The module loop further down is correctly quoted, so module migrations then ran against an
169
+ # EMPTY schema and reported a bogus `relation "achievements" does not exist` from modules/economy —
170
+ # `achievements` is created by CORE 006_pms_v2.sql, which never ran. Hence the guard after the loop.
171
+ # `<<<` (bash — see the shebang) keeps the loop in the CURRENT shell, so the counters survive it; a
172
+ # `... | while read` pipeline would both subshell the counters away and interact badly with
173
+ # `set -euo pipefail`. `|| true` on each `ls` keeps an empty glob from tripping pipefail.
134
174
  MIGRATIONS=$( {
135
- ls "$CORE_ROOT"/migrations/*.sql 2>/dev/null
136
- if [ "$SEED_INSTANCE" = "on" ]; then ls "$INSTANCE_ROOT"/migrations/instance/*.sql 2>/dev/null; fi
175
+ ls "$CORE_ROOT"/migrations/*.sql 2>/dev/null || true
176
+ if [ "$SEED_INSTANCE" = "on" ]; then ls "$INSTANCE_ROOT"/migrations/instance/*.sql 2>/dev/null || true; fi
137
177
  } | awk -F/ '{ print $NF "\t" $0 }' | sort | cut -f2- )
138
- for f in $MIGRATIONS; do
139
- [ -e "$f" ] || continue
178
+ migrations_listed=0
179
+ migrations_unreadable=0
180
+ before_applied=$MIGRATIONS_APPLIED
181
+ before_already=$MIGRATIONS_ALREADY
182
+ while IFS= read -r f; do
183
+ # An empty $MIGRATIONS still yields one empty line through `<<<`, so skip blanks before -e.
184
+ [ -n "$f" ] || continue
185
+ migrations_listed=$((migrations_listed + 1))
186
+ if [ ! -e "$f" ]; then
187
+ migrations_unreadable=$((migrations_unreadable + 1))
188
+ echo "migrate.sh: listed migration is not a readable file, skipping: $f" >&2
189
+ continue
190
+ fi
140
191
  apply_migration "$f"
141
- done
192
+ done <<< "$MIGRATIONS"
193
+
194
+ # --- No-op guard (task 1002611) -------------------------------------------
195
+ # Silence is what made the space-in-path bug expensive to diagnose: core migrations were skipped
196
+ # without a word, and the first error the operator saw came from an unrelated MODULE migration
197
+ # hitting the empty schema. If the glob LISTED core migrations but we neither applied nor recognized
198
+ # a single one, the list itself is broken — stop here rather than let the module loop below produce a
199
+ # misleading error about a table that a core migration was supposed to create.
200
+ core_applied=$((MIGRATIONS_APPLIED - before_applied))
201
+ core_already=$((MIGRATIONS_ALREADY - before_already))
202
+ if [ "$migrations_listed" -gt 0 ] && [ "$core_applied" -eq 0 ] && [ "$core_already" -eq 0 ]; then
203
+ {
204
+ echo
205
+ echo "migrate.sh: FATAL — listed $migrations_listed core migration(s) but applied 0 and found 0 already applied."
206
+ echo " $migrations_unreadable of the $migrations_listed listed path(s) were not readable files."
207
+ echo " CORE_ROOT=$CORE_ROOT"
208
+ echo " Refusing to continue to module migrations: they would run against an empty schema and"
209
+ echo " fail with a misleading error (e.g. relation \"achievements\" does not exist) from a module,"
210
+ echo " when the real cause is that no core migration ran."
211
+ echo " Run 'npm run migrate -- --print-roots' to see the roots migrate resolved."
212
+ } >&2
213
+ exit 1
214
+ fi
215
+
216
+ # The --print-migrations seam stops here: the core list is what it verifies, and going further would
217
+ # need a DB. The summary line is the machine-readable assertion target.
218
+ if [ "$DRY_RUN" = 1 ]; then
219
+ printf 'listed=%d unreadable=%d\n' "$migrations_listed" "$migrations_unreadable"
220
+ exit 0
221
+ fi
142
222
  [ "$SEED_INSTANCE" = "on" ] || echo "instance seeds skipped (migrations/instance/) — non-owning / vanilla instance"
143
223
 
144
224
  # Module-owned migrations (modules/<key>/migrations/*.sql) — applied only when the module is
@@ -208,7 +288,12 @@ if [ "${OTB_POST_DEPLOY_CHECK:-0}" = "1" ]; then
208
288
  BASELINE_FLAG="--baseline-only"
209
289
  echo "post-deploy: no prior score ledger — running in baseline-only mode"
210
290
  fi
211
- if ! python3 modules/art-pipeline/pipeline/post_deploy_check.py $BASELINE_FLAG; then
291
+ # `${VAR:+"$VAR"}` expands to NOTHING when empty and to the quoted value otherwise. A bare
292
+ # $BASELINE_FLAG relied on word-splitting to vanish (the habit that caused task 1002611), and
293
+ # quoting it outright would instead pass an empty argv entry that argparse rejects. An array
294
+ # would need bash 4 — `set -u` + `"${arr[@]}"` on an empty array aborts under the bash 3.2 that
295
+ # macOS still ships, and this script's shebang takes whatever bash is on PATH.
296
+ if ! python3 modules/art-pipeline/pipeline/post_deploy_check.py ${BASELINE_FLAG:+"$BASELINE_FLAG"}; then
212
297
  echo "post-deploy: whole-world check FAILED — see output above" >&2
213
298
  exit 1
214
299
  fi
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // scripts/status-mirror-sync.js — publish the status surface to the off-box status
3
- // mirror (lars589/amazonprimea-status → GitHub Pages → status.amazonprimea.com).
3
+ // mirror (a separate repo → a static Pages host → the instance's status domain).
4
4
  //
5
5
  // SOURCE PATH (BV1.R84 / task 1635): the status dashboard moved from public-status/
6
6
  // to modules/status-ui/public/ when the status surface became a module (a web
@@ -14,19 +14,28 @@
14
14
  // (that's how #726's reorg + #731's theming never showed up). This makes the
15
15
  // publish deterministic and CI-driven.
16
16
  //
17
+ // CONFIG (task 1002510 — no host identity is baked in here; ADR 0062 §3):
18
+ // domains.publicOrigin — the origin the mirror calls cross-origin
19
+ // (window.STATUS_API_BASE), and the site label the
20
+ // availability banner names as up/down.
21
+ // domains.statusDataUrl — the off-box prober's status.json URL the availability
22
+ // banner reads. EMPTY ⇒ this instance runs no off-box
23
+ // mirror, and the publish REFUSES rather than shipping a
24
+ // page whose banner points nowhere.
25
+ //
17
26
  // DESIGN: status.js + style.css + cursors/ are byte-identical between the
18
27
  // droplet copy and the mirror (#737 parameterized the one differing constant via
19
28
  // window.STATUS_API_BASE), so they are PURE COPIES. Only index.html carries the
20
29
  // three off-box additions, injected here at stable anchors:
21
- // 1. <script>window.STATUS_API_BASE='https://amazonprimea.com'</script> (head)
22
- // 2. <div id="offbox-status">…</div> (after <body>)
23
- // 3. the availability-banner driver <script> (reads the status-data branch) (before status.js)
30
+ // 1. <script>window.STATUS_API_BASE = <domains.publicOrigin></script> (head)
31
+ // 2. <div id="offbox-status">…</div> (after <body>)
32
+ // 3. the availability-banner driver <script> (reads domains.statusDataUrl) (before status.js)
24
33
  // If any anchor is missing the script THROWS — a silent mis-sync is worse than
25
34
  // a failed run.
26
35
  //
27
36
  // USAGE:
28
37
  // node scripts/status-mirror-sync.js --repo-dir <path-to-mirror-checkout> [--push]
29
- // --repo-dir : a checkout of lars589/amazonprimea-status (its `main` branch).
38
+ // --repo-dir : a checkout of the status-mirror repo (its `main` branch).
30
39
  // --push : commit + push the result. Without it the files are written
31
40
  // and `git status` is printed for inspection (dry-ish run).
32
41
  //
@@ -42,52 +51,36 @@ const SRC = path.join(REPO_ROOT, 'modules', 'status-ui', 'public');
42
51
  const { branding, clientBranding } = require('../src/branding');
43
52
 
44
53
  // Stamp instance-identity tokens into the mirror HTML. The off-box mirror is
45
- // served by GitHub Pages, NOT the branding-injecting Node server — so this script
46
- // must reproduce serve-internal.applyBrandTokens (task 1705, ADR 0062 §3), else the
47
- // mirror would render literal {{worldName}}/{{currency}} placeholders. KEEP IN SYNC
48
- // with src/bongos/serve-internal.js applyBrandTokens.
49
- function brandTokens(html) {
50
- const b = clientBranding();
51
- const world = (b.identity && b.identity.worldName) || '';
52
- const product = (b.identity && b.identity.productName) || '';
53
- const cur = (b.currency && b.currency.label) || 'credits';
54
+ // served by a static Pages host, NOT the branding-injecting Node server — so this
55
+ // script must reproduce serve-internal.applyBrandTokens (task 1705, ADR 0062 §3), else
56
+ // the mirror would render literal {{worldName}}/{{currency}} placeholders. KEEP IN SYNC
57
+ // with src/bongos/serve-internal.js applyBrandTokens. `brand` is the client-safe pack
58
+ // and `publicOrigin` the resolved origin — both injected, never read ambiently, so the
59
+ // transform stays pure and testable.
60
+ function brandTokens(html, brand = {}, publicOrigin = '') {
61
+ const world = (brand.identity && brand.identity.worldName) || '';
62
+ const product = (brand.identity && brand.identity.productName) || '';
63
+ const cur = (brand.currency && brand.currency.label) || 'credits';
54
64
  const Cur = cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : cur;
55
- let origin = '';
56
- try { origin = branding().domains.publicOrigin || ''; } catch (_) { /* neutral */ }
57
65
  return html
58
66
  .split('{{worldName}}').join(world)
59
67
  .split('{{productName}}').join(product)
60
68
  .split('{{Currency}}').join(Cur)
61
69
  .split('{{currency}}').join(cur)
62
- .split('{{publicOrigin}}').join(origin);
63
- }
64
-
65
- function arg(name) {
66
- const i = process.argv.indexOf(name);
67
- return i >= 0 ? process.argv[i + 1] : null;
68
- }
69
- const repoDir = arg('--repo-dir');
70
- const doPush = process.argv.includes('--push');
71
- if (!repoDir) {
72
- console.error('usage: node scripts/status-mirror-sync.js --repo-dir <mirror-checkout> [--push]');
73
- process.exit(2);
74
- }
75
- if (!fs.existsSync(path.join(repoDir, 'CNAME'))) {
76
- console.error(`refusing: ${repoDir} does not look like the status mirror (no CNAME). Clone lars589/amazonprimea-status first.`);
77
- process.exit(2);
70
+ .split('{{publicOrigin}}').join(publicOrigin || '');
78
71
  }
79
72
 
80
73
  // ---- the three off-box index.html injections -------------------------------
81
74
 
82
- const HEAD_INJECT =
75
+ const headInject = (apiBase) =>
83
76
  ` <!-- #737: off-box mirror runs cross-origin against the droplet. -->\n` +
84
- ` <script>window.STATUS_API_BASE = 'https://amazonprimea.com';</script>\n`;
77
+ ` <script>window.STATUS_API_BASE = ${JSON.stringify(apiBase)};</script>\n`;
85
78
 
86
79
  // The client-safe branding pack, so status.js reads the instance currency label
87
80
  // on the mirror exactly as it does on the droplet (serve-internal injects the same
88
81
  // global there). Without this, status.js would fall back to the neutral 'credits'.
89
- const BRANDING_INJECT =
90
- ` <script>window.__BRANDING__ = ${JSON.stringify(clientBranding())};</script>\n`;
82
+ const brandingInject = (brand) =>
83
+ ` <script>window.__BRANDING__ = ${JSON.stringify(brand)};</script>\n`;
91
84
 
92
85
  const BANNER_DIV =
93
86
  `\n<!-- Off-box availability banner (ADR 0029) — injected by scripts/status-mirror-sync.js.\n` +
@@ -95,11 +88,17 @@ const BANNER_DIV =
95
88
  ` prober's status.json from this repo's status-data branch, no droplet dependency. -->\n` +
96
89
  `<div id="offbox-status" role="status" aria-live="polite"></div>\n`;
97
90
 
98
- const BANNER_DRIVER = `<!-- Off-box availability banner driver (ADR 0029) — loaded before status.js so
91
+ // The banner names the site it is reporting on. Derived from the configured origin
92
+ // so no hostname is baked in; a non-URL origin falls back to the raw string.
93
+ function siteLabel(origin) {
94
+ try { return new URL(origin).host; } catch { return String(origin || ''); }
95
+ }
96
+
97
+ const bannerDriver = (statusDataUrl, label) => `<!-- Off-box availability banner driver (ADR 0029) — loaded before status.js so
99
98
  it lights up even if the dashboard script or its cross-origin fetches fail. -->
100
99
  <script>
101
100
  (function () {
102
- var OFFBOX_URL = 'https://raw.githubusercontent.com/lars589/amazonprimea-status/status-data/status.json';
101
+ var OFFBOX_URL = ${JSON.stringify(statusDataUrl)};
103
102
  var el = document.getElementById('offbox-status');
104
103
  if (!el) return;
105
104
  function fmtUtc(iso) {
@@ -113,8 +112,8 @@ const BANNER_DRIVER = `<!-- Off-box availability banner driver (ADR 0029) — lo
113
112
  .then(function (res) { if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); })
114
113
  .then(function (data) {
115
114
  var cur = data && data.current, status = cur && cur.status, when = fmtUtc(cur && cur.t);
116
- if (status === 'up') setState('up', '\\uD83D\\uDFE2 amazonprimea.com is operational \\u2014 checked ' + when);
117
- else if (status === 'down') setState('down', '\\uD83D\\uDD34 amazonprimea.com is DOWN \\u2014 checked ' + when);
115
+ if (status === 'up') setState('up', '\\uD83D\\uDFE2 ${label} is operational \\u2014 checked ' + when);
116
+ else if (status === 'down') setState('down', '\\uD83D\\uDD34 ${label} is DOWN \\u2014 checked ' + when);
118
117
  else setState('unknown', 'status data is momentarily unavailable');
119
118
  })
120
119
  .catch(function () { setState('unknown', 'status data is momentarily unavailable'); });
@@ -134,59 +133,90 @@ function injectOnce(haystack, anchor, insertion, where) {
134
133
  return haystack.slice(0, idx + anchor.length) + insertion + haystack.slice(idx + anchor.length);
135
134
  }
136
135
 
137
- function buildIndexHtml() {
138
- let html = fs.readFileSync(path.join(SRC, 'index.html'), 'utf8');
136
+ // Pure: droplet index.html + resolved config → the mirror's index.html. No disk, no
137
+ // network, no ambient branding read — everything the output depends on is a parameter.
138
+ function buildMirrorIndex({ srcHtml, apiBase, statusDataUrl, brand }) {
139
+ if (!statusDataUrl) {
140
+ throw new Error('domains.statusDataUrl is empty — the off-box availability banner would point nowhere. Set domains.statusDataUrl (the prober\'s status.json URL) in the branding config, or do not publish a mirror for this instance.');
141
+ }
142
+ let html = String(srcHtml == null ? '' : srcHtml);
139
143
  if (html.includes('offbox-status')) {
140
144
  throw new Error('modules/status-ui/public/index.html already contains offbox markup — the source should be the droplet (no-banner) copy. Aborting.');
141
145
  }
142
- html = injectOnce(html, '</head>', HEAD_INJECT + BRANDING_INJECT, 'before');
146
+ html = injectOnce(html, '</head>', headInject(apiBase) + brandingInject(brand), 'before');
143
147
  html = injectOnce(html, '<body>', BANNER_DIV, 'after');
144
148
  // Insert the driver right before the main status.js script tag.
145
149
  const scriptAnchor = '<script src="/status.js';
146
- html = injectOnce(html, scriptAnchor, BANNER_DRIVER + '', 'before');
147
- // Stamp the instance branding tokens LAST — GitHub Pages won't run the server's
150
+ html = injectOnce(html, scriptAnchor, bannerDriver(statusDataUrl, siteLabel(apiBase)), 'before');
151
+ // Stamp the instance branding tokens LAST — the Pages host won't run the server's
148
152
  // injection, so the mirror must resolve {{worldName}}/{{currency}}/… itself.
149
- html = brandTokens(html);
150
- return html;
153
+ return brandTokens(html, brand, apiBase);
151
154
  }
152
155
 
153
- // ---- copy the verbatim assets + the transformed index ----------------------
156
+ // ---- CLI -------------------------------------------------------------------
157
+
158
+ if (require.main === module) {
159
+ const arg = (name) => {
160
+ const i = process.argv.indexOf(name);
161
+ return i >= 0 ? process.argv[i + 1] : null;
162
+ };
163
+ const repoDir = arg('--repo-dir');
164
+ const doPush = process.argv.includes('--push');
165
+ if (!repoDir) {
166
+ console.error('usage: node scripts/status-mirror-sync.js --repo-dir <mirror-checkout> [--push]');
167
+ process.exit(2);
168
+ }
169
+ if (!fs.existsSync(path.join(repoDir, 'CNAME'))) {
170
+ console.error(`refusing: ${repoDir} does not look like the status mirror (no CNAME). Clone the status-mirror repo first.`);
171
+ process.exit(2);
172
+ }
154
173
 
155
- function copyFile(rel) {
156
- fs.copyFileSync(path.join(SRC, rel), path.join(repoDir, rel));
157
- console.error(` copied ${rel}`);
158
- }
159
- function copyDir(rel) {
160
- const from = path.join(SRC, rel), to = path.join(repoDir, rel);
161
- if (!fs.existsSync(from)) return;
162
- fs.rmSync(to, { recursive: true, force: true });
163
- fs.cpSync(from, to, { recursive: true });
164
- console.error(` copied ${rel}/`);
174
+ const b = branding();
175
+
176
+ // ---- copy the verbatim assets + the transformed index ----------------------
177
+
178
+ const copyFile = (rel) => {
179
+ fs.copyFileSync(path.join(SRC, rel), path.join(repoDir, rel));
180
+ console.error(` copied ${rel}`);
181
+ };
182
+ const copyDir = (rel) => {
183
+ const from = path.join(SRC, rel), to = path.join(repoDir, rel);
184
+ if (!fs.existsSync(from)) return;
185
+ fs.rmSync(to, { recursive: true, force: true });
186
+ fs.cpSync(from, to, { recursive: true });
187
+ console.error(` copied ${rel}/`);
188
+ };
189
+
190
+ console.error('[status-mirror-sync] building mirror from modules/status-ui/public/ …');
191
+ const indexHtml = buildMirrorIndex({
192
+ srcHtml: fs.readFileSync(path.join(SRC, 'index.html'), 'utf8'),
193
+ apiBase: b.domains.publicOrigin,
194
+ statusDataUrl: b.domains.statusDataUrl,
195
+ brand: clientBranding(),
196
+ });
197
+ fs.writeFileSync(path.join(repoDir, 'index.html'), indexHtml);
198
+ console.error(' wrote index.html (with off-box injections)');
199
+ copyFile('status.js');
200
+ copyFile('style.css');
201
+ copyDir('cursors');
202
+
203
+ // ---- commit + (optionally) push -------------------------------------------
204
+
205
+ const git = (args) => execFileSync('git', ['-C', repoDir, ...args], { encoding: 'utf8' });
206
+ git(['add', 'index.html', 'status.js', 'style.css', 'cursors']);
207
+ const status = git(['status', '--porcelain']);
208
+ if (!status.trim()) {
209
+ console.error('[status-mirror-sync] mirror already up to date — nothing to publish.');
210
+ process.exit(0);
211
+ }
212
+ console.error('[status-mirror-sync] changes staged:\n' + status);
213
+ if (!doPush) {
214
+ console.error('[status-mirror-sync] dry run (no --push). Review the checkout, then re-run with --push.');
215
+ process.exit(0);
216
+ }
217
+ git(['commit', '-m', 'chore(status): sync off-box mirror from modules/status-ui/public/ (#737)']);
218
+ git(['push', 'origin', 'HEAD:main']);
219
+ console.error(`[status-mirror-sync] published. The Pages host rebuilds ${b.domains.statusOrigin || 'the mirror'} in ~1 min.`);
165
220
  }
166
221
 
167
- console.error('[status-mirror-sync] building mirror from modules/status-ui/public/ …');
168
- fs.writeFileSync(path.join(repoDir, 'index.html'), buildIndexHtml());
169
- console.error(' wrote index.html (with off-box injections)');
170
- copyFile('status.js');
171
- copyFile('style.css');
172
- copyDir('cursors');
173
-
174
- // ---- commit + (optionally) push -------------------------------------------
175
-
176
- function git(args) {
177
- return execFileSync('git', ['-C', repoDir, ...args], { encoding: 'utf8' });
178
- }
179
- git(['add', 'index.html', 'status.js', 'style.css', 'cursors']);
180
- const status = git(['status', '--porcelain']);
181
- if (!status.trim()) {
182
- console.error('[status-mirror-sync] mirror already up to date — nothing to publish.');
183
- process.exit(0);
184
- }
185
- console.error('[status-mirror-sync] changes staged:\n' + status);
186
- if (!doPush) {
187
- console.error('[status-mirror-sync] dry run (no --push). Review the checkout, then re-run with --push.');
188
- process.exit(0);
189
- }
190
- git(['commit', '-m', 'chore(status): sync off-box mirror from modules/status-ui/public/ (#737)']);
191
- git(['push', 'origin', 'HEAD:main']);
192
- console.error('[status-mirror-sync] published. GitHub Pages rebuilds status.amazonprimea.com in ~1 min.');
222
+ module.exports = { buildMirrorIndex, injectOnce, brandTokens };
@@ -66,6 +66,7 @@ const OVERRIDES = Object.freeze({
66
66
  // --- forbidden: 403 (rank/ownership, not "missing" or "bad input") ---
67
67
  forbidden: 'forbidden',
68
68
  rank_forbidden: 'forbidden',
69
+ permission_forbidden: 'forbidden',
69
70
  archon_approval_required: 'forbidden',
70
71
  archon_required_for_protected_scope: 'forbidden',
71
72
  metic_required_for_ownership: 'forbidden',