@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,121 @@
1
+ // tests/github_token_store.mjs — DB-free tests for the encrypted repo-token store
2
+ // and the /github/repos gating (task 1002099 / ADR 0141). No real Postgres: db
3
+ // helpers take an injectable pool `p`; no real GitHub: the route's 503/403 branches
4
+ // don't reach GitHub. secret-box reads BUILDER_SECRET_KEY fresh each call, so we set
5
+ // a valid key up front and toggle it per test.
6
+
7
+ import { strict as assert } from 'node:assert';
8
+ import { test } from 'node:test';
9
+ import { createRequire } from 'node:module';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ // A valid 32-byte key (64 hex chars) BEFORE requiring the modules — secret-box configured.
14
+ process.env.BUILDER_SECRET_KEY = 'ab'.repeat(32);
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+ const db = require(path.join(ROOT, 'src/bongos/db.js'));
19
+ const secretBox = require(path.join(ROOT, 'src/bongos/secret-box.js'));
20
+ const buildGithubRouter = require(path.join(ROOT, 'src/bongos/routes/github.js'));
21
+ const { attachFail } = require(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
22
+
23
+ // Mock pool: records every query, returns programmed rows for SELECTs.
24
+ function mockPool(selectRows) {
25
+ const calls = [];
26
+ return {
27
+ calls,
28
+ query: async (text, params) => {
29
+ calls.push({ text, params });
30
+ return { rows: /^\s*SELECT/i.test(text) ? (selectRows || []) : [] };
31
+ },
32
+ };
33
+ }
34
+
35
+ test('secret-box is configured for this test file', () => {
36
+ assert.equal(secretBox.isConfigured(), true);
37
+ });
38
+
39
+ test('upsertGithubToken: stores an ENCRYPTED blob (never plaintext) + a future expires_at', async () => {
40
+ const p = mockPool();
41
+ const out = await db.upsertGithubToken({ builderId: 7, token: 'ghs_secret', scope: 'read:user public_repo', p });
42
+ assert.equal(out.stored, true);
43
+ const insert = p.calls.find((c) => /INSERT INTO github_oauth_tokens/i.test(c.text));
44
+ assert.ok(insert, 'issued an INSERT');
45
+ const enc = insert.params[1];
46
+ assert.notEqual(enc, 'ghs_secret', 'the plaintext token must never be stored');
47
+ assert.equal(secretBox.decrypt(enc), 'ghs_secret', 'the stored blob round-trips back to the token');
48
+ const expiresAt = insert.params[3];
49
+ assert.ok(expiresAt instanceof Date && expiresAt.getTime() > Date.now(), 'has a future expires_at');
50
+ });
51
+
52
+ test('getGithubToken: decrypts a live (unexpired) row', async () => {
53
+ const enc = secretBox.encrypt('ghs_live');
54
+ const p = mockPool([{ token_enc: enc, scope: 'x', expires_at: new Date(Date.now() + 60_000) }]);
55
+ const row = await db.getGithubToken({ builderId: 7, p });
56
+ assert.equal(row.token, 'ghs_live');
57
+ });
58
+
59
+ test('getGithubToken: an EXPIRED row returns null AND is swept (DELETE issued)', async () => {
60
+ const enc = secretBox.encrypt('ghs_old');
61
+ const p = mockPool([{ token_enc: enc, scope: 'x', expires_at: new Date(Date.now() - 60_000) }]);
62
+ const row = await db.getGithubToken({ builderId: 7, p });
63
+ assert.equal(row, null);
64
+ assert.ok(p.calls.some((c) => /DELETE FROM github_oauth_tokens/i.test(c.text)), 'lazily swept the expired row');
65
+ });
66
+
67
+ test('secret-box UNCONFIGURED: upsert no-ops (no plaintext ever written), get returns null', async () => {
68
+ const saved = process.env.BUILDER_SECRET_KEY;
69
+ delete process.env.BUILDER_SECRET_KEY;
70
+ try {
71
+ const p = mockPool();
72
+ const out = await db.upsertGithubToken({ builderId: 7, token: 'ghs_secret', p });
73
+ assert.equal(out.stored, false);
74
+ assert.equal(p.calls.length, 0, 'no query issued when secret-box is off');
75
+ assert.equal(await db.getGithubToken({ builderId: 7, p }), null);
76
+ } finally {
77
+ process.env.BUILDER_SECRET_KEY = saved;
78
+ }
79
+ });
80
+
81
+ // ---- route gating: the 503 / 403 signals the wizard branches on ----
82
+ function fakeRes() {
83
+ const res = { _status: 200, _body: null };
84
+ res.status = (c) => { res._status = c; return res; };
85
+ res.json = (b) => { res._body = b; return res; };
86
+ attachFail({}, res, () => {}); // installs res.fail exactly as the router does
87
+ return res;
88
+ }
89
+ function finalHandler(router, method, pth) {
90
+ const layer = router.stack.find((l) => l.route && l.route.path === pth && l.route.methods[method]);
91
+ const stack = layer.route.stack;
92
+ return stack[stack.length - 1].handle; // the handler after requireBuilder
93
+ }
94
+
95
+ test('GET /github/repos → 503 github_repos_unavailable when secret-box is unconfigured', async () => {
96
+ const saved = process.env.BUILDER_SECRET_KEY;
97
+ delete process.env.BUILDER_SECRET_KEY;
98
+ try {
99
+ const handler = finalHandler(buildGithubRouter(), 'get', '/github/repos');
100
+ const res = fakeRes();
101
+ await handler({ builder: { id: 1 }, query: {} }, res);
102
+ assert.equal(res._status, 503);
103
+ assert.equal(res._body.error.code, 'github_repos_unavailable');
104
+ } finally {
105
+ process.env.BUILDER_SECRET_KEY = saved;
106
+ }
107
+ });
108
+
109
+ test('GET /github/repos → 403 repo_scope_required when configured but no stored token', async () => {
110
+ const orig = db.getGithubToken;
111
+ db.getGithubToken = async () => null; // configured, but this builder never granted repo access
112
+ try {
113
+ const handler = finalHandler(buildGithubRouter(), 'get', '/github/repos');
114
+ const res = fakeRes();
115
+ await handler({ builder: { id: 1 }, query: {} }, res);
116
+ assert.equal(res._status, 403);
117
+ assert.equal(res._body.error.code, 'repo_scope_required');
118
+ } finally {
119
+ db.getGithubToken = orig;
120
+ }
121
+ });
@@ -0,0 +1,251 @@
1
+ // tests/goal_advisory.mjs — the orphan-task advisory (task 1002539).
2
+ //
3
+ // A task with no goal is NOT an error and was never blocked. db.createTask falls
4
+ // back to the version's "<version> — general" catch-all; for any version created
5
+ // after migration 160 that goal does not exist, so goal_id lands NULL silently.
6
+ // `goal_advisory` on the POST /tasks 201 surfaces that without blocking anything.
7
+ //
8
+ // Two properties matter and both are pinned here:
9
+ // 1. DETECTION is exact — the em dash is load-bearing, and a real goal is silent.
10
+ // 2. The advisory can NEVER fail the create. The route's try/catch is asserted by
11
+ // driving it with a db that throws (the last test), because that invariant is
12
+ // the one a future refactor will quietly break.
13
+ //
14
+ // Run: node tests/goal_advisory.mjs
15
+
16
+ import { strict as assert } from 'node:assert';
17
+ import { createRequire } from 'node:module';
18
+ import { readFileSync } from 'node:fs';
19
+ import http from 'node:http';
20
+ import { makeRunner } from './helpers.mjs';
21
+
22
+ process.env.NODE_ENV = 'test';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const { adviseGoal, candidateGoals, generalGoalTitle, buildGoalAdvisory } = require('../modules/lifecycle/goal-advisory.js');
26
+
27
+ const tasksSrc = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
28
+ const dbSrc = readFileSync(new URL('../modules/lifecycle/db.js', import.meta.url), 'utf8');
29
+ const { test, summary } = makeRunner();
30
+
31
+ const V = 'BONGOS-V1';
32
+
33
+ // ---- generalGoalTitle: the em dash is the whole contract ----
34
+
35
+ await test('generalGoalTitle matches migration 160 exactly — spaced EM DASH (U+2014)', () => {
36
+ const t = generalGoalTitle(V);
37
+ assert.equal(t, 'BONGOS-V1 — general');
38
+ assert.equal(t.charCodeAt(V.length + 1), 0x2014, 'U+2014 EM DASH, not a hyphen or en dash');
39
+ });
40
+
41
+ // ---- adviseGoal: the two reasons ----
42
+
43
+ await test('no_goal — a NULL goal_id is the structurally orphaned case', () => {
44
+ const a = adviseGoal({ versionId: V, goalId: null });
45
+ assert.equal(a.reason, 'no_goal');
46
+ assert.equal(a.goal_id, null);
47
+ assert.equal(a.version_id, V);
48
+ });
49
+
50
+ await test('catch_all — landing in "<version> — general" is a bucket, not a home', () => {
51
+ const a = adviseGoal({ versionId: V, goalId: 3, goalTitle: generalGoalTitle(V) });
52
+ assert.equal(a.reason, 'catch_all');
53
+ assert.equal(a.goal_id, 3, 'names the catch-all so a caller can PATCH away from it');
54
+ });
55
+
56
+ await test('a REAL goal is silent — null, not an empty object', () => {
57
+ assert.equal(adviseGoal({ versionId: V, goalId: 1000025, goalTitle: 'Builder experience — hall, dev box, and skills' }), null);
58
+ });
59
+
60
+ // The bug this guards: a near-miss title must not be mistaken for the catch-all.
61
+ // db.generalGoalIdForVersion resolves the SAME string, so if these two ever disagree
62
+ // a task would be filed into a goal the advisory then calls orphaned (or vice versa).
63
+ await test('near-miss titles are NOT the catch-all — ascii hyphen, en dash, no spaces, other version', () => {
64
+ for (const title of ['BONGOS-V1 - general', 'BONGOS-V1 – general', 'BONGOS-V1—general', 'CB-V1 — general', 'general', '']) {
65
+ assert.equal(adviseGoal({ versionId: V, goalId: 5, goalTitle: title }), null, `"${title}" must not match`);
66
+ }
67
+ });
68
+
69
+ // ---- fail-OPEN (the inverse posture of an authorization gate) ----
70
+
71
+ await test('FAIL-OPEN: ambiguous input says nothing rather than nagging', () => {
72
+ assert.equal(adviseGoal({ versionId: V, goalId: 5, goalTitle: undefined }), null, 'unknown title → silent');
73
+ assert.equal(adviseGoal({ versionId: null, goalId: 5, goalTitle: 'x — general' }), null, 'unknown version → silent');
74
+ assert.equal(adviseGoal({ versionId: V, goalId: 5, goalTitle: null }), null);
75
+ });
76
+
77
+ await test('FAIL-OPEN: a bare {} is the no_goal case, and nothing throws on junk', () => {
78
+ assert.equal(adviseGoal({}).reason, 'no_goal');
79
+ assert.doesNotThrow(() => adviseGoal());
80
+ assert.doesNotThrow(() => adviseGoal({ versionId: 1, goalId: 'x', goalTitle: 7 }));
81
+ });
82
+
83
+ // ---- candidateGoals ----
84
+
85
+ await test('candidateGoals drops the catch-all, preserves order, shapes to id/title/scope', () => {
86
+ const goals = [
87
+ { id: 1, title: generalGoalTitle(V), scope_modules: [], status: 'open', created_by: null },
88
+ { id: 2, title: 'Builder experience', scope_modules: ['hall-ui'], status: 'open', extra: 'dropped' },
89
+ { id: 3, title: 'Governance', scope_modules: ['governance'], status: 'open' },
90
+ ];
91
+ const out = candidateGoals(goals, V);
92
+ assert.deepEqual(out, [
93
+ { id: 2, title: 'Builder experience', scope_modules: ['hall-ui'] },
94
+ { id: 3, title: 'Governance', scope_modules: ['governance'] },
95
+ ]);
96
+ });
97
+
98
+ await test('candidateGoals is total — non-array, empty, and missing scope_modules', () => {
99
+ assert.deepEqual(candidateGoals(null, V), []);
100
+ assert.deepEqual(candidateGoals(undefined, V), []);
101
+ assert.deepEqual(candidateGoals([], V), []);
102
+ assert.deepEqual(candidateGoals([{ id: 9, title: 'G' }], V), [{ id: 9, title: 'G', scope_modules: [] }]);
103
+ });
104
+
105
+ // ---- the ONE definition of the catch-all title ----
106
+
107
+ await test('db.js resolves the catch-all through generalGoalTitle — no second spelling', () => {
108
+ assert.match(dbSrc, /require\('\.\/goal-advisory\.js'\)/, 'db.js imports the shared helper');
109
+ assert.match(dbSrc, /title = \$2 LIMIT 1/, 'the SQL binds the title rather than inlining it');
110
+ assert.equal(
111
+ (dbSrc.match(/' — general'/g) || []).length, 0,
112
+ 'the em-dash literal must exist in exactly ONE place (goal-advisory.js), not in db.js too'
113
+ );
114
+ });
115
+
116
+ // ---- the non-blocking contract, asserted rather than commented ----
117
+
118
+ await test('the route delegates to buildGoalAdvisory in one line — no inline logic to drift', () => {
119
+ assert.match(tasksSrc, /const goal_advisory = await buildGoalAdvisory\(\{ db, versionId: body\.version_id, task \}\);/);
120
+ assert.match(tasksSrc, /goal_advisory,\s*\}\);/, 'included in the 201 body');
121
+ });
122
+
123
+ // ---- buildGoalAdvisory: the real function, driven with a stub db ----
124
+
125
+ const stubDb = ({ goal = null, goals = [], throwOn = null } = {}) => ({
126
+ async getGoal() { if (throwOn === 'getGoal') throw new Error('db down'); return goal; },
127
+ async listGoals() { if (throwOn === 'listGoals') throw new Error('db down'); return goals; },
128
+ });
129
+
130
+ await test('buildGoalAdvisory: a goal-less task yields the no_goal advisory + candidates', async () => {
131
+ const db = stubDb({ goals: [
132
+ { id: 1, title: generalGoalTitle(V), scope_modules: [] },
133
+ { id: 2, title: 'Builder experience', scope_modules: ['hall-ui'] },
134
+ ] });
135
+ const a = await buildGoalAdvisory({ db, versionId: V, task: { id: 42, goal_id: null } });
136
+ assert.equal(a.reason, 'no_goal');
137
+ assert.deepEqual(a.candidate_goals, [{ id: 2, title: 'Builder experience', scope_modules: ['hall-ui'] }],
138
+ 'the catch-all is never offered as a home');
139
+ assert.match(a.next_step, /\/goal-create/);
140
+ assert.match(a.next_step, /PATCH \/tasks\/42/, 'names the actual task id');
141
+ });
142
+
143
+ await test('buildGoalAdvisory: a task in a REAL goal is silent, and never reads the goal list', async () => {
144
+ let listed = false;
145
+ const db = {
146
+ async getGoal() { return { id: 9, title: 'Builder experience' }; },
147
+ async listGoals() { listed = true; return []; },
148
+ };
149
+ assert.equal(await buildGoalAdvisory({ db, versionId: V, task: { id: 1, goal_id: 9 } }), null);
150
+ assert.equal(listed, false, 'the happy path costs exactly one query');
151
+ });
152
+
153
+ await test('buildGoalAdvisory: the catch-all is detected and named', async () => {
154
+ const db = stubDb({ goal: { id: 3, title: generalGoalTitle(V) }, goals: [] });
155
+ const a = await buildGoalAdvisory({ db, versionId: V, task: { id: 7, goal_id: 3 } });
156
+ assert.equal(a.reason, 'catch_all');
157
+ assert.equal(a.goal_id, 3);
158
+ });
159
+
160
+ // The invariant a future refactor will quietly break. Both DB calls are covered:
161
+ // getGoal is only reached when goal_id is set, listGoals only on the unhappy path.
162
+ await test('NON-BLOCKING: buildGoalAdvisory returns null when getGoal throws', async () => {
163
+ const a = await buildGoalAdvisory({ db: stubDb({ throwOn: 'getGoal' }), versionId: V, task: { id: 1, goal_id: 5 } });
164
+ assert.equal(a, null, 'degrades to silence — never propagates into the already-committed create');
165
+ });
166
+
167
+ await test('NON-BLOCKING: buildGoalAdvisory returns null when listGoals throws', async () => {
168
+ const a = await buildGoalAdvisory({ db: stubDb({ throwOn: 'listGoals' }), versionId: V, task: { id: 1, goal_id: null } });
169
+ assert.equal(a, null);
170
+ });
171
+
172
+ await test('NON-BLOCKING: a db missing the methods entirely still yields null, not a crash', async () => {
173
+ assert.equal(await buildGoalAdvisory({ db: {}, versionId: V, task: { id: 1, goal_id: 5 } }), null);
174
+ });
175
+
176
+
177
+ // ---- ROUTER-LEVEL: the invariant asserted over real HTTP ----
178
+ // The unit tests above prove buildGoalAdvisory degrades to null. This proves the
179
+ // thing that actually matters to a caller: when the advisory's DB reads blow up,
180
+ // POST /tasks still answers 201 with the task, and goal_advisory is null. If this
181
+ // ever regresses, a created task becomes a 500, the caller retries, and without a
182
+ // source_ref that mints a duplicate.
183
+
184
+ const express = require('express');
185
+
186
+ function listen(app) {
187
+ return new Promise((resolve) => {
188
+ const server = app.listen(0, '127.0.0.1', () => resolve(server));
189
+ });
190
+ }
191
+
192
+ function post(port, path, body) {
193
+ return new Promise((resolve, reject) => {
194
+ const payload = JSON.stringify(body);
195
+ const req = http.request(
196
+ { host: '127.0.0.1', port, path, method: 'POST',
197
+ headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) } },
198
+ (res) => {
199
+ let data = '';
200
+ res.on('data', (c) => { data += c; });
201
+ res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(data || '{}') }));
202
+ }
203
+ );
204
+ req.on('error', reject);
205
+ req.end(payload);
206
+ });
207
+ }
208
+
209
+ // A miniature stand-in for the POST /tasks tail: the task row is already created,
210
+ // then the advisory runs, then the 201 goes out — the exact ordering in tasks.js.
211
+ function makeApp(db) {
212
+ const app = express();
213
+ app.use(express.json());
214
+ app.post('/tasks', async (req, res) => {
215
+ try {
216
+ const task = { id: 99, goal_id: null, credits_reward: 60 }; // already committed
217
+ const goal_advisory = await buildGoalAdvisory({ db, versionId: req.body.version_id, task });
218
+ res.status(201).json({ task, criteria: [], goal_advisory });
219
+ } catch (err) {
220
+ res.status(500).json({ error: { code: 'create_failed' } });
221
+ }
222
+ });
223
+ return app;
224
+ }
225
+
226
+ await test('HTTP: a healthy db yields 201 with a populated goal_advisory', async () => {
227
+ const db = { async getGoal() { return null; }, async listGoals() { return [{ id: 2, title: 'Builder experience', scope_modules: [] }]; } };
228
+ const server = await listen(makeApp(db));
229
+ try {
230
+ const r = await post(server.address().port, '/tasks', { version_id: V });
231
+ assert.equal(r.status, 201);
232
+ assert.equal(r.body.goal_advisory.reason, 'no_goal');
233
+ assert.deepEqual(r.body.goal_advisory.candidate_goals, [{ id: 2, title: 'Builder experience', scope_modules: [] }]);
234
+ } finally { server.close(); }
235
+ });
236
+
237
+ await test('HTTP: db.listGoals THROWS → still 201, task returned, goal_advisory null (never a 500)', async () => {
238
+ const db = {
239
+ async getGoal() { throw new Error('db down'); },
240
+ async listGoals() { throw new Error('db down'); },
241
+ };
242
+ const server = await listen(makeApp(db));
243
+ try {
244
+ const r = await post(server.address().port, '/tasks', { version_id: V });
245
+ assert.equal(r.status, 201, 'the create must NOT become a 500 the caller retries into a duplicate');
246
+ assert.equal(r.body.task.id, 99, 'the created task is still returned');
247
+ assert.equal(r.body.goal_advisory, null, 'the advisory degrades to silence');
248
+ } finally { server.close(); }
249
+ });
250
+
251
+ summary();
@@ -0,0 +1,269 @@
1
+ // tests/goal_criterion_confirm_ui.mjs — in-hall criterion confirmation (task 1002164).
2
+ //
3
+ // The goal's bx-hall criterion says core task actions must work in the builders
4
+ // hall WITHOUT invoking the LLM. Criterion confirmation violated that: the goal
5
+ // detail page rendered only two states (satisfied ✓ / not) and its summary line
6
+ // literally told the builder to go run the /goal-review CLI skill. The server
7
+ // side already derived "met — pending review" (ADR 0086 §6 / BV1.R63) and already
8
+ // exposed POST /done-when/:id/satisfy at Metic+; only the UI was missing.
9
+ //
10
+ // This EXECUTES the real modules/hall-ui/public/goals-page.js in a vm under a
11
+ // minimal DOM/kit stub (the tests/hall_signout.mjs precedent — there is no jsdom
12
+ // in this repo), boots it against a fake API serving one goal with all three
13
+ // criterion states, and asserts on the HTML it actually writes. It then drives a
14
+ // real click on the rendered Confirm button and proves the POST fires.
15
+ //
16
+ // Covers:
17
+ // • the three criterion states render distinguishably (satisfied / pending / open)
18
+ // • a Metic sees Confirm on a pending criterion — and only on a pending one
19
+ // • a Xenos sees the state but NOT the control (server still enforces)
20
+ // • an achieved or archived goal offers no Confirm at all
21
+ // • the summary line no longer points at /goal-review, and counts what's ready
22
+ // • clicking Confirm POSTs /done-when/:id/satisfy and re-reads the goal
23
+ // • goal_achieved:true is announced to the builder
24
+ //
25
+ // Run: node tests/goal_criterion_confirm_ui.mjs
26
+ import assert from 'node:assert/strict';
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import vm from 'node:vm';
30
+ import { fileURLToPath } from 'node:url';
31
+ import { makeRunner } from './helpers.mjs';
32
+
33
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+ const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-page.js'), 'utf8');
35
+
36
+ const { test, summary } = makeRunner();
37
+
38
+ const CRIT = {
39
+ satisfied: { id: 501, criterion_id: 'c-done', criterion_md: 'Already confirmed', satisfied: true, pending_review: false },
40
+ pending: { id: 502, criterion_id: 'c-pending', criterion_md: 'All its tasks shipped', satisfied: false, pending_review: true },
41
+ open: { id: 503, criterion_id: 'c-open', criterion_md: 'Still has open work', satisfied: false, pending_review: false },
42
+ };
43
+
44
+ // Boot goals-page.js against a fake API and return what it rendered + recorded.
45
+ // `rank` drives the client-side control gate; `goalStatus` drives the goal state.
46
+ async function render({ rank = 'metic', goalStatus = 'open', criteria = Object.values(CRIT), satisfyResponse } = {}) {
47
+ const calls = { requests: [], toasts: [] };
48
+ const els = new Map();
49
+ const listeners = new Map(); // selector -> [handler]
50
+
51
+ const makeEl = (key) => {
52
+ const el = {
53
+ _key: key, dataset: {}, style: {}, title: '', textContent: '', hidden: false, disabled: false, value: '',
54
+ innerHTML: '',
55
+ classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
56
+ setAttribute() {}, getAttribute() { return null; }, removeAttribute() {},
57
+ appendChild() {}, focus() {}, click() {}, querySelectorAll() { return []; },
58
+ addEventListener(type, fn) {
59
+ if (type !== 'click') return;
60
+ if (!listeners.has(key)) listeners.set(key, []);
61
+ listeners.get(key).push(fn);
62
+ },
63
+ };
64
+ return el;
65
+ };
66
+ const $ = (sel) => {
67
+ if (!sel) return null;
68
+ if (!els.has(sel)) els.set(sel, makeEl(sel));
69
+ return els.get(sel);
70
+ };
71
+
72
+ const goal = { id: 7, version_id: 'V1', title: 'Builder experience', description: '', status: goalStatus, visibility: 'public', created_by: 999, scope_modules: [] };
73
+ const detail = { goal, members: [], criteria, dependencies: [] };
74
+
75
+ const api = {
76
+ request(method, p) {
77
+ calls.requests.push([method, p]);
78
+ const reply = (data, status = 200) => Promise.resolve({ status, ok: true, data });
79
+ if (method === 'GET' && /\/me$/.test(p)) return reply({ builder: { id: 1, rank }, active_claims: [] });
80
+ if (method === 'GET' && /\/goals$/.test(p)) return reply({ goals: [goal] });
81
+ if (method === 'GET' && /\/goals\/7$/.test(p)) return reply(detail);
82
+ if (method === 'GET' && /\/tasks\?/.test(p)) return reply({ tasks: [] });
83
+ if (method === 'GET' && /\/conflicts$/.test(p)) return reply({ pairs: [] });
84
+ if (method === 'GET' && /\/requests$/.test(p)) return reply({ requests: [] });
85
+ if (method === 'POST' && /\/done-when\/\d+\/satisfy$/.test(p)) {
86
+ return Promise.resolve(satisfyResponse || { status: 200, ok: true, data: { criterion: {}, goal_achieved: false, goal: null } });
87
+ }
88
+ return reply({});
89
+ },
90
+ };
91
+
92
+ const pager = (prefix) => ({ prefix, page: 1, perPage: 25 });
93
+ const windowObj = {
94
+ OTB: {
95
+ escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch])),
96
+ $, fmtNum: (n) => String(n), fmtDate: () => '', rankLabel: (r) => r,
97
+ toast: (m) => calls.toasts.push(m),
98
+ },
99
+ OTBKit: {
100
+ makePager: (prefix) => pager(prefix),
101
+ paginate: (rows) => rows,
102
+ wirePager() {}, pagerBarHtml: () => '',
103
+ makeTabs: () => ({ setCount() {}, select() {}, activate() {} }),
104
+ makeFilterBar() {},
105
+ emptyStateHtml: (m) => `<div class="kit-empty">${m}</div>`,
106
+ skeletonRows: () => '',
107
+ },
108
+ OTBBranding: { currencyLabel: () => 'credits', taskSettled: () => false, taskCreditValue: () => 0 },
109
+ BongosClient: { createClient: () => api },
110
+ location: { hash: '#/goal/7', pathname: '/builders/goals', assign() {} },
111
+ addEventListener() {},
112
+ scrollTo() {},
113
+ matchMedia: () => ({ matches: false, addEventListener() {} }),
114
+ localStorage: { getItem() { return null; }, setItem() {} },
115
+ };
116
+ const documentObj = {
117
+ body: { dataset: {} },
118
+ documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
119
+ getElementById: (id) => $(`#${id}`),
120
+ querySelector: (sel) => $(sel),
121
+ querySelectorAll: () => [],
122
+ createElement: () => makeEl('created'),
123
+ addEventListener() {},
124
+ };
125
+
126
+ const sandbox = { window: windowObj, document: documentObj, location: windowObj.location, console, setTimeout, clearTimeout, Set, Map, Number, String, Array, Object, JSON, Math, encodeURIComponent, Promise };
127
+ sandbox.globalThis = sandbox;
128
+ vm.createContext(sandbox);
129
+ vm.runInContext(SRC, sandbox, { filename: 'goals-page.js' });
130
+
131
+ // Let boot()'s promise chain settle (me → goals → details → route → render).
132
+ for (let i = 0; i < 25; i++) await new Promise((r) => setTimeout(r, 0));
133
+
134
+ const clickConfirm = async (critId) => {
135
+ const handlers = listeners.get('#goal-completion-body') || [];
136
+ assert.ok(handlers.length > 0, 'a click listener must be wired on the completion band');
137
+ const btn = { disabled: false, textContent: 'Confirm', dataset: { confirmCrit: String(critId) } };
138
+ const ev = {
139
+ target: { closest: (sel) => (sel === '[data-confirm-crit]' ? btn : null) },
140
+ preventDefault() {},
141
+ };
142
+ for (const fn of handlers) fn(ev);
143
+ for (let i = 0; i < 25; i++) await new Promise((r) => setTimeout(r, 0));
144
+ return btn;
145
+ };
146
+
147
+ return { calls, band: $('#goal-completion-body').innerHTML, full: $('#goal-completion-full').innerHTML, clickConfirm };
148
+ }
149
+
150
+ // =========================================================================
151
+ // The three criterion states
152
+ // =========================================================================
153
+
154
+ await test('the goal detail renders all three criterion states distinguishably', async () => {
155
+ const { band } = await render();
156
+ assert.match(band, /crit--done/, 'a confirmed criterion keeps its done styling');
157
+ assert.match(band, /crit--pending/, 'a met-but-unconfirmed criterion gets its own state');
158
+ // The open criterion is the one with neither modifier.
159
+ const items = band.match(/<li class="crit[^"]*"/g) || [];
160
+ assert.equal(items.length, 3, `three criteria render — got ${JSON.stringify(items)}`);
161
+ assert.equal(items.filter((s) => /crit--done/.test(s)).length, 1, 'exactly one done');
162
+ assert.equal(items.filter((s) => /crit--pending/.test(s)).length, 1, 'exactly one pending');
163
+ assert.equal(items.filter((s) => !/crit--(done|pending)/.test(s)).length, 1, 'exactly one plain/open');
164
+ });
165
+
166
+ await test('a pending criterion is the ONLY one offering Confirm (Metic)', async () => {
167
+ const { band } = await render({ rank: 'metic' });
168
+ const buttons = band.match(/data-confirm-crit="(\d+)"/g) || [];
169
+ assert.equal(buttons.length, 1, `exactly one Confirm control — got ${JSON.stringify(buttons)}`);
170
+ assert.match(buttons[0], new RegExp(`"${CRIT.pending.id}"`), 'it targets the pending criterion');
171
+ });
172
+
173
+ await test('an Archon also gets Confirm (Metic+ means at-or-above, not equals)', async () => {
174
+ const { band } = await render({ rank: 'archon' });
175
+ assert.match(band, /data-confirm-crit/, 'an Archon outranks Metic and keeps the control');
176
+ });
177
+
178
+ await test('a Xenos sees the pending STATE but never the Confirm control', async () => {
179
+ const { band } = await render({ rank: 'xenos' });
180
+ assert.match(band, /crit--pending/, 'the state is still visible to everyone');
181
+ assert.doesNotMatch(band, /data-confirm-crit/, 'the action is hidden below Metic');
182
+ assert.match(band, /crit__flag/, 'a read-only "Ready" flag stands in for the button');
183
+ });
184
+
185
+ await test('a thetes (below Metic) also gets no Confirm control', async () => {
186
+ const { band } = await render({ rank: 'thetes' });
187
+ assert.doesNotMatch(band, /data-confirm-crit/, 'thetes is below metic in the ladder');
188
+ });
189
+
190
+ await test('an achieved goal offers no Confirm (nothing left to confirm)', async () => {
191
+ const { band } = await render({ goalStatus: 'achieved' });
192
+ assert.doesNotMatch(band, /data-confirm-crit/, 'a closed goal is not actionable');
193
+ });
194
+
195
+ await test('an archived goal offers no Confirm', async () => {
196
+ const { band } = await render({ goalStatus: 'archived' });
197
+ assert.doesNotMatch(band, /data-confirm-crit/, 'an archived goal is not actionable');
198
+ });
199
+
200
+ // =========================================================================
201
+ // The summary line — the /goal-review punt is gone
202
+ // =========================================================================
203
+
204
+ await test('the summary no longer sends the builder to the /goal-review CLI skill', async () => {
205
+ const { band } = await render();
206
+ assert.doesNotMatch(band, /goal-review/, 'the LLM/CLI punt is removed — this is the whole point of the task');
207
+ });
208
+
209
+ await test('the summary counts what is actually ready to confirm', async () => {
210
+ const { band } = await render();
211
+ assert.match(band, /1 criterion is met and ready to confirm/, 'singular phrasing for one pending criterion');
212
+ });
213
+
214
+ await test('with nothing pending, the summary explains when a criterion becomes confirmable', async () => {
215
+ const { band } = await render({ criteria: [CRIT.satisfied, CRIT.open] });
216
+ assert.doesNotMatch(band, /ready to confirm/, 'no false "ready" claim when nothing is pending');
217
+ assert.match(band, /once every task linked to it has shipped/, 'explains the precondition instead');
218
+ });
219
+
220
+ await test('an achieved goal keeps its own summary line', async () => {
221
+ const { band } = await render({ goalStatus: 'achieved', criteria: [CRIT.satisfied] });
222
+ assert.match(band, /Goal achieved/, 'achieved wording is preserved');
223
+ });
224
+
225
+ // =========================================================================
226
+ // The action itself
227
+ // =========================================================================
228
+
229
+ await test('clicking Confirm POSTs /done-when/:id/satisfy for that criterion', async () => {
230
+ const { calls, clickConfirm } = await render();
231
+ await clickConfirm(CRIT.pending.id);
232
+ const posts = calls.requests.filter(([m, p]) => m === 'POST' && /\/done-when\/\d+\/satisfy$/.test(p));
233
+ assert.equal(posts.length, 1, `exactly one satisfy POST — got ${JSON.stringify(calls.requests)}`);
234
+ assert.match(posts[0][1], new RegExp(`/done-when/${CRIT.pending.id}/satisfy$`), 'targets the clicked criterion');
235
+ });
236
+
237
+ await test('confirming re-reads the goal so the row updates in place (no manual refresh)', async () => {
238
+ const { calls, clickConfirm } = await render();
239
+ const before = calls.requests.filter(([m, p]) => m === 'GET' && /\/goals\/7$/.test(p)).length;
240
+ await clickConfirm(CRIT.pending.id);
241
+ const after = calls.requests.filter(([m, p]) => m === 'GET' && /\/goals\/7$/.test(p)).length;
242
+ assert.ok(after > before, 'the detail is force-reloaded after a successful confirm');
243
+ });
244
+
245
+ await test('confirming the LAST criterion announces the goal auto-achieved', async () => {
246
+ const { calls, clickConfirm } = await render({
247
+ satisfyResponse: { status: 200, ok: true, data: { criterion: {}, goal_achieved: true, goal: { id: 7, status: 'achieved' } } },
248
+ });
249
+ await clickConfirm(CRIT.pending.id);
250
+ assert.ok(calls.toasts.some((t) => /achieved/i.test(t)), `the auto-achieve is surfaced — got ${JSON.stringify(calls.toasts)}`);
251
+ });
252
+
253
+ await test('an ordinary confirm reports success without claiming the goal is achieved', async () => {
254
+ const { calls, clickConfirm } = await render();
255
+ await clickConfirm(CRIT.pending.id);
256
+ assert.ok(calls.toasts.some((t) => /Criterion confirmed\.$/.test(t)), `plain confirmation — got ${JSON.stringify(calls.toasts)}`);
257
+ assert.ok(!calls.toasts.some((t) => /achieved/i.test(t)), 'no false auto-achieve claim');
258
+ });
259
+
260
+ await test('a double-click fires the satisfy POST only once', async () => {
261
+ const { calls, clickConfirm } = await render();
262
+ const handlersFired = clickConfirm(CRIT.pending.id);
263
+ await clickConfirm(CRIT.pending.id);
264
+ await handlersFired;
265
+ const posts = calls.requests.filter(([m, p]) => m === 'POST' && /\/satisfy$/.test(p));
266
+ assert.equal(posts.length, 1, `in-flight guard holds — got ${posts.length} POSTs`);
267
+ });
268
+
269
+ summary();