@bongos/core 1.18.3 → 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 +1575 -285
- 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 +4 -2
- 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 +6 -1
- 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/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 +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- 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 +8 -0
- 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/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/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/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/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/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/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 +51 -58
- 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 -27
- 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/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- 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 +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- 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 +124 -2
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -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 +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +112 -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 +12 -0
- 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/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 +93 -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/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/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 +132 -7
- 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 +80 -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 +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// tests/governance_config.mjs — the governance config contract (BV1.R96, ADR 0151).
|
|
2
|
+
//
|
|
3
|
+
// DB-free: exercises the PURE resolver (all inputs injected) plus the disk loader
|
|
4
|
+
// against the real neutral starter. The invariants that matter here are trust-boundary
|
|
5
|
+
// invariants — a config file is editable by anyone who can write the checkout, so it
|
|
6
|
+
// must never be able to grant a system permission, invent a permission the catalog
|
|
7
|
+
// does not define, or re-grant a seeded rank-role.
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const GOV = path.join(ROOT, 'modules', 'governance');
|
|
18
|
+
const config = require(path.join(GOV, 'config.js'));
|
|
19
|
+
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
20
|
+
const { envPrefix } = require(path.join(ROOT, 'src', 'instance-config'));
|
|
21
|
+
const { resolveGovernanceConfig, loadGovernanceConfig, grantableOnly, NEUTRAL_PATH, INSTANCE_PATH } = config;
|
|
22
|
+
|
|
23
|
+
const NEUTRAL = JSON.parse(fs.readFileSync(NEUTRAL_PATH, 'utf8'));
|
|
24
|
+
// No env by default — every precedence test injects exactly the env it is asserting on.
|
|
25
|
+
const NO_ENV = {};
|
|
26
|
+
|
|
27
|
+
// ── precedence: neutral -> instance -> env ──────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
test('neutral-only resolution yields sane defaults (the vanilla boot R102 proves)', () => {
|
|
30
|
+
const out = resolveGovernanceConfig({ neutral: NEUTRAL, env: NO_ENV });
|
|
31
|
+
assert.deepEqual(Object.keys(out.roles).sort(), [...catalog.RANK_ORDER].sort());
|
|
32
|
+
assert.equal(out.roles.metic.label, 'Metic');
|
|
33
|
+
assert.equal(out.roles.archon.seeded, true);
|
|
34
|
+
for (const rank of catalog.RANK_ORDER) {
|
|
35
|
+
assert.ok(out.roles[rank].description, `${rank} template must carry a description`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('an empty config still resolves the four rank-roles to their canonical names', () => {
|
|
40
|
+
// governance_002 seeds them unconditionally, so a missing/mangled config must fall
|
|
41
|
+
// back rather than drop a role out of the set.
|
|
42
|
+
const out = resolveGovernanceConfig({ env: NO_ENV });
|
|
43
|
+
assert.deepEqual(Object.keys(out.roles).sort(), [...catalog.RANK_ORDER].sort());
|
|
44
|
+
assert.equal(out.roles.xenos.label, 'xenos');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('the instance file overrides the neutral starter, per role and per field', () => {
|
|
48
|
+
const out = resolveGovernanceConfig({
|
|
49
|
+
neutral: NEUTRAL,
|
|
50
|
+
instance: { roles: { metic: { label: 'Trusted' } } },
|
|
51
|
+
env: NO_ENV,
|
|
52
|
+
});
|
|
53
|
+
assert.equal(out.roles.metic.label, 'Trusted');
|
|
54
|
+
// The untouched field survives the merge, and so do the other three roles.
|
|
55
|
+
assert.equal(out.roles.metic.description, NEUTRAL.roles.metic.description);
|
|
56
|
+
assert.equal(out.roles.archon.label, 'Archon');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('the instance file may add a custom role template the neutral starter has not', () => {
|
|
60
|
+
const out = resolveGovernanceConfig({
|
|
61
|
+
neutral: NEUTRAL,
|
|
62
|
+
instance: { roles: { triager: { label: 'Triager', permissions: ['idea.triage', 'blocker.review'] } } },
|
|
63
|
+
env: NO_ENV,
|
|
64
|
+
});
|
|
65
|
+
assert.equal(out.roles.triager.seeded, false);
|
|
66
|
+
assert.deepEqual(out.roles.triager.permissions, ['blocker.review', 'idea.triage']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('an env override beats both the instance file and the neutral starter', () => {
|
|
70
|
+
const env = { [`${envPrefix()}_GOVERNANCE_ROLE_LABELS`]: 'metic=Crew,archon=Captain' };
|
|
71
|
+
const out = resolveGovernanceConfig({
|
|
72
|
+
neutral: NEUTRAL,
|
|
73
|
+
instance: { roles: { metic: { label: 'Trusted' } } },
|
|
74
|
+
env,
|
|
75
|
+
});
|
|
76
|
+
assert.equal(out.roles.metic.label, 'Crew');
|
|
77
|
+
assert.equal(out.roles.archon.label, 'Captain');
|
|
78
|
+
assert.equal(out.roles.xenos.label, 'Xenos', 'an unnamed role keeps its resolved label');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('the env layer can relabel a role but can never grant it a permission', () => {
|
|
82
|
+
// The whole point of restricting env to labels: an env var must not widen authority.
|
|
83
|
+
const env = { [`${envPrefix()}_GOVERNANCE_ROLE_LABELS`]: 'triager=Renamed' };
|
|
84
|
+
const out = resolveGovernanceConfig({
|
|
85
|
+
neutral: NEUTRAL,
|
|
86
|
+
instance: { roles: { triager: { label: 'Triager', permissions: ['idea.triage'] } } },
|
|
87
|
+
env,
|
|
88
|
+
});
|
|
89
|
+
assert.equal(out.roles.triager.label, 'Renamed');
|
|
90
|
+
assert.deepEqual(out.roles.triager.permissions, ['idea.triage']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('an env label naming a role no config defines is inert (a typo cannot grow the role set)', () => {
|
|
94
|
+
const env = { [`${envPrefix()}_GOVERNANCE_ROLE_LABELS`]: 'metics=Typo' };
|
|
95
|
+
const out = resolveGovernanceConfig({ neutral: NEUTRAL, env });
|
|
96
|
+
assert.deepEqual(Object.keys(out.roles).sort(), [...catalog.RANK_ORDER].sort());
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── fail-closed: config is not a trust boundary ─────────────────────────────────
|
|
100
|
+
|
|
101
|
+
test('a template naming a system permission is stripped (config cannot grant system power)', () => {
|
|
102
|
+
const systemKeys = catalog.systemKeys();
|
|
103
|
+
assert.ok(systemKeys.includes('deploy.push_main'), 'fixture guard: the catalog still flags deploy.push_main');
|
|
104
|
+
const out = resolveGovernanceConfig({
|
|
105
|
+
neutral: NEUTRAL,
|
|
106
|
+
instance: { roles: { sneaky: { label: 'Sneaky', permissions: ['deploy.push_main', 'builder.rank.set', 'idea.triage'] } } },
|
|
107
|
+
env: NO_ENV,
|
|
108
|
+
});
|
|
109
|
+
assert.deepEqual(out.roles.sneaky.permissions, ['idea.triage']);
|
|
110
|
+
// Belt and braces: NO system key survives a template, whichever one is named.
|
|
111
|
+
const all = resolveGovernanceConfig({
|
|
112
|
+
neutral: NEUTRAL,
|
|
113
|
+
instance: { roles: { everything: { permissions: catalog.permissionKeys() } } },
|
|
114
|
+
env: NO_ENV,
|
|
115
|
+
});
|
|
116
|
+
for (const k of systemKeys) {
|
|
117
|
+
assert.ok(!all.roles.everything.permissions.includes(k), `config must not be able to grant system permission "${k}"`);
|
|
118
|
+
}
|
|
119
|
+
assert.deepEqual(all.roles.everything.permissions, [...catalog.grantableKeys()].sort());
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('a template naming an unknown permission key is dropped (the catalog is the truth)', () => {
|
|
123
|
+
const out = resolveGovernanceConfig({
|
|
124
|
+
neutral: NEUTRAL,
|
|
125
|
+
instance: { roles: { odd: { permissions: ['idea.triage', 'not.a.real.permission', 'deploy.everything'] } } },
|
|
126
|
+
env: NO_ENV,
|
|
127
|
+
});
|
|
128
|
+
assert.deepEqual(out.roles.odd.permissions, ['idea.triage']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('permissions on a SEEDED rank-role are dropped wholesale (grants derive from floors)', () => {
|
|
132
|
+
const out = resolveGovernanceConfig({
|
|
133
|
+
neutral: NEUTRAL,
|
|
134
|
+
instance: { roles: { xenos: { label: 'Newcomer', permissions: ['task.create', 'audit_log.read'] } } },
|
|
135
|
+
env: NO_ENV,
|
|
136
|
+
});
|
|
137
|
+
assert.equal(out.roles.xenos.label, 'Newcomer', 'a seeded role may be relabelled');
|
|
138
|
+
assert.deepEqual(out.roles.xenos.permissions, [], 'a seeded role may never be re-granted from config');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('grantableOnly de-duplicates, sorts, and rejects non-string entries', () => {
|
|
142
|
+
assert.deepEqual(grantableOnly(['cost.log', 'idea.triage', 'cost.log', 42, null]), ['cost.log', 'idea.triage']);
|
|
143
|
+
assert.deepEqual(grantableOnly(undefined), []);
|
|
144
|
+
assert.deepEqual(grantableOnly('idea.triage'), [], 'a bare string is not a permission list');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('the resolved config is frozen — a caller cannot mutate it into a grant', () => {
|
|
148
|
+
const out = resolveGovernanceConfig({ neutral: NEUTRAL, env: NO_ENV });
|
|
149
|
+
assert.ok(Object.isFrozen(out) && Object.isFrozen(out.roles) && Object.isFrozen(out.roles.metic));
|
|
150
|
+
assert.throws(() => out.roles.metic.permissions.push('deploy.push_main'), TypeError);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── the neutral starter itself ──────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
test('the neutral starter carries no host-specific identity (a vanilla instance boots from it alone)', () => {
|
|
156
|
+
// Reuses fitness.js's own definition of "host identity" so this cannot drift from
|
|
157
|
+
// the CI gate (which does not scan .json itself — IDENTITY_SCAN_EXTS is js/html/css).
|
|
158
|
+
const { scanPromptIdentity } = require(path.join(ROOT, 'scripts', 'gds', 'fitness.js'));
|
|
159
|
+
const raw = fs.readFileSync(NEUTRAL_PATH, 'utf8');
|
|
160
|
+
assert.deepEqual(scanPromptIdentity('config/governance.neutral.json', raw, '.json'), []);
|
|
161
|
+
// And it carries only templates — never a live per-builder assignment.
|
|
162
|
+
assert.deepEqual(Object.keys(NEUTRAL).filter((k) => k !== '_note'), ['roles']);
|
|
163
|
+
for (const role of Object.values(NEUTRAL.roles)) {
|
|
164
|
+
assert.ok(!('permissions' in role), 'the neutral seeded roles carry labels only, never grants');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('the two config paths are the branding-shaped neutral/instance pair under config/', () => {
|
|
169
|
+
assert.equal(path.relative(ROOT, NEUTRAL_PATH).split(path.sep).join('/'), 'config/governance.neutral.json');
|
|
170
|
+
assert.equal(path.relative(ROOT, INSTANCE_PATH).split(path.sep).join('/'), 'config/governance.json');
|
|
171
|
+
assert.ok(fs.existsSync(NEUTRAL_PATH), 'the neutral starter must ship — it is the vanilla default');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('loadGovernanceConfig reads the real neutral starter and tolerates a missing instance pack', () => {
|
|
175
|
+
// A vanilla instance has no config/governance.json at all — the default INSTANCE_PATH
|
|
176
|
+
// is genuinely absent in this checkout, so the default call exercises that path.
|
|
177
|
+
assert.equal(fs.existsSync(INSTANCE_PATH), false, 'fixture guard: this checkout is the vanilla case');
|
|
178
|
+
const out = loadGovernanceConfig({ env: NO_ENV });
|
|
179
|
+
assert.equal(out.roles.archon.label, 'Archon');
|
|
180
|
+
assert.deepEqual(Object.keys(out.roles).sort(), [...catalog.RANK_ORDER].sort());
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('the neutral starter publishes with the core (a packaged vanilla instance needs it)', () => {
|
|
184
|
+
const { isPublishable } = require(path.join(ROOT, 'scripts', 'gds', 'publish-manifest.js'));
|
|
185
|
+
assert.equal(isPublishable('config/governance.neutral.json'), true);
|
|
186
|
+
assert.equal(isPublishable('config/governance.json'), false, "a host's filled-in pack is instance content");
|
|
187
|
+
});
|