@bongos/core 1.18.3 → 1.18.18

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 (469) hide show
  1. package/.bongos-core.json +1575 -285
  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/status-mirror-sync.js +114 -84
  347. package/src/bongos/api-errors.js +1 -0
  348. package/src/bongos/auth.js +447 -102
  349. package/src/bongos/db-kernel.js +27 -0
  350. package/src/bongos/db.js +39 -0
  351. package/src/bongos/module-scope-map.js +27 -1
  352. package/src/bongos/permission-path-check.js +136 -102
  353. package/src/bongos/repo-info.js +47 -11
  354. package/src/bongos/route-rank-check.js +429 -46
  355. package/src/bongos/routes/_helpers.js +12 -1
  356. package/src/bongos/routes/auth.js +219 -2
  357. package/src/bongos/routes/backup.js +58 -2
  358. package/src/bongos/routes/me.js +35 -1
  359. package/src/bongos/serve-internal.js +84 -15
  360. package/src/branding.js +32 -0
  361. package/src/instance-config.js +30 -0
  362. package/src/module-api.js +112 -1
  363. package/src/platform-server.js +86 -9
  364. package/tests/abandon_task.mjs +59 -3
  365. package/tests/api_cli_flags.mjs +53 -0
  366. package/tests/auth_page_gate.mjs +13 -0
  367. package/tests/backfill_sessions.mjs +127 -1
  368. package/tests/backup_cli.mjs +54 -0
  369. package/tests/bongos_box_connect.mjs +77 -0
  370. package/tests/box_access.mjs +130 -1
  371. package/tests/box_credential.mjs +125 -0
  372. package/tests/box_host_keys.mjs +24 -0
  373. package/tests/box_infra_resolve.mjs +149 -0
  374. package/tests/box_scope_session.mjs +6 -2
  375. package/tests/box_sync.mjs +55 -0
  376. package/tests/box_task_scope.mjs +99 -0
  377. package/tests/branding.mjs +12 -0
  378. package/tests/cf_ruleset_preflight.mjs +142 -0
  379. package/tests/cf_tunnel.mjs +37 -0
  380. package/tests/claim_gate_rebase.mjs +98 -0
  381. package/tests/clawback_confirmed_backlog.mjs +103 -0
  382. package/tests/cli_lib_formatters.mjs +20 -0
  383. package/tests/confirm_attribution.mjs +30 -46
  384. package/tests/credit_grant.mjs +198 -10
  385. package/tests/criterion_authoring_gate.mjs +272 -0
  386. package/tests/criterion_progress.mjs +33 -1
  387. package/tests/dev_lib.mjs +253 -0
  388. package/tests/devbox_artifact_gate.mjs +75 -0
  389. package/tests/devbox_dmg_backdrop.mjs +137 -0
  390. package/tests/devbox_icons.mjs +304 -0
  391. package/tests/devbox_installer_brand.mjs +232 -0
  392. package/tests/devbox_key_register.mjs +234 -0
  393. package/tests/docs_openapi_route.mjs +110 -0
  394. package/tests/doctor_hooks.mjs +141 -0
  395. package/tests/extract_json.mjs +140 -0
  396. package/tests/fitness.mjs +89 -0
  397. package/tests/gen_freshness.mjs +96 -0
  398. package/tests/goal_advisory.mjs +251 -0
  399. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  400. package/tests/goal_routes.mjs +59 -4
  401. package/tests/goal_tier.mjs +12 -3
  402. package/tests/governance_catalog.mjs +129 -0
  403. package/tests/governance_claim_eligibility.mjs +262 -0
  404. package/tests/governance_config.mjs +187 -0
  405. package/tests/governance_ownership_scope.mjs +410 -0
  406. package/tests/governance_page_gates.mjs +324 -0
  407. package/tests/governance_parity.mjs +336 -0
  408. package/tests/governance_principals.mjs +232 -0
  409. package/tests/governance_protected_surfaces.mjs +208 -0
  410. package/tests/governance_require_permission.mjs +223 -0
  411. package/tests/governance_resolver.mjs +84 -0
  412. package/tests/governance_role_sync.mjs +231 -0
  413. package/tests/governance_seed.mjs +127 -0
  414. package/tests/governance_session_scope.mjs +434 -0
  415. package/tests/governance_vanilla_boot.mjs +203 -0
  416. package/tests/grade_attribution.mjs +7 -7
  417. package/tests/hall_audit.mjs +223 -0
  418. package/tests/hall_nav.mjs +191 -0
  419. package/tests/hall_palette.mjs +206 -0
  420. package/tests/hall_signout.mjs +124 -0
  421. package/tests/hook_user_agents.mjs +10 -0
  422. package/tests/idea_triage.mjs +162 -0
  423. package/tests/idp_device_exchange.mjs +93 -0
  424. package/tests/init.mjs +51 -0
  425. package/tests/instance_config.mjs +65 -0
  426. package/tests/instance_mismatch_guard.mjs +23 -6
  427. package/tests/instance_release_config.mjs +104 -0
  428. package/tests/linkify_refs.mjs +15 -2
  429. package/tests/module_contributions.mjs +55 -0
  430. package/tests/module_loader.mjs +10 -5
  431. package/tests/module_scope_active_claims.mjs +94 -0
  432. package/tests/needs_rebase.mjs +92 -0
  433. package/tests/one_active_claim_per_session.mjs +76 -0
  434. package/tests/override_attribution.mjs +10 -8
  435. package/tests/override_request_ownership.mjs +8 -4
  436. package/tests/parse_cookie_decode.mjs +42 -0
  437. package/tests/pay_on_land.mjs +129 -0
  438. package/tests/platform_boot.mjs +100 -1
  439. package/tests/post_login_landing.mjs +42 -0
  440. package/tests/preflight_checks.mjs +72 -0
  441. package/tests/provision.mjs +132 -7
  442. package/tests/publish_branch_route.mjs +53 -0
  443. package/tests/publish_reconciler.mjs +187 -2
  444. package/tests/repo_info_binding.mjs +248 -0
  445. package/tests/reward_gate.mjs +11 -2
  446. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  447. package/tests/reward_mode_surfacing.mjs +168 -0
  448. package/tests/route_permission_check.mjs +301 -0
  449. package/tests/route_rank_check.mjs +254 -12
  450. package/tests/route_validation.mjs +30 -0
  451. package/tests/sequence.mjs +267 -0
  452. package/tests/session_digest.mjs +44 -0
  453. package/tests/session_index.mjs +18 -6
  454. package/tests/session_records.mjs +39 -1
  455. package/tests/session_reward_on_land.mjs +381 -0
  456. package/tests/session_reward_reconcile.mjs +229 -0
  457. package/tests/ship_api_client_pathspec.mjs +100 -0
  458. package/tests/ship_base_flag.mjs +93 -0
  459. package/tests/ship_ci_deploy.mjs +80 -3
  460. package/tests/ship_upstream_deletion.mjs +57 -0
  461. package/tests/smoke_target.mjs +233 -0
  462. package/tests/start_env_guard.mjs +87 -0
  463. package/tests/start_rebase_warning.mjs +103 -0
  464. package/tests/status_mirror_sync.mjs +122 -0
  465. package/tests/stop_refcheck.mjs +19 -1
  466. package/tests/task_goal_link.mjs +15 -6
  467. package/tests/upgrade.mjs +139 -0
  468. package/tests/web_admission_status.mjs +162 -0
  469. package/tests/write_validation_gaps.mjs +110 -0
@@ -0,0 +1,137 @@
1
+ // tests/devbox_dmg_backdrop.mjs — the Dev Box DMG install-window backdrop is
2
+ // BRANDING-DRIVEN, not baked in (task 1002372).
3
+ //
4
+ // modules/dev-box/app/scripts/gen-dmg-backdrop.py used to hardcode one host's
5
+ // install screen — its palette, its archaic copy, its world motifs — so the
6
+ // vanilla Cloud Bongos build shipped another instance's art. The generator now
7
+ // reads the pack the same way gen-brand.js does (ADR 0145). The fitness identity
8
+ // scan could not have caught this: it only reads .js/.html/.css, so a .py
9
+ // generator is invisible to it — hence this test.
10
+ //
11
+ // It is a SOURCE + ARTIFACT test rather than an execution test on purpose: the
12
+ // generator needs Pillow, which no manifest in this repo declares, so running it
13
+ // is not available in the DB-free unit gate. What is checked instead is the part
14
+ // that actually rots — that every colour literal left in the generator is the
15
+ // NEUTRAL pack's own value (i.e. a fallback, not a brand), and that the committed
16
+ // art still matches the DMG window the installer opens.
17
+ //
18
+ // Run: node tests/devbox_dmg_backdrop.mjs
19
+
20
+ import { strict as assert } from 'node:assert';
21
+ import { test } from 'node:test';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
+ const APP_DIR = path.join(REPO_ROOT, 'modules', 'dev-box', 'app');
28
+ const GENERATOR = path.join(APP_DIR, 'scripts', 'gen-dmg-backdrop.py');
29
+
30
+ const source = fs.readFileSync(GENERATOR, 'utf8');
31
+ const neutral = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'config', 'branding.neutral.json'), 'utf8'));
32
+
33
+ // Strip Python comments + every docstring so rationale prose — which must be free to
34
+ // name the old brand, or the artifact a fix deliberately stopped reading — never trips
35
+ // a scan of the CODE (the stripCommentsForIdentity contract in scripts/gds/fitness.js,
36
+ // applied to `#` comments and """…""" blocks).
37
+ function codeOnly(text) {
38
+ return text
39
+ .replace(/"""[\s\S]*?"""/g, '')
40
+ .split(/\r?\n/)
41
+ .map((line) => line.replace(/(^|[^"'])#.*$/, '$1'))
42
+ .join('\n');
43
+ }
44
+
45
+ // COLOR_ROLES maps a theme.ui.colors key → the fallback used when a pack omits it.
46
+ function parseColorRoles(text) {
47
+ const block = text.match(/COLOR_ROLES = \{([\s\S]*?)\n\}/);
48
+ assert.ok(block, 'gen-dmg-backdrop.py must declare a COLOR_ROLES map of theme.ui.colors keys');
49
+ const roles = {};
50
+ for (const [, key, hex] of block[1].matchAll(/"([A-Za-z]+)":\s*"(#[0-9a-fA-F]{3,6})"/g)) roles[key] = hex;
51
+ return roles;
52
+ }
53
+
54
+ test('the backdrop generator resolves the instance branding pack, like gen-brand.js', () => {
55
+ const code = codeOnly(source);
56
+ assert.match(code, /BONGOS_BRANDING_FILE/, 'honors the same env override gen-brand.js does');
57
+ assert.match(code, /config", "branding\.json/, 'reads the instance pack first');
58
+ assert.match(code, /config", "branding\.neutral\.json/, 'falls back to the neutral starter');
59
+ });
60
+
61
+ test('the product name + tagline come from the pack, never the committed build artifact', () => {
62
+ // src/brand.generated.json holds the same two strings, but it is a TRACKED file
63
+ // carrying the neutral values and nothing on the backdrop path regenerates it.
64
+ // Reading it made a host render draw its own palette + type under the neutral
65
+ // product's name — the pack has to decide both, the same way gen-brand.js does.
66
+ const code = codeOnly(source);
67
+ assert.doesNotMatch(code, /brand\.generated\.json/,
68
+ 'the stale tracked artifact must not outrank the resolved pack');
69
+ assert.match(code, /identity\.get\("worldName"\)/, 'the product name derives from the pack identity');
70
+ assert.match(code, /\.get\("tagline"\)/, 'the subhead derives from the pack copy');
71
+ });
72
+
73
+ test('every colour literal in the generator is the NEUTRAL pack value, not a brand palette', () => {
74
+ const roles = parseColorRoles(source);
75
+ const packColors = neutral.theme.ui.colors;
76
+ assert.ok(Object.keys(roles).length >= 6, 'the art draws with a real palette, not one or two colours');
77
+ for (const [role, fallback] of Object.entries(roles)) {
78
+ assert.ok(packColors[role], `COLOR_ROLES key "${role}" must exist in config/branding.neutral.json theme.ui.colors`);
79
+ assert.equal(fallback.toLowerCase(), String(packColors[role]).toLowerCase(),
80
+ `the "${role}" fallback must equal the neutral pack's value — a divergent literal is a hardcoded brand`);
81
+ }
82
+ // …and no OTHER colour is hardcoded: the only hex literals are those fallbacks.
83
+ const hexes = codeOnly(source).match(/#[0-9a-fA-F]{6}\b/g) || [];
84
+ const allowed = new Set(Object.values(roles).map((h) => h.toLowerCase()));
85
+ for (const hex of hexes) {
86
+ assert.ok(allowed.has(hex.toLowerCase()), `hardcoded colour ${hex} — pull it from theme.ui.colors instead`);
87
+ }
88
+ });
89
+
90
+ test('a pack filling the documented theme surface leaves NO neutral colour on the canvas', () => {
91
+ // The one hole a fallback-equals-neutral check cannot see. `sea` used to be a
92
+ // role here, but gen-brand.js's :root emission — the surface a host pack author
93
+ // actually fills for this app — never mentioned it, so a fully-specified host
94
+ // pack still got Cloud Bongos sky-blue ringing the Applications drop target.
95
+ // A role is only safe to fall back on if the pack was PROMPTED to define it.
96
+ const genBrand = fs.readFileSync(path.join(APP_DIR, 'scripts', 'gen-brand.js'), 'utf8');
97
+ const themeCss = genBrand.match(/function buildThemeCss\([\s\S]*?\n\}/);
98
+ assert.ok(themeCss, 'gen-brand.js must declare buildThemeCss — it defines the app theme surface');
99
+ const documented = new Set([...themeCss[0].matchAll(/\bc\.([A-Za-z]+)\b/g)].map(([, k]) => k));
100
+ const roles = parseColorRoles(source);
101
+ for (const role of Object.keys(roles)) {
102
+ assert.ok(documented.has(role),
103
+ `COLOR_ROLES key "${role}" is not on the documented theme surface (gen-brand.js buildThemeCss) — ` +
104
+ 'a host pack has no prompt to define it, so its neutral fallback would paint a Cloud Bongos colour ' +
105
+ 'into a fully-specified host build');
106
+ }
107
+ // …and nothing reaches the canvas except through that map, so filling the
108
+ // documented roles overrides every literal in the file.
109
+ for (const [, key] of codeOnly(source).matchAll(/\bp\["([A-Za-z]+)"\]/g)) {
110
+ assert.ok(roles[key], `the render draws with p["${key}"], which COLOR_ROLES does not declare`);
111
+ }
112
+ });
113
+
114
+ test('the generator carries no host-brand copy or motif of its own', () => {
115
+ const code = codeOnly(source);
116
+ // The archaic install copy + the world motifs that shipped into the vanilla build.
117
+ for (const re of [/thine/i, /ashore/i, /amazonprimea/i, /off the boats/i, /meander/i, /greek/i, /marble/i]) {
118
+ assert.doesNotMatch(code, re, `host-brand literal ${re} must not appear in the generator's code`);
119
+ }
120
+ // Copy is derived from the pack (or overridden by it), never a fixed sentence.
121
+ assert.match(code, /dmgHeadline/, 'a pack may state its own headline');
122
+ assert.match(code, /copy\.get\("dmgSubhead"\)|dmgSubhead/, 'a pack may state its own subhead');
123
+ });
124
+
125
+ test('the committed art matches the DMG window the installer opens (660x400 @1x, @2x)', () => {
126
+ const dims = (file) => {
127
+ const buf = fs.readFileSync(path.join(APP_DIR, 'build-resources', file));
128
+ assert.equal(buf.subarray(1, 4).toString('latin1'), 'PNG', `${file} is a PNG`);
129
+ return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
130
+ };
131
+ assert.deepEqual(dims('dmg-background.png'), [660, 400]);
132
+ assert.deepEqual(dims('dmg-background@2x.png'), [1320, 800]);
133
+ // The window/backdrop size is duplicated in dmgbuild-settings.py — drift there
134
+ // means Finder scales the art and the icon wells stop lining up with the icons.
135
+ const dmgbuild = fs.readFileSync(path.join(APP_DIR, 'scripts', 'dmgbuild-settings.py'), 'utf8');
136
+ assert.match(dmgbuild, /window_rect = \(\(150, 150\), \(660, 400\)\)/);
137
+ });
@@ -0,0 +1,304 @@
1
+ // tests/devbox_icons.mjs — the Dev Box app + tray icons are BRANDING-DRIVEN and
2
+ // carry no host-brand motif (task 1002261).
3
+ //
4
+ // modules/dev-box/app/scripts/gen-icons.js used to draw one host's world into
5
+ // every build — a shipping container with a wave footer, in a hardcoded palette —
6
+ // so the vanilla Cloud Bongos app shipped another instance's emblem on its tray
7
+ // and in its Dock. The motif is now a bongo pair (brand-neutral geometry) and the
8
+ // palette is resolved from the instance pack the way gen-brand.js resolves it
9
+ // (ADR 0145), matching what task 1002372 did for the DMG backdrop.
10
+ //
11
+ // SOURCE + ARTIFACT rather than an execution test, for two reasons: running the
12
+ // generator REWRITES the committed art (so a test run would dirty the tree), and
13
+ // it needs `sharp`, which config-wise belongs to the art-pipeline module — an
14
+ // instance that doesn't enable that module need not have it installed, and this
15
+ // file is not module-gated. What is checked instead is the part that actually
16
+ // rots: that every colour literal left in the generator is the NEUTRAL pack's own
17
+ // value, that nothing reaches the canvas except through the resolved palette, and
18
+ // that the committed PNGs are still the neutral render at the sizes the app loads.
19
+ //
20
+ // Run: node tests/devbox_icons.mjs
21
+
22
+ import { strict as assert } from 'node:assert';
23
+ import { test } from 'node:test';
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import zlib from 'node:zlib';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
30
+ const APP_DIR = path.join(REPO_ROOT, 'modules', 'dev-box', 'app');
31
+ const GENERATOR = path.join(APP_DIR, 'scripts', 'gen-icons.js');
32
+ const TRAY_DIR = path.join(APP_DIR, 'assets', 'tray');
33
+ const BUILD_DIR = path.join(APP_DIR, 'build-resources');
34
+
35
+ const STATES = ['off', 'on', 'busy', 'alert'];
36
+ // macOS recolours a *Template.png itself (menu-bar light/dark, inverted when
37
+ // selected) and discards whatever colour the source held. Pure black is an OS
38
+ // requirement, not a brand choice — so it is the one literal exempt below.
39
+ const MAC_TEMPLATE_BLACK = '#000000';
40
+
41
+ const source = fs.readFileSync(GENERATOR, 'utf8');
42
+ const neutral = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'config', 'branding.neutral.json'), 'utf8'));
43
+
44
+ // Strip comments so rationale prose — which must be free to name the motif a fix
45
+ // deliberately removed, or the artifact it deliberately stopped reading — never
46
+ // trips a scan of the CODE (the stripCommentsForIdentity contract in
47
+ // scripts/gds/fitness.js; the `//` strip is `://`-safe so a URL stays intact).
48
+ function codeOnly(text) {
49
+ return text
50
+ .replace(/\/\*[\s\S]*?\*\//g, '')
51
+ .split(/\r?\n/)
52
+ .map((line) => line.replace(/(^|[^:])\/\/.*$/, '$1'))
53
+ .join('\n');
54
+ }
55
+ const code = codeOnly(source);
56
+
57
+ // Grab a top-level `const NAME = { … }` / `[ … ]` by brace balance, so the tests
58
+ // below don't depend on how the declaration happens to be wrapped.
59
+ function declBlock(name) {
60
+ const start = code.indexOf(`const ${name} = `);
61
+ assert.ok(start >= 0, `gen-icons.js must declare ${name}`);
62
+ const open = code.indexOf('{', start);
63
+ let depth = 0;
64
+ for (let i = open; i < code.length; i++) {
65
+ if (code[i] === '{') depth += 1;
66
+ else if (code[i] === '}' && (depth -= 1) === 0) return code.slice(open, i + 1);
67
+ }
68
+ throw new Error(`gen-icons.js: unbalanced braces in ${name}`);
69
+ }
70
+
71
+ // COLOR_ROLES maps a theme.ui.colors key → the fallback used when a pack omits it.
72
+ function colorRoles() {
73
+ const roles = {};
74
+ for (const [, key, hex] of declBlock('COLOR_ROLES').matchAll(/([A-Za-z]+):\s*'(#[0-9a-fA-F]{3,6})'/g)) roles[key] = hex;
75
+ assert.ok(Object.keys(roles).length >= 6, 'the art draws with a real palette, not one or two colours');
76
+ return roles;
77
+ }
78
+
79
+ // `{ '#': p.accentDeep, f: p.accent }` → { '#': 'accentDeep', f: 'accent' }
80
+ function inkRoles(block) {
81
+ const out = {};
82
+ for (const [, quoted, bare, role] of block.matchAll(/(?:'([^']*)'|([A-Za-z]+))\s*:\s*p\.([A-Za-z]+)/g)) {
83
+ out[quoted ?? bare] = role;
84
+ }
85
+ return out;
86
+ }
87
+
88
+ function trayGrids() {
89
+ const grids = {};
90
+ for (const [, state, body] of declBlock('GRIDS').matchAll(/(off|on|busy|alert):\s*\[([\s\S]*?)\]/g)) {
91
+ grids[state] = [...body.matchAll(/'([^']*)'/g)].map(([, row]) => row);
92
+ }
93
+ return grids;
94
+ }
95
+
96
+ const inksOf = (grid) => new Set(grid.join('').split('').filter((c) => c !== '.'));
97
+ const sorted = (set) => [...set].sort().join('');
98
+
99
+ // --- a minimal PNG reader (8-bit RGBA, non-interlaced — what the generator emits).
100
+ // Deliberately dependency-free: this gate must run on an instance that has no
101
+ // image library installed.
102
+ function readRgba(file) {
103
+ const buf = fs.readFileSync(file);
104
+ assert.equal(buf.subarray(1, 4).toString('latin1'), 'PNG', `${path.basename(file)} is a PNG`);
105
+ const width = buf.readUInt32BE(16);
106
+ const height = buf.readUInt32BE(20);
107
+ assert.equal(buf[24], 8, `${path.basename(file)} is 8-bit`);
108
+ assert.equal(buf[25], 6, `${path.basename(file)} is RGBA`);
109
+ assert.equal(buf[28], 0, `${path.basename(file)} is non-interlaced`);
110
+ const idat = [];
111
+ for (let o = 8; o < buf.length;) {
112
+ const len = buf.readUInt32BE(o);
113
+ const type = buf.toString('latin1', o + 4, o + 8);
114
+ if (type === 'IDAT') idat.push(buf.subarray(o + 8, o + 8 + len));
115
+ o += 12 + len;
116
+ if (type === 'IEND') break;
117
+ }
118
+ const raw = zlib.inflateSync(Buffer.concat(idat));
119
+ const bpp = 4;
120
+ const stride = width * bpp;
121
+ const out = Buffer.alloc(height * stride);
122
+ const paeth = (a, b, c) => {
123
+ const da = Math.abs(b - c); // |p - a| where p = a + b - c
124
+ const db = Math.abs(a - c); // |p - b|
125
+ const dc = Math.abs(a + b - 2 * c); // |p - c|
126
+ return da <= db && da <= dc ? a : db <= dc ? b : c;
127
+ };
128
+ for (let y = 0; y < height; y++) {
129
+ const filter = raw[y * (stride + 1)];
130
+ const line = raw.subarray(y * (stride + 1) + 1, y * (stride + 1) + 1 + stride);
131
+ for (let i = 0; i < stride; i++) {
132
+ const a = i >= bpp ? out[y * stride + i - bpp] : 0;
133
+ const b = y > 0 ? out[(y - 1) * stride + i] : 0;
134
+ const c = i >= bpp && y > 0 ? out[(y - 1) * stride + i - bpp] : 0;
135
+ let v = line[i];
136
+ if (filter === 1) v += a;
137
+ else if (filter === 2) v += b;
138
+ else if (filter === 3) v += (a + b) >> 1;
139
+ else if (filter === 4) v += paeth(a, b, c);
140
+ out[y * stride + i] = v & 255;
141
+ }
142
+ }
143
+ return { width, height, data: out };
144
+ }
145
+
146
+ // Every colour that is actually painted (alpha > 0), lowercased.
147
+ function opaqueColors(file) {
148
+ const { width, height, data } = readRgba(file);
149
+ const seen = new Set();
150
+ for (let i = 0; i < width * height; i++) {
151
+ if (data[i * 4 + 3] === 0) continue;
152
+ seen.add(`#${[0, 1, 2].map((j) => data[i * 4 + j].toString(16).padStart(2, '0')).join('')}`);
153
+ }
154
+ return seen;
155
+ }
156
+
157
+ test('the icon generator resolves the instance branding pack, like gen-brand.js', () => {
158
+ assert.match(code, /BONGOS_BRANDING_FILE/, 'honors the same env override gen-brand.js does');
159
+ assert.match(code, /'config', 'branding\.json'/, 'reads the instance pack first');
160
+ assert.match(code, /'config', 'branding\.neutral\.json'/, 'falls back to the neutral starter');
161
+ assert.match(code, /branding\.theme/, 'the palette comes from the pack theme');
162
+ assert.match(code, /\.colors/, '…specifically theme.ui.colors');
163
+ });
164
+
165
+ test('the palette comes from the pack, never the committed build artifact', () => {
166
+ // src/brand.generated.json carries colours too, but it is a TRACKED file holding
167
+ // the neutral values and nothing on the icon path regenerates it. Preferring it
168
+ // let a host render draw the host palette under the neutral product's name once
169
+ // already (task 1002372) — the pack has to decide, every time.
170
+ assert.doesNotMatch(code, /brand\.generated\.json/,
171
+ 'the stale tracked artifact must not outrank the resolved pack');
172
+ });
173
+
174
+ test('every colour literal in the generator is the NEUTRAL pack value, not a brand palette', () => {
175
+ const roles = colorRoles();
176
+ const packColors = neutral.theme.ui.colors;
177
+ for (const [role, fallback] of Object.entries(roles)) {
178
+ assert.ok(packColors[role], `COLOR_ROLES key "${role}" must exist in config/branding.neutral.json theme.ui.colors`);
179
+ assert.equal(fallback.toLowerCase(), String(packColors[role]).toLowerCase(),
180
+ `the "${role}" fallback must equal the neutral pack's value — a divergent literal is a hardcoded brand`);
181
+ }
182
+ // …and no OTHER colour is hardcoded. The one exemption is the macOS template
183
+ // black, which is an OS requirement (see MAC_TEMPLATE_BLACK above), so it is
184
+ // also pinned to appear ONLY in the template palette.
185
+ const allowed = new Set([...Object.values(roles).map((h) => h.toLowerCase()), MAC_TEMPLATE_BLACK]);
186
+ const withoutGrids = code.replace(declBlock('GRIDS'), '');
187
+ for (const hex of withoutGrids.match(/#[0-9a-fA-F]{6}\b/g) || []) {
188
+ assert.ok(allowed.has(hex.toLowerCase()), `hardcoded colour ${hex} — pull it from theme.ui.colors instead`);
189
+ }
190
+ const macBlock = declBlock('MAC');
191
+ const macHexes = [...macBlock.matchAll(/'(#[0-9a-fA-F]{6})'/g)].map(([, h]) => h.toLowerCase());
192
+ assert.ok(macHexes.length > 0, 'the macOS template palette must state its own black');
193
+ for (const hex of macHexes) {
194
+ assert.equal(hex, MAC_TEMPLATE_BLACK, 'a *Template.png must be pure black — the OS recolours it');
195
+ }
196
+ const nonMacBlack = withoutGrids.replace(macBlock, '').match(/#0{6}\b/g) || [];
197
+ assert.equal(nonMacBlack.length, 0, 'black outside the macOS template palette is a hardcoded colour, not an OS rule');
198
+ });
199
+
200
+ test('a pack filling the documented theme surface leaves NO neutral colour on the canvas', () => {
201
+ // The hole a fallback-equals-neutral check cannot see. A role is only safe to
202
+ // fall back on if the host pack author was PROMPTED to define it — and the
203
+ // prompt is gen-brand.js's :root emission, the app's own theme surface. A role
204
+ // outside it (this is what `sea` did to the DMG backdrop) keeps its Cloud
205
+ // Bongos value even in a fully-specified host build.
206
+ const genBrand = fs.readFileSync(path.join(APP_DIR, 'scripts', 'gen-brand.js'), 'utf8');
207
+ const themeCss = genBrand.match(/function buildThemeCss\([\s\S]*?\n\}/);
208
+ assert.ok(themeCss, 'gen-brand.js must declare buildThemeCss — it defines the app theme surface');
209
+ const documented = new Set([...themeCss[0].matchAll(/\bc\.([A-Za-z]+)\b/g)].map(([, k]) => k));
210
+ const roles = colorRoles();
211
+ for (const role of Object.keys(roles)) {
212
+ assert.ok(documented.has(role),
213
+ `COLOR_ROLES key "${role}" is not on the documented theme surface (gen-brand.js buildThemeCss) — ` +
214
+ 'a host pack has no prompt to define it, so its neutral fallback would paint a Cloud Bongos colour ' +
215
+ 'into a fully-specified host build');
216
+ }
217
+ // …and nothing reaches the canvas except through that map, so filling the
218
+ // documented roles overrides every literal in the file.
219
+ for (const [, key] of code.matchAll(/\bp\.([A-Za-z]+)\b/g)) {
220
+ assert.ok(roles[key], `the render draws with p.${key}, which COLOR_ROLES does not declare`);
221
+ }
222
+ const used = new Set([...code.matchAll(/\bp\.([A-Za-z]+)\b/g)].map(([, k]) => k));
223
+ for (const role of Object.keys(roles)) {
224
+ assert.ok(used.has(role), `COLOR_ROLES declares "${role}" but nothing draws with it — drop it or use it`);
225
+ }
226
+ });
227
+
228
+ test('the generator carries no host-brand motif of its own', () => {
229
+ for (const re of [/off the boats/i, /amazonprimea/i, /shipping[- ]container/i, /greek/i, /meander/i, /parchment/i]) {
230
+ assert.doesNotMatch(code, re, `host-brand literal ${re} must not appear in the generator's code`);
231
+ }
232
+ });
233
+
234
+ test('the four tray states are 16x16 and differ in SHAPE, not only colour', () => {
235
+ // The macOS template icons are monochrome — the OS throws the colour away — so
236
+ // two states that differ only in their palette are the SAME icon on a Mac.
237
+ const grids = trayGrids();
238
+ assert.deepEqual(Object.keys(grids).sort(), [...STATES].sort(), 'exactly the four tray states');
239
+ for (const [state, grid] of Object.entries(grids)) {
240
+ assert.equal(grid.length, 16, `${state} grid is 16 rows`);
241
+ for (const row of grid) assert.equal(row.length, 16, `${state} grid rows are 16 cells`);
242
+ }
243
+ const silhouette = (grid) => grid.join('\n').replace(/[^.]/g, '#');
244
+ for (let i = 0; i < STATES.length; i++) {
245
+ for (let j = i + 1; j < STATES.length; j++) {
246
+ assert.notEqual(silhouette(grids[STATES[i]]), silhouette(grids[STATES[j]]),
247
+ `tray states "${STATES[i]}" and "${STATES[j]}" share one silhouette — indistinguishable as macOS templates`);
248
+ }
249
+ }
250
+ });
251
+
252
+ test('every mark the art draws has a colour, and every colour marks something', () => {
253
+ // A mark with no palette entry renders as a hole; a palette entry with no mark
254
+ // is dead weight that outlives the shape it was for.
255
+ const grids = trayGrids();
256
+ const macInks = new Set(Object.keys(inkRoles(declBlock('MAC').replace(/'#[0-9a-fA-F]{6}'/g, 'p.macBlack'))));
257
+ assert.equal(sorted(macInks), sorted(new Set(STATES.flatMap((s) => [...inksOf(grids[s])]))),
258
+ 'the macOS template palette must cover exactly the inks the grids use');
259
+ const win = declBlock('WIN');
260
+ for (const [, state, block] of win.matchAll(/(off|on|busy|alert):\s*(\{[^}]*\})/g)) {
261
+ assert.equal(sorted(new Set(Object.keys(inkRoles(block)))), sorted(inksOf(grids[state])),
262
+ `the win-${state} palette must cover exactly the inks the ${state} grid uses`);
263
+ }
264
+ // Same contract for the app icon, whose grid is built imperatively.
265
+ const appGrid = code.slice(code.indexOf('function buildAppGrid('), code.indexOf('async function main('));
266
+ const appInks = new Set([...appGrid.matchAll(/'([a-z])'/g)].map(([, c]) => c));
267
+ assert.equal(sorted(appInks), sorted(new Set(Object.keys(inkRoles(declBlock('APP_PALETTE'))))),
268
+ 'the app palette must cover exactly the inks buildAppGrid draws');
269
+ });
270
+
271
+ test('the committed PNGs are the 13 files the app and electron-builder load', () => {
272
+ const dims = (file) => {
273
+ const buf = fs.readFileSync(file);
274
+ assert.equal(buf.subarray(1, 4).toString('latin1'), 'PNG', `${path.basename(file)} is a PNG`);
275
+ return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
276
+ };
277
+ // electron-builder derives .icns/.ico from this one, so it must stay 1024².
278
+ assert.deepEqual(dims(path.join(BUILD_DIR, 'icon.png')), [1024, 1024]);
279
+ for (const state of STATES) {
280
+ assert.deepEqual(dims(path.join(TRAY_DIR, `mac-${state}Template.png`)), [16, 16]);
281
+ assert.deepEqual(dims(path.join(TRAY_DIR, `mac-${state}Template@2x.png`)), [32, 32]);
282
+ assert.deepEqual(dims(path.join(TRAY_DIR, `win-${state}.png`)), [32, 32]);
283
+ }
284
+ });
285
+
286
+ test('the committed art is the NEUTRAL render — no host palette was checked in', () => {
287
+ // The source checks above prove the generator READS the pack; only the pixels
288
+ // prove the committed artifact is the vanilla one. Someone rendering with their
289
+ // own config/branding.json present and committing the result is exactly the
290
+ // failure this catches.
291
+ const packValues = new Set(Object.values(neutral.theme.ui.colors).map((h) => String(h).toLowerCase()));
292
+ for (const state of STATES) {
293
+ for (const file of [`mac-${state}Template.png`, `mac-${state}Template@2x.png`]) {
294
+ assert.deepEqual([...opaqueColors(path.join(TRAY_DIR, file))], [MAC_TEMPLATE_BLACK],
295
+ `${file} must be pure black + alpha — the OS recolours a template icon`);
296
+ }
297
+ for (const hex of opaqueColors(path.join(TRAY_DIR, `win-${state}.png`))) {
298
+ assert.ok(packValues.has(hex), `win-${state}.png paints ${hex}, which is not a config/branding.neutral.json colour`);
299
+ }
300
+ }
301
+ for (const hex of opaqueColors(path.join(BUILD_DIR, 'icon.png'))) {
302
+ assert.ok(packValues.has(hex), `icon.png paints ${hex}, which is not a config/branding.neutral.json colour`);
303
+ }
304
+ });