@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
package/tests/goal_routes.mjs
CHANGED
|
@@ -141,16 +141,71 @@ await test('builder_id coercion contract: Number() accepts int + numeric string,
|
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
// ---- listCriteriaForGoal (db helper, deps.pool seam) ----
|
|
144
|
+
// task 1002164: the projection now carries the per-criterion link counts and the
|
|
145
|
+
// DERIVED pending_review flag, so the hall's goal-detail page can render the
|
|
146
|
+
// third criterion state (met — ready to confirm) without a second round-trip.
|
|
147
|
+
const CRIT_SQL_RE = /FROM done_when_criteria WHERE goal_id = \$1/;
|
|
148
|
+
const critRow = (over = {}) => ({
|
|
149
|
+
id: 1, version_id: 'V1', criterion_id: 'c1', criterion_md: 'md', satisfied: false,
|
|
150
|
+
sort_order: 6, goal_id: 3, linked_tasks: 0, unshipped_tasks: 0, ...over,
|
|
151
|
+
});
|
|
152
|
+
const critPool = (rows) => fakePool((s) => (CRIT_SQL_RE.test(s) ? { rows } : null));
|
|
153
|
+
|
|
144
154
|
await test('listCriteriaForGoal: SELECT by goal_id, ordered, returns rows', async () => {
|
|
145
|
-
const ROW =
|
|
146
|
-
const pool =
|
|
155
|
+
const ROW = critRow();
|
|
156
|
+
const pool = critPool([ROW]);
|
|
147
157
|
const out = await listCriteriaForGoal(3, { pool });
|
|
148
|
-
assert.deepEqual(out, [ROW]);
|
|
149
|
-
const sel = find(pool.queries,
|
|
158
|
+
assert.deepEqual(out, [{ ...ROW, pending_review: false }]);
|
|
159
|
+
const sel = find(pool.queries, CRIT_SQL_RE);
|
|
150
160
|
assert.equal(sel.params[0], 3);
|
|
151
161
|
assert.match(sel.sql, /ORDER BY sort_order, id/);
|
|
152
162
|
});
|
|
153
163
|
|
|
164
|
+
await test('listCriteriaForGoal: pending_review true when every linked task shipped and not yet confirmed', async () => {
|
|
165
|
+
const pool = critPool([critRow({ satisfied: false, linked_tasks: 2, unshipped_tasks: 0 })]);
|
|
166
|
+
const [out] = await listCriteriaForGoal(3, { pool });
|
|
167
|
+
assert.equal(out.pending_review, true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await test('listCriteriaForGoal: pending_review false once the criterion is confirmed', async () => {
|
|
171
|
+
const pool = critPool([critRow({ satisfied: true, linked_tasks: 2, unshipped_tasks: 0 })]);
|
|
172
|
+
const [out] = await listCriteriaForGoal(3, { pool });
|
|
173
|
+
assert.equal(out.pending_review, false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await test('listCriteriaForGoal: pending_review false with NO linked tasks (vacuous truth guard)', async () => {
|
|
177
|
+
const pool = critPool([critRow({ satisfied: false, linked_tasks: 0, unshipped_tasks: 0 })]);
|
|
178
|
+
const [out] = await listCriteriaForGoal(3, { pool });
|
|
179
|
+
assert.equal(out.pending_review, false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await test('listCriteriaForGoal: pending_review false while any linked task is unshipped', async () => {
|
|
183
|
+
const pool = critPool([critRow({ satisfied: false, linked_tasks: 3, unshipped_tasks: 1 })]);
|
|
184
|
+
const [out] = await listCriteriaForGoal(3, { pool });
|
|
185
|
+
assert.equal(out.pending_review, false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await test('listCriteriaForGoal: counts are aggregated ONCE, not per-row correlated subqueries', async () => {
|
|
189
|
+
// Regression guard on the query SHAPE (grader finding, task 1002164). The first
|
|
190
|
+
// cut ran two correlated COUNT(*) subqueries per criterion row — 2N index
|
|
191
|
+
// lookups for N criteria. The counts must come from a single grouped pass that
|
|
192
|
+
// is itself narrowed to this goal's criteria before grouping.
|
|
193
|
+
const pool = critPool([critRow()]);
|
|
194
|
+
await listCriteriaForGoal(3, { pool });
|
|
195
|
+
const sql = String(find(pool.queries, CRIT_SQL_RE).sql).replace(/\s+/g, ' ');
|
|
196
|
+
assert.match(sql, /GROUP BY tc\.criterion_id/, 'counts come from one GROUP BY');
|
|
197
|
+
assert.match(sql, /WHERE tc\.criterion_id IN \(SELECT id FROM crit\)/, 'the aggregate is narrowed to this goal before grouping, not over all task_criteria');
|
|
198
|
+
assert.doesNotMatch(sql, /\(SELECT count\(\*\)[^)]*FROM task_criteria[^)]*WHERE tc\.criterion_id = done_when_criteria\.id/, 'no per-row correlated count subquery');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await test('listCriteriaForGoal: counts arrive as numbers even if the driver returns strings', async () => {
|
|
202
|
+
// node-postgres renders bigint/count as a string unless cast; the SQL casts to
|
|
203
|
+
// ::int, but the derivation must not silently flip on a string either way.
|
|
204
|
+
const pool = critPool([critRow({ satisfied: false, linked_tasks: '2', unshipped_tasks: '0' })]);
|
|
205
|
+
const [out] = await listCriteriaForGoal(3, { pool });
|
|
206
|
+
assert.equal(out.pending_review, true);
|
|
207
|
+
});
|
|
208
|
+
|
|
154
209
|
// ---- POST /goals/:id/tasks — request-validation schema (the bounded-authoring
|
|
155
210
|
// gate's DECISION logic + abuse matrix live in tests/goal_abuse_matrix.mjs) ----
|
|
156
211
|
// The EXACT schema declared in goals.js's POST /goals/:id/tasks — keep in sync.
|
package/tests/goal_tier.mjs
CHANGED
|
@@ -110,19 +110,28 @@ await test('getGoal: unknown id returns null (not undefined)', async () => {
|
|
|
110
110
|
// =========================================================================
|
|
111
111
|
// listGoals
|
|
112
112
|
// =========================================================================
|
|
113
|
-
await test('listGoals: no args binds [null, null] (
|
|
113
|
+
await test('listGoals: no args binds [null, null, null] (all three filters off)', async () => {
|
|
114
114
|
const pool = fakePool((s) => (/FROM goals/.test(s) ? { rows: [GOAL_ROW] } : null));
|
|
115
115
|
const out = await listGoals(undefined, { pool });
|
|
116
116
|
assert.deepEqual(out, [GOAL_ROW]);
|
|
117
117
|
const sel = find(pool.queries, /FROM goals/);
|
|
118
|
-
|
|
118
|
+
// task 1002539 added an OPTIONAL limit. Postgres treats `LIMIT NULL` as unbounded,
|
|
119
|
+
// so the goal board's read is byte-for-byte unchanged in behaviour.
|
|
120
|
+
assert.deepEqual(sel.params, [null, null, null]);
|
|
119
121
|
assert.match(sel.sql, /ORDER BY version_id, sort_order, created_at, id/, 'priority-first, then stable ordering');
|
|
122
|
+
assert.match(sel.sql, /LIMIT \$3::int/, 'the limit is bound, never interpolated');
|
|
120
123
|
});
|
|
121
124
|
|
|
122
125
|
await test('listGoals: versionId + status flow through to the bound params', async () => {
|
|
123
126
|
const pool = fakePool((s) => (/FROM goals/.test(s) ? { rows: [] } : null));
|
|
124
127
|
await listGoals({ versionId: 'BONGOS-V1', status: 'open' }, { pool });
|
|
125
|
-
assert.deepEqual(find(pool.queries, /FROM goals/).params, ['BONGOS-V1', 'open']);
|
|
128
|
+
assert.deepEqual(find(pool.queries, /FROM goals/).params, ['BONGOS-V1', 'open', null]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await test('listGoals: an explicit limit is bound so Postgres truncates, not JS', async () => {
|
|
132
|
+
const pool = fakePool((s) => (/FROM goals/.test(s) ? { rows: [] } : null));
|
|
133
|
+
await listGoals({ versionId: 'BONGOS-V1', status: 'open', limit: 6 }, { pool });
|
|
134
|
+
assert.deepEqual(find(pool.queries, /FROM goals/).params, ['BONGOS-V1', 'open', 6]);
|
|
126
135
|
});
|
|
127
136
|
|
|
128
137
|
// =========================================================================
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// tests/governance_catalog.mjs — the permission catalog + compat seed (BV1.R94, ADR 0151).
|
|
2
|
+
//
|
|
3
|
+
// Guards the "zero day-one behavior change" promise structurally: the seed is
|
|
4
|
+
// DERIVED from per-permission floors, so these invariants prove the derivation is
|
|
5
|
+
// sound before R93 transcribes it into DB rows and R103 runs the live parity test.
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
const cat = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
|
|
15
|
+
const { PERMISSIONS, RANK_ORDER, RANK_ROLE_SEED, permissionKeys, byKey, isSystem, isGrantable, systemKeys, grantableKeys, seedForRank } = cat;
|
|
16
|
+
|
|
17
|
+
test('every permission is well-formed (key, boolean system, valid floor)', () => {
|
|
18
|
+
assert.ok(PERMISSIONS.length >= 60, `expected a substantial catalog, saw ${PERMISSIONS.length}`);
|
|
19
|
+
for (const p of PERMISSIONS) {
|
|
20
|
+
assert.equal(typeof p.key, 'string');
|
|
21
|
+
assert.ok(p.key.length > 0, 'non-empty key');
|
|
22
|
+
assert.equal(typeof p.system, 'boolean', `${p.key} system must be boolean`);
|
|
23
|
+
assert.ok(p.floor === null || RANK_ORDER.includes(p.floor), `${p.key} floor "${p.floor}" invalid`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('keys are unique', () => {
|
|
28
|
+
const keys = permissionKeys();
|
|
29
|
+
assert.equal(new Set(keys).size, keys.length, 'duplicate permission key');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('system and operational partition the catalog exactly', () => {
|
|
33
|
+
const sys = new Set(systemKeys());
|
|
34
|
+
const op = new Set(grantableKeys());
|
|
35
|
+
assert.equal(sys.size + op.size, PERMISSIONS.length);
|
|
36
|
+
for (const k of sys) assert.ok(!op.has(k), `${k} in both sets`);
|
|
37
|
+
// union === all
|
|
38
|
+
assert.deepEqual([...sys, ...op].sort(), permissionKeys().slice().sort());
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('isGrantable: operational→true, system→false, unknown→false', () => {
|
|
42
|
+
assert.equal(isGrantable('idea.triage'), true);
|
|
43
|
+
assert.equal(isGrantable('builder.rank.set'), false); // system
|
|
44
|
+
assert.equal(isGrantable('deploy.push_main'), false); // system
|
|
45
|
+
assert.equal(isGrantable('nope.not.a.key'), false); // unknown fails closed
|
|
46
|
+
assert.equal(isSystem('nope.not.a.key'), null); // unknown → null (caller fails closed)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('the four rank-roles exist; seed keys all reference real permissions', () => {
|
|
50
|
+
assert.deepEqual(Object.keys(RANK_ROLE_SEED).sort(), [...RANK_ORDER].sort());
|
|
51
|
+
const all = new Set(permissionKeys());
|
|
52
|
+
for (const rank of RANK_ORDER) {
|
|
53
|
+
for (const k of RANK_ROLE_SEED[rank]) assert.ok(all.has(k), `seed[${rank}] references unknown ${k}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('seed is cumulative: archon ⊇ metic ⊇ thetes ⊇ xenos (nested supersets)', () => {
|
|
58
|
+
for (let i = 1; i < RANK_ORDER.length; i++) {
|
|
59
|
+
const lower = new Set(RANK_ROLE_SEED[RANK_ORDER[i - 1]]);
|
|
60
|
+
const higher = new Set(RANK_ROLE_SEED[RANK_ORDER[i]]);
|
|
61
|
+
for (const k of lower) assert.ok(higher.has(k), `${RANK_ORDER[i]} missing ${k} that ${RANK_ORDER[i - 1]} has`);
|
|
62
|
+
assert.ok(higher.size > lower.size, `${RANK_ORDER[i]} should strictly extend ${RANK_ORDER[i - 1]}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a role holds a permission iff its floor is a real rank at-or-below (derivation is faithful)', () => {
|
|
67
|
+
for (const rank of RANK_ORDER) {
|
|
68
|
+
const held = new Set(RANK_ROLE_SEED[rank]);
|
|
69
|
+
const tier = RANK_ORDER.indexOf(rank);
|
|
70
|
+
for (const p of PERMISSIONS) {
|
|
71
|
+
const shouldHold = p.floor !== null && RANK_ORDER.indexOf(p.floor) <= tier;
|
|
72
|
+
assert.equal(held.has(p.key), shouldHold, `${rank} vs ${p.key} (floor ${p.floor})`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('system permissions are Archon-only in the seed (never in xenos/thetes/metic)', () => {
|
|
78
|
+
for (const rank of ['xenos', 'thetes', 'metic']) {
|
|
79
|
+
for (const k of RANK_ROLE_SEED[rank]) {
|
|
80
|
+
assert.equal(isSystem(k), false, `${rank} seed must not hold system perm ${k}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// and archon DOES hold the archon-floored system perms (that is how Archon gets its power)
|
|
84
|
+
const archon = new Set(RANK_ROLE_SEED.archon);
|
|
85
|
+
assert.ok(archon.has('builder.rank.set'));
|
|
86
|
+
assert.ok(archon.has('deploy.push_main'));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('machine principals are held by NO rank-role and are system + floor null', () => {
|
|
90
|
+
for (const p of PERMISSIONS.filter((x) => x.key.startsWith('principal.'))) {
|
|
91
|
+
assert.equal(p.system, true, `${p.key} must be system`);
|
|
92
|
+
assert.equal(p.floor, null, `${p.key} must have floor null (held by no rank-role)`);
|
|
93
|
+
for (const rank of RANK_ORDER) {
|
|
94
|
+
assert.ok(!RANK_ROLE_SEED[rank].includes(p.key), `${rank} must not hold principal ${p.key}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
assert.ok(PERMISSIONS.some((p) => p.key === 'principal.bfg'));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('threshold spot-checks reproduce today\'s requireRank floors', () => {
|
|
101
|
+
const has = (rank, k) => RANK_ROLE_SEED[rank].includes(k);
|
|
102
|
+
// idea.triage: metic+ only (requireRank('metic','archon'))
|
|
103
|
+
assert.equal(has('xenos', 'idea.triage'), false);
|
|
104
|
+
assert.equal(has('thetes', 'idea.triage'), false);
|
|
105
|
+
assert.equal(has('metic', 'idea.triage'), true);
|
|
106
|
+
assert.equal(has('archon', 'idea.triage'), true);
|
|
107
|
+
// task.claim.any: thetes+ (the general queue)
|
|
108
|
+
assert.equal(has('xenos', 'task.claim.any'), false);
|
|
109
|
+
assert.equal(has('thetes', 'task.claim.any'), true);
|
|
110
|
+
// builder.rank.set: archon only
|
|
111
|
+
assert.equal(has('metic', 'builder.rank.set'), false);
|
|
112
|
+
assert.equal(has('archon', 'builder.rank.set'), true);
|
|
113
|
+
// task.ship / own-work: everyone incl. xenos
|
|
114
|
+
assert.equal(has('xenos', 'task.ship'), true);
|
|
115
|
+
assert.equal(has('xenos', 'task.act.own'), true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('an unknown/reserved rank resolves to no permissions (fail-closed; ADR 0151 divergence)', () => {
|
|
119
|
+
assert.deepEqual(seedForRank('olympian'), []);
|
|
120
|
+
assert.deepEqual(seedForRank('helot'), []);
|
|
121
|
+
assert.deepEqual(seedForRank('bogus'), []);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('byKey returns a frozen record or null', () => {
|
|
125
|
+
const p = byKey('governance.manage');
|
|
126
|
+
assert.ok(p && p.system === true && p.floor === 'archon');
|
|
127
|
+
assert.throws(() => { p.system = false; }, /Cannot assign|read only|frozen/i);
|
|
128
|
+
assert.equal(byKey('nope'), null);
|
|
129
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// tests/governance_claim_eligibility.mjs — claim eligibility as PERMISSIONS
|
|
2
|
+
// (BV1.R106, ADR 0151).
|
|
3
|
+
//
|
|
4
|
+
// DB-free. The whole point of R106 is that the queue half of claim-time authority
|
|
5
|
+
// stops asking "what rank is this builder" and starts asking "does this builder
|
|
6
|
+
// hold `task.claim.any` / `task.claim.newcomer`" — WITHOUT changing who can claim
|
|
7
|
+
// what, and WITHOUT swallowing the per-task `requires_rank` floor, which is a
|
|
8
|
+
// property of the TASK and a separate axis entirely.
|
|
9
|
+
//
|
|
10
|
+
// Three things are proved here:
|
|
11
|
+
//
|
|
12
|
+
// A. THE TWO AXES STAY SEPARATE. Holding `task.claim.any` does not lift a task's
|
|
13
|
+
// `requires_rank` floor. Behavioural (the composed gate) + structural (the
|
|
14
|
+
// permission predicate cannot even see requires_rank, and db.js still calls
|
|
15
|
+
// rankAllowsTask as its own gate at every claim site).
|
|
16
|
+
// B. THE PERMISSION PREDICATE IS CORRECT on its own terms — newcomer-only vs the
|
|
17
|
+
// general queue, and fail-closed for a builder holding no claim permission.
|
|
18
|
+
// C. RANK-BY-RANK PARITY: the new predicate admits exactly the set the old rank
|
|
19
|
+
// rule admitted. Two INDEPENDENT oracles, deliberately:
|
|
20
|
+
// • a hand-written literal table (written from ADR 0018/0034, not from any
|
|
21
|
+
// code in the repo), and
|
|
22
|
+
// • db.xenosClaimAllowed itself — the real incumbent function, which reads
|
|
23
|
+
// GENERAL_QUEUE_RANKS and knows nothing about catalog floors.
|
|
24
|
+
// The new side reads catalog.seedForRank. A check whose two sides both read
|
|
25
|
+
// catalog.floor would pass while proving nothing (the lesson tests/
|
|
26
|
+
// governance_parity.mjs writes down); neither oracle here does.
|
|
27
|
+
import assert from 'node:assert/strict';
|
|
28
|
+
import { test } from 'node:test';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
36
|
+
const LIFECYCLE = path.join(ROOT, 'modules', 'lifecycle');
|
|
37
|
+
const eligibility = require(path.join(LIFECYCLE, 'claim-eligibility.js'));
|
|
38
|
+
const db = require(path.join(LIFECYCLE, 'db.js'));
|
|
39
|
+
const catalog = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
|
|
40
|
+
|
|
41
|
+
const { CLAIM_ANY, CLAIM_NEWCOMER, permissionAllowsClaim, claimEligibilityAllows } = eligibility;
|
|
42
|
+
|
|
43
|
+
// The four live ranks (ADR 0018 + ADR 0034), and the newcomer_friendly values the
|
|
44
|
+
// column can actually hold — it is nullable, so null/undefined must read falsey.
|
|
45
|
+
const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
46
|
+
const NF_VALUES = [true, false, null, undefined];
|
|
47
|
+
|
|
48
|
+
// A builder's seeded permission set, as the compat seed grants it.
|
|
49
|
+
const seedSet = (rank) => new Set(catalog.seedForRank(rank));
|
|
50
|
+
|
|
51
|
+
// The composed claim gate, OLD and NEW, so parity is compared end to end rather
|
|
52
|
+
// than one half at a time. Both call rankAllowsTask as a SEPARATE conjunct — that
|
|
53
|
+
// shape is the thing under test.
|
|
54
|
+
const oldGate = (rank, nf, requiresRank) =>
|
|
55
|
+
db.xenosClaimAllowed(rank, nf) && db.rankAllowsTask(rank, requiresRank);
|
|
56
|
+
|
|
57
|
+
const newGate = (rank, nf, requiresRank) =>
|
|
58
|
+
claimEligibilityAllows({
|
|
59
|
+
heldPermissions: seedSet(rank),
|
|
60
|
+
newcomerFriendly: nf,
|
|
61
|
+
rankRuleAllows: db.xenosClaimAllowed(rank, nf),
|
|
62
|
+
}) && db.rankAllowsTask(rank, requiresRank);
|
|
63
|
+
|
|
64
|
+
// ── the catalog anchor ───────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
test('the two claim keys exist in the catalog at the floors this gate assumes', () => {
|
|
67
|
+
const any = catalog.byKey(CLAIM_ANY);
|
|
68
|
+
const newcomer = catalog.byKey(CLAIM_NEWCOMER);
|
|
69
|
+
assert.ok(any, `${CLAIM_ANY} must be a real catalog permission`);
|
|
70
|
+
assert.ok(newcomer, `${CLAIM_NEWCOMER} must be a real catalog permission`);
|
|
71
|
+
assert.equal(any.floor, 'thetes', 'task.claim.any is the general queue — thetes and up');
|
|
72
|
+
assert.equal(newcomer.floor, 'xenos', 'task.claim.newcomer is held by every authenticated builder');
|
|
73
|
+
// Operational, not trust-boundary: a custom role may legitimately grant either.
|
|
74
|
+
assert.equal(any.system, false);
|
|
75
|
+
assert.equal(newcomer.system, false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── B. the permission predicate on its own terms ─────────────────────────────
|
|
79
|
+
|
|
80
|
+
test('a xenos (task.claim.newcomer only) may claim a newcomer_friendly task', () => {
|
|
81
|
+
const held = seedSet('xenos');
|
|
82
|
+
assert.ok(held.has(CLAIM_NEWCOMER));
|
|
83
|
+
assert.ok(!held.has(CLAIM_ANY), 'the xenos role must NOT hold the general-queue permission');
|
|
84
|
+
assert.equal(permissionAllowsClaim(held, true), true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a xenos may NOT claim a general-queue task', () => {
|
|
88
|
+
const held = seedSet('xenos');
|
|
89
|
+
assert.equal(permissionAllowsClaim(held, false), false);
|
|
90
|
+
assert.equal(permissionAllowsClaim(held, null), false, 'a NULL newcomer_friendly is not "open"');
|
|
91
|
+
assert.equal(permissionAllowsClaim(held, undefined), false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('thetes and up (task.claim.any) may claim the general queue', () => {
|
|
95
|
+
for (const rank of ['thetes', 'metic', 'archon']) {
|
|
96
|
+
const held = seedSet(rank);
|
|
97
|
+
assert.ok(held.has(CLAIM_ANY), `${rank} must hold ${CLAIM_ANY}`);
|
|
98
|
+
assert.equal(permissionAllowsClaim(held, false), true, `${rank} claims the general queue`);
|
|
99
|
+
assert.equal(permissionAllowsClaim(held, true), true, `${rank} also claims newcomer rows`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('a builder holding NO claim permission is refused (fail-closed)', () => {
|
|
104
|
+
for (const nf of NF_VALUES) {
|
|
105
|
+
assert.equal(permissionAllowsClaim(new Set(), nf), false, 'empty permission set');
|
|
106
|
+
assert.equal(permissionAllowsClaim([], nf), false, 'empty array');
|
|
107
|
+
assert.equal(permissionAllowsClaim(null, nf), false, 'no set at all');
|
|
108
|
+
assert.equal(permissionAllowsClaim(['idea.triage', 'cost.log'], nf), false, 'unrelated grants');
|
|
109
|
+
assert.equal(permissionAllowsClaim(['task.claim.everything'], nf), false, 'an invented key grants nothing');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('an array of keys is accepted as well as a Set (the resolver returns an array)', () => {
|
|
114
|
+
assert.equal(permissionAllowsClaim([CLAIM_ANY], false), true);
|
|
115
|
+
assert.equal(permissionAllowsClaim([CLAIM_NEWCOMER], true), true);
|
|
116
|
+
assert.equal(permissionAllowsClaim([CLAIM_NEWCOMER], false), false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── A. the per-task requires_rank floor is a SEPARATE axis ───────────────────
|
|
120
|
+
|
|
121
|
+
test('requires_rank still refuses a builder below it EVEN when they hold task.claim.any', () => {
|
|
122
|
+
// The load-bearing case. A thetes holds the general-queue permission outright,
|
|
123
|
+
// so the queue axis says yes — and the task's own floor must still say no.
|
|
124
|
+
const held = seedSet('thetes');
|
|
125
|
+
assert.ok(held.has(CLAIM_ANY));
|
|
126
|
+
assert.equal(permissionAllowsClaim(held, false), true, 'the queue axis admits them');
|
|
127
|
+
assert.equal(db.rankAllowsTask('thetes', 'metic'), false, 'the task floor does not');
|
|
128
|
+
assert.equal(newGate('thetes', false, 'metic'), false, 'the composed gate refuses');
|
|
129
|
+
|
|
130
|
+
// Same shape one rung up: a metic holds task.claim.any and is still below an
|
|
131
|
+
// archon-floored task.
|
|
132
|
+
assert.equal(permissionAllowsClaim(seedSet('metic'), false), true);
|
|
133
|
+
assert.equal(newGate('metic', false, 'archon'), false);
|
|
134
|
+
|
|
135
|
+
// ...and a newcomer_friendly flag does NOT open a floored task either: the two
|
|
136
|
+
// gates are AND-ed, so opening the door does not lower the floor.
|
|
137
|
+
assert.equal(permissionAllowsClaim(seedSet('xenos'), true), true);
|
|
138
|
+
assert.equal(newGate('xenos', true, 'metic'), false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('the permission predicate cannot see requires_rank at all (structural)', () => {
|
|
142
|
+
const src = fs.readFileSync(path.join(LIFECYCLE, 'claim-eligibility.js'), 'utf8')
|
|
143
|
+
.split('\n').filter((l) => !l.trim().startsWith('//')).join('\n');
|
|
144
|
+
for (const banned of ['requires_rank', 'rankAllowsTask', 'REQUIRES_RANK_TIER']) {
|
|
145
|
+
assert.ok(!src.includes(banned), `claim-eligibility.js must not reach into the task-floor axis ("${banned}")`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('every claim path in db.js still gates on requires_rank as its own check', () => {
|
|
150
|
+
const src = fs.readFileSync(path.join(LIFECYCLE, 'db.js'), 'utf8');
|
|
151
|
+
// claimTask, claimTasksBatch, validateClaimBatch — three call sites, three
|
|
152
|
+
// separate rankAllowsTask conjuncts. If a refactor ever folds the floor into the
|
|
153
|
+
// permission predicate, this count drops and the test reds.
|
|
154
|
+
const floorChecks = src.match(/rankAllowsTask\((builderRank|builderRank), (task|t)\.requires_rank\)/g) || [];
|
|
155
|
+
assert.equal(floorChecks.length, 3, `expected 3 requires_rank gates in the claim paths, found ${floorChecks.length}`);
|
|
156
|
+
const queueChecks = src.match(/claimEligibilityAllows\(\{/g) || [];
|
|
157
|
+
assert.equal(queueChecks.length, 3, `expected 3 permission-expressed queue gates, found ${queueChecks.length}`);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ── the "authority could not be established" fallback ────────────────────────
|
|
161
|
+
|
|
162
|
+
test('a null permission set falls back to the incumbent rank rule, never to allow', () => {
|
|
163
|
+
for (const rank of LIVE_RANKS) {
|
|
164
|
+
for (const nf of NF_VALUES) {
|
|
165
|
+
const rankRuleAllows = db.xenosClaimAllowed(rank, nf);
|
|
166
|
+
assert.equal(
|
|
167
|
+
claimEligibilityAllows({ heldPermissions: null, newcomerFriendly: nf, rankRuleAllows }),
|
|
168
|
+
rankRuleAllows,
|
|
169
|
+
`${rank}/${String(nf)}: the fallback must reproduce the rank rule exactly`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// ...and it is a fallback to the RULE, not an open door.
|
|
174
|
+
assert.equal(claimEligibilityAllows({ heldPermissions: null, newcomerFriendly: false, rankRuleAllows: false }), false);
|
|
175
|
+
assert.equal(claimEligibilityAllows({ heldPermissions: undefined, newcomerFriendly: true, rankRuleAllows: false }), false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('a reserved/unwired rank keeps TODAY behaviour through the fallback (no silent narrowing)', () => {
|
|
179
|
+
// A rank outside the four seeded ones resolves to zero permissions (the
|
|
180
|
+
// deliberate divergence governance_002 documents), so the claim path sees NULL
|
|
181
|
+
// and falls back — which is what keeps this swap behaviour-identical rather than
|
|
182
|
+
// quietly locking such a builder out of the newcomer queue.
|
|
183
|
+
for (const rank of ['helot', 'doulos', 'olympian', null, undefined]) {
|
|
184
|
+
assert.deepEqual(catalog.seedForRank(rank), [], `${String(rank)} has no seeded role`);
|
|
185
|
+
for (const nf of NF_VALUES) {
|
|
186
|
+
const rankRuleAllows = db.xenosClaimAllowed(rank, nf);
|
|
187
|
+
assert.equal(
|
|
188
|
+
claimEligibilityAllows({ heldPermissions: null, newcomerFriendly: nf, rankRuleAllows }),
|
|
189
|
+
rankRuleAllows,
|
|
190
|
+
`${String(rank)}/${String(nf)}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── C. rank-by-rank parity, against two independent oracles ──────────────────
|
|
197
|
+
|
|
198
|
+
test('PARITY vs a hand-written table — the new predicate admits exactly today\'s set', () => {
|
|
199
|
+
// Written from the rules, not from the code: the sandboxed entry rank reaches
|
|
200
|
+
// only rows opened to it; thetes and up reach everything (ADR 0018 / ADR 0034).
|
|
201
|
+
// [rank][newcomer_friendly] → may claim (ignoring the task floor, tested above).
|
|
202
|
+
const EXPECTED = {
|
|
203
|
+
xenos: { true: true, false: false },
|
|
204
|
+
thetes: { true: true, false: true },
|
|
205
|
+
metic: { true: true, false: true },
|
|
206
|
+
archon: { true: true, false: true },
|
|
207
|
+
};
|
|
208
|
+
for (const rank of LIVE_RANKS) {
|
|
209
|
+
for (const nf of NF_VALUES) {
|
|
210
|
+
const expected = EXPECTED[rank][nf === true ? 'true' : 'false'];
|
|
211
|
+
assert.equal(
|
|
212
|
+
permissionAllowsClaim(seedSet(rank), nf),
|
|
213
|
+
expected,
|
|
214
|
+
`${rank} / newcomer_friendly=${String(nf)} should be ${expected}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('PARITY vs db.xenosClaimAllowed — the real incumbent gate, over every live rank', () => {
|
|
221
|
+
for (const rank of LIVE_RANKS) {
|
|
222
|
+
for (const nf of NF_VALUES) {
|
|
223
|
+
assert.equal(
|
|
224
|
+
permissionAllowsClaim(seedSet(rank), nf),
|
|
225
|
+
db.xenosClaimAllowed(rank, nf),
|
|
226
|
+
`${rank} / newcomer_friendly=${String(nf)} diverges from the rank rule`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('PARITY of the COMPOSED gate — rank x newcomer_friendly x requires_rank', () => {
|
|
233
|
+
const REQUIRES_RANK = ['xenos', 'thetes', 'metic', 'archon', null];
|
|
234
|
+
let checked = 0;
|
|
235
|
+
for (const rank of LIVE_RANKS) {
|
|
236
|
+
for (const nf of NF_VALUES) {
|
|
237
|
+
for (const rr of REQUIRES_RANK) {
|
|
238
|
+
assert.equal(
|
|
239
|
+
newGate(rank, nf, rr),
|
|
240
|
+
oldGate(rank, nf, rr),
|
|
241
|
+
`${rank} / nf=${String(nf)} / requires_rank=${String(rr)}: permission gate diverges from the rank gate`,
|
|
242
|
+
);
|
|
243
|
+
checked++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
assert.equal(checked, LIVE_RANKS.length * NF_VALUES.length * REQUIRES_RANK.length);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('the abuse case stays blocked: a xenos cannot reach the general queue by any route', () => {
|
|
251
|
+
const held = seedSet('xenos');
|
|
252
|
+
for (const rr of ['xenos', 'thetes', 'metic', 'archon']) {
|
|
253
|
+
assert.equal(newGate('xenos', false, rr), false, `xenos must not claim a general-queue task (requires_rank=${rr})`);
|
|
254
|
+
}
|
|
255
|
+
// Not even by holding every OTHER permission the catalog defines — only
|
|
256
|
+
// task.claim.any opens the general queue.
|
|
257
|
+
const everythingElse = catalog.permissionKeys().filter((k) => k !== CLAIM_ANY);
|
|
258
|
+
assert.equal(permissionAllowsClaim(everythingElse, false), false);
|
|
259
|
+
// And the one permission they do hold is scoped to opened rows only.
|
|
260
|
+
assert.equal(permissionAllowsClaim(held, false), false);
|
|
261
|
+
assert.equal(permissionAllowsClaim(held, true), true);
|
|
262
|
+
});
|