@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
package/tests/upgrade.mjs CHANGED
@@ -796,6 +796,145 @@ t('runUpgrade: no managed restart + no --health-url proceeds but flags the bump
796
796
  assert.ok(logs.some((m) => /not confirmed serving/.test(m)), logs.join('\n'));
797
797
  });
798
798
 
799
+ // ---- regenerateNavDocs (ADR 0147 / task 1002314) ----------------------------
800
+ t('regenerateNavDocs: default set is the whole-file nav docs (session-index only)', () => {
801
+ assert.deepEqual(u.NAV_WHOLE_FILE_GENERATORS, ['gen-session-index.js']);
802
+ });
803
+
804
+ t('regenerateNavDocs: spawns each generator from the CORE against the INSTANCE root (cwd + CLOUDBONGOS_INSTANCE_ROOT), ok when all succeed', () => {
805
+ const calls = [];
806
+ const run = (cmd, args, opts) => { calls.push({ cmd, arg0: args[0], cwd: opts && opts.cwd, env: opts && opts.env }); return { status: 0 }; };
807
+ const fsImpl = { existsSync: () => true };
808
+ const res = u.regenerateNavDocs({ instanceDir: '/inst', coreRoot: '/core', generators: ['gen-session-index.js'] }, run, fsImpl);
809
+ assert.equal(res.ok, true);
810
+ assert.deepEqual(res.ran, ['gen-session-index.js']);
811
+ assert.deepEqual(res.failed, []);
812
+ assert.equal(calls.length, 1);
813
+ assert.equal(calls[0].cwd, '/inst', 'runs with cwd = the instance dir');
814
+ assert.equal(calls[0].env.CLOUDBONGOS_INSTANCE_ROOT, '/inst', 'sets the instance-root env so the generator writes into the instance');
815
+ assert.equal(calls[0].arg0, join('/core', 'scripts', 'gds', 'gen-session-index.js'), 'spawns the generator from the CORE package');
816
+ });
817
+
818
+ t('regenerateNavDocs: a MISSING generator is recorded as failed but the others still run (best-effort)', () => {
819
+ const spawned = [];
820
+ const run = (_cmd, args) => { spawned.push(args[0]); return { status: 0 }; };
821
+ const fsImpl = { existsSync: (p) => !String(p).endsWith('gen-missing.js') };
822
+ const res = u.regenerateNavDocs({ instanceDir: '/inst', coreRoot: '/core', generators: ['gen-missing.js', 'gen-session-index.js'] }, run, fsImpl);
823
+ assert.equal(res.ok, false);
824
+ assert.deepEqual(res.ran, ['gen-session-index.js']);
825
+ assert.equal(res.failed.length, 1);
826
+ assert.equal(res.failed[0].gen, 'gen-missing.js');
827
+ assert.equal(spawned.length, 1, 'the missing generator is not spawned; the present one still is');
828
+ });
829
+
830
+ t('regenerateNavDocs: a generator that exits nonzero is LOUD-failed (ok:false) without aborting the rest', () => {
831
+ const run = (_cmd, args) => ({ status: String(args[0]).endsWith('gen-boom.js') ? 2 : 0 });
832
+ const fsImpl = { existsSync: () => true };
833
+ const res = u.regenerateNavDocs({ instanceDir: '/inst', coreRoot: '/core', generators: ['gen-boom.js', 'gen-session-index.js'] }, run, fsImpl);
834
+ assert.equal(res.ok, false);
835
+ assert.deepEqual(res.ran, ['gen-session-index.js']);
836
+ assert.equal(res.failed.length, 1);
837
+ assert.equal(res.failed[0].gen, 'gen-boom.js');
838
+ assert.match(res.failed[0].reason, /exited 2/);
839
+ });
840
+
841
+ // ---- healDocAssets (task 1002441) -------------------------------------------
842
+ // An upgrade replaces node_modules wholesale, so it WIPES the two gitignored, core-scoped
843
+ // generated doc assets (atlas.json + the diagram .svg/.png renders) and the new tarball carries
844
+ // only the .mmd sources. These pin that the upgrade puts them back.
845
+
846
+ // A fake core tree. `renders` controls whether each .mmd already has its .svg + .png beside it.
847
+ function docAssetFs({ atlas = true, sources = ['01-a.mmd', '02-b.mmd'], renders = true } = {}) {
848
+ const present = new Set();
849
+ if (atlas) present.add(join('/core', 'modules', 'hall-ui', 'public', 'atlas.json'));
850
+ present.add(join('/core', 'scripts', 'gds', 'gen-atlas.js'));
851
+ present.add(join('/core', 'scripts', 'render-diagrams.sh'));
852
+ if (renders) {
853
+ for (const s of sources) {
854
+ const base = join('/core', 'docs', 'onboarding', 'diagrams', s.replace(/\.mmd$/, ''));
855
+ present.add(`${base}.svg`); present.add(`${base}.png`);
856
+ }
857
+ }
858
+ return { existsSync: (p) => present.has(String(p)), readdirSync: () => sources, _present: present };
859
+ }
860
+
861
+ t('missingDocAssets: a freshly-upgraded core (only .mmd sources, no atlas.json) reports BOTH groups missing', () => {
862
+ const fsImpl = docAssetFs({ atlas: false, renders: false });
863
+ assert.deepEqual(u.missingDocAssets('/core', fsImpl), ['atlas', 'diagrams']);
864
+ });
865
+
866
+ t('missingDocAssets: a fully-generated core reports nothing missing (steady state — no work)', () => {
867
+ assert.deepEqual(u.missingDocAssets('/core', docAssetFs()), []);
868
+ });
869
+
870
+ t('missingDocAssets: only atlas.json gone → only the atlas group', () => {
871
+ assert.deepEqual(u.missingDocAssets('/core', docAssetFs({ atlas: false })), ['atlas']);
872
+ });
873
+
874
+ t('missingDocAssets: a half-rendered diagram set (one .mmd lacking its .png) counts as missing', () => {
875
+ const fsImpl = docAssetFs();
876
+ fsImpl._present.delete(join('/core', 'docs', 'onboarding', 'diagrams', '02-b.png'));
877
+ assert.deepEqual(u.missingDocAssets('/core', fsImpl), ['diagrams']);
878
+ });
879
+
880
+ t('missingDocAssets: a core with NO diagram dir does not report diagrams (readdir throws → nothing to render)', () => {
881
+ const fsImpl = docAssetFs();
882
+ fsImpl.readdirSync = () => { throw new Error('ENOENT'); };
883
+ assert.deepEqual(u.missingDocAssets('/core', fsImpl), []);
884
+ });
885
+
886
+ t('healDocAssets: the post-upgrade case regenerates BOTH, node for the atlas and bash for the renders, cwd = coreRoot', () => {
887
+ const calls = [];
888
+ const run = (cmd, args, opts) => { calls.push({ cmd, arg0: args[0], cwd: opts && opts.cwd }); return { status: 0 }; };
889
+ const res = u.healDocAssets({ coreRoot: '/core' }, run, docAssetFs({ atlas: false, renders: false }));
890
+ assert.equal(res.ok, true);
891
+ assert.deepEqual(res.ran, ['atlas', 'diagrams']);
892
+ assert.equal(calls.length, 2);
893
+ assert.equal(calls[0].cmd, process.execPath, 'atlas generator runs under node');
894
+ assert.equal(calls[0].arg0, join('/core', 'scripts', 'gds', 'gen-atlas.js'));
895
+ assert.equal(calls[1].cmd, 'bash', 'the diagram renderer is a shell script');
896
+ assert.equal(calls[1].arg0, join('/core', 'scripts', 'render-diagrams.sh'));
897
+ assert.ok(calls.every((c) => c.cwd === '/core'), 'both run against the CORE root, where the assets live');
898
+ });
899
+
900
+ t('healDocAssets: nothing missing → spawns NOTHING (safe to call from a deploy loop)', () => {
901
+ let spawned = 0;
902
+ const res = u.healDocAssets({ coreRoot: '/core' }, () => { spawned++; return { status: 0 }; }, docAssetFs());
903
+ assert.equal(spawned, 0);
904
+ assert.deepEqual(res.missing, []);
905
+ assert.equal(res.ok, true);
906
+ });
907
+
908
+ t('healDocAssets: only the MISSING group is regenerated (atlas present → renderer only)', () => {
909
+ const spawned = [];
910
+ const run = (_c, args) => { spawned.push(args[0]); return { status: 0 }; };
911
+ const res = u.healDocAssets({ coreRoot: '/core' }, run, docAssetFs({ renders: false }));
912
+ assert.deepEqual(res.ran, ['diagrams']);
913
+ assert.equal(spawned.length, 1);
914
+ assert.match(String(spawned[0]), /render-diagrams\.sh$/);
915
+ });
916
+
917
+ t('healDocAssets: a failing renderer (no Chromium) is LOUD-failed but the atlas still heals — never aborts the bump', () => {
918
+ const run = (_c, args) => ({ status: String(args[0]).endsWith('render-diagrams.sh') ? 1 : 0 });
919
+ const res = u.healDocAssets({ coreRoot: '/core' }, run, docAssetFs({ atlas: false, renders: false }));
920
+ assert.equal(res.ok, false, 'reported as incomplete');
921
+ assert.deepEqual(res.ran, ['atlas'], 'the independent atlas still regenerated');
922
+ assert.equal(res.failed.length, 1);
923
+ assert.equal(res.failed[0].group, 'diagrams');
924
+ assert.match(res.failed[0].reason, /exited 1/);
925
+ });
926
+
927
+ t('healDocAssets: a generator absent from this core is recorded, not spawned', () => {
928
+ const fsImpl = docAssetFs({ atlas: false, renders: false });
929
+ fsImpl._present.delete(join('/core', 'scripts', 'render-diagrams.sh'));
930
+ const spawned = [];
931
+ const res = u.healDocAssets({ coreRoot: '/core' }, (_c, args) => { spawned.push(args[0]); return { status: 0 }; }, fsImpl);
932
+ assert.equal(res.ok, false);
933
+ assert.deepEqual(res.ran, ['atlas']);
934
+ assert.match(res.failed[0].reason, /not found in core/);
935
+ assert.equal(spawned.length, 1, 'the absent script is never spawned');
936
+ });
937
+
799
938
  await Promise.all(pending);
800
939
  console.log(`\nupgrade: ${passed} passed, ${failed} failed`);
801
940
  process.exit(failed ? 1 : 0);
@@ -0,0 +1,162 @@
1
+ // tests/web_admission_status.mjs — auth.webAdmissionStatus (task 1002097).
2
+ //
3
+ // The stub must go into require.cache BEFORE auth.js loads: auth.js destructures
4
+ // accessStateByLogin at require time, so patching the export object afterwards
5
+ // would not rebind what it already captured.
6
+ //
7
+ // Run: node tests/web_admission_status.mjs
8
+
9
+ import { strict as assert } from 'node:assert';
10
+ import { createRequire } from 'node:module';
11
+
12
+ const require = createRequire(import.meta.url);
13
+
14
+ // --- stub the kernel BEFORE auth.js captures its bindings ------------------
15
+ const kernelPath = require.resolve('../src/bongos/db-kernel.js');
16
+ const realKernel = require(kernelPath);
17
+
18
+ let dbState = { member: false, invited: false, pending: false };
19
+ let dbThrows = false;
20
+
21
+ require.cache[kernelPath].exports = {
22
+ ...realKernel,
23
+ accessStateByLogin: async () => {
24
+ if (dbThrows) throw new Error('simulated DB outage');
25
+ return dbState;
26
+ },
27
+ };
28
+
29
+ const auth = require('../src/bongos/auth.js');
30
+
31
+ const FIRST_ADMIN = 'ownerlogin';
32
+ // OTB is a permanent entry in LEGACY_ENV_PREFIXES, so this resolves regardless
33
+ // of the instance's own env prefix — and regardless of whether the branding
34
+ // pack configures a firstAdmin (this repo's does not).
35
+ process.env.OTB_FIRST_ADMIN = FIRST_ADMIN;
36
+
37
+ function setDb({ member = false, invited = false, pending = false } = {}) {
38
+ dbState = { member, invited, pending };
39
+ dbThrows = false;
40
+ }
41
+
42
+ function withOpenEnrollment(val, fn) {
43
+ const prev = process.env.OTB_OPEN_XENOS_ENROLLMENT;
44
+ if (val === undefined) delete process.env.OTB_OPEN_XENOS_ENROLLMENT;
45
+ else process.env.OTB_OPEN_XENOS_ENROLLMENT = val;
46
+ return Promise.resolve()
47
+ .then(fn)
48
+ .finally(() => {
49
+ if (prev === undefined) delete process.env.OTB_OPEN_XENOS_ENROLLMENT;
50
+ else process.env.OTB_OPEN_XENOS_ENROLLMENT = prev;
51
+ });
52
+ }
53
+
54
+ let passed = 0;
55
+ let failed = 0;
56
+ async function t(name, fn) {
57
+ try { await fn(); passed++; console.log(` ok ${name}`); }
58
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
59
+ }
60
+
61
+ await t('a blank or missing login is never admitted', async () => {
62
+ setDb({ member: true });
63
+ for (const bad of ['', ' ', null, undefined]) {
64
+ assert.deepEqual(await auth.webAdmissionStatus(bad), { admitted: false, expectedSoon: false });
65
+ }
66
+ });
67
+
68
+ await t('an existing builder is admitted', async () => {
69
+ setDb({ member: true });
70
+ assert.deepEqual(await auth.webAdmissionStatus('someone'), { admitted: true, expectedSoon: true });
71
+ });
72
+
73
+ await t('an invited login is admitted', async () => {
74
+ setDb({ invited: true });
75
+ assert.deepEqual(await auth.webAdmissionStatus('someone'), { admitted: true, expectedSoon: true });
76
+ });
77
+
78
+ await t('PENDING is the keep-polling case: not admitted, but expected soon', async () => {
79
+ // The whole point of the waiting page — an Archon is about to flip this to
80
+ // invited, so the visitor should wait rather than see a rejection.
81
+ setDb({ pending: true });
82
+ assert.deepEqual(await auth.webAdmissionStatus('someone'), { admitted: false, expectedSoon: true });
83
+ });
84
+
85
+ await t('a genuine stranger is neither admitted nor expected', async () => {
86
+ // Must be false, or the stranger gets a spinner that never resolves instead
87
+ // of the request-access page.
88
+ setDb({});
89
+ assert.deepEqual(await auth.webAdmissionStatus('nobody'), { admitted: false, expectedSoon: false });
90
+ });
91
+
92
+ await t('the configured firstAdmin is admitted with nothing in the DB', async () => {
93
+ // The bootstrap window this task was filed for: a fresh instance where the
94
+ // owner has no builder row and no access_request yet.
95
+ setDb({});
96
+ assert.deepEqual(await auth.webAdmissionStatus(FIRST_ADMIN), { admitted: true, expectedSoon: true });
97
+ });
98
+
99
+ await t('the firstAdmin match is case-insensitive and trims', async () => {
100
+ setDb({});
101
+ assert.equal((await auth.webAdmissionStatus('OwNerLogin')).admitted, true);
102
+ assert.equal((await auth.webAdmissionStatus(` ${FIRST_ADMIN.toUpperCase()} `)).admitted, true);
103
+ });
104
+
105
+ await t('open enrollment admits anyone', async () => {
106
+ setDb({});
107
+ await withOpenEnrollment('1', async () => {
108
+ assert.deepEqual(await auth.webAdmissionStatus('anyone'), { admitted: true, expectedSoon: true });
109
+ });
110
+ });
111
+
112
+ await t('a DB outage fails CLOSED on admitted', async () => {
113
+ // Never let an error read as "you're in".
114
+ dbThrows = true;
115
+ // The helper logs the outage by design; mute it so the unit gate's output
116
+ // stays readable (the assertions below are what prove the behaviour).
117
+ const realError = console.error;
118
+ console.error = () => {};
119
+ try {
120
+ const stranger = await auth.webAdmissionStatus('nobody');
121
+ assert.equal(stranger.admitted, false);
122
+ assert.equal(stranger.expectedSoon, false);
123
+ // ...but the owner may still keep polling through a transient outage, since
124
+ // the bootstrap window is exactly when the DB may be settling.
125
+ const owner = await auth.webAdmissionStatus(FIRST_ADMIN);
126
+ assert.equal(owner.admitted, false);
127
+ assert.equal(owner.expectedSoon, true);
128
+ } finally {
129
+ console.error = realError;
130
+ dbThrows = false;
131
+ }
132
+ });
133
+
134
+ await t('ANTI-DRIFT: admitted always equals the real gate\'s verdict', async () => {
135
+ // The probe must never disagree with firstTimeAdmissionBlocked — that is the
136
+ // one gate both sign-in flows call. Sweep the full input space.
137
+ for (const member of [false, true]) {
138
+ for (const invited of [false, true]) {
139
+ for (const openEnroll of [undefined, '1']) {
140
+ for (const login of ['nobody', FIRST_ADMIN]) {
141
+ setDb({ member, invited });
142
+ await withOpenEnrollment(openEnroll, async () => {
143
+ const probe = await auth.webAdmissionStatus(login);
144
+ const blocked = auth.firstTimeAdmissionBlocked({
145
+ existing: member ? { id: 1 } : null,
146
+ approved: invited,
147
+ login,
148
+ });
149
+ assert.equal(
150
+ probe.admitted, !blocked,
151
+ `drift: member=${member} invited=${invited} open=${openEnroll} login=${login} `
152
+ + `→ probe.admitted=${probe.admitted} but gate blocked=${blocked}`,
153
+ );
154
+ });
155
+ }
156
+ }
157
+ }
158
+ }
159
+ });
160
+
161
+ console.log(`\nweb admission status (task 1002097): ${passed} passed, ${failed} failed`);
162
+ process.exit(failed > 0 ? 1 : 0);
@@ -0,0 +1,110 @@
1
+ // tests/write_validation_gaps.mjs — the three write routes the old route scanner
2
+ // could not see (BV1.R109c, task 1002552).
3
+ //
4
+ // ADR 0118 / R12 requires every body-reading write route to call validateOrRespond.
5
+ // Three routes were exempt not by decision but by accident: the route-discovery
6
+ // regex required the method and path on the SAME line, and these three are declared
7
+ // multi-line, so the fitness check never saw them. BV1.R109b fixed discovery and
8
+ // recorded them in a named, non-growing WRITE_VALIDATION_BASELINE; this closes them
9
+ // and empties that list.
10
+ //
11
+ // The risk being guarded here is NOT "is there a schema" (fitness asserts that) but
12
+ // "does the schema still accept exactly what the live contract accepted". Each route
13
+ // hand-coerced its one field, so a too-strict schema would silently break a caller.
14
+ import assert from 'node:assert/strict';
15
+ import { test } from 'node:test';
16
+ import { createRequire } from 'node:module';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
+ const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
24
+
25
+ test('the baseline is empty — the invariant is back to hard-zero', () => {
26
+ const fit = require(path.join(ROOT, 'scripts', 'gds', 'fitness.js'));
27
+ const baseline = fit.WRITE_VALIDATION_BASELINE;
28
+ if (baseline !== undefined) {
29
+ assert.equal(baseline.size, 0, `still-exempt write routes: ${[...baseline].join(', ')}`);
30
+ } else {
31
+ // Deleted outright rather than emptied — also acceptable, and the fitness
32
+ // check below is the real assertion either way.
33
+ assert.doesNotMatch(read('scripts/gds/fitness.js'), /'POST \/inbox\/:id\/ratify'/,
34
+ 'the ratify entry must be gone from the baseline');
35
+ }
36
+ });
37
+
38
+ test('fitness reports ZERO unvalidated write routes', () => {
39
+ const fit = require(path.join(ROOT, 'scripts', 'gds', 'fitness.js'));
40
+ assert.equal(typeof fit.scanUnvalidatedWriteRoutes, 'function',
41
+ 'the scanner must stay exported so this cannot silently stop being checked');
42
+ const unvalidated = fit.scanUnvalidatedWriteRoutes();
43
+ assert.deepEqual(unvalidated, [], `unvalidated write routes: ${JSON.stringify(unvalidated)}`);
44
+ });
45
+
46
+ test('each of the three routes now calls validateOrRespond', () => {
47
+ const inbox = read('modules/ideas/routes/inbox.js');
48
+ const reports = read('modules/security/routes/reports.js');
49
+ // ratify
50
+ const ratify = inbox.slice(inbox.indexOf("'/inbox/:id/ratify'"));
51
+ assert.match(ratify.slice(0, 900), /validateOrRespond\(req, res, \{[\s\S]*notes_md/, 'ratify must validate notes_md');
52
+ // confirm
53
+ const confirm = reports.slice(reports.indexOf("'/security/reports/:id/confirm'"));
54
+ assert.match(confirm.slice(0, 1200), /validateOrRespond\(req, res, \{[\s\S]*drachmae_awarded/, 'confirm must validate drachmae_awarded');
55
+ // link-fix
56
+ const linkFix = reports.slice(reports.indexOf("'/security/reports/:id/link-fix'"));
57
+ assert.match(linkFix.slice(0, 1200), /validateOrRespond\(req, res, \{[\s\S]*task_id/, 'link-fix must validate task_id');
58
+ });
59
+
60
+ // ---- contract preservation: the schemas must not narrow what was accepted ----
61
+ //
62
+ // These assert the SHAPE of each declaration, because the live coercion happens in
63
+ // the handler and a type= on the schema would pre-empt it. A regression here means
64
+ // a caller that worked yesterday starts getting a 400.
65
+
66
+ test('drachmae_awarded stays UNTYPED — number, stringified number, "" and null all still reach the handler', () => {
67
+ const reports = read('modules/security/routes/reports.js');
68
+ const confirm = reports.slice(reports.indexOf("'/security/reports/:id/confirm'"), reports.indexOf("'/security/reports/:id/dispute'"));
69
+ assert.match(confirm, /drachmae_awarded:\s*\{\s*\}/,
70
+ 'declaring a type would reject the stringified/"" forms the handler accepts and coerces');
71
+ // The inline check remains the thing that enforces finite/non-negative.
72
+ assert.match(confirm, /Number\.isFinite\(n\)/, 'the handler-side numeric check must survive');
73
+ assert.match(confirm, /bad_drachmae_awarded/, 'its error code must be unchanged');
74
+ });
75
+
76
+ test('task_id stays REQUIRED but untyped — the stringified form still works', () => {
77
+ const reports = read('modules/security/routes/reports.js');
78
+ const linkFix = reports.slice(reports.indexOf("'/security/reports/:id/link-fix'"));
79
+ assert.match(linkFix, /task_id:\s*\{\s*required:\s*true\s*\}/,
80
+ 'same idiom as POST /tasks/:id/dependencies — required, coerced in-handler');
81
+ assert.match(linkFix, /Number\.isInteger\(taskId\)/, 'the handler-side integer check must survive');
82
+ assert.match(linkFix, /bad_task_id/, 'its error code must be unchanged');
83
+ });
84
+
85
+ test('notes_md stays OPTIONAL — a ratify with no body still works', () => {
86
+ const inbox = read('modules/ideas/routes/inbox.js');
87
+ const ratify = inbox.slice(inbox.indexOf("'/inbox/:id/ratify'"));
88
+ const decl = ratify.slice(0, 900);
89
+ assert.doesNotMatch(decl, /notes_md:\s*\{[^}]*required:\s*true/,
90
+ 'notes_md was optional (absent => empty string) and must stay optional');
91
+ assert.match(ratify, /typeof req\.body\.notes_md === 'string'/,
92
+ 'the absent-to-empty-string coercion must survive');
93
+ });
94
+
95
+ test('validation runs BEFORE the handler consumes the field', () => {
96
+ // A schema declared after the read would validate nothing on the path that matters.
97
+ for (const [file, route, field] of [
98
+ ['modules/ideas/routes/inbox.js', "'/inbox/:id/ratify'", 'notes_md'],
99
+ ['modules/security/routes/reports.js', "'/security/reports/:id/link-fix'", 'task_id'],
100
+ ]) {
101
+ const src = read(file);
102
+ const body = src.slice(src.indexOf(route));
103
+ const validateAt = body.indexOf('validateOrRespond');
104
+ const readAt = body.indexOf(`req.body?.${field}`) >= 0
105
+ ? body.indexOf(`req.body?.${field}`)
106
+ : body.indexOf(`req.body.${field}`);
107
+ assert.ok(validateAt > 0 && validateAt < readAt,
108
+ `${route}: validateOrRespond must precede the req.body.${field} read`);
109
+ }
110
+ });