@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
@@ -0,0 +1,116 @@
1
+ // tests/reward_mode_cost_plus_only.mjs — task 1002270 / ADR 0146 (real DB).
2
+ //
3
+ // End-to-end proof that the per-instance reward MODE actually flips the LIVE
4
+ // ship path — not just the pure helper. With branding config
5
+ // reward.mode='cost-plus-only', db.shipTask awards NO per-task estimate credit
6
+ // (no 'task.shipped' credit_log row) and no idea-promotion slice, while the
7
+ // task's credits_reward stays on the row (shown at claim time + the no-zero
8
+ // claim gate). The cost-plus session reward is a separate path (the sessions
9
+ // upload) and is unaffected here.
10
+ //
11
+ // This is the integration seam the DB-free credit_grant.mjs cases can't reach:
12
+ // the REAL awardLandingCredit → reward port → rewardConfig() → branding() wire.
13
+ //
14
+ // Real-DB (self-skips without Postgres; in the INTEGRATION set so the DB-free
15
+ // lane never runs it).
16
+ //
17
+ // Run: DATABASE_URL=... node tests/reward_mode_cost_plus_only.mjs
18
+
19
+ import { createRequire } from 'node:module';
20
+ import { strict as assert } from 'node:assert';
21
+ import os from 'node:os';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+
25
+ // The branding override MUST be set before anything calls the memoized
26
+ // branding() — so before the first app require below. A partial instance file
27
+ // deep-merges over neutral, so we override only the reward block. PID-suffixed
28
+ // so a parallel process can't collide (Date.now/random are unavailable here).
29
+ const brandingFile = path.join(os.tmpdir(), `bongos-reward-cpo-${process.pid}.json`);
30
+ fs.writeFileSync(brandingFile, JSON.stringify({ reward: { mode: 'cost-plus-only', marginPct: 20, usdPeg: 1 } }));
31
+ process.env.GDS_BRANDING_FILE = brandingFile;
32
+
33
+ const require = createRequire(import.meta.url);
34
+ const { Pool } = require('pg');
35
+
36
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
37
+ try {
38
+ await pool.query('SELECT 1');
39
+ } catch (e) {
40
+ console.log(`reward_mode_cost_plus_only.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
41
+ fs.rmSync(brandingFile, { force: true });
42
+ await pool.end().catch(() => {});
43
+ process.exit(0);
44
+ }
45
+
46
+ const seams = require('../src/module-seams.js');
47
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
48
+ const credits = require('../modules/economy/credits.js');
49
+ const db = require('../modules/lifecycle/db.js');
50
+
51
+ const VER = 'ZZZ-RM', WORKER = 90000301, TASK = 90000310;
52
+ const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
53
+
54
+ async function cleanup() {
55
+ await q(`DELETE FROM builder_achievements WHERE builder_id=$1`, [WORKER]).catch(() => {});
56
+ await q(`DELETE FROM credit_log WHERE builder_id=$1 OR task_id=$2`, [WORKER, TASK]).catch(() => {});
57
+ await q(`DELETE FROM claims WHERE task_id=$1`, [TASK]).catch(() => {});
58
+ await q(`DELETE FROM tasks WHERE id=$1`, [TASK]).catch(() => {});
59
+ await q(`DELETE FROM builders WHERE id=$1`, [WORKER]).catch(() => {});
60
+ await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
61
+ }
62
+
63
+ let pass = 0;
64
+ const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
65
+
66
+ try {
67
+ // Precondition: the branding override actually resolved to cost-plus-only. If
68
+ // this fails, the env/cache ordering broke and the rest is meaningless — so we
69
+ // assert it up front rather than let a silent default mask a real regression.
70
+ ok(credits.rewardConfig().mode === 'cost-plus-only',
71
+ 'branding override resolved (GDS_BRANDING_FILE read before branding() memoized)');
72
+ ok(credits.rewardConfig().perTaskCredit === false,
73
+ 'cost-plus-only ⇒ perTaskCredit is off');
74
+
75
+ await cleanup();
76
+ await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'rm','planning','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
77
+ // Archon so no rank/newcomer gate interferes with the ship.
78
+ await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'rm-w','rm-worker','RM Worker','archon')`, [WORKER]);
79
+ // A confirmed task owned by WORKER with a real estimate. In DEFAULT mode this
80
+ // would pay 60 × 1.0 (feature) at ship; in cost-plus-only it must pay nothing.
81
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by) VALUES ($1,$2,'rm task','confirmed',60,'feature',$3)`, [TASK, VER, WORKER]);
82
+ await q(`INSERT INTO claims (task_id,builder_id,claimed_at) VALUES ($1,$2, now())`, [TASK, WORKER]);
83
+
84
+ // Drive the REAL ship transition (the same call the reconciler/ship.js use).
85
+ await db.shipTask({ taskId: String(TASK), builderId: String(WORKER) });
86
+
87
+ const status = (await q(`SELECT status FROM tasks WHERE id=$1`, [TASK]))[0].status;
88
+ ok(status === 'shipped', `the task shipped (status=${status})`);
89
+
90
+ const shippedCredit = await q(
91
+ `SELECT delta FROM credit_log WHERE task_id=$1 AND reason='task.shipped'`, [TASK]);
92
+ ok(shippedCredit.length === 0,
93
+ 'cost-plus-only: NO per-task estimate credit_log row was written at ship');
94
+
95
+ // Belt-and-suspenders: no per-task estimate row under EITHER pay-on-land reason,
96
+ // and no idea-promotion slice (it cascades from the 0 base credit). Achievement
97
+ // rewards are a separate stream and intentionally out of scope (not asserted).
98
+ const anyTaskCredit = await q(
99
+ `SELECT reason FROM credit_log
100
+ WHERE task_id=$1 AND reason IN ('task.shipped','task.confirmed','idea_promotion_bonus')`, [TASK]);
101
+ ok(anyTaskCredit.length === 0,
102
+ 'no per-task estimate credit nor idea-promotion slice was paid');
103
+
104
+ const est = (await q(`SELECT credits_reward FROM tasks WHERE id=$1`, [TASK]))[0].credits_reward;
105
+ ok(Number(est) === 60,
106
+ 'the estimate (credits_reward=60) stays on the row — shown at claim time, just not paid');
107
+
108
+ console.log(`\nreward_mode_cost_plus_only (task 1002270 / ADR 0146): ${pass} passed, 0 failed`);
109
+ } catch (e) {
110
+ console.error('reward_mode_cost_plus_only.mjs FAILED:', e.stack || e.message);
111
+ process.exitCode = 1;
112
+ } finally {
113
+ await cleanup();
114
+ fs.rmSync(brandingFile, { force: true });
115
+ await pool.end();
116
+ }
@@ -0,0 +1,168 @@
1
+ // tests/reward_mode_surfacing.mjs
2
+ //
3
+ // Task 1002469 — "say that this instance pays cost-plus only". ADR 0146 lets an
4
+ // instance withhold the per-task ESTIMATE payout (`mode: 'cost-plus-only'`), but
5
+ // every builder-facing surface still advertised that estimate as if it were the
6
+ // payout. A builder claimed a 60-credit task, shipped clean, saw `Credits +0`,
7
+ // and spent a real session (2026-07-26) diagnosing a reward gate that was
8
+ // working exactly as configured.
9
+ //
10
+ // These prove the three display seams tell the truth in cost-plus-only mode AND
11
+ // are byte-unchanged on a default (paying) instance — the ADR's hard
12
+ // compatibility requirement.
13
+ //
14
+ // Run: node tests/reward_mode_surfacing.mjs
15
+
16
+ import { strict as assert } from 'node:assert';
17
+ import { createRequire } from 'node:module';
18
+ import fs from 'node:fs';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const card = require('../modules/lifecycle/ship-card.js');
22
+ const credits = require('../modules/economy/credits.js');
23
+ const reward = require('../modules/economy/reward.js');
24
+ const cli = require('../scripts/gds/cli-lib.js');
25
+
26
+ let passed = 0, failed = 0;
27
+ function test(name, fn) {
28
+ try { fn(); passed++; console.log(`PASS ${name}`); }
29
+ catch (err) { failed++; console.log(`FAIL ${name}\n ${err.message}`); }
30
+ }
31
+
32
+ const CPO = { mode: 'cost-plus-only', perTaskCredit: false };
33
+ const PAYS = { mode: 'cost-plus-and-estimate', perTaskCredit: true };
34
+
35
+ // --- (1) the port exposes the policy, so nobody re-derives it ----------------
36
+ // ADR 0146 put the mode→perTaskCredit mapping INSIDE economy on purpose. Display
37
+ // surfaces must ASK, not reimplement `mode !== 'cost-plus-only'`.
38
+
39
+ test('reward port exposes rewardConfig + rewardPolicy (display surfaces can ask)', () => {
40
+ assert.equal(typeof reward.rewardConfig, 'function');
41
+ assert.equal(typeof reward.rewardPolicy, 'function');
42
+ assert.equal(reward.rewardPolicy({ mode: 'cost-plus-only' }).perTaskCredit, false);
43
+ assert.equal(reward.rewardPolicy({}).perTaskCredit, true);
44
+ });
45
+
46
+ test('port delegates to credits (a stub bites at call time, per the reward.js contract)', () => {
47
+ const original = credits.rewardPolicy;
48
+ try {
49
+ credits.rewardPolicy = () => ({ mode: 'stubbed', perTaskCredit: false });
50
+ assert.equal(reward.rewardPolicy({}).mode, 'stubbed', 'port must delegate, not capture at load');
51
+ } finally { credits.rewardPolicy = original; }
52
+ });
53
+
54
+ // --- (2) the CLI prefers the SERVER's answer --------------------------------
55
+ // The mode is INSTANCE config and config/branding.json is NOT in a builder's
56
+ // checkout — only config/branding.neutral.json is. A laptop resolving locally
57
+ // therefore reports the paying default on the very instance that withholds, so
58
+ // the server value must win.
59
+
60
+ test('cli rewardPolicy: a server-supplied cost-plus-only wins over local config', () => {
61
+ assert.equal(cli.rewardPolicy(CPO).perTaskCredit, false);
62
+ assert.equal(cli.rewardPolicy(CPO).mode, 'cost-plus-only');
63
+ });
64
+
65
+ test('cli rewardPolicy: no server value → falls back, never crashes', () => {
66
+ const p = cli.rewardPolicy(null);
67
+ assert.equal(typeof p.mode, 'string');
68
+ assert.equal(p.perTaskCredit, true, 'fallback must be the PAYING default');
69
+ });
70
+
71
+ test('cli rewardPolicy: a malformed server block degrades to the paying default', () => {
72
+ for (const bad of [{}, { mode: 42 }, { perTaskCredit: false }, 'nonsense', 0]) {
73
+ assert.equal(cli.rewardPolicy(bad).perTaskCredit, true,
74
+ `malformed ${JSON.stringify(bad)} must never claim "not paid"`);
75
+ }
76
+ });
77
+
78
+ test('cli costPlusOnlyNote: names the real payer in cost-plus-only, silent otherwise', () => {
79
+ const note = cli.costPlusOnlyNote(CPO);
80
+ assert.ok(note && /cost-plus only/i.test(note), 'must name the mode');
81
+ assert.ok(/not the estimate/i.test(note), 'must say the estimate is not what pays');
82
+ assert.equal(cli.costPlusOnlyNote(PAYS), null, 'a paying instance shows nothing');
83
+ assert.equal(cli.costPlusOnlyNote(null), null);
84
+ });
85
+
86
+ // --- (3) the ship completion card explains a correct +0 ---------------------
87
+
88
+ const shipped = {
89
+ postGradeStatus: 'shipped', taskId: 1002469, title: 't', valueSummary: 'v',
90
+ grade: { passed: true }, fileCount: 1, added: 1, removed: 0,
91
+ usage: { tokens: 1000, costUsd: 0.1 }, deploy: { commit: 'abc1234' },
92
+ };
93
+ const creditsRow = (m) => { const r = m.rows.find((x) => x.k === 'Credits'); return r ? r.v : undefined; };
94
+
95
+ test('ship card: cost-plus-only annotates the +0 so it does not read as a bug', () => {
96
+ const v = creditsRow(card.buildCardModel({
97
+ ...shipped, credits: { awarded: 0, total: 5321, perTaskCredit: false },
98
+ }));
99
+ assert.match(v, /est\. not paid \(cost-plus only\)/, 'the 0 must explain itself');
100
+ });
101
+
102
+ test('ship card: a PAYING instance is byte-unchanged (ADR 0146 compatibility)', () => {
103
+ const withFlag = creditsRow(card.buildCardModel({
104
+ ...shipped, credits: { awarded: 18, total: 5321, perTaskCredit: true },
105
+ }));
106
+ const withoutFlag = creditsRow(card.buildCardModel({
107
+ ...shipped, credits: { awarded: 18, total: 5321 },
108
+ }));
109
+ assert.equal(withFlag, withoutFlag, 'the flag must not perturb a paying instance');
110
+ assert.doesNotMatch(withFlag, /cost-plus only/);
111
+ });
112
+
113
+ test('ship card: an ABSENT perTaskCredit (older caller) assumes paying, adds nothing', () => {
114
+ const v = creditsRow(card.buildCardModel({ ...shipped, credits: { awarded: 0, total: 5321 } }));
115
+ assert.doesNotMatch(v, /cost-plus only/, 'absent ⇒ paying default, never a wrong "not paid"');
116
+ });
117
+
118
+ // --- (4) the hall never drops "Est." when the instance does not pay ---------
119
+ // The live bug: taskCreditValue falls back to the ESTIMATE when a settled task
120
+ // has credits_awarded === 0, and under cost-plus-only that is EVERY shipped
121
+ // task. The plain (non-"Est.") label then presents that estimate as the actual
122
+ // ledger payout. Driven against the real file so the assertion can't drift.
123
+
124
+ function loadBrandApply(rewardBlock) {
125
+ const src = fs.readFileSync(new URL('../modules/hall-ui/public/brand-apply.js', import.meta.url), 'utf8');
126
+ const window = { __BRANDING__: { currency: { label: 'credits' }, reward: rewardBlock } };
127
+ const document = {
128
+ getElementById: () => null, querySelector: () => null, querySelectorAll: () => [],
129
+ readyState: 'complete', addEventListener: () => {}, title: '',
130
+ createTreeWalker: () => ({ nextNode: () => null }),
131
+ };
132
+ new Function('window', 'document', 'NodeFilter', src)(window, document, { SHOW_TEXT: 4 });
133
+ return window.OTBBranding;
134
+ }
135
+
136
+ const SHIPPED_TASK = { status: 'shipped', credits_awarded: 0, credits_reward: 60 };
137
+ const READY_TASK = { status: 'ready', credits_reward: 60 };
138
+
139
+ test('hall: cost-plus-only keeps "Est." on a SHIPPED task (the 60 is not a payout)', () => {
140
+ const B = loadBrandApply(CPO);
141
+ assert.equal(B.taskCreditLabel(SHIPPED_TASK), 'credits Est.',
142
+ 'dropping "Est." presents the unpaid estimate as the real payout');
143
+ assert.equal(B.taskCreditLabel(READY_TASK), 'credits Est.');
144
+ assert.equal(B.perTaskCredit(), false);
145
+ });
146
+
147
+ test('hall: a PAYING instance keeps the original settled/unsettled split', () => {
148
+ const B = loadBrandApply(PAYS);
149
+ assert.equal(B.taskCreditLabel(SHIPPED_TASK), 'credits', 'settled ⇒ plain label, unchanged');
150
+ assert.equal(B.taskCreditLabel(READY_TASK), 'credits Est.');
151
+ assert.equal(B.perTaskCredit(), true);
152
+ });
153
+
154
+ test('hall: an OLDER server (no reward block) behaves exactly as before', () => {
155
+ const B = loadBrandApply(undefined);
156
+ assert.equal(B.perTaskCredit(), true, 'absent ⇒ paying default');
157
+ assert.equal(B.taskCreditLabel(SHIPPED_TASK), 'credits');
158
+ assert.equal(B.rewardModeNote(), '', 'no note on a paying/unknown instance');
159
+ });
160
+
161
+ test('hall: the tooltip names the mode and what actually pays', () => {
162
+ const note = loadBrandApply(CPO).rewardModeNote();
163
+ assert.match(note, /cost-plus only/i);
164
+ assert.match(note, /guide, not a payout/i);
165
+ });
166
+
167
+ console.log(`\nreward_mode_surfacing: ${passed} passed, ${failed} failed`);
168
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,301 @@
1
+ // tests/route_permission_check.mjs — the route auditor understands PERMISSION gates
2
+ // (BV1.R109, ADR 0151).
3
+ //
4
+ // WHY THIS FILE EXISTS. src/bongos/route-rank-check.js is a trust primitive: a CI gate
5
+ // that hard-fails the build when a write route is ungated or silently downgraded. Before
6
+ // R109 it only recognised requireRank/requireBuilder/requireBfgPrincipal. Because
7
+ // `requirePermission` must mount AFTER `requireBuilder`, a route migrated to the
8
+ // permission gate fell through to the requireBuilder arm and classified 'any-builder' —
9
+ // a downgrade that reads as legitimate, and a published API doc that misstates authority.
10
+ //
11
+ // THE ORACLE PROBLEM (the trap R103 fell into). The classifier DERIVES a permission's
12
+ // rank from modules/governance/catalog.js, so asserting it back against catalog.floor
13
+ // would move both sides together and prove nothing. Every expectation below is therefore
14
+ // either a HAND-WRITTEN literal (a floor read off the catalog once, by a human, and
15
+ // frozen here — so a floor edit reds this file and has to be argued with) or
16
+ // auth.rankMeetsThreshold, the real production threshold function. The ONE place the
17
+ // catalog is used as an oracle is the source-parser drift check (P0), where the two
18
+ // sides are genuinely different mechanisms: a regex over the file's text vs. Node
19
+ // evaluating the module.
20
+ //
21
+ // DB-free. Run: node --test tests/route_permission_check.mjs
22
+
23
+ import assert from 'node:assert/strict';
24
+ import { test } from 'node:test';
25
+ import { createRequire } from 'node:module';
26
+ import fs from 'node:fs';
27
+ import os from 'node:os';
28
+ import path from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
33
+ const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
34
+ const auth = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
35
+ const genApiDocs = require(path.join(ROOT, 'scripts', 'gds', 'gen-api-docs.js'));
36
+
37
+ const rankOf = (src) => rrc.classifyText(src)[0].rank;
38
+ const rowOf = (src) => rrc.classifyText(src)[0];
39
+
40
+ // A hand-written floor table: each atom's floor as declared in catalog.js, transcribed
41
+ // here ONCE by hand. This is the independent side of every classification assertion
42
+ // below — if someone lowers `backup.manage` to 'xenos', the classifier follows the
43
+ // catalog and this file goes red, which is the whole point.
44
+ const HAND_WRITTEN_FLOORS = {
45
+ 'builder.rank.set': 'archon',
46
+ 'audit_log.read': 'archon',
47
+ 'auth.session.revoke': 'archon',
48
+ 'builder.roster.read': 'archon',
49
+ 'builder.status.set': 'archon',
50
+ 'backup.manage': 'metic',
51
+ 'idea.triage': 'metic',
52
+ 'security.report.file': 'metic',
53
+ 'task.claim.any': 'thetes',
54
+ 'idea.file': 'xenos',
55
+ 'cost.log': 'xenos',
56
+ };
57
+
58
+ // ═══ P0 — the source parser is faithful to the catalog it reads ═══════════════
59
+ // route-rank-check reads catalog.js as DATA (fs + regex), never require() — ADR 0083's
60
+ // one-way rule forbids core importing a module, and fitness.js enforces it on require
61
+ // edges. That parse must not silently drop or mis-read an atom, so pin it against the
62
+ // catalog as Node actually evaluates it. Two different mechanisms, so this is a real
63
+ // oracle, not a tautology.
64
+
65
+ test('P0: the parsed floor map matches the catalog module exactly (key set and floors)', () => {
66
+ const catalog = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
67
+ const parsed = rrc.permissionFloors();
68
+ const evaluated = new Map(catalog.PERMISSIONS.map((p) => [p.key, p.floor]));
69
+ assert.equal(parsed.size, evaluated.size,
70
+ `parser found ${parsed.size} atoms, the catalog defines ${evaluated.size} — the source-shape parse has drifted`);
71
+ const drift = [];
72
+ for (const [key, floor] of evaluated) {
73
+ if (parsed.get(key) !== floor) drift.push(`${key}: catalog '${floor}' vs parsed '${parsed.get(key)}'`);
74
+ }
75
+ assert.deepEqual(drift, []);
76
+ });
77
+
78
+ test('P0: core reads the catalog as DATA — no require() edge into modules/', () => {
79
+ const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'), 'utf8');
80
+ const codeLines = src.split('\n').filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l));
81
+ assert.equal(codeLines.some((l) => /require\(\s*['"][^'"]*modules\//.test(l)), false,
82
+ 'route-rank-check.js must not require() a module — read the catalog off disk (ADR 0083)');
83
+ });
84
+
85
+ test('P0: an unreadable or empty catalog THROWS rather than degrading to "nothing is gated"', () => {
86
+ assert.throws(() => rrc.permissionFloors(path.join(os.tmpdir(), 'definitely-not-a-catalog.js')),
87
+ /unreadable/, 'a missing catalog must be a broken install, not an opted-out module');
88
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rpc-cat-'));
89
+ const emptyCatalog = path.join(dir, 'catalog.js');
90
+ fs.writeFileSync(emptyCatalog, '// a catalog with no entries\n');
91
+ assert.throws(() => rrc.permissionFloors(emptyCatalog), /declares no permissions/);
92
+ fs.rmSync(dir, { recursive: true, force: true });
93
+ // And the pure half: with no floor data, every atom is unverifiable — never gated.
94
+ const empty = rrc.parsePermissionFloors('// a catalog with no entries\n');
95
+ assert.equal(empty.size, 0);
96
+ assert.equal(rrc.rankForPermissions(['backup.manage'], empty), 'unknown');
97
+ });
98
+
99
+ // ═══ P1 — classification: a permission gate resolves to its real floor ════════
100
+
101
+ test('P1: requirePermission resolves to the atom floor, NOT any-builder', () => {
102
+ for (const [key, floor] of Object.entries(HAND_WRITTEN_FLOORS)) {
103
+ const src = `router.post('/x', auth.requireBuilder, auth.requirePermission('${key}'), h);`;
104
+ const expected = floor === 'archon' ? 'archon' : floor === 'metic' ? 'metic+archon' : 'any-builder';
105
+ assert.equal(rankOf(src), expected,
106
+ `requirePermission('${key}') (floor ${floor}) must classify '${expected}'`);
107
+ }
108
+ });
109
+
110
+ test('P1: the label agrees with the REAL threshold function over the whole rank ladder', () => {
111
+ // auth.rankMeetsThreshold is the production gate. For every atom, the label the
112
+ // classifier emits must admit exactly the ranks the atom's floor admits — so the
113
+ // label is a faithful restatement of enforcement, not a parallel opinion.
114
+ const { RANKS } = require(path.join(ROOT, 'src', 'bongos', 'db-kernel.js'));
115
+ const LABEL_ADMITS = {
116
+ archon: (rank) => auth.rankMeetsThreshold(rank, ['archon']),
117
+ 'metic+archon': (rank) => auth.rankMeetsThreshold(rank, ['metic']),
118
+ 'any-builder': (rank) => auth.rankMeetsThreshold(rank, ['xenos']),
119
+ };
120
+ for (const [key, floor] of Object.entries(HAND_WRITTEN_FLOORS)) {
121
+ const label = rankOf(`router.post('/x', auth.requireBuilder, auth.requirePermission('${key}'), h);`);
122
+ for (const rank of RANKS) {
123
+ // 'thetes'-floored atoms are the one documented approximation: the label
124
+ // vocabulary has no thetes tier, so they read 'any-builder' — the label may
125
+ // UNDER-report strictness, never over-report it.
126
+ const strictEnough = auth.rankMeetsThreshold(rank, [floor]);
127
+ if (LABEL_ADMITS[label](rank)) continue; // label denies nothing the floor allows
128
+ assert.equal(strictEnough, false,
129
+ `${key}: rank ${rank} clears the real floor '${floor}' but label '${label}' would deny it`);
130
+ }
131
+ }
132
+ });
133
+
134
+ test('P1: AND semantics — a multi-atom gate takes the STRICTEST floor', () => {
135
+ const src = "router.post('/x', auth.requireBuilder, auth.requirePermission('idea.file', 'audit_log.read'), h);";
136
+ assert.equal(rankOf(src), 'archon',
137
+ 'requirePermission ANDs, so the gate is as strict as its strictest atom (archon), not its loosest');
138
+ const src2 = "router.post('/x', auth.requireBuilder, auth.requirePermission('cost.log', 'backup.manage'), h);";
139
+ assert.equal(rankOf(src2), 'metic+archon');
140
+ // order must not matter
141
+ const src3 = "router.post('/x', auth.requireBuilder, auth.requirePermission('audit_log.read', 'idea.file'), h);";
142
+ assert.equal(rankOf(src3), 'archon');
143
+ });
144
+
145
+ test('P1: the module doorway spelling (api.requirePermission) classifies identically', () => {
146
+ assert.equal(rankOf("router.post('/x', api.requireBuilder, api.requirePermission('backup.manage'), h);"), 'metic+archon');
147
+ });
148
+
149
+ test('P1: a stricter requireRank beside a permission gate is NOT understated', () => {
150
+ // The two gates AND. Reporting the looser one would understate authority in the docs.
151
+ const src = "router.post('/x', auth.requireBuilder, auth.requireRank('archon'), auth.requirePermission('idea.file'), h);";
152
+ assert.equal(rankOf(src), 'archon');
153
+ });
154
+
155
+ test('P1: the row carries the atoms it gates on', () => {
156
+ const r = rowOf("router.post('/x', auth.requireBuilder, auth.requirePermission('backup.manage'), h);");
157
+ assert.deepEqual(r.perms, ['backup.manage']);
158
+ assert.equal(rowOf("router.post('/x', auth.requireRank('archon'), h);").perms, null,
159
+ 'a rank-gated route names no atoms');
160
+ });
161
+
162
+ // ═══ P2 — fail-closed: unverifiable never reads as gated ══════════════════════
163
+
164
+ test('P2: an UNKNOWN/typo permission key fails closed to unknown, even beside requireBuilder', () => {
165
+ // The dangerous shape: requirePermission MUST mount after requireBuilder, so a typo'd
166
+ // atom (which grants nothing) sits next to a real authentication middleware. If the
167
+ // classifier deferred to that requireBuilder the route would read 'any-builder' — i.e.
168
+ // gated — while enforcing a permission nobody holds.
169
+ const src = "router.post('/x', auth.requireBuilder, auth.requirePermission('backup.manag'), h);";
170
+ assert.equal(rankOf(src), 'unknown', "a misspelled atom must surface, not hide behind requireBuilder");
171
+ assert.equal(rrc.rankForPermissions(['not.a.real.permission']), 'unknown');
172
+ // ...and one bad atom poisons the whole gate, even mixed with good ones.
173
+ assert.equal(rrc.rankForPermissions(['audit_log.read', 'not.a.real.permission']), 'unknown');
174
+ });
175
+
176
+ test('P2: a PRINCIPAL atom (floor null) fails closed — no rank-role holds it', () => {
177
+ // principal.* is gated on an exact builders.system_role, never on a role, so
178
+ // requirePermission('principal.bfg') would deny everyone. A route wanting it must use
179
+ // requireBfgPrincipal; classifying it as some rank would be a lie either way.
180
+ assert.equal(rrc.rankForPermissions(['principal.bfg']), 'unknown');
181
+ assert.equal(rankOf("router.post('/x', auth.requireBuilder, auth.requirePermission('principal.bfg'), h);"), 'unknown');
182
+ });
183
+
184
+ test('P2: an EMPTY permission gate fails closed', () => {
185
+ assert.equal(rankOf("router.post('/x', auth.requireBuilder, auth.requirePermission(), h);"), 'unknown');
186
+ assert.equal(rrc.rankForPermissions([]), 'unknown');
187
+ });
188
+
189
+ test('P2: a write route with NO gate at all still hard-fails the build', () => {
190
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rpc-'));
191
+ fs.writeFileSync(path.join(dir, 'shim.js'), " router.post('/demo/ping', h);\n");
192
+ const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, 'no-modules') });
193
+ fs.rmSync(dir, { recursive: true, force: true });
194
+ const ping = res.findings.find((f) => f.route === 'POST /demo/ping');
195
+ assert.ok(ping, 'an ungated write route must surface a finding');
196
+ assert.equal(ping.severity, 'blocker');
197
+ assert.equal(res.hardFail, true);
198
+ });
199
+
200
+ test('P2: a write route gated on a TYPO permission also hard-fails the build', () => {
201
+ // The regression this task exists to prevent: the migration must not be able to turn
202
+ // a gated write route into an ungated one that still looks gated.
203
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rpc-'));
204
+ fs.writeFileSync(path.join(dir, 'shim.js'),
205
+ " router.post('/demo/ping', auth.requireBuilder, auth.requirePermission('bakcup.manage'), h);\n");
206
+ const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, 'no-modules') });
207
+ fs.rmSync(dir, { recursive: true, force: true });
208
+ assert.equal(res.hardFail, true, 'an unresolvable permission gate on a write route is a hard fail');
209
+ assert.equal(res.findings[0].severity, 'blocker');
210
+ });
211
+
212
+ test('P2: a correctly permission-gated write route does NOT trip a false blocker', () => {
213
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rpc-'));
214
+ fs.writeFileSync(path.join(dir, 'shim.js'),
215
+ " router.post('/demo/ping', auth.requireBuilder, auth.requirePermission('backup.manage'), h);\n");
216
+ const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, 'no-modules') });
217
+ fs.rmSync(dir, { recursive: true, force: true });
218
+ assert.equal(res.findings.filter((f) => f.route === 'POST /demo/ping').length, 0);
219
+ });
220
+
221
+ // ═══ P3 — pins are satisfied by BOTH gate forms (the migration window) ════════
222
+
223
+ test('P3: a permission pin is satisfied by the requirePermission form', () => {
224
+ const r = rowOf("router.patch('/builders/:id/rank', auth.requireBuilder, auth.requirePermission('builder.rank.set'), h);");
225
+ assert.equal(rrc.pinMismatch('perm:builder.rank.set', r), null);
226
+ });
227
+
228
+ test('P3: the SAME permission pin is satisfied by the pre-migration requireRank form', () => {
229
+ const r = rowOf("router.patch('/builders/:id/rank', auth.requireBuilder, auth.requireRank('archon'), h);");
230
+ assert.equal(rrc.pinMismatch('perm:builder.rank.set', r), null,
231
+ 'an un-migrated route must still clear its permission pin, or R104/R105 deadlock');
232
+ });
233
+
234
+ test('P3: a rank pin is satisfied by BOTH forms too', () => {
235
+ const viaRank = rowOf("router.get('/backup/status', auth.requireBuilder, auth.requireRank('metic', 'archon'), h);");
236
+ const viaPerm = rowOf("router.get('/backup/status', auth.requireBuilder, auth.requirePermission('backup.manage'), h);");
237
+ assert.equal(rrc.pinMismatch('metic+archon', viaRank), null);
238
+ assert.equal(rrc.pinMismatch('metic+archon', viaPerm), null);
239
+ });
240
+
241
+ test('P3: a permission pin still catches a DOWNGRADE in either form', () => {
242
+ const downgradedRank = rowOf("router.patch('/builders/:id/rank', auth.requireBuilder, h);");
243
+ assert.match(rrc.pinMismatch('perm:builder.rank.set', downgradedRank) || '', /any-builder/);
244
+ const wrongAtom = rowOf("router.patch('/builders/:id/rank', auth.requireBuilder, auth.requirePermission('idea.file'), h);");
245
+ assert.ok(rrc.pinMismatch('perm:builder.rank.set', wrongAtom), 'gating on a different atom must red');
246
+ });
247
+
248
+ test('P3: a pin naming an atom the catalog does not define is itself a finding', () => {
249
+ const r = rowOf("router.patch('/x', auth.requireBuilder, auth.requireRank('archon'), h);");
250
+ assert.match(rrc.pinMismatch('perm:no.such.atom', r) || '', /does not define/);
251
+ });
252
+
253
+ test('P3: parsePin round-trips both spellings', () => {
254
+ assert.deepEqual(rrc.parsePin('archon'), { kind: 'rank', rank: 'archon' });
255
+ assert.deepEqual(rrc.parsePin('perm:a.b, c.d'), { kind: 'permission', perms: ['a.b', 'c.d'] });
256
+ });
257
+
258
+ // ═══ P4 — the published docs stop lying ══════════════════════════════════════
259
+
260
+ test('P4: gen-api-docs labels a permission-gated route by its floor, not any-builder', () => {
261
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rpc-doc-'));
262
+ const abs = path.join(dir, 'demo.js');
263
+ fs.writeFileSync(abs, [
264
+ " // Trigger a backup.",
265
+ " router.post('/backup/trigger', auth.requireBuilder, auth.requirePermission('backup.manage'), h);",
266
+ '',
267
+ " // Read the audit log.",
268
+ " router.get('/audit-log', auth.requireBuilder, auth.requirePermission('audit_log.read'), h);",
269
+ '',
270
+ ].join('\n'));
271
+ const model = genApiDocs.extractModel({ routeFiles: [{ abs, rel: 'src/bongos/routes/demo.js' }] });
272
+ fs.rmSync(dir, { recursive: true, force: true });
273
+
274
+ const trigger = model.find((r) => r.route === '/backup/trigger');
275
+ const audit = model.find((r) => r.route === '/audit-log');
276
+ assert.equal(trigger.rank, 'metic+archon', 'the published rank must be the real floor');
277
+ assert.equal(audit.rank, 'archon');
278
+ assert.deepEqual(trigger.perms, ['backup.manage'], 'the docs also name the authority atom');
279
+
280
+ const spec = genApiDocs.buildOpenapi(model);
281
+ const op = spec.paths['/backup/trigger'].post;
282
+ assert.equal(op['x-rank'], 'metic+archon');
283
+ assert.deepEqual(op['x-permissions'], ['backup.manage']);
284
+ assert.match(op.description, /backup\.manage/);
285
+
286
+ const md = genApiDocs.buildReferenceMd(model, spec);
287
+ assert.match(md, /\/backup\/trigger` \| `metic\+archon`/,
288
+ 'the reference table must print the real gate, not any-builder');
289
+ });
290
+
291
+ test('P4: a rank-gated route carries no x-permissions (todays spec stays byte-identical)', () => {
292
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rpc-doc-'));
293
+ const abs = path.join(dir, 'demo.js');
294
+ fs.writeFileSync(abs, " router.post('/legacy', auth.requireBuilder, auth.requireRank('archon'), h);\n");
295
+ const model = genApiDocs.extractModel({ routeFiles: [{ abs, rel: 'src/bongos/routes/demo.js' }] });
296
+ fs.rmSync(dir, { recursive: true, force: true });
297
+ const spec = genApiDocs.buildOpenapi(model);
298
+ const op = spec.paths['/legacy'].post;
299
+ assert.equal(op['x-rank'], 'archon');
300
+ assert.equal('x-permissions' in op, false);
301
+ });