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