@bongos/core 1.18.0 → 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 (553) hide show
  1. package/.bongos-core.json +1740 -640
  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 +5 -3
  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 +11 -2
  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/0142-project-context-is-prose-only.md +41 -0
  27. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  28. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  29. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  30. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  31. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  32. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  33. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  34. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  35. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  36. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  37. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  38. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  39. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  40. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  41. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  42. package/docs/adr/README.md +15 -2
  43. package/docs/api/openapi.json +2648 -483
  44. package/docs/api-reference.md +73 -13
  45. package/docs/architecture.md +4 -2
  46. package/docs/branding-contract.md +3 -1
  47. package/docs/canonical-permissions.md +21 -8
  48. package/docs/file-map.md +15 -7
  49. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  50. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  51. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  52. package/docs/onboarding/diagrams/assertions.json +4 -7
  53. package/docs/onboarding/drachmae-and-karma.md +3 -3
  54. package/docs/onboarding/primer.md +17 -16
  55. package/docs/onboarding/slash-commands.md +124 -0
  56. package/docs/recipes/builder-box-lifecycle.md +23 -0
  57. package/docs/recipes/devbox-app-release.md +14 -4
  58. package/docs/recipes/instance-first-run.md +16 -0
  59. package/docs/recipes/local-dev.md +81 -0
  60. package/docs/recipes/ops-gotchas.md +19 -1
  61. package/docs/recipes/overnight-builder.md +1 -1
  62. package/docs/recipes/packaging-the-core.md +13 -0
  63. package/docs/recipes/parallel-goal-waves.md +174 -0
  64. package/docs/recipes/ship-and-regrade.md +2 -2
  65. package/docs/recipes/standalone-live-docs.md +48 -0
  66. package/docs/recipes/upgrading-the-core.md +5 -2
  67. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  68. package/migrations/003_pms.sql +6 -7
  69. package/migrations/006_pms_v2.sql +3 -31
  70. package/migrations/039_newcomer_friendly_column.sql +5 -0
  71. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  72. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  73. package/migrations/core_193_task_needs_rebase.sql +28 -0
  74. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  75. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  76. package/migrations/core_196_session_reward_topup.sql +69 -0
  77. package/modules/autonomy/pollers/routine-timers.js +9 -8
  78. package/modules/builder-settings/wandering-prefs.js +61 -18
  79. package/modules/dev-box/app/README.md +73 -0
  80. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  81. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  82. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  83. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  84. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  85. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  86. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  87. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  88. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  89. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  90. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  91. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  92. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  93. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  94. package/modules/dev-box/app/build-resources/icon.png +0 -0
  95. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  96. package/modules/dev-box/app/electron-builder.yml +95 -0
  97. package/modules/dev-box/app/package-lock.json +5291 -0
  98. package/modules/dev-box/app/package.json +30 -0
  99. package/modules/dev-box/app/renderer/app.js +319 -0
  100. package/modules/dev-box/app/renderer/index.html +45 -0
  101. package/modules/dev-box/app/renderer/styles.css +246 -0
  102. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  103. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  104. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  105. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  106. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  107. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  108. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  109. package/modules/dev-box/app/src/box-controller.js +287 -0
  110. package/modules/dev-box/app/src/brand.generated.json +13 -0
  111. package/modules/dev-box/app/src/brand.js +16 -0
  112. package/modules/dev-box/app/src/config.js +19 -0
  113. package/modules/dev-box/app/src/gds-client.js +90 -0
  114. package/modules/dev-box/app/src/main.js +610 -0
  115. package/modules/dev-box/app/src/pairing.js +57 -0
  116. package/modules/dev-box/app/src/preload.js +29 -0
  117. package/modules/dev-box/app/src/session-store.js +58 -0
  118. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  119. package/modules/dev-box/app/src/task-picker.js +70 -0
  120. package/modules/dev-box/app/src/tray.js +99 -0
  121. package/modules/dev-box/app/src/updater.js +55 -0
  122. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  123. package/modules/dev-box/app/test/unit.mjs +526 -0
  124. package/modules/dev-box/box-access.js +121 -4
  125. package/modules/dev-box/boxes.js +20 -0
  126. package/modules/dev-box/module.json +1 -1
  127. package/modules/dev-box/routes/box.js +87 -3
  128. package/modules/economy/credits.js +116 -25
  129. package/modules/economy/reward.js +8 -0
  130. package/modules/economy/streak.js +2 -1
  131. package/modules/governance/CLAUDE.md +15 -0
  132. package/modules/governance/catalog.js +419 -0
  133. package/modules/governance/config.js +174 -0
  134. package/modules/governance/db.js +125 -0
  135. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  136. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  137. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  138. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  139. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  140. package/modules/governance/module.json +10 -0
  141. package/modules/governance/protected-surfaces.js +88 -0
  142. package/modules/governance/protected-surfaces.json +80 -0
  143. package/modules/governance/rank-roles.js +83 -0
  144. package/modules/governance/resolver.js +221 -0
  145. package/modules/governance/routes/governance.js +89 -0
  146. package/modules/governance/routes/role-sync.js +49 -0
  147. package/modules/governance/session-scopes.js +127 -0
  148. package/modules/governance/session-scopes.json +38 -0
  149. package/modules/hall-ui/public/atlas.css +117 -0
  150. package/modules/hall-ui/public/atlas.html +3 -114
  151. package/modules/hall-ui/public/brand-apply.js +34 -6
  152. package/modules/hall-ui/public/builders.js +187 -219
  153. package/modules/hall-ui/public/diagrams.html +2 -0
  154. package/modules/hall-ui/public/dom-utils.js +27 -0
  155. package/modules/hall-ui/public/drachmae.html +2 -0
  156. package/modules/hall-ui/public/drachmae.js +54 -1
  157. package/modules/hall-ui/public/gate.css +98 -0
  158. package/modules/hall-ui/public/gate.html +3 -95
  159. package/modules/hall-ui/public/goal-inbox.js +4 -1
  160. package/modules/hall-ui/public/goals-page.js +626 -114
  161. package/modules/hall-ui/public/goals.css +79 -1
  162. package/modules/hall-ui/public/goals.html +38 -11
  163. package/modules/hall-ui/public/hall-widgets.js +62 -52
  164. package/modules/hall-ui/public/harbor.css +123 -0
  165. package/modules/hall-ui/public/harbor.html +3 -120
  166. package/modules/hall-ui/public/index.html +11 -4
  167. package/modules/hall-ui/public/instances-panel.js +4 -1
  168. package/modules/hall-ui/public/modules.css +63 -0
  169. package/modules/hall-ui/public/modules.html +3 -60
  170. package/modules/hall-ui/public/my-projects.html +78 -0
  171. package/modules/hall-ui/public/my-projects.js +142 -0
  172. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  173. package/modules/hall-ui/public/not-ready.html +2 -0
  174. package/modules/hall-ui/public/pair.css +73 -0
  175. package/modules/hall-ui/public/pair.html +3 -70
  176. package/modules/hall-ui/public/palette.js +302 -0
  177. package/modules/hall-ui/public/primer.css +13 -0
  178. package/modules/hall-ui/public/primer.html +2 -0
  179. package/modules/hall-ui/public/profile.html +2 -0
  180. package/modules/hall-ui/public/ranks.html +2 -0
  181. package/modules/hall-ui/public/roadmap.css +179 -0
  182. package/modules/hall-ui/public/roadmap.html +9 -172
  183. package/modules/hall-ui/public/roadmap.js +329 -316
  184. package/modules/hall-ui/public/scouting.html +45 -0
  185. package/modules/hall-ui/public/scouting.js +73 -0
  186. package/modules/hall-ui/public/sessions.css +16 -0
  187. package/modules/hall-ui/public/sessions.html +3 -13
  188. package/modules/hall-ui/public/settings.html +2 -0
  189. package/modules/hall-ui/public/settings.js +4 -4
  190. package/modules/hall-ui/public/shell.js +143 -18
  191. package/modules/hall-ui/public/style.css +231 -39
  192. package/modules/hall-ui/public/task-detail.js +347 -26
  193. package/modules/hall-ui/public/task.html +6 -1
  194. package/modules/hall-ui/public/watch.html +2 -0
  195. package/modules/hall-ui/public/work.html +8 -6
  196. package/modules/hall-ui/public/work.js +1 -1
  197. package/modules/ideas/inbox.js +61 -11
  198. package/modules/ideas/routes/inbox.js +23 -0
  199. package/modules/lifecycle/claim-eligibility.js +72 -0
  200. package/modules/lifecycle/db.js +519 -139
  201. package/modules/lifecycle/done-when.js +18 -3
  202. package/modules/lifecycle/github-push.js +19 -2
  203. package/modules/lifecycle/goal-advisory.js +114 -0
  204. package/modules/lifecycle/goal-authz.js +62 -0
  205. package/modules/lifecycle/lifecycle.js +9 -0
  206. package/modules/lifecycle/publish-reconciler.js +109 -6
  207. package/modules/lifecycle/routes/claims.js +68 -23
  208. package/modules/lifecycle/routes/done-when.js +110 -15
  209. package/modules/lifecycle/routes/goals.js +8 -38
  210. package/modules/lifecycle/routes/tasks.js +38 -6
  211. package/modules/lifecycle/ship-card.js +6 -0
  212. package/modules/onboarding/onboarding-state.js +1 -1
  213. package/modules/platform-identity/CLAUDE.md +15 -0
  214. package/modules/platform-identity/hub-device.js +147 -0
  215. package/modules/platform-identity/hub-keys.js +151 -0
  216. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  217. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  218. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  219. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  220. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  221. package/modules/platform-identity/module.json +18 -0
  222. package/modules/platform-identity/platform-identity.js +531 -0
  223. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  224. package/modules/platform-identity/routes/my-projects.js +140 -0
  225. package/modules/platform-identity/routes/projects.js +107 -0
  226. package/modules/platform-identity/routes/scouting.js +35 -0
  227. package/modules/platform-identity/routes/sso.js +402 -0
  228. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  229. package/modules/platform-identity/ui/.gitkeep +0 -0
  230. package/modules/security/routes/reports.js +10 -0
  231. package/modules/sessions/db.js +166 -21
  232. package/modules/sessions/module.json +1 -1
  233. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  234. package/modules/sessions/routes/sessions.js +33 -0
  235. package/modules/status-ui/public/explore.html +54 -0
  236. package/modules/status-ui/public/explore.js +67 -0
  237. package/package-lock.json +2 -2
  238. package/package.json +1 -1
  239. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  270. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  271. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  272. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  273. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  274. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  275. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  276. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  277. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  278. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  279. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  280. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  281. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  282. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  283. package/public-landing/create.html +252 -62
  284. package/public-landing/gate/README.md +137 -0
  285. package/public-landing/gate/dispmaps/bar.png +0 -0
  286. package/public-landing/gate/dispmaps/flat.png +0 -0
  287. package/public-landing/gate/dispmaps/key.png +0 -0
  288. package/public-landing/gate/index.html +587 -0
  289. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  290. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  291. package/public-landing/gate/space.css +799 -0
  292. package/public-landing/gate/space.js +1826 -0
  293. package/public-landing/gate/sprites/enter.png +0 -0
  294. package/public-landing/gate/sprites/search.png +0 -0
  295. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  296. package/public-landing/gate/sprites/tile.png +0 -0
  297. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  298. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  299. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  300. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  301. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  302. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  303. package/scripts/gds/abandon.js +6 -4
  304. package/scripts/gds/api.js +10 -5
  305. package/scripts/gds/api.ps1 +26 -0
  306. package/scripts/gds/api.sh +6 -0
  307. package/scripts/gds/audit-ships.js +7 -1
  308. package/scripts/gds/backfill-sessions.js +78 -18
  309. package/scripts/gds/backup.js +23 -1
  310. package/scripts/gds/box-connect-lib.js +124 -31
  311. package/scripts/gds/box-sync.js +66 -0
  312. package/scripts/gds/box.js +225 -53
  313. package/scripts/gds/cf-tunnel.js +145 -6
  314. package/scripts/gds/claim.js +34 -3
  315. package/scripts/gds/cli-lib.js +161 -21
  316. package/scripts/gds/code.js +6 -2
  317. package/scripts/gds/deploy-divergence.js +209 -0
  318. package/scripts/gds/dev-lib.js +309 -0
  319. package/scripts/gds/dev.js +322 -0
  320. package/scripts/gds/doctor.js +90 -61
  321. package/scripts/gds/fetch-art-key.js +1 -1
  322. package/scripts/gds/fetch-sound-prefs.js +2 -2
  323. package/scripts/gds/fitness.js +228 -12
  324. package/scripts/gds/gds-db-backup.service +16 -1
  325. package/scripts/gds/gds-db-offsite.service +9 -1
  326. package/scripts/gds/gen-api-client.js +1 -1
  327. package/scripts/gds/gen-api-docs.js +36 -14
  328. package/scripts/gds/gen-diagrams.js +45 -28
  329. package/scripts/gds/gen-file-map.js +2 -1
  330. package/scripts/gds/gen-freshness.js +32 -0
  331. package/scripts/gds/gen-repo-map.js +3 -2
  332. package/scripts/gds/gen-session-index.js +14 -5
  333. package/scripts/gds/hooks/pre-push.js +2 -2
  334. package/scripts/gds/init.js +60 -3
  335. package/scripts/gds/install-git-hooks.js +1 -1
  336. package/scripts/gds/key-card.js +15 -6
  337. package/scripts/gds/key-status.js +1 -1
  338. package/scripts/gds/linkify-refs.js +35 -9
  339. package/scripts/gds/lint-migrations.js +102 -0
  340. package/scripts/gds/login.js +58 -11
  341. package/scripts/gds/mirror-redact.js +5 -0
  342. package/scripts/gds/package-core.js +263 -4
  343. package/scripts/gds/paste-token.js +7 -1
  344. package/scripts/gds/preflight.js +126 -0
  345. package/scripts/gds/provision.js +268 -19
  346. package/scripts/gds/publish-manifest.js +22 -1
  347. package/scripts/gds/reauth.js +7 -3
  348. package/scripts/gds/regen-instance-docs.js +47 -0
  349. package/scripts/gds/run-unit-tests.js +42 -16
  350. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  351. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  352. package/scripts/gds/sequence.js +425 -0
  353. package/scripts/gds/session-cost.js +10 -1
  354. package/scripts/gds/setup.js +72 -5
  355. package/scripts/gds/shell.js +5 -3
  356. package/scripts/gds/ship.js +433 -59
  357. package/scripts/gds/smoke-gds.js +139 -8
  358. package/scripts/gds/start.js +171 -22
  359. package/scripts/gds/status.js +1 -1
  360. package/scripts/gds/triage.js +10 -9
  361. package/scripts/gds/upgrade.js +271 -7
  362. package/scripts/migrate.sh +10 -0
  363. package/scripts/status-mirror-sync.js +114 -84
  364. package/src/bongos/api-errors.js +1 -0
  365. package/src/bongos/auth.js +809 -107
  366. package/src/bongos/db-kernel.js +27 -0
  367. package/src/bongos/db.js +105 -0
  368. package/src/bongos/github-repos.js +93 -0
  369. package/src/bongos/module-scope-map.js +27 -1
  370. package/src/bongos/permission-path-check.js +136 -102
  371. package/src/bongos/repo-info.js +47 -11
  372. package/src/bongos/route-rank-check.js +429 -46
  373. package/src/bongos/routes/_helpers.js +12 -1
  374. package/src/bongos/routes/auth.js +312 -13
  375. package/src/bongos/routes/backup.js +58 -2
  376. package/src/bongos/routes/github.js +90 -0
  377. package/src/bongos/routes/instance.js +15 -3
  378. package/src/bongos/routes/me.js +35 -1
  379. package/src/bongos/routes.js +2 -0
  380. package/src/bongos/serve-internal.js +84 -15
  381. package/src/branding.js +49 -2
  382. package/src/instance-config.js +46 -0
  383. package/src/module-api.js +140 -1
  384. package/src/platform-server.js +86 -9
  385. package/tests/abandon_task.mjs +59 -3
  386. package/tests/api_cli_flags.mjs +53 -0
  387. package/tests/auth_page_gate.mjs +13 -0
  388. package/tests/backfill_sessions.mjs +127 -1
  389. package/tests/backup_cli.mjs +54 -0
  390. package/tests/bongos_box_connect.mjs +77 -0
  391. package/tests/box_access.mjs +130 -1
  392. package/tests/box_credential.mjs +125 -0
  393. package/tests/box_host_keys.mjs +24 -0
  394. package/tests/box_infra_resolve.mjs +149 -0
  395. package/tests/box_scope_session.mjs +6 -2
  396. package/tests/box_sync.mjs +55 -0
  397. package/tests/box_task_scope.mjs +99 -0
  398. package/tests/branding.mjs +41 -23
  399. package/tests/cf_ruleset_preflight.mjs +142 -0
  400. package/tests/cf_tunnel.mjs +37 -0
  401. package/tests/claim_gate_rebase.mjs +98 -0
  402. package/tests/clawback_confirmed_backlog.mjs +103 -0
  403. package/tests/cli_lib_formatters.mjs +20 -0
  404. package/tests/confirm_attribution.mjs +30 -46
  405. package/tests/credit_grant.mjs +198 -10
  406. package/tests/criterion_authoring_gate.mjs +272 -0
  407. package/tests/criterion_progress.mjs +33 -1
  408. package/tests/dev_lib.mjs +253 -0
  409. package/tests/devbox_artifact_gate.mjs +75 -0
  410. package/tests/devbox_dmg_backdrop.mjs +137 -0
  411. package/tests/devbox_icons.mjs +304 -0
  412. package/tests/devbox_installer_brand.mjs +232 -0
  413. package/tests/devbox_key_register.mjs +234 -0
  414. package/tests/docs_openapi_route.mjs +110 -0
  415. package/tests/doctor_hooks.mjs +141 -0
  416. package/tests/extract_json.mjs +140 -0
  417. package/tests/fitness.mjs +89 -0
  418. package/tests/gen_freshness.mjs +96 -0
  419. package/tests/github_repos_helpers.mjs +91 -0
  420. package/tests/github_token_store.mjs +121 -0
  421. package/tests/goal_advisory.mjs +251 -0
  422. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  423. package/tests/goal_routes.mjs +59 -4
  424. package/tests/goal_tier.mjs +12 -3
  425. package/tests/governance_catalog.mjs +129 -0
  426. package/tests/governance_claim_eligibility.mjs +262 -0
  427. package/tests/governance_config.mjs +187 -0
  428. package/tests/governance_ownership_scope.mjs +410 -0
  429. package/tests/governance_page_gates.mjs +324 -0
  430. package/tests/governance_parity.mjs +336 -0
  431. package/tests/governance_principals.mjs +232 -0
  432. package/tests/governance_protected_surfaces.mjs +208 -0
  433. package/tests/governance_require_permission.mjs +223 -0
  434. package/tests/governance_resolver.mjs +84 -0
  435. package/tests/governance_role_sync.mjs +231 -0
  436. package/tests/governance_seed.mjs +127 -0
  437. package/tests/governance_session_scope.mjs +434 -0
  438. package/tests/governance_vanilla_boot.mjs +203 -0
  439. package/tests/grade_attribution.mjs +7 -7
  440. package/tests/hall_audit.mjs +223 -0
  441. package/tests/hall_nav.mjs +191 -0
  442. package/tests/hall_palette.mjs +206 -0
  443. package/tests/hall_signout.mjs +124 -0
  444. package/tests/hook_user_agents.mjs +10 -0
  445. package/tests/idea_triage.mjs +162 -0
  446. package/tests/idp_device_exchange.mjs +198 -0
  447. package/tests/idp_federation.mjs +100 -0
  448. package/tests/init.mjs +51 -0
  449. package/tests/instance_config.mjs +65 -0
  450. package/tests/instance_mismatch_guard.mjs +23 -6
  451. package/tests/instance_release_config.mjs +104 -0
  452. package/tests/linkify_refs.mjs +15 -2
  453. package/tests/migrations_forward_safe.mjs +86 -0
  454. package/tests/module_api.mjs +1 -1
  455. package/tests/module_contributions.mjs +55 -0
  456. package/tests/module_loader.mjs +10 -5
  457. package/tests/module_scope_active_claims.mjs +94 -0
  458. package/tests/needs_rebase.mjs +92 -0
  459. package/tests/one_active_claim_per_session.mjs +76 -0
  460. package/tests/override_attribution.mjs +10 -8
  461. package/tests/override_request_ownership.mjs +8 -4
  462. package/tests/package_core.mjs +246 -1
  463. package/tests/parse_cookie_decode.mjs +42 -0
  464. package/tests/pay_on_land.mjs +129 -0
  465. package/tests/platform_boot.mjs +100 -1
  466. package/tests/post_login_landing.mjs +42 -0
  467. package/tests/preflight_checks.mjs +72 -0
  468. package/tests/provision.mjs +289 -7
  469. package/tests/pruned_lockfile_integrity.mjs +79 -0
  470. package/tests/publish_branch_route.mjs +53 -0
  471. package/tests/publish_reconciler.mjs +187 -2
  472. package/tests/repo_info_binding.mjs +248 -0
  473. package/tests/reward_gate.mjs +11 -2
  474. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  475. package/tests/reward_mode_surfacing.mjs +168 -0
  476. package/tests/route_permission_check.mjs +301 -0
  477. package/tests/route_rank_check.mjs +254 -12
  478. package/tests/route_validation.mjs +30 -0
  479. package/tests/sequence.mjs +267 -0
  480. package/tests/session_digest.mjs +44 -0
  481. package/tests/session_index.mjs +18 -6
  482. package/tests/session_records.mjs +39 -1
  483. package/tests/session_reward_on_land.mjs +381 -0
  484. package/tests/session_reward_reconcile.mjs +229 -0
  485. package/tests/ship_api_client_pathspec.mjs +100 -0
  486. package/tests/ship_base_flag.mjs +93 -0
  487. package/tests/ship_ci_deploy.mjs +140 -3
  488. package/tests/ship_upstream_deletion.mjs +57 -0
  489. package/tests/smoke_target.mjs +233 -0
  490. package/tests/start_env_guard.mjs +87 -0
  491. package/tests/start_rebase_warning.mjs +103 -0
  492. package/tests/status_mirror_sync.mjs +122 -0
  493. package/tests/stop_refcheck.mjs +19 -1
  494. package/tests/task_goal_link.mjs +15 -6
  495. package/tests/upgrade.mjs +393 -5
  496. package/tests/web_admission_status.mjs +162 -0
  497. package/tests/write_validation_gaps.mjs +110 -0
  498. package/.gitignore +0 -91
  499. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  500. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  501. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  502. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  503. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  504. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  505. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  506. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  507. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  508. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  509. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  510. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  511. package/scripts/hall-preview/fixtures/goals.json +0 -502
  512. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  513. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  514. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  515. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  516. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  517. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  518. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  519. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  520. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  521. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  522. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  523. package/scripts/hall-preview/fixtures/instance.json +0 -124
  524. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  525. package/scripts/hall-preview/fixtures/me.json +0 -476
  526. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  527. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  528. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  529. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  530. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  531. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  532. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  533. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  534. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  535. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  536. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  537. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  538. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  539. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  540. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  541. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  542. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  543. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  544. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  545. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  546. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  547. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  548. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  549. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  550. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  551. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  552. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  553. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -80,6 +80,109 @@ test('a route declared with NO middleware does not borrow the NEXT route\'s rank
80
80
  assert.equal(rows.find((x) => x.method === 'POST').rank, 'archon');
81
81
  });
82
82
 
83
+ // ---------- BV1.R109b: multi-line route DECLARATIONS are visible at all ----------
84
+ //
85
+ // Distinct from the "multi-line middleware" test above: there the METHOD AND PATH were
86
+ // still on one line and only the middleware wrapped. Here the declaration itself opens
87
+ // on its own line and the PATH is below it — the form that made 10 real routes (6 of
88
+ // them writes) invisible to this auditor entirely: never gate-checked, never pinned,
89
+ // never published to the API contract.
90
+
91
+ test('multi-line DECLARATION (path on the next line) is discovered [BV1.R109b]', () => {
92
+ const src = [
93
+ ' router.get(',
94
+ " '/security/adr',",
95
+ ' auth.requireBuilder,',
96
+ " auth.requireRank('metic', 'archon'),",
97
+ ' async (_req, res) => {',
98
+ ' res.json({});',
99
+ ' }',
100
+ ' );',
101
+ ].join('\n');
102
+ const r = rrc.classifyText(src);
103
+ assert.equal(r.length, 1, 'a multi-line declaration must emit exactly one route');
104
+ assert.equal(r[0].key, 'GET /security/adr');
105
+ assert.equal(r[0].rank, 'metic+archon', 'the gate scan must still find middleware below the path line');
106
+ });
107
+
108
+ test('multi-line declaration with an interleaved comment still resolves [BV1.R109b]', () => {
109
+ const src = [
110
+ ' router.post(',
111
+ ' // the path lives below this note',
112
+ " '/x',",
113
+ " auth.requireRank('archon'),",
114
+ ' h);',
115
+ ].join('\n');
116
+ assert.equal(rrc.classifyText(src)[0].key, 'POST /x');
117
+ });
118
+
119
+ test('an UNRESOLVABLE path is not silently emitted, and reds as a finding [BV1.R109b]', () => {
120
+ // A computed path can't be classified. The old scanner dropped it silently; the fix
121
+ // must FAIL CLOSED — no invented route row, and a hard finding from checkRouteRanks.
122
+ const src = " router.post(\n BASE + '/x',\n auth.requireRank('archon'),\n h);";
123
+ assert.equal(rrc.classifyText(src).length, 0, 'must not invent a route with a bogus path');
124
+ const decls = rrc.scanDeclarations(src);
125
+ assert.equal(decls.length, 1, 'the declaration must still be COUNTED (the census sees it)');
126
+ assert.equal(decls[0].route, null);
127
+ assert.equal(decls[0].method, 'POST');
128
+ });
129
+
130
+ test('scanDeclarations counts both forms and never loses one [BV1.R109b]', () => {
131
+ const src = [
132
+ " router.get('/a', h);",
133
+ ' router.post(',
134
+ " '/b',",
135
+ ' h);',
136
+ " // router.get('/not-a-route', h); <- a commented-out line is not a declaration",
137
+ ].join('\n');
138
+ const decls = rrc.scanDeclarations(src);
139
+ assert.deepEqual(decls.map((d) => `${d.method} ${d.route}`), ['GET /a', 'POST /b']);
140
+ });
141
+
142
+ test('a multi-line WRITE route with NO gate hard-fails the build [BV1.R109b fixture]', () => {
143
+ // The whole point of the fix: before it, this route was INVISIBLE, so deleting its
144
+ // gate kept the build green. Now it must trip the ungated-write blocker.
145
+ const os = require('node:os');
146
+ const fs = require('node:fs');
147
+ const path = require('node:path');
148
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-multiline-'));
149
+ fs.writeFileSync(path.join(dir, 'shim.js'), [
150
+ ' router.post(',
151
+ " '/danger',",
152
+ ' async (req, res) => {',
153
+ ' res.json({});',
154
+ ' }',
155
+ ' );',
156
+ ].join('\n'));
157
+ const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, '__no_modules__') });
158
+ fs.rmSync(dir, { recursive: true, force: true });
159
+
160
+ const ungated = res.findings.find((f) => f.route === 'POST /danger');
161
+ assert.ok(ungated, 'the multi-line write route must be DISCOVERED (it was invisible before R109b)');
162
+ assert.equal(ungated.severity, 'blocker', 'an ungated write route is a hard fail');
163
+ assert.equal(res.hardFail, true);
164
+ });
165
+
166
+ test('an UNRESOLVABLE multi-line write declaration also hard-fails [BV1.R109b fixture]', () => {
167
+ const os = require('node:os');
168
+ const fs = require('node:fs');
169
+ const path = require('node:path');
170
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-unresolved-'));
171
+ fs.writeFileSync(path.join(dir, 'shim.js'), [
172
+ ' router.post(',
173
+ ' ROUTES.danger,',
174
+ " auth.requireRank('archon'),",
175
+ ' h);',
176
+ ].join('\n'));
177
+ const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, '__no_modules__') });
178
+ fs.rmSync(dir, { recursive: true, force: true });
179
+
180
+ const f = res.findings.find((x) => x.rank === '(unparsed)');
181
+ assert.ok(f, 'a declaration whose path cannot be resolved must surface a finding, not vanish');
182
+ assert.equal(f.severity, 'blocker', 'an unverifiable WRITE declaration is a hard fail');
183
+ assert.equal(res.hardFail, true);
184
+ });
185
+
83
186
  // ---------- classifyText: `rank:` annotation fallback ----------
84
187
 
85
188
  test('rank: annotation directly above → classifies', () => {
@@ -118,6 +221,79 @@ test('typo annotation (not a known rank) → unknown', () => {
118
221
  assert.equal(r[0].rank, 'unknown', 'a misspelled rank surfaces rather than being trusted');
119
222
  });
120
223
 
224
+ // ---------- BV1.R109d: a COMMENT is not a gate ----------
225
+ // A `rank:` annotation is reached ONLY when no middleware was found, so a comment
226
+ // naming a privileged rank asserts a gate that provably does not exist. Before this,
227
+ // such a route satisfied the ungated-write invariant AND every exact pin.
228
+
229
+ test('provenance: a real gate is `middleware`, a bare comment is `annotation` [BV1.R109d]', () => {
230
+ const gated = rrc.classifyText(` router.post('/x', auth.requireBuilder, auth.requireRank('archon'), h);`);
231
+ assert.equal(gated[0].via, 'middleware');
232
+ const claimed = rrc.classifyText(` // rank: archon\n router.post('/x', h);`);
233
+ assert.equal(claimed[0].via, 'annotation');
234
+ assert.equal(claimed[0].rank, 'archon', 'the label is still read — it is the PROVENANCE that differs');
235
+ });
236
+
237
+ test('a bare identifier gate (destructured doorway) counts as middleware [BV1.R109d]', () => {
238
+ // modules/platform-identity does `const { requireBuilder, requireRank } = api` and
239
+ // passes BARE identifiers. Recognising only the namespaced spelling made 11 correctly
240
+ // gated routes — 5 of them archon — visible to the auditor only via their comment.
241
+ const r = rrc.classifyText(` router.post('/projects', requireBuilder, requireRank('archon'), h);`);
242
+ assert.equal(r[0].rank, 'archon');
243
+ assert.equal(r[0].via, 'middleware', 'a destructured guard is a real gate, not prose');
244
+ });
245
+
246
+ test('the identifier in PROSE never counts as a gate [BV1.R109d]', () => {
247
+ const r = rrc.classifyText(` // this route used to call requireRank('archon') before the migration\n // rank: public\n router.get('/x', h);`);
248
+ assert.equal(r[0].via, 'annotation', 'a guard name inside a comment is not argument position');
249
+ assert.equal(r[0].rank, 'public');
250
+ });
251
+
252
+ test('a privileged comment-only WRITE route hard-fails the build [BV1.R109d]', () => {
253
+ const os = require('node:os'); const fs = require('node:fs'); const path = require('node:path');
254
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-r109d-'));
255
+ fs.writeFileSync(path.join(dir, 'shim.js'), ` // rank: archon\n router.patch('/builders/:id/rank', h);\n`);
256
+ const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, '__none__') });
257
+ // The load-bearing assertion is the SPECIFIC finding below, not hardFail — a shim dir
258
+ // hardFails anyway on the missing pinned routes, so hardFail alone proves nothing here.
259
+ const f = res.findings.find((x) => x.route.includes('/builders/:id/rank') && /carries NO auth middleware/.test(x.summary));
260
+ assert.ok(f, `expected a comment-only finding; got: ${res.findings.map((x) => x.summary).join(' | ')}`);
261
+ assert.equal(f.severity, 'blocker');
262
+ });
263
+
264
+ test('`rank: public` on an unauthenticated route still passes — no false positive [BV1.R109d]', () => {
265
+ const os = require('node:os'); const fs = require('node:fs'); const path = require('node:path');
266
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-r109d-pub-'));
267
+ fs.writeFileSync(path.join(dir, 'shim.js'), ` // rank: public\n router.get('/healthz', h);\n`);
268
+ const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, '__none__') });
269
+ // NB: assert on the comment-only findings specifically, NOT on hardFail — a shim dir
270
+ // contains none of the pinned routes, so every EXPECTED_RANKS pin reports "vanished"
271
+ // and hardFail is true for reasons this test is not about.
272
+ assert.equal(res.findings.filter((x) => /carries NO auth middleware/.test(x.summary)).length, 0,
273
+ 'public is the annotation’s legitimate use and must not red');
274
+ });
275
+
276
+ test('a pin is NOT satisfiable by a comment [BV1.R109d]', () => {
277
+ // The pins are the deterministic alarm for "routes whose downgrade would be a real
278
+ // authority breach". A comment satisfying one would let prose certify authority.
279
+ const claimed = rrc.classifyText(` // rank: archon\n router.patch('/builders/:id/rank', h);`)[0];
280
+ assert.match(rrc.pinMismatch('archon', claimed) || '', /NO auth middleware/);
281
+ assert.match(rrc.pinMismatch('perm:builder.rank.set', claimed) || '', /NO auth middleware/);
282
+ const real = rrc.classifyText(` router.patch('/builders/:id/rank', auth.requireBuilder, auth.requireRank('archon'), h);`)[0];
283
+ assert.equal(rrc.pinMismatch('archon', real), null, 'a real gate still satisfies its pin');
284
+ });
285
+
286
+ test('LIVE: no privileged route in this repo is gated only by a comment [BV1.R109d]', () => {
287
+ const fs = require('node:fs');
288
+ const offenders = [];
289
+ for (const { abs, rel } of rrc.discoverRouteFiles()) {
290
+ for (const r of rrc.classifyText(fs.readFileSync(abs, 'utf8'))) {
291
+ if (r.via === 'annotation' && r.rank !== 'public') offenders.push(`${r.key} (${rel}) claims '${r.rank}'`);
292
+ }
293
+ }
294
+ assert.deepEqual(offenders, [], 'a privileged route certified only by prose');
295
+ });
296
+
121
297
  // ---------- checkRouteRanks invariant (via a fixture-free routesDir shim) ----------
122
298
  // checkRouteRanks reads files; we exercise the finding logic through the pure
123
299
  // path by constructing routes through classifyText + the documented severity
@@ -147,11 +323,17 @@ test('toWorkerResult: read-unknown only → concerns (not fail)', () => {
147
323
 
148
324
  // ---------- EXPECTED_RANKS: exact-rank pin on privileged routes (#999) ----------
149
325
 
150
- test('EXPECTED_RANKS: pins are non-empty and all to known ranks', () => {
326
+ test('EXPECTED_RANKS: pins are non-empty and every value is a known rank OR a defined permission', () => {
151
327
  const keys = Object.keys(rrc.EXPECTED_RANKS);
152
328
  assert.ok(keys.length > 0, 'should pin at least one privileged route');
153
329
  for (const [k, v] of Object.entries(rrc.EXPECTED_RANKS)) {
154
- assert.ok(rrc.KNOWN_RANKS.has(v), `pinned expected rank '${v}' for ${k} must be a known rank`);
330
+ const pin = rrc.parsePin(v);
331
+ if (pin.kind === 'rank') {
332
+ assert.ok(rrc.KNOWN_RANKS.has(pin.rank), `pinned expected rank '${v}' for ${k} must be a known rank`);
333
+ } else {
334
+ assert.notEqual(rrc.rankForPermissions(pin.perms), 'unknown',
335
+ `pinned permission(s) '${pin.perms.join(', ')}' for ${k} must exist in the governance catalog`);
336
+ }
155
337
  }
156
338
  });
157
339
 
@@ -168,21 +350,34 @@ test('EXPECTED_RANKS: a DOWNGRADE on a pinned route trips a blocker via a routes
168
350
  const emit = (key, expected) => {
169
351
  const [method, route] = key.split(' ');
170
352
  const m = method.toLowerCase();
353
+ const pin = rrc.parsePin(expected);
171
354
  if (key === 'PATCH /builders/:id/rank') {
172
355
  // DOWNGRADE: requireBuilder only → classifies any-builder, not archon.
173
356
  lines.push(` router.${m}('${route}', auth.requireBuilder, h);`);
174
- } else if (expected === 'archon') {
357
+ } else if (pin.kind === 'permission') {
358
+ // The MIGRATED spelling. A permission pin is satisfied by naming exactly its
359
+ // atoms — this arm is what proves the post-R104 gate form clears the pin.
360
+ lines.push(` router.${m}('${route}', auth.requireBuilder, auth.requirePermission(${pin.perms.map((p) => `'${p}'`).join(', ')}), h);`);
361
+ } else if (pin.rank === 'archon') {
175
362
  lines.push(` router.${m}('${route}', auth.requireBuilder, auth.requireRank('archon'), h);`);
176
- } else if (expected === 'metic+archon') {
363
+ } else if (pin.rank === 'metic+archon') {
177
364
  lines.push(` router.${m}('${route}', auth.requireRank('metic', 'archon'), h);`);
178
- } else {
179
- lines.push(` // rank: ${expected}`);
365
+ } else if (pin.rank === 'bfg-principal') {
366
+ lines.push(` router.${m}('${route}', auth.requireBuilder, auth.requireBfgPrincipal, h);`);
367
+ } else if (pin.rank === 'public') {
368
+ // The ONE honest comment-only gate: 'public' asserts nothing is protected.
369
+ lines.push(` // rank: public`);
180
370
  lines.push(` router.${m}('${route}', h);`);
371
+ } else {
372
+ // Real middleware, NOT a `rank:` comment (BV1.R109d). This branch used to emit
373
+ // a bare annotation, which only classified because a comment could stand in for
374
+ // a gate — the hole R109d closed. A pinned route must carry its real gate, or
375
+ // the fixture reds for a reason this test is not about.
376
+ lines.push(` router.${m}('${route}', auth.requireBuilder, h);`);
181
377
  }
182
- // A blank gap matching a real handler body — keeps classifyRank's 20-line
183
- // middleware lookahead on the downgraded route from "borrowing" a NEIGHBOUR
184
- // route's requireRank('archon') line.
185
- lines.push(' });', '', '', '', '', '', '');
378
+ // A gap wider than classifyGate's 20-line middleware lookahead, so no route can
379
+ // "borrow" a NEIGHBOUR's gate — every emitted route stands on its own.
380
+ lines.push(' });', ...Array(21).fill(''));
186
381
  };
187
382
  // Emit every pinned route EXCEPT the downgraded one first, then the downgraded
188
383
  // one LAST (so nothing with requireRank('archon') sits within its lookahead).
@@ -198,6 +393,11 @@ test('EXPECTED_RANKS: a DOWNGRADE on a pinned route trips a blocker via a routes
198
393
  assert.ok(downgrade, 'a downgraded pinned route must surface a finding');
199
394
  assert.equal(downgrade.severity, 'blocker', 'a rank downgrade on a privileged route is a hard fail');
200
395
  assert.equal(res.hardFail, true);
396
+ // The other side of the same fixture: every OTHER pin is satisfied by the gate
397
+ // spelling it was emitted in — rank pins by requireRank/annotation, permission
398
+ // pins by requirePermission. A stray finding here means a pin is unsatisfiable.
399
+ assert.deepEqual(res.findings.map((f) => f.route), ['PATCH /builders/:id/rank'],
400
+ `only the deliberately-downgraded route should red; got: ${res.findings.map((f) => `${f.route} — ${f.summary}`).join(' | ')}`);
201
401
  });
202
402
 
203
403
  test('EXPECTED_RANKS: a MISSING pinned route trips a blocker (a protected route vanished)', () => {
@@ -227,14 +427,56 @@ test('LIVE: checkRouteRanks() on src/bongos/routes/ has zero unknowns', () => {
227
427
  assert.equal(res.hardFail, false);
228
428
  });
229
429
 
230
- test('LIVE: every pinned privileged route still classifies to its EXPECTED rank', () => {
430
+ // The census invariant (BV1.R109b). Asserting that the 10 known multi-line paths parse
431
+ // would prove nothing about the ELEVENTH — the next route someone declares in a form
432
+ // this scanner doesn't handle. So count instead: every `router.<method>(` line in every
433
+ // discovered route file must yield exactly one classified route.
434
+ //
435
+ // The census regex below is HAND-WRITTEN here on purpose and NOT imported from the
436
+ // module. Importing ROUTE_DECL_RE would move both sides of the comparison together and
437
+ // prove nothing (the oracle trap tests/route_permission_check.mjs documents).
438
+ const CENSUS_DECL_RE = /^\s*router\.(?:get|post|patch|put|delete)\s*\(/;
439
+ // A floor, so a change that silently guts BOTH sides (e.g. discoverRouteFiles stops
440
+ // finding module routes) reds instead of trivially "matching" at a smaller number.
441
+ const ROUTE_CENSUS_FLOOR = 258;
442
+
443
+ test('LIVE census: every router.<method>( declaration classifies to exactly one route [BV1.R109b]', () => {
444
+ const fs = require('node:fs');
445
+ let declared = 0;
446
+ let classified = 0;
447
+ const shortfall = [];
448
+ for (const { abs, rel } of rrc.discoverRouteFiles()) {
449
+ const text = fs.readFileSync(abs, 'utf8');
450
+ const d = text.split('\n').filter((l) => CENSUS_DECL_RE.test(l)).length;
451
+ const c = rrc.classifyText(text).length;
452
+ declared += d;
453
+ classified += c;
454
+ if (d !== c) shortfall.push(`${rel}: ${d} declared vs ${c} classified`);
455
+ }
456
+ assert.equal(shortfall.length, 0,
457
+ `every declared route must be visible to the auditor; invisible in: ${shortfall.join('; ')}`);
458
+ assert.equal(classified, declared, 'declaration census must equal the classified route count');
459
+ assert.ok(declared >= ROUTE_CENSUS_FLOOR,
460
+ `expected at least ${ROUTE_CENSUS_FLOOR} route declarations, found ${declared} — did route discovery break?`);
461
+ });
462
+
463
+ test('LIVE: checkRouteRanks reports ZERO unparsed declarations [BV1.R109b]', () => {
464
+ const res = rrc.checkRouteRanks();
465
+ const unparsed = res.findings.filter((f) => f.rank === '(unparsed)');
466
+ assert.equal(unparsed.length, 0,
467
+ `no route declaration may be unresolvable: ${unparsed.map((f) => f.route).join(', ')}`);
468
+ assert.ok(res.routeCount >= ROUTE_CENSUS_FLOOR,
469
+ `checkRouteRanks must cover at least ${ROUTE_CENSUS_FLOOR} routes, saw ${res.routeCount}`);
470
+ });
471
+
472
+ test('LIVE: every pinned privileged route still satisfies its pin', () => {
231
473
  const res = rrc.checkRouteRanks();
232
474
  const byKey = new Map();
233
475
  for (const r of res.routes) if (!byKey.has(r.key)) byKey.set(r.key, r);
234
476
  for (const [key, expected] of Object.entries(rrc.EXPECTED_RANKS)) {
235
477
  const r = byKey.get(key);
236
478
  assert.ok(r, `pinned route ${key} must exist in the live route set`);
237
- assert.equal(r.rank, expected, `pinned route ${key} must classify as '${expected}', got '${r.rank}'`);
479
+ assert.equal(rrc.pinMismatch(expected, r), null, `pinned route ${key}: ${rrc.pinMismatch(expected, r)}`);
238
480
  }
239
481
  });
240
482
 
@@ -196,10 +196,18 @@ const BLOCKERS_POST_SCHEMA = {
196
196
  source_ref: { type: 'string', maxLength: LIMITS.SOURCE_REF },
197
197
  };
198
198
 
199
+ // Hand-mirrored from modules/ideas/routes/inbox.js's PATCH validator (the route
200
+ // declares it inline, so there is nothing to import). Strict mode rejects
201
+ // unknown fields, so a field missing HERE makes the mirror assert the opposite
202
+ // of the route — keep the two in step. `reviewed_at` had already drifted out
203
+ // when task 1002447 added the last two.
199
204
  const INBOX_PATCH_SCHEMA = {
200
205
  status: { type: 'string', enum: ['open', 'promoted', 'discarded', 'merged'] },
201
206
  kind: { type: 'string', maxLength: 64 },
202
207
  promoted_to_task_id: { type: 'integer', min: 1 },
208
+ reviewed_at: {},
209
+ merged_into_idea_id: { type: 'integer', min: 1 },
210
+ triage_note: { type: 'string', maxLength: 1000 },
203
211
  };
204
212
 
205
213
  // ---- supervisor hot path 1: POST /api/gds/claims with full body ----
@@ -332,6 +340,22 @@ test('inbox: PATCH /inbox/:id with status promote passes', () => {
332
340
  assert.equal(validate({ status: 'promoted', promoted_to_task_id: 254 }, INBOX_PATCH_SCHEMA), null);
333
341
  });
334
342
 
343
+ // task 1002447 — the merge target + the verdict's reason (migration core_195).
344
+ test('inbox: PATCH /inbox/:id merge carries a target + a note', () => {
345
+ const body = { status: 'merged', merged_into_idea_id: 412, triage_note: 'same request as #412' };
346
+ assert.equal(validate(body, INBOX_PATCH_SCHEMA), null);
347
+ });
348
+
349
+ test('inbox: PATCH /inbox/:id discard carries a reason', () => {
350
+ assert.equal(validate({ status: 'discarded', triage_note: 'superseded by the new gate' }, INBOX_PATCH_SCHEMA), null);
351
+ });
352
+
353
+ // A typed field still accepts an explicit null — validateField returns early
354
+ // for a null non-required field — so the un-merge needs no untyped escape hatch.
355
+ test('inbox: PATCH /inbox/:id may clear the merge link with null', () => {
356
+ assert.equal(validate({ merged_into_idea_id: null }, INBOX_PATCH_SCHEMA), null);
357
+ });
358
+
335
359
  // ===========================================================================
336
360
  // C. Bad input must return structured 400, never 500.
337
361
  // ===========================================================================
@@ -400,6 +424,12 @@ test('BAD: PATCH /inbox/:id with bad status enum', () => {
400
424
  assert.ok(r.details.some((d) => d.reason === 'not_in_enum'));
401
425
  });
402
426
 
427
+ test('BAD: PATCH /inbox/:id with an oversize triage_note → too_long', () => {
428
+ const r = validate({ status: 'discarded', triage_note: 'x'.repeat(1001) }, INBOX_PATCH_SCHEMA);
429
+ assert.ok(r);
430
+ assert.ok(r.details.some((d) => d.field === 'triage_note' && d.reason === 'too_long'));
431
+ });
432
+
403
433
  test('BAD: POST /tasks/:id/grade with signals=string (must be object)', () => {
404
434
  const r = validate({ signals: 'oops' }, TASKS_GRADE_SCHEMA);
405
435
  assert.ok(r);
@@ -0,0 +1,267 @@
1
+ // tests/sequence.mjs — pure-core coverage for the sequential chain runner
2
+ // (task 1002484). Hermetic: feeds fixtures to topoOrder/chainFrom/haltReasons;
3
+ // no DB, no API. Run: node tests/sequence.mjs (also runs in the unit gate).
4
+
5
+ import { strict as assert } from 'node:assert';
6
+ import { createRequire } from 'node:module';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const {
10
+ readArgv,
11
+ edgeIds,
12
+ rowsOf,
13
+ topoOrder,
14
+ chainFrom,
15
+ haltReasons,
16
+ summarize,
17
+ MIN_SPEC_CHARS,
18
+ } = require('../scripts/gds/sequence.js');
19
+
20
+ let passed = 0, failed = 0;
21
+ const t = (n, f) => { try { f(); passed++; console.log(` PASS ${n}`); } catch (e) { failed++; console.log(` FAIL ${n}\n ${e.message}`); } };
22
+
23
+ // A description long enough to clear the thin-spec gate, so fixtures testing
24
+ // OTHER halt reasons don't trip it incidentally.
25
+ const SPEC = 'x'.repeat(MIN_SPEC_CHARS + 1);
26
+ const task = (over = {}) => ({
27
+ id: 1, title: 'T', status: 'ready', kind: 'feature', description: SPEC,
28
+ touches: [], dependencies: [], dependents: [], requires_rank: 'xenos',
29
+ credits_reward: 10, est_minutes: 30, priority: null, ...over,
30
+ });
31
+
32
+ // ---------------------------------------------------------------- argv
33
+
34
+ t('readArgv parses --from/--goal/--check in both spellings', () => {
35
+ assert.equal(readArgv(['--from', '7']).from, 7);
36
+ assert.equal(readArgv(['--from=7']).from, 7);
37
+ assert.equal(readArgv(['--goal=42']).goal, 42);
38
+ assert.equal(readArgv(['--check=9']).check, 9);
39
+ assert.equal(readArgv(['--from', '7', '--next']).next, true);
40
+ assert.equal(readArgv(['--from', '7', '--json']).json, true);
41
+ });
42
+
43
+ t('readArgv accepts a bare task id as --from', () => {
44
+ assert.equal(readArgv(['1002277']).from, 1002277);
45
+ });
46
+
47
+ // ---------------------------------------------------------------- edge shapes
48
+
49
+ t('edgeIds tolerates both bare-number and {id} edge rows', () => {
50
+ assert.deepEqual(edgeIds([1, '2', { id: 3 }, { id: '4' }]), [1, 2, 3, 4]);
51
+ assert.deepEqual(edgeIds(null), []);
52
+ assert.deepEqual(edgeIds([null, undefined, 'nope']), []);
53
+ });
54
+
55
+ t('rowsOf finds the array under each envelope key the API uses', () => {
56
+ assert.deepEqual(rowsOf({ claimable: [{ id: 1 }] }), [{ id: 1 }]);
57
+ assert.deepEqual(rowsOf({ tasks: [{ id: 2 }] }), [{ id: 2 }]);
58
+ assert.deepEqual(rowsOf([{ id: 3 }]), [{ id: 3 }]);
59
+ });
60
+
61
+ t('rowsOf returns null (not []) for an unrecognized envelope', () => {
62
+ // This distinction is load-bearing: null means "could not check", which
63
+ // haltReasons must treat as silence rather than halting every task.
64
+ assert.equal(rowsOf({ error: 'boom' }), null);
65
+ assert.equal(rowsOf(null), null);
66
+ });
67
+
68
+ // ---------------------------------------------------------------- ordering
69
+
70
+ t('topoOrder puts a dependency before its dependent', () => {
71
+ const { order, cycle } = topoOrder([
72
+ task({ id: 2, dependencies: [1] }),
73
+ task({ id: 1 }),
74
+ ]);
75
+ assert.deepEqual(order.map((x) => x.id), [1, 2]);
76
+ assert.deepEqual(cycle, []);
77
+ });
78
+
79
+ t('topoOrder handles a three-deep chain', () => {
80
+ const { order } = topoOrder([
81
+ task({ id: 3, dependencies: [2] }),
82
+ task({ id: 1 }),
83
+ task({ id: 2, dependencies: [1] }),
84
+ ]);
85
+ assert.deepEqual(order.map((x) => x.id), [1, 2, 3]);
86
+ });
87
+
88
+ t('topoOrder reports a cycle instead of silently dropping it', () => {
89
+ const { order, cycle } = topoOrder([
90
+ task({ id: 1, dependencies: [2] }),
91
+ task({ id: 2, dependencies: [1] }),
92
+ ]);
93
+ assert.deepEqual(order, []);
94
+ assert.deepEqual(cycle, [1, 2]);
95
+ });
96
+
97
+ t('topoOrder keeps the acyclic part and reports only the cyclic remainder', () => {
98
+ const { order, cycle } = topoOrder([
99
+ task({ id: 1 }),
100
+ task({ id: 2, dependencies: [3] }),
101
+ task({ id: 3, dependencies: [2] }),
102
+ ]);
103
+ assert.deepEqual(order.map((x) => x.id), [1]);
104
+ assert.deepEqual(cycle, [2, 3]);
105
+ });
106
+
107
+ t('topoOrder ignores dependencies pointing OUTSIDE the scope', () => {
108
+ // An out-of-scope dep must not stall ordering — it surfaces later as a
109
+ // deps_unmet halt, which is the honest place for it.
110
+ const { order, cycle } = topoOrder([task({ id: 5, dependencies: [999] })]);
111
+ assert.deepEqual(order.map((x) => x.id), [5]);
112
+ assert.deepEqual(cycle, []);
113
+ });
114
+
115
+ t('topoOrder breaks ties on priority, then estimate, then id', () => {
116
+ const { order } = topoOrder([
117
+ task({ id: 10, priority: 3, est_minutes: 5 }),
118
+ task({ id: 11, priority: 1, est_minutes: 90 }),
119
+ task({ id: 12, priority: 1, est_minutes: 10 }),
120
+ ]);
121
+ assert.deepEqual(order.map((x) => x.id), [11, 12, 10].sort((a, b) => {
122
+ const p = { 10: 3, 11: 1, 12: 1 }, m = { 10: 5, 11: 90, 12: 10 };
123
+ return p[a] - p[b] || m[a] - m[b] || a - b;
124
+ }));
125
+ assert.equal(order[0].id, 12, 'priority 1 + shortest estimate leads');
126
+ });
127
+
128
+ // ---------------------------------------------------------------- chain scope
129
+
130
+ t('chainFrom walks dependents transitively', () => {
131
+ const rows = [
132
+ task({ id: 1, dependents: [2] }),
133
+ task({ id: 2, dependents: [3], status: 'backlog' }),
134
+ task({ id: 3, status: 'backlog' }),
135
+ ];
136
+ const byId = new Map(rows.map((r) => [r.id, r]));
137
+ assert.deepEqual(chainFrom(1, byId).map((x) => x.id), [1, 2, 3]);
138
+ });
139
+
140
+ t('chainFrom walks THROUGH a shipped task but excludes it from the plan', () => {
141
+ const rows = [
142
+ task({ id: 1, status: 'shipped', dependents: [2] }),
143
+ task({ id: 2, status: 'ready' }),
144
+ ];
145
+ const byId = new Map(rows.map((r) => [r.id, r]));
146
+ assert.deepEqual(chainFrom(1, byId).map((x) => x.id), [2]);
147
+ });
148
+
149
+ t('chainFrom does not loop forever on a cyclic dependents graph', () => {
150
+ const rows = [
151
+ task({ id: 1, dependents: [2] }),
152
+ task({ id: 2, dependents: [1] }),
153
+ ];
154
+ const byId = new Map(rows.map((r) => [r.id, r]));
155
+ assert.deepEqual(chainFrom(1, byId).map((x) => x.id).sort(), [1, 2]);
156
+ });
157
+
158
+ t('chainFrom honours the limit', () => {
159
+ const rows = [1, 2, 3, 4, 5].map((id) => task({ id, dependents: id < 5 ? [id + 1] : [] }));
160
+ const byId = new Map(rows.map((r) => [r.id, r]));
161
+ assert.equal(chainFrom(1, byId, { limit: 3 }).length, 3);
162
+ });
163
+
164
+ t('chainFrom stays inside its scope — an unrelated task is not pulled in', () => {
165
+ const rows = [task({ id: 1, dependents: [2] }), task({ id: 2 }), task({ id: 99 })];
166
+ const byId = new Map(rows.map((r) => [r.id, r]));
167
+ assert.deepEqual(chainFrom(1, byId).map((x) => x.id), [1, 2]);
168
+ });
169
+
170
+ // ---------------------------------------------------------------- halt matrix
171
+
172
+ const clean = { rank: 'archon', claimableIds: new Set([1]) };
173
+ const codes = (tk, opts = clean) => haltReasons(tk, opts).map((r) => r.code);
174
+
175
+ t('a clean ready task produces no halt reasons', () => {
176
+ assert.deepEqual(codes(task()), []);
177
+ });
178
+
179
+ t('halts on needs_migration', () => {
180
+ assert.ok(codes(task({ needs_migration: true })).includes('needs_migration'));
181
+ });
182
+
183
+ t('halts on security_sensitive', () => {
184
+ assert.ok(codes(task({ security_sensitive: true })).includes('security_sensitive'));
185
+ });
186
+
187
+ t('halts when touches[] hits a protected path', () => {
188
+ const c = codes(task({ touches: ['src/bongos/auth.js'] }));
189
+ assert.ok(c.includes('protected_path'), `expected protected_path, got ${c}`);
190
+ });
191
+
192
+ t('does NOT flag an ordinary path as protected', () => {
193
+ assert.ok(!codes(task({ touches: ['modules/hall-ui/public/work.js'] })).includes('protected_path'));
194
+ });
195
+
196
+ t('halts when the task requires a rank above the builder', () => {
197
+ const c = haltReasons(task({ requires_rank: 'archon' }), { rank: 'xenos', claimableIds: new Set([1]) });
198
+ assert.ok(c.map((r) => r.code).includes('rank_floor'));
199
+ assert.match(c.find((r) => r.code === 'rank_floor').detail, /archon/);
200
+ });
201
+
202
+ t('does not flag rank_floor when the builder outranks the floor', () => {
203
+ assert.ok(!codes(task({ requires_rank: 'metic' })).includes('rank_floor'));
204
+ });
205
+
206
+ t('halts on a judgement kind (decision, spike)', () => {
207
+ assert.ok(codes(task({ kind: 'decision' })).includes('judgement_kind'));
208
+ assert.ok(codes(task({ kind: 'spike' })).includes('judgement_kind'));
209
+ assert.ok(!codes(task({ kind: 'feature' })).includes('judgement_kind'));
210
+ });
211
+
212
+ t('halts on a thin spec', () => {
213
+ assert.ok(codes(task({ description: '' })).includes('thin_spec'));
214
+ assert.ok(codes(task({ description: 'do the thing' })).includes('thin_spec'));
215
+ assert.ok(!codes(task({ description: SPEC })).includes('thin_spec'));
216
+ });
217
+
218
+ t('halts when the task is still gated (not ready)', () => {
219
+ assert.ok(codes(task({ status: 'backlog' })).includes('deps_unmet'));
220
+ assert.ok(codes(task({ status: 'blocked' })).includes('deps_unmet'));
221
+ });
222
+
223
+ t('halts when the task is already mid-ship, distinctly from deps_unmet', () => {
224
+ for (const status of ['active', 'completed', 'confirmed']) {
225
+ const c = codes(task({ status }));
226
+ assert.ok(c.includes('in_flight'), `${status} should read in_flight`);
227
+ assert.ok(!c.includes('deps_unmet'), `${status} should not read deps_unmet`);
228
+ }
229
+ });
230
+
231
+ t('halts when ready but absent from the server claimable feed', () => {
232
+ assert.ok(haltReasons(task(), { rank: 'archon', claimableIds: new Set([42]) })
233
+ .map((r) => r.code).includes('not_claimable'));
234
+ });
235
+
236
+ t('stays quiet about claimability when the feed could not be read', () => {
237
+ // The regression this guards: reading the claimable envelope under the wrong
238
+ // key yielded an empty set, which halted every task with not_claimable.
239
+ assert.deepEqual(haltReasons(task(), { rank: 'archon', claimableIds: null }), []);
240
+ });
241
+
242
+ t('reports every applicable reason, not just the first', () => {
243
+ const c = codes(task({ needs_migration: true, security_sensitive: true, description: '' }));
244
+ assert.ok(c.includes('needs_migration') && c.includes('security_sensitive') && c.includes('thin_spec'));
245
+ });
246
+
247
+ t('a missing task reports not_found rather than throwing', () => {
248
+ assert.deepEqual(haltReasons(null, clean).map((r) => r.code), ['not_found']);
249
+ });
250
+
251
+ // ---------------------------------------------------------------- totals
252
+
253
+ t('summarize totals count, credits and calibrated minutes', () => {
254
+ const s = summarize([
255
+ task({ id: 1, credits_reward: 30, est_minutes: 20 }),
256
+ task({ id: 2, credits_reward: 15, est_minutes_calibrated: 7, est_minutes: 99 }),
257
+ ]);
258
+ assert.deepEqual(s, { count: 2, credits: 45, minutes: 27 });
259
+ });
260
+
261
+ t('summarize tolerates missing numbers', () => {
262
+ assert.deepEqual(summarize([task({ credits_reward: null, est_minutes: null })]),
263
+ { count: 1, credits: 0, minutes: 0 });
264
+ });
265
+
266
+ console.log(`\nsequence: ${passed} passed, ${failed} failed`);
267
+ process.exit(failed ? 1 : 0);