@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.
- package/.bongos-core.json +1740 -640
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +5 -3
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +11 -2
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +15 -2
- package/docs/api/openapi.json +2648 -483
- package/docs/api-reference.md +73 -13
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +15 -7
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +252 -62
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -21
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +58 -11
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +268 -19
- package/scripts/gds/publish-manifest.js +22 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +42 -16
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +72 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +433 -59
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +271 -7
- package/scripts/migrate.sh +10 -0
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +809 -107
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +105 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +312 -13
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/routes.js +2 -0
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +49 -2
- package/src/instance-config.js +46 -0
- package/src/module-api.js +140 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +41 -23
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +198 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/package_core.mjs +246 -1
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +289 -7
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +140 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +393 -5
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// tests/governance_resolver.mjs — the permission resolver (BV1.R95, ADR 0151).
|
|
2
|
+
//
|
|
3
|
+
// DB-free: exercises the resolver's PURE core (effectivePermissions) plus the
|
|
4
|
+
// fail-closed short-circuits and the kernel-port shape. The live DB join is
|
|
5
|
+
// covered by R103's parity test, which needs Postgres.
|
|
6
|
+
//
|
|
7
|
+
// The invariants that matter here are trust-boundary invariants: an unknown grant
|
|
8
|
+
// key must not grant anything, a role-less builder must resolve to nothing, and
|
|
9
|
+
// the resolver must not memoise (ADR 0016 — authority is read fresh per request).
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const GOV = path.join(ROOT, 'modules', 'governance');
|
|
20
|
+
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
21
|
+
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
22
|
+
const { effectivePermissions, resolveBuilderPermissions, builderHasPermissions } = resolver;
|
|
23
|
+
|
|
24
|
+
test('effectivePermissions unions, de-duplicates, and sorts', () => {
|
|
25
|
+
const out = effectivePermissions(['task.ship', 'idea.triage', 'task.ship', 'cost.log']);
|
|
26
|
+
assert.deepEqual(out, ['cost.log', 'idea.triage', 'task.ship']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('a grant naming a key the catalog does not define grants nothing', () => {
|
|
30
|
+
const out = effectivePermissions(['idea.triage', 'not.a.real.permission', 'deploy.everything']);
|
|
31
|
+
assert.deepEqual(out, ['idea.triage']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('no roles / no grants resolves to the empty set (fail-closed)', () => {
|
|
35
|
+
assert.deepEqual(effectivePermissions([]), []);
|
|
36
|
+
assert.deepEqual(effectivePermissions(null), []);
|
|
37
|
+
assert.deepEqual(effectivePermissions(undefined), []);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('the result is frozen — a caller cannot mutate a resolved permission set', () => {
|
|
41
|
+
const out = effectivePermissions(['task.ship']);
|
|
42
|
+
assert.ok(Object.isFrozen(out));
|
|
43
|
+
assert.throws(() => out.push('deploy.push_main'), TypeError);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// R98 narrowed this: the role axis carries every catalog key EXCEPT the machine-
|
|
47
|
+
// principal ones, which come from an identity and never from a role. The principal
|
|
48
|
+
// axis has its own file (tests/governance_principals.mjs).
|
|
49
|
+
test('every role-held catalog key survives resolution (the seed can grant anything defined)', () => {
|
|
50
|
+
const roleHeld = catalog.permissionKeys().filter((k) => !catalog.isPrincipalKey(k));
|
|
51
|
+
assert.deepEqual(effectivePermissions(catalog.permissionKeys()), [...roleHeld].sort());
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('a null/blank builder id resolves to empty without touching the DB', async () => {
|
|
55
|
+
assert.deepEqual(await resolveBuilderPermissions(null), []);
|
|
56
|
+
assert.deepEqual(await resolveBuilderPermissions(undefined), []);
|
|
57
|
+
assert.deepEqual(await resolveBuilderPermissions(''), []);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('builderHasPermissions denies on an empty required list (a mis-authored gate must not open)', async () => {
|
|
61
|
+
assert.equal(await builderHasPermissions(1, []), false);
|
|
62
|
+
assert.equal(await builderHasPermissions(1, null), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('the resolver holds no cache — authority is read fresh per request (ADR 0016)', () => {
|
|
66
|
+
// A structural guard, not a behavioural one: the failure mode is a cached grant
|
|
67
|
+
// outliving its revocation, so the rule is that this file introduces no store at
|
|
68
|
+
// all. Catches a well-meaning "let's memoise the join" edit in review.
|
|
69
|
+
const code = fs.readFileSync(path.join(GOV, 'resolver.js'), 'utf8')
|
|
70
|
+
.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
|
|
71
|
+
for (const banned of ['new Map(', 'new WeakMap(', 'setTimeout(', 'cache']) {
|
|
72
|
+
assert.ok(!code.includes(banned), `resolver.js must not introduce "${banned}" — authority is uncached`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('the kernel port exposes the resolver without core importing the module', () => {
|
|
77
|
+
const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
|
|
78
|
+
assert.equal(typeof governancePort.resolveBuilderPermissions, 'function');
|
|
79
|
+
assert.equal(typeof governancePort.builderHasPermissions, 'function');
|
|
80
|
+
assert.equal(typeof governancePort.isSystem, 'function');
|
|
81
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(GOV, 'module.json'), 'utf8'));
|
|
82
|
+
assert.ok(manifest.provides.includes('governance'), 'module.json must declare the governance port');
|
|
83
|
+
assert.ok(manifest.contributes.routes.includes('governance'), 'the port registers from the route factory');
|
|
84
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// tests/governance_role_sync.mjs — rank→role assignment stays in sync (BV1.R95b, ADR 0151).
|
|
2
|
+
//
|
|
3
|
+
// DB-free. The decision the writer executes is a pure function (modules/governance/
|
|
4
|
+
// rank-roles.js planRankRoleSync), so these exercise PRODUCTION logic directly rather
|
|
5
|
+
// than a re-implementation: db.syncRankRole reads the two row sets, calls this exact
|
|
6
|
+
// function, and executes its verdict. The SQL halves — the DELETE/INSERT the writer
|
|
7
|
+
// issues and the repair migration — are asserted structurally.
|
|
8
|
+
//
|
|
9
|
+
// The invariant under test is a TRUST-BOUNDARY invariant, not a bookkeeping one: a
|
|
10
|
+
// DEMOTION must REMOVE the higher seeded role. An additive-only sync would leave a
|
|
11
|
+
// demoted Archon still holding `archon`, so the (correctly uncached) resolver would
|
|
12
|
+
// keep returning Archon permissions — ADR 0016's "a demotion takes effect on the very
|
|
13
|
+
// next request" promise broken one layer beneath the resolver. Every assertion about
|
|
14
|
+
// `remove` below is that promise.
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { test } from 'node:test';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
+
const GOV = path.join(ROOT, 'modules', 'governance');
|
|
25
|
+
const { planRankRoleSync, isSeededRankRole } = require(path.join(GOV, 'rank-roles.js'));
|
|
26
|
+
|
|
27
|
+
// The seeded rank-roles exactly as governance_002 writes them.
|
|
28
|
+
const SEEDED = [
|
|
29
|
+
{ role_key: 'xenos', seeded_rank: 'xenos', is_system: true },
|
|
30
|
+
{ role_key: 'thetes', seeded_rank: 'thetes', is_system: true },
|
|
31
|
+
{ role_key: 'metic', seeded_rank: 'metic', is_system: true },
|
|
32
|
+
{ role_key: 'archon', seeded_rank: 'archon', is_system: true },
|
|
33
|
+
];
|
|
34
|
+
const seededRole = (rank) => ({ role_key: rank, seeded_rank: rank, is_system: true });
|
|
35
|
+
// An Archon-authored custom role from the R110 tab: no seeded_rank, not a system role.
|
|
36
|
+
const customRole = (key) => ({ role_key: key, seeded_rank: null, is_system: false });
|
|
37
|
+
|
|
38
|
+
const plan = (rank, currentRoles) => planRankRoleSync({ rank, currentRoles, seededRoles: SEEDED });
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// registration — a builder with no roles gets the one matching their rank
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
test('registration: a role-less builder is assigned the role matching their rank', () => {
|
|
45
|
+
const p = plan('xenos', []);
|
|
46
|
+
assert.equal(p.target, 'xenos');
|
|
47
|
+
assert.deepEqual([...p.add], ['xenos']);
|
|
48
|
+
assert.deepEqual([...p.remove], []);
|
|
49
|
+
assert.deepEqual([...p.next], ['xenos']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('registration: the firstAdmin bootstrap (seated straight to archon) assigns archon', () => {
|
|
53
|
+
const p = plan('archon', []);
|
|
54
|
+
assert.deepEqual([...p.add], ['archon']);
|
|
55
|
+
assert.deepEqual([...p.next], ['archon']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// promotion — the new role is held and the OLD one is gone
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
test('promotion xenos→thetes: the new role is added and the old one removed', () => {
|
|
63
|
+
const p = plan('thetes', [seededRole('xenos')]);
|
|
64
|
+
assert.equal(p.target, 'thetes');
|
|
65
|
+
assert.deepEqual([...p.add], ['thetes']);
|
|
66
|
+
assert.deepEqual([...p.remove], ['xenos'], 'the superseded rank-role must be removed');
|
|
67
|
+
assert.deepEqual([...p.next], ['thetes'], 'exactly one seeded rank-role is held afterwards');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('promotion via the auto-graduation path leaves exactly one seeded role', () => {
|
|
71
|
+
// maybeAutoGraduateToThetes runs the same _applyRankChangeTx, so the same plan.
|
|
72
|
+
const p = plan('thetes', [seededRole('xenos')]);
|
|
73
|
+
const seededHeld = p.next.filter((k) => SEEDED.some((s) => s.role_key === k));
|
|
74
|
+
assert.equal(seededHeld.length, 1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// demotion — THE SECURITY CASE. Assert the REMOVAL explicitly.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
test('demotion archon→metic REMOVES the archon role (not merely adds metic)', () => {
|
|
82
|
+
const p = plan('metic', [seededRole('archon')]);
|
|
83
|
+
assert.deepEqual([...p.remove], ['archon'], 'the higher rank-role must be REMOVED on demotion');
|
|
84
|
+
assert.deepEqual([...p.add], ['metic']);
|
|
85
|
+
// The absence assertion is the one that matters: an additive-only sync would
|
|
86
|
+
// still hold `archon` here and the resolver would keep granting Archon powers.
|
|
87
|
+
assert.ok(!p.next.includes('archon'), 'a demoted Archon must NOT still hold the archon role');
|
|
88
|
+
assert.deepEqual([...p.next], ['metic']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('demotion archon→xenos (a multi-rung drop) removes archon just the same', () => {
|
|
92
|
+
const p = plan('xenos', [seededRole('archon')]);
|
|
93
|
+
assert.ok(p.remove.includes('archon'));
|
|
94
|
+
assert.ok(!p.next.includes('archon'));
|
|
95
|
+
assert.deepEqual([...p.next], ['xenos']);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a builder holding several stale seeded roles is reduced to exactly one', () => {
|
|
99
|
+
// Defence in depth: if drift ever left two seeded roles on one builder, the sync
|
|
100
|
+
// must not pick one and leave the other — every non-matching seeded role goes.
|
|
101
|
+
const p = plan('thetes', [seededRole('archon'), seededRole('metic'), seededRole('xenos')]);
|
|
102
|
+
assert.deepEqual([...p.remove].sort(), ['archon', 'metic', 'xenos']);
|
|
103
|
+
assert.deepEqual([...p.next], ['thetes']);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// custom roles survive; legacy ranks resolve to nothing
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
test('an Archon-assigned custom role survives a rank change', () => {
|
|
111
|
+
const p = plan('metic', [seededRole('archon'), customRole('release-manager')]);
|
|
112
|
+
assert.deepEqual([...p.remove], ['archon'], 'only the seeded rank-role rotates');
|
|
113
|
+
assert.ok(!p.remove.includes('release-manager'), 'a custom role must never be revoked by a rank change');
|
|
114
|
+
assert.deepEqual([...p.next], ['metic', 'release-manager']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('a custom role is not mistaken for the rank-role even when its key matches a rank name', () => {
|
|
118
|
+
// role_key is UNIQUE so this cannot happen today, but the predicate must key on
|
|
119
|
+
// is_system + seeded_rank rather than on the key's spelling.
|
|
120
|
+
const impostor = { role_key: 'archon', seeded_rank: null, is_system: false };
|
|
121
|
+
assert.equal(isSeededRankRole(impostor), false);
|
|
122
|
+
const p = plan('metic', [impostor]);
|
|
123
|
+
assert.deepEqual([...p.remove], [], 'a non-system role is never rotated away');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('a builder on a non-seeded legacy rank resolves to zero seeded roles', () => {
|
|
127
|
+
// migration 023's CHECK still admits twelve reserved ranks. governance_002's INNER
|
|
128
|
+
// JOIN gives them no row BY DESIGN (ADR 0151's fail-closed divergence) — preserve
|
|
129
|
+
// it, and strip any seeded role they were carrying.
|
|
130
|
+
const p = plan('hippeis', [seededRole('archon')]);
|
|
131
|
+
assert.equal(p.target, null, 'no seeded role exists for a legacy rank — invent none');
|
|
132
|
+
assert.deepEqual([...p.add], []);
|
|
133
|
+
assert.deepEqual([...p.remove], ['archon'], 'a legacy rank must not retain a seeded rank-role');
|
|
134
|
+
assert.deepEqual([...p.next], []);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('a legacy-rank builder still keeps their custom roles', () => {
|
|
138
|
+
const p = plan('hippeis', [seededRole('metic'), customRole('release-manager')]);
|
|
139
|
+
assert.deepEqual([...p.next], ['release-manager']);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// idempotence
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
test('the sync is idempotent — running it twice changes nothing the second time', () => {
|
|
147
|
+
const first = plan('metic', [seededRole('archon')]);
|
|
148
|
+
assert.ok(first.add.length + first.remove.length > 0, 'the first run must do work');
|
|
149
|
+
const after = first.next.map((key) => (SEEDED.find((s) => s.role_key === key) || customRole(key)));
|
|
150
|
+
const second = planRankRoleSync({ rank: 'metic', currentRoles: after, seededRoles: SEEDED });
|
|
151
|
+
assert.deepEqual([...second.add], []);
|
|
152
|
+
assert.deepEqual([...second.remove], []);
|
|
153
|
+
assert.deepEqual([...second.next], [...first.next]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('a builder already holding the right role produces no write at all', () => {
|
|
157
|
+
const p = plan('metic', [seededRole('metic'), customRole('release-manager')]);
|
|
158
|
+
assert.deepEqual([...p.add], []);
|
|
159
|
+
assert.deepEqual([...p.remove], []);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('the plan is frozen — a caller cannot widen it after the decision', () => {
|
|
163
|
+
const p = plan('metic', [seededRole('archon')]);
|
|
164
|
+
assert.ok(Object.isFrozen(p) && Object.isFrozen(p.add) && Object.isFrozen(p.remove));
|
|
165
|
+
assert.throws(() => p.add.push('archon'), TypeError);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// the SQL halves — asserted structurally
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
const dbSrc = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
|
|
173
|
+
const migDir = path.join(GOV, 'migrations');
|
|
174
|
+
const repairSql = fs.readFileSync(path.join(migDir, 'governance_005_resync_builder_rank_roles.sql'), 'utf8');
|
|
175
|
+
const stripSqlComments = (sql) => sql.replace(/--[^\n]*/g, '');
|
|
176
|
+
|
|
177
|
+
test('the writer DELETEs the superseded assignments, not just INSERTs the new one', () => {
|
|
178
|
+
assert.match(dbSrc, /DELETE FROM governance_builder_roles/,
|
|
179
|
+
'syncRankRole must delete superseded rank-roles — an additive-only writer preserves privilege retention');
|
|
180
|
+
assert.match(dbSrc, /INSERT INTO governance_builder_roles[\s\S]*ON CONFLICT \(builder_id, role_key\) DO NOTHING/,
|
|
181
|
+
'the assignment insert must be idempotent');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('the writer runs on the CALLER\'s transaction client when given one', () => {
|
|
185
|
+
assert.match(dbSrc, /async function syncRankRole\(client,/,
|
|
186
|
+
'syncRankRole must take the caller\'s client so the rotation is atomic with the rank write');
|
|
187
|
+
assert.match(dbSrc, /const q = client \|\| pool;/,
|
|
188
|
+
'and fall back to the pool only when there is no surrounding transaction');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('both rank-change twins rotate the role inside their transaction', () => {
|
|
192
|
+
for (const rel of ['src/bongos/db.js', 'modules/lifecycle/db.js']) {
|
|
193
|
+
const src = fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
194
|
+
const body = src.slice(src.indexOf('async function _applyRankChangeTx'));
|
|
195
|
+
const upd = body.indexOf('UPDATE builders SET rank');
|
|
196
|
+
const sync = body.indexOf('await syncGovernanceRankRole(client');
|
|
197
|
+
assert.ok(upd !== -1, `${rel}: expected the rank UPDATE in _applyRankChangeTx`);
|
|
198
|
+
assert.ok(sync !== -1, `${rel}: the rank write must rotate the seeded rank-role`);
|
|
199
|
+
assert.ok(sync > upd, `${rel}: the rotation must follow the rank write inside the same transaction`);
|
|
200
|
+
assert.match(src, /resolveOptional\('governance\.roleSync'\)/,
|
|
201
|
+
`${rel}: must reach governance through the kernel port, never by importing the module`);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('the repair migration removes stale seeded roles and is idempotent', () => {
|
|
206
|
+
const code = stripSqlComments(repairSql);
|
|
207
|
+
assert.match(code, /DELETE FROM governance_builder_roles/,
|
|
208
|
+
'the repair must DELETE stale rank-roles — otherwise a demoted Archon keeps Archon permissions');
|
|
209
|
+
assert.match(code, /r\.is_system = true/, 'the delete must be keyed on the SEEDED roles only');
|
|
210
|
+
assert.match(code, /seeded_rank IS DISTINCT FROM b\.rank/,
|
|
211
|
+
'only assignments disagreeing with the builder\'s current rank may be removed');
|
|
212
|
+
assert.match(code, /ON CONFLICT \(builder_id, role_key\) DO NOTHING/, 'must be idempotent');
|
|
213
|
+
assert.ok(/BEGIN;/.test(code) && /COMMIT;/.test(code), 'must be transactional');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('the repair migration touches ASSIGNMENTS, never GRANTS', () => {
|
|
217
|
+
// tests/governance_seed.mjs builds RANK_ROLE_SEED as the UNION of every grant
|
|
218
|
+
// migration and fails loudly if one ever revokes. This migration must stay out of
|
|
219
|
+
// that model entirely: it never writes governance_role_permissions.
|
|
220
|
+
const code = stripSqlComments(repairSql);
|
|
221
|
+
assert.ok(!/governance_role_permissions/i.test(code),
|
|
222
|
+
'the rank-role repair must not touch the grant table (it would break the seed union model)');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('every governance migration keeps its objects namespaced and additive', () => {
|
|
226
|
+
const code = stripSqlComments(repairSql);
|
|
227
|
+
assert.ok(!/\bDROP\s+(TABLE|COLUMN|CONSTRAINT|INDEX)\b/i.test(code), 'module migrations are additive — no destructive DDL');
|
|
228
|
+
for (const m of code.matchAll(/CREATE (?:TABLE|INDEX)(?: IF NOT EXISTS)? (\w+)/gi)) {
|
|
229
|
+
assert.ok(m[1].startsWith('governance_'), `object ${m[1]} is not governance_-namespaced`);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// tests/governance_seed.mjs — the compat-seed migration drift guard (BV1.R93, ADR 0151).
|
|
2
|
+
//
|
|
3
|
+
// The seed migration (governance_002_seed_and_backfill.sql) MATERIALISES the role→
|
|
4
|
+
// permission grants as SQL. This parses that file and asserts the four role arrays
|
|
5
|
+
// equal the code catalog's RANK_ROLE_SEED exactly — so the SQL snapshot can never
|
|
6
|
+
// silently drift from the catalog (the "no second list that drifts" discipline). If
|
|
7
|
+
// you change catalog.js, this goes red until the migration is regenerated (a NEW
|
|
8
|
+
// migration for a NEW grant — migrations are run-once).
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const { RANK_ROLE_SEED, RANK_ORDER } = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
|
|
19
|
+
|
|
20
|
+
const MIG_DIR = path.join(ROOT, 'modules', 'governance', 'migrations');
|
|
21
|
+
const seedSql = fs.readFileSync(path.join(MIG_DIR, 'governance_002_seed_and_backfill.sql'), 'utf8');
|
|
22
|
+
const tablesSql = fs.readFileSync(path.join(MIG_DIR, 'governance_001_tables.sql'), 'utf8');
|
|
23
|
+
|
|
24
|
+
// The grant SQL is the UNION of the compat seed (002) and every LATER governance_*
|
|
25
|
+
// grant migration. Migrations are run-once, so a floor change in catalog.js can never
|
|
26
|
+
// be made by editing 002 — an already-migrated database would keep the old grant. It
|
|
27
|
+
// arrives as a follow-on migration instead (governance_003 = task.abandon → metic,
|
|
28
|
+
// ADR 0152), and what must equal RANK_ROLE_SEED is the union of them all, not any one
|
|
29
|
+
// file. Read in filename order; every file is scanned for `SELECT '<role>', unnest(…)`
|
|
30
|
+
// blocks, so a new grant migration needs no change here.
|
|
31
|
+
const grantMigrationFiles = fs.readdirSync(MIG_DIR)
|
|
32
|
+
.filter((f) => f.endsWith('.sql') && f !== 'governance_001_tables.sql')
|
|
33
|
+
.sort();
|
|
34
|
+
const grantSqlByFile = grantMigrationFiles.map((f) => [f, fs.readFileSync(path.join(MIG_DIR, f), 'utf8')]);
|
|
35
|
+
|
|
36
|
+
// Extract, per role, every permission key granted across the grant migrations, plus
|
|
37
|
+
// the per-file blocks (so a duplicate WITHIN one block is still catchable).
|
|
38
|
+
function grantBlocksForRole(role) {
|
|
39
|
+
const re = new RegExp(`SELECT '${role}', unnest\\(ARRAY\\[([\\s\\S]*?)\\]\\)`, 'g');
|
|
40
|
+
const blocks = [];
|
|
41
|
+
for (const [file, sql] of grantSqlByFile) {
|
|
42
|
+
for (const m of sql.matchAll(re)) {
|
|
43
|
+
blocks.push({ file, keys: [...m[1].matchAll(/'([^']+)'/g)].map((x) => x[1]) });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return blocks;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('the union of every grant migration matches RANK_ROLE_SEED exactly (no drift)', () => {
|
|
50
|
+
for (const role of RANK_ORDER) {
|
|
51
|
+
const blocks = grantBlocksForRole(role);
|
|
52
|
+
assert.ok(blocks.length > 0, `no grant block for ${role} in any governance migration`);
|
|
53
|
+
for (const b of blocks) {
|
|
54
|
+
assert.equal(b.keys.length, new Set(b.keys).size, `${role}: duplicate permission in the ${b.file} array`);
|
|
55
|
+
}
|
|
56
|
+
const granted = blocks.flatMap((b) => b.keys);
|
|
57
|
+
// set-equality (order in SQL is illustrative; the grant set is what matters)
|
|
58
|
+
assert.deepEqual(new Set(granted), new Set(RANK_ROLE_SEED[role]),
|
|
59
|
+
`${role}: grant migrations drift from catalog RANK_ROLE_SEED`);
|
|
60
|
+
// no key re-granted in a LATER migration (a no-op INSERT that muddies the ledger)
|
|
61
|
+
assert.equal(granted.length, new Set(granted).size, `${role}: the same permission is granted twice across migrations`);
|
|
62
|
+
assert.equal(new Set(granted).size, RANK_ROLE_SEED[role].length, `${role}: grant count mismatch`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// The union model above is only faithful while grant migrations are ADDITIVE. A future
|
|
67
|
+
// migration that REVOKES (raising a floor) would make the union overstate what the DB
|
|
68
|
+
// holds, so fail loudly here rather than let the guard quietly go wrong.
|
|
69
|
+
test('no grant migration revokes — the union model stays faithful', () => {
|
|
70
|
+
for (const [file, sql] of grantSqlByFile) {
|
|
71
|
+
const code = sql.replace(/--[^\n]*/g, '');
|
|
72
|
+
assert.ok(!/DELETE\s+FROM\s+governance_role_permissions/i.test(code),
|
|
73
|
+
`${file} revokes a grant — teach grantBlocksForRole to subtract before allowing this`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('the seed inserts exactly the four seeded rank-roles', () => {
|
|
78
|
+
const roleRows = [...seedSql.matchAll(/\('(xenos|thetes|metic|archon)',\s*'[^']+',/g)].map((m) => m[1]);
|
|
79
|
+
assert.deepEqual(new Set(roleRows), new Set(RANK_ORDER));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// strip `-- ...` line comments so prose ("ON CONFLICT DO NOTHING throughout") is not
|
|
83
|
+
// counted as SQL.
|
|
84
|
+
const stripComments = (sql) => sql.replace(/--[^\n]*/g, '');
|
|
85
|
+
|
|
86
|
+
test('seed + tables are idempotent (ON CONFLICT / IF NOT EXISTS) and transactional', () => {
|
|
87
|
+
const seedCode = stripComments(seedSql);
|
|
88
|
+
const tablesCode = stripComments(tablesSql);
|
|
89
|
+
// every INSERT in the seed carries ON CONFLICT ... DO NOTHING
|
|
90
|
+
const inserts = seedCode.match(/INSERT INTO/g) || [];
|
|
91
|
+
const onConflict = seedCode.match(/ON CONFLICT/g) || [];
|
|
92
|
+
assert.equal(inserts.length, 6, 'expected 6 seed INSERTs (1 roles + 4 grant blocks + 1 backfill)');
|
|
93
|
+
assert.equal(inserts.length, onConflict.length, 'every seed INSERT must be ON CONFLICT DO NOTHING (idempotent)');
|
|
94
|
+
assert.ok(/BEGIN;/.test(seedCode) && /COMMIT;/.test(seedCode), 'seed must be transactional');
|
|
95
|
+
// tables use CREATE TABLE IF NOT EXISTS
|
|
96
|
+
const creates = tablesCode.match(/CREATE TABLE/g) || [];
|
|
97
|
+
const ifNotExists = tablesCode.match(/CREATE TABLE IF NOT EXISTS/g) || [];
|
|
98
|
+
assert.equal(creates.length, 3, 'expected 3 governance_* tables');
|
|
99
|
+
assert.equal(creates.length, ifNotExists.length, 'every CREATE TABLE must be IF NOT EXISTS');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('the backfill assigns each builder their rank-role (join on a seeded role)', () => {
|
|
103
|
+
assert.match(seedSql, /INSERT INTO governance_builder_roles[\s\S]*FROM builders b[\s\S]*JOIN governance_roles r ON r\.role_key = b\.rank/,
|
|
104
|
+
'backfill must join builders.rank to a seeded governance_role');
|
|
105
|
+
assert.match(seedSql, /governance_builder_roles[\s\S]*ON CONFLICT \(builder_id, role_key\) DO NOTHING/,
|
|
106
|
+
'backfill must be idempotent');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('later grant migrations are idempotent + transactional', () => {
|
|
110
|
+
for (const [file, sql] of grantSqlByFile) {
|
|
111
|
+
if (file === 'governance_002_seed_and_backfill.sql') continue; // covered above
|
|
112
|
+
const code = stripComments(sql);
|
|
113
|
+
const inserts = code.match(/INSERT INTO/g) || [];
|
|
114
|
+
const onConflict = code.match(/ON CONFLICT/g) || [];
|
|
115
|
+
assert.ok(inserts.length > 0, `${file}: a grant migration must INSERT something`);
|
|
116
|
+
assert.equal(inserts.length, onConflict.length, `${file}: every INSERT must be ON CONFLICT DO NOTHING`);
|
|
117
|
+
assert.ok(/BEGIN;/.test(code) && /COMMIT;/.test(code), `${file}: must be transactional`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('migration objects are all governance_-namespaced (ADR 0083 §5)', () => {
|
|
122
|
+
for (const sql of [tablesSql, seedSql]) {
|
|
123
|
+
for (const m of sql.matchAll(/CREATE (?:TABLE|INDEX)(?: IF NOT EXISTS)? (\w+)/g)) {
|
|
124
|
+
assert.ok(m[1].startsWith('governance_'), `object ${m[1]} is not governance_-namespaced`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|