@bongos/core 1.18.0 → 1.18.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +1740 -640
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +5 -3
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +11 -2
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +15 -2
- package/docs/api/openapi.json +2648 -483
- package/docs/api-reference.md +73 -13
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +15 -7
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +252 -62
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -21
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +58 -11
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +268 -19
- package/scripts/gds/publish-manifest.js +22 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +42 -16
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +72 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +433 -59
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +271 -7
- package/scripts/migrate.sh +10 -0
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +809 -107
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +105 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +312 -13
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/routes.js +2 -0
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +49 -2
- package/src/instance-config.js +46 -0
- package/src/module-api.js +140 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +41 -23
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +198 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/package_core.mjs +246 -1
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +289 -7
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +140 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +393 -5
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
|
@@ -80,6 +80,109 @@ test('a route declared with NO middleware does not borrow the NEXT route\'s rank
|
|
|
80
80
|
assert.equal(rows.find((x) => x.method === 'POST').rank, 'archon');
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
// ---------- BV1.R109b: multi-line route DECLARATIONS are visible at all ----------
|
|
84
|
+
//
|
|
85
|
+
// Distinct from the "multi-line middleware" test above: there the METHOD AND PATH were
|
|
86
|
+
// still on one line and only the middleware wrapped. Here the declaration itself opens
|
|
87
|
+
// on its own line and the PATH is below it — the form that made 10 real routes (6 of
|
|
88
|
+
// them writes) invisible to this auditor entirely: never gate-checked, never pinned,
|
|
89
|
+
// never published to the API contract.
|
|
90
|
+
|
|
91
|
+
test('multi-line DECLARATION (path on the next line) is discovered [BV1.R109b]', () => {
|
|
92
|
+
const src = [
|
|
93
|
+
' router.get(',
|
|
94
|
+
" '/security/adr',",
|
|
95
|
+
' auth.requireBuilder,',
|
|
96
|
+
" auth.requireRank('metic', 'archon'),",
|
|
97
|
+
' async (_req, res) => {',
|
|
98
|
+
' res.json({});',
|
|
99
|
+
' }',
|
|
100
|
+
' );',
|
|
101
|
+
].join('\n');
|
|
102
|
+
const r = rrc.classifyText(src);
|
|
103
|
+
assert.equal(r.length, 1, 'a multi-line declaration must emit exactly one route');
|
|
104
|
+
assert.equal(r[0].key, 'GET /security/adr');
|
|
105
|
+
assert.equal(r[0].rank, 'metic+archon', 'the gate scan must still find middleware below the path line');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('multi-line declaration with an interleaved comment still resolves [BV1.R109b]', () => {
|
|
109
|
+
const src = [
|
|
110
|
+
' router.post(',
|
|
111
|
+
' // the path lives below this note',
|
|
112
|
+
" '/x',",
|
|
113
|
+
" auth.requireRank('archon'),",
|
|
114
|
+
' h);',
|
|
115
|
+
].join('\n');
|
|
116
|
+
assert.equal(rrc.classifyText(src)[0].key, 'POST /x');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('an UNRESOLVABLE path is not silently emitted, and reds as a finding [BV1.R109b]', () => {
|
|
120
|
+
// A computed path can't be classified. The old scanner dropped it silently; the fix
|
|
121
|
+
// must FAIL CLOSED — no invented route row, and a hard finding from checkRouteRanks.
|
|
122
|
+
const src = " router.post(\n BASE + '/x',\n auth.requireRank('archon'),\n h);";
|
|
123
|
+
assert.equal(rrc.classifyText(src).length, 0, 'must not invent a route with a bogus path');
|
|
124
|
+
const decls = rrc.scanDeclarations(src);
|
|
125
|
+
assert.equal(decls.length, 1, 'the declaration must still be COUNTED (the census sees it)');
|
|
126
|
+
assert.equal(decls[0].route, null);
|
|
127
|
+
assert.equal(decls[0].method, 'POST');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('scanDeclarations counts both forms and never loses one [BV1.R109b]', () => {
|
|
131
|
+
const src = [
|
|
132
|
+
" router.get('/a', h);",
|
|
133
|
+
' router.post(',
|
|
134
|
+
" '/b',",
|
|
135
|
+
' h);',
|
|
136
|
+
" // router.get('/not-a-route', h); <- a commented-out line is not a declaration",
|
|
137
|
+
].join('\n');
|
|
138
|
+
const decls = rrc.scanDeclarations(src);
|
|
139
|
+
assert.deepEqual(decls.map((d) => `${d.method} ${d.route}`), ['GET /a', 'POST /b']);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('a multi-line WRITE route with NO gate hard-fails the build [BV1.R109b fixture]', () => {
|
|
143
|
+
// The whole point of the fix: before it, this route was INVISIBLE, so deleting its
|
|
144
|
+
// gate kept the build green. Now it must trip the ungated-write blocker.
|
|
145
|
+
const os = require('node:os');
|
|
146
|
+
const fs = require('node:fs');
|
|
147
|
+
const path = require('node:path');
|
|
148
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-multiline-'));
|
|
149
|
+
fs.writeFileSync(path.join(dir, 'shim.js'), [
|
|
150
|
+
' router.post(',
|
|
151
|
+
" '/danger',",
|
|
152
|
+
' async (req, res) => {',
|
|
153
|
+
' res.json({});',
|
|
154
|
+
' }',
|
|
155
|
+
' );',
|
|
156
|
+
].join('\n'));
|
|
157
|
+
const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, '__no_modules__') });
|
|
158
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
159
|
+
|
|
160
|
+
const ungated = res.findings.find((f) => f.route === 'POST /danger');
|
|
161
|
+
assert.ok(ungated, 'the multi-line write route must be DISCOVERED (it was invisible before R109b)');
|
|
162
|
+
assert.equal(ungated.severity, 'blocker', 'an ungated write route is a hard fail');
|
|
163
|
+
assert.equal(res.hardFail, true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('an UNRESOLVABLE multi-line write declaration also hard-fails [BV1.R109b fixture]', () => {
|
|
167
|
+
const os = require('node:os');
|
|
168
|
+
const fs = require('node:fs');
|
|
169
|
+
const path = require('node:path');
|
|
170
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-unresolved-'));
|
|
171
|
+
fs.writeFileSync(path.join(dir, 'shim.js'), [
|
|
172
|
+
' router.post(',
|
|
173
|
+
' ROUTES.danger,',
|
|
174
|
+
" auth.requireRank('archon'),",
|
|
175
|
+
' h);',
|
|
176
|
+
].join('\n'));
|
|
177
|
+
const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, '__no_modules__') });
|
|
178
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
179
|
+
|
|
180
|
+
const f = res.findings.find((x) => x.rank === '(unparsed)');
|
|
181
|
+
assert.ok(f, 'a declaration whose path cannot be resolved must surface a finding, not vanish');
|
|
182
|
+
assert.equal(f.severity, 'blocker', 'an unverifiable WRITE declaration is a hard fail');
|
|
183
|
+
assert.equal(res.hardFail, true);
|
|
184
|
+
});
|
|
185
|
+
|
|
83
186
|
// ---------- classifyText: `rank:` annotation fallback ----------
|
|
84
187
|
|
|
85
188
|
test('rank: annotation directly above → classifies', () => {
|
|
@@ -118,6 +221,79 @@ test('typo annotation (not a known rank) → unknown', () => {
|
|
|
118
221
|
assert.equal(r[0].rank, 'unknown', 'a misspelled rank surfaces rather than being trusted');
|
|
119
222
|
});
|
|
120
223
|
|
|
224
|
+
// ---------- BV1.R109d: a COMMENT is not a gate ----------
|
|
225
|
+
// A `rank:` annotation is reached ONLY when no middleware was found, so a comment
|
|
226
|
+
// naming a privileged rank asserts a gate that provably does not exist. Before this,
|
|
227
|
+
// such a route satisfied the ungated-write invariant AND every exact pin.
|
|
228
|
+
|
|
229
|
+
test('provenance: a real gate is `middleware`, a bare comment is `annotation` [BV1.R109d]', () => {
|
|
230
|
+
const gated = rrc.classifyText(` router.post('/x', auth.requireBuilder, auth.requireRank('archon'), h);`);
|
|
231
|
+
assert.equal(gated[0].via, 'middleware');
|
|
232
|
+
const claimed = rrc.classifyText(` // rank: archon\n router.post('/x', h);`);
|
|
233
|
+
assert.equal(claimed[0].via, 'annotation');
|
|
234
|
+
assert.equal(claimed[0].rank, 'archon', 'the label is still read — it is the PROVENANCE that differs');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('a bare identifier gate (destructured doorway) counts as middleware [BV1.R109d]', () => {
|
|
238
|
+
// modules/platform-identity does `const { requireBuilder, requireRank } = api` and
|
|
239
|
+
// passes BARE identifiers. Recognising only the namespaced spelling made 11 correctly
|
|
240
|
+
// gated routes — 5 of them archon — visible to the auditor only via their comment.
|
|
241
|
+
const r = rrc.classifyText(` router.post('/projects', requireBuilder, requireRank('archon'), h);`);
|
|
242
|
+
assert.equal(r[0].rank, 'archon');
|
|
243
|
+
assert.equal(r[0].via, 'middleware', 'a destructured guard is a real gate, not prose');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('the identifier in PROSE never counts as a gate [BV1.R109d]', () => {
|
|
247
|
+
const r = rrc.classifyText(` // this route used to call requireRank('archon') before the migration\n // rank: public\n router.get('/x', h);`);
|
|
248
|
+
assert.equal(r[0].via, 'annotation', 'a guard name inside a comment is not argument position');
|
|
249
|
+
assert.equal(r[0].rank, 'public');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('a privileged comment-only WRITE route hard-fails the build [BV1.R109d]', () => {
|
|
253
|
+
const os = require('node:os'); const fs = require('node:fs'); const path = require('node:path');
|
|
254
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-r109d-'));
|
|
255
|
+
fs.writeFileSync(path.join(dir, 'shim.js'), ` // rank: archon\n router.patch('/builders/:id/rank', h);\n`);
|
|
256
|
+
const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, '__none__') });
|
|
257
|
+
// The load-bearing assertion is the SPECIFIC finding below, not hardFail — a shim dir
|
|
258
|
+
// hardFails anyway on the missing pinned routes, so hardFail alone proves nothing here.
|
|
259
|
+
const f = res.findings.find((x) => x.route.includes('/builders/:id/rank') && /carries NO auth middleware/.test(x.summary));
|
|
260
|
+
assert.ok(f, `expected a comment-only finding; got: ${res.findings.map((x) => x.summary).join(' | ')}`);
|
|
261
|
+
assert.equal(f.severity, 'blocker');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('`rank: public` on an unauthenticated route still passes — no false positive [BV1.R109d]', () => {
|
|
265
|
+
const os = require('node:os'); const fs = require('node:fs'); const path = require('node:path');
|
|
266
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-r109d-pub-'));
|
|
267
|
+
fs.writeFileSync(path.join(dir, 'shim.js'), ` // rank: public\n router.get('/healthz', h);\n`);
|
|
268
|
+
const res = rrc.checkRouteRanks({ routesDir: dir, modulesDir: path.join(dir, '__none__') });
|
|
269
|
+
// NB: assert on the comment-only findings specifically, NOT on hardFail — a shim dir
|
|
270
|
+
// contains none of the pinned routes, so every EXPECTED_RANKS pin reports "vanished"
|
|
271
|
+
// and hardFail is true for reasons this test is not about.
|
|
272
|
+
assert.equal(res.findings.filter((x) => /carries NO auth middleware/.test(x.summary)).length, 0,
|
|
273
|
+
'public is the annotation’s legitimate use and must not red');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('a pin is NOT satisfiable by a comment [BV1.R109d]', () => {
|
|
277
|
+
// The pins are the deterministic alarm for "routes whose downgrade would be a real
|
|
278
|
+
// authority breach". A comment satisfying one would let prose certify authority.
|
|
279
|
+
const claimed = rrc.classifyText(` // rank: archon\n router.patch('/builders/:id/rank', h);`)[0];
|
|
280
|
+
assert.match(rrc.pinMismatch('archon', claimed) || '', /NO auth middleware/);
|
|
281
|
+
assert.match(rrc.pinMismatch('perm:builder.rank.set', claimed) || '', /NO auth middleware/);
|
|
282
|
+
const real = rrc.classifyText(` router.patch('/builders/:id/rank', auth.requireBuilder, auth.requireRank('archon'), h);`)[0];
|
|
283
|
+
assert.equal(rrc.pinMismatch('archon', real), null, 'a real gate still satisfies its pin');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('LIVE: no privileged route in this repo is gated only by a comment [BV1.R109d]', () => {
|
|
287
|
+
const fs = require('node:fs');
|
|
288
|
+
const offenders = [];
|
|
289
|
+
for (const { abs, rel } of rrc.discoverRouteFiles()) {
|
|
290
|
+
for (const r of rrc.classifyText(fs.readFileSync(abs, 'utf8'))) {
|
|
291
|
+
if (r.via === 'annotation' && r.rank !== 'public') offenders.push(`${r.key} (${rel}) claims '${r.rank}'`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
assert.deepEqual(offenders, [], 'a privileged route certified only by prose');
|
|
295
|
+
});
|
|
296
|
+
|
|
121
297
|
// ---------- checkRouteRanks invariant (via a fixture-free routesDir shim) ----------
|
|
122
298
|
// checkRouteRanks reads files; we exercise the finding logic through the pure
|
|
123
299
|
// path by constructing routes through classifyText + the documented severity
|
|
@@ -147,11 +323,17 @@ test('toWorkerResult: read-unknown only → concerns (not fail)', () => {
|
|
|
147
323
|
|
|
148
324
|
// ---------- EXPECTED_RANKS: exact-rank pin on privileged routes (#999) ----------
|
|
149
325
|
|
|
150
|
-
test('EXPECTED_RANKS: pins are non-empty and
|
|
326
|
+
test('EXPECTED_RANKS: pins are non-empty and every value is a known rank OR a defined permission', () => {
|
|
151
327
|
const keys = Object.keys(rrc.EXPECTED_RANKS);
|
|
152
328
|
assert.ok(keys.length > 0, 'should pin at least one privileged route');
|
|
153
329
|
for (const [k, v] of Object.entries(rrc.EXPECTED_RANKS)) {
|
|
154
|
-
|
|
330
|
+
const pin = rrc.parsePin(v);
|
|
331
|
+
if (pin.kind === 'rank') {
|
|
332
|
+
assert.ok(rrc.KNOWN_RANKS.has(pin.rank), `pinned expected rank '${v}' for ${k} must be a known rank`);
|
|
333
|
+
} else {
|
|
334
|
+
assert.notEqual(rrc.rankForPermissions(pin.perms), 'unknown',
|
|
335
|
+
`pinned permission(s) '${pin.perms.join(', ')}' for ${k} must exist in the governance catalog`);
|
|
336
|
+
}
|
|
155
337
|
}
|
|
156
338
|
});
|
|
157
339
|
|
|
@@ -168,21 +350,34 @@ test('EXPECTED_RANKS: a DOWNGRADE on a pinned route trips a blocker via a routes
|
|
|
168
350
|
const emit = (key, expected) => {
|
|
169
351
|
const [method, route] = key.split(' ');
|
|
170
352
|
const m = method.toLowerCase();
|
|
353
|
+
const pin = rrc.parsePin(expected);
|
|
171
354
|
if (key === 'PATCH /builders/:id/rank') {
|
|
172
355
|
// DOWNGRADE: requireBuilder only → classifies any-builder, not archon.
|
|
173
356
|
lines.push(` router.${m}('${route}', auth.requireBuilder, h);`);
|
|
174
|
-
} else if (
|
|
357
|
+
} else if (pin.kind === 'permission') {
|
|
358
|
+
// The MIGRATED spelling. A permission pin is satisfied by naming exactly its
|
|
359
|
+
// atoms — this arm is what proves the post-R104 gate form clears the pin.
|
|
360
|
+
lines.push(` router.${m}('${route}', auth.requireBuilder, auth.requirePermission(${pin.perms.map((p) => `'${p}'`).join(', ')}), h);`);
|
|
361
|
+
} else if (pin.rank === 'archon') {
|
|
175
362
|
lines.push(` router.${m}('${route}', auth.requireBuilder, auth.requireRank('archon'), h);`);
|
|
176
|
-
} else if (
|
|
363
|
+
} else if (pin.rank === 'metic+archon') {
|
|
177
364
|
lines.push(` router.${m}('${route}', auth.requireRank('metic', 'archon'), h);`);
|
|
178
|
-
} else {
|
|
179
|
-
lines.push(`
|
|
365
|
+
} else if (pin.rank === 'bfg-principal') {
|
|
366
|
+
lines.push(` router.${m}('${route}', auth.requireBuilder, auth.requireBfgPrincipal, h);`);
|
|
367
|
+
} else if (pin.rank === 'public') {
|
|
368
|
+
// The ONE honest comment-only gate: 'public' asserts nothing is protected.
|
|
369
|
+
lines.push(` // rank: public`);
|
|
180
370
|
lines.push(` router.${m}('${route}', h);`);
|
|
371
|
+
} else {
|
|
372
|
+
// Real middleware, NOT a `rank:` comment (BV1.R109d). This branch used to emit
|
|
373
|
+
// a bare annotation, which only classified because a comment could stand in for
|
|
374
|
+
// a gate — the hole R109d closed. A pinned route must carry its real gate, or
|
|
375
|
+
// the fixture reds for a reason this test is not about.
|
|
376
|
+
lines.push(` router.${m}('${route}', auth.requireBuilder, h);`);
|
|
181
377
|
}
|
|
182
|
-
// A
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
lines.push(' });', '', '', '', '', '', '');
|
|
378
|
+
// A gap wider than classifyGate's 20-line middleware lookahead, so no route can
|
|
379
|
+
// "borrow" a NEIGHBOUR's gate — every emitted route stands on its own.
|
|
380
|
+
lines.push(' });', ...Array(21).fill(''));
|
|
186
381
|
};
|
|
187
382
|
// Emit every pinned route EXCEPT the downgraded one first, then the downgraded
|
|
188
383
|
// one LAST (so nothing with requireRank('archon') sits within its lookahead).
|
|
@@ -198,6 +393,11 @@ test('EXPECTED_RANKS: a DOWNGRADE on a pinned route trips a blocker via a routes
|
|
|
198
393
|
assert.ok(downgrade, 'a downgraded pinned route must surface a finding');
|
|
199
394
|
assert.equal(downgrade.severity, 'blocker', 'a rank downgrade on a privileged route is a hard fail');
|
|
200
395
|
assert.equal(res.hardFail, true);
|
|
396
|
+
// The other side of the same fixture: every OTHER pin is satisfied by the gate
|
|
397
|
+
// spelling it was emitted in — rank pins by requireRank/annotation, permission
|
|
398
|
+
// pins by requirePermission. A stray finding here means a pin is unsatisfiable.
|
|
399
|
+
assert.deepEqual(res.findings.map((f) => f.route), ['PATCH /builders/:id/rank'],
|
|
400
|
+
`only the deliberately-downgraded route should red; got: ${res.findings.map((f) => `${f.route} — ${f.summary}`).join(' | ')}`);
|
|
201
401
|
});
|
|
202
402
|
|
|
203
403
|
test('EXPECTED_RANKS: a MISSING pinned route trips a blocker (a protected route vanished)', () => {
|
|
@@ -227,14 +427,56 @@ test('LIVE: checkRouteRanks() on src/bongos/routes/ has zero unknowns', () => {
|
|
|
227
427
|
assert.equal(res.hardFail, false);
|
|
228
428
|
});
|
|
229
429
|
|
|
230
|
-
|
|
430
|
+
// The census invariant (BV1.R109b). Asserting that the 10 known multi-line paths parse
|
|
431
|
+
// would prove nothing about the ELEVENTH — the next route someone declares in a form
|
|
432
|
+
// this scanner doesn't handle. So count instead: every `router.<method>(` line in every
|
|
433
|
+
// discovered route file must yield exactly one classified route.
|
|
434
|
+
//
|
|
435
|
+
// The census regex below is HAND-WRITTEN here on purpose and NOT imported from the
|
|
436
|
+
// module. Importing ROUTE_DECL_RE would move both sides of the comparison together and
|
|
437
|
+
// prove nothing (the oracle trap tests/route_permission_check.mjs documents).
|
|
438
|
+
const CENSUS_DECL_RE = /^\s*router\.(?:get|post|patch|put|delete)\s*\(/;
|
|
439
|
+
// A floor, so a change that silently guts BOTH sides (e.g. discoverRouteFiles stops
|
|
440
|
+
// finding module routes) reds instead of trivially "matching" at a smaller number.
|
|
441
|
+
const ROUTE_CENSUS_FLOOR = 258;
|
|
442
|
+
|
|
443
|
+
test('LIVE census: every router.<method>( declaration classifies to exactly one route [BV1.R109b]', () => {
|
|
444
|
+
const fs = require('node:fs');
|
|
445
|
+
let declared = 0;
|
|
446
|
+
let classified = 0;
|
|
447
|
+
const shortfall = [];
|
|
448
|
+
for (const { abs, rel } of rrc.discoverRouteFiles()) {
|
|
449
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
450
|
+
const d = text.split('\n').filter((l) => CENSUS_DECL_RE.test(l)).length;
|
|
451
|
+
const c = rrc.classifyText(text).length;
|
|
452
|
+
declared += d;
|
|
453
|
+
classified += c;
|
|
454
|
+
if (d !== c) shortfall.push(`${rel}: ${d} declared vs ${c} classified`);
|
|
455
|
+
}
|
|
456
|
+
assert.equal(shortfall.length, 0,
|
|
457
|
+
`every declared route must be visible to the auditor; invisible in: ${shortfall.join('; ')}`);
|
|
458
|
+
assert.equal(classified, declared, 'declaration census must equal the classified route count');
|
|
459
|
+
assert.ok(declared >= ROUTE_CENSUS_FLOOR,
|
|
460
|
+
`expected at least ${ROUTE_CENSUS_FLOOR} route declarations, found ${declared} — did route discovery break?`);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test('LIVE: checkRouteRanks reports ZERO unparsed declarations [BV1.R109b]', () => {
|
|
464
|
+
const res = rrc.checkRouteRanks();
|
|
465
|
+
const unparsed = res.findings.filter((f) => f.rank === '(unparsed)');
|
|
466
|
+
assert.equal(unparsed.length, 0,
|
|
467
|
+
`no route declaration may be unresolvable: ${unparsed.map((f) => f.route).join(', ')}`);
|
|
468
|
+
assert.ok(res.routeCount >= ROUTE_CENSUS_FLOOR,
|
|
469
|
+
`checkRouteRanks must cover at least ${ROUTE_CENSUS_FLOOR} routes, saw ${res.routeCount}`);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('LIVE: every pinned privileged route still satisfies its pin', () => {
|
|
231
473
|
const res = rrc.checkRouteRanks();
|
|
232
474
|
const byKey = new Map();
|
|
233
475
|
for (const r of res.routes) if (!byKey.has(r.key)) byKey.set(r.key, r);
|
|
234
476
|
for (const [key, expected] of Object.entries(rrc.EXPECTED_RANKS)) {
|
|
235
477
|
const r = byKey.get(key);
|
|
236
478
|
assert.ok(r, `pinned route ${key} must exist in the live route set`);
|
|
237
|
-
assert.equal(
|
|
479
|
+
assert.equal(rrc.pinMismatch(expected, r), null, `pinned route ${key}: ${rrc.pinMismatch(expected, r)}`);
|
|
238
480
|
}
|
|
239
481
|
});
|
|
240
482
|
|
|
@@ -196,10 +196,18 @@ const BLOCKERS_POST_SCHEMA = {
|
|
|
196
196
|
source_ref: { type: 'string', maxLength: LIMITS.SOURCE_REF },
|
|
197
197
|
};
|
|
198
198
|
|
|
199
|
+
// Hand-mirrored from modules/ideas/routes/inbox.js's PATCH validator (the route
|
|
200
|
+
// declares it inline, so there is nothing to import). Strict mode rejects
|
|
201
|
+
// unknown fields, so a field missing HERE makes the mirror assert the opposite
|
|
202
|
+
// of the route — keep the two in step. `reviewed_at` had already drifted out
|
|
203
|
+
// when task 1002447 added the last two.
|
|
199
204
|
const INBOX_PATCH_SCHEMA = {
|
|
200
205
|
status: { type: 'string', enum: ['open', 'promoted', 'discarded', 'merged'] },
|
|
201
206
|
kind: { type: 'string', maxLength: 64 },
|
|
202
207
|
promoted_to_task_id: { type: 'integer', min: 1 },
|
|
208
|
+
reviewed_at: {},
|
|
209
|
+
merged_into_idea_id: { type: 'integer', min: 1 },
|
|
210
|
+
triage_note: { type: 'string', maxLength: 1000 },
|
|
203
211
|
};
|
|
204
212
|
|
|
205
213
|
// ---- supervisor hot path 1: POST /api/gds/claims with full body ----
|
|
@@ -332,6 +340,22 @@ test('inbox: PATCH /inbox/:id with status promote passes', () => {
|
|
|
332
340
|
assert.equal(validate({ status: 'promoted', promoted_to_task_id: 254 }, INBOX_PATCH_SCHEMA), null);
|
|
333
341
|
});
|
|
334
342
|
|
|
343
|
+
// task 1002447 — the merge target + the verdict's reason (migration core_195).
|
|
344
|
+
test('inbox: PATCH /inbox/:id merge carries a target + a note', () => {
|
|
345
|
+
const body = { status: 'merged', merged_into_idea_id: 412, triage_note: 'same request as #412' };
|
|
346
|
+
assert.equal(validate(body, INBOX_PATCH_SCHEMA), null);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('inbox: PATCH /inbox/:id discard carries a reason', () => {
|
|
350
|
+
assert.equal(validate({ status: 'discarded', triage_note: 'superseded by the new gate' }, INBOX_PATCH_SCHEMA), null);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// A typed field still accepts an explicit null — validateField returns early
|
|
354
|
+
// for a null non-required field — so the un-merge needs no untyped escape hatch.
|
|
355
|
+
test('inbox: PATCH /inbox/:id may clear the merge link with null', () => {
|
|
356
|
+
assert.equal(validate({ merged_into_idea_id: null }, INBOX_PATCH_SCHEMA), null);
|
|
357
|
+
});
|
|
358
|
+
|
|
335
359
|
// ===========================================================================
|
|
336
360
|
// C. Bad input must return structured 400, never 500.
|
|
337
361
|
// ===========================================================================
|
|
@@ -400,6 +424,12 @@ test('BAD: PATCH /inbox/:id with bad status enum', () => {
|
|
|
400
424
|
assert.ok(r.details.some((d) => d.reason === 'not_in_enum'));
|
|
401
425
|
});
|
|
402
426
|
|
|
427
|
+
test('BAD: PATCH /inbox/:id with an oversize triage_note → too_long', () => {
|
|
428
|
+
const r = validate({ status: 'discarded', triage_note: 'x'.repeat(1001) }, INBOX_PATCH_SCHEMA);
|
|
429
|
+
assert.ok(r);
|
|
430
|
+
assert.ok(r.details.some((d) => d.field === 'triage_note' && d.reason === 'too_long'));
|
|
431
|
+
});
|
|
432
|
+
|
|
403
433
|
test('BAD: POST /tasks/:id/grade with signals=string (must be object)', () => {
|
|
404
434
|
const r = validate({ signals: 'oops' }, TASKS_GRADE_SCHEMA);
|
|
405
435
|
assert.ok(r);
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// tests/sequence.mjs — pure-core coverage for the sequential chain runner
|
|
2
|
+
// (task 1002484). Hermetic: feeds fixtures to topoOrder/chainFrom/haltReasons;
|
|
3
|
+
// no DB, no API. Run: node tests/sequence.mjs (also runs in the unit gate).
|
|
4
|
+
|
|
5
|
+
import { strict as assert } from 'node:assert';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const {
|
|
10
|
+
readArgv,
|
|
11
|
+
edgeIds,
|
|
12
|
+
rowsOf,
|
|
13
|
+
topoOrder,
|
|
14
|
+
chainFrom,
|
|
15
|
+
haltReasons,
|
|
16
|
+
summarize,
|
|
17
|
+
MIN_SPEC_CHARS,
|
|
18
|
+
} = require('../scripts/gds/sequence.js');
|
|
19
|
+
|
|
20
|
+
let passed = 0, failed = 0;
|
|
21
|
+
const t = (n, f) => { try { f(); passed++; console.log(` PASS ${n}`); } catch (e) { failed++; console.log(` FAIL ${n}\n ${e.message}`); } };
|
|
22
|
+
|
|
23
|
+
// A description long enough to clear the thin-spec gate, so fixtures testing
|
|
24
|
+
// OTHER halt reasons don't trip it incidentally.
|
|
25
|
+
const SPEC = 'x'.repeat(MIN_SPEC_CHARS + 1);
|
|
26
|
+
const task = (over = {}) => ({
|
|
27
|
+
id: 1, title: 'T', status: 'ready', kind: 'feature', description: SPEC,
|
|
28
|
+
touches: [], dependencies: [], dependents: [], requires_rank: 'xenos',
|
|
29
|
+
credits_reward: 10, est_minutes: 30, priority: null, ...over,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------- argv
|
|
33
|
+
|
|
34
|
+
t('readArgv parses --from/--goal/--check in both spellings', () => {
|
|
35
|
+
assert.equal(readArgv(['--from', '7']).from, 7);
|
|
36
|
+
assert.equal(readArgv(['--from=7']).from, 7);
|
|
37
|
+
assert.equal(readArgv(['--goal=42']).goal, 42);
|
|
38
|
+
assert.equal(readArgv(['--check=9']).check, 9);
|
|
39
|
+
assert.equal(readArgv(['--from', '7', '--next']).next, true);
|
|
40
|
+
assert.equal(readArgv(['--from', '7', '--json']).json, true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
t('readArgv accepts a bare task id as --from', () => {
|
|
44
|
+
assert.equal(readArgv(['1002277']).from, 1002277);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------- edge shapes
|
|
48
|
+
|
|
49
|
+
t('edgeIds tolerates both bare-number and {id} edge rows', () => {
|
|
50
|
+
assert.deepEqual(edgeIds([1, '2', { id: 3 }, { id: '4' }]), [1, 2, 3, 4]);
|
|
51
|
+
assert.deepEqual(edgeIds(null), []);
|
|
52
|
+
assert.deepEqual(edgeIds([null, undefined, 'nope']), []);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
t('rowsOf finds the array under each envelope key the API uses', () => {
|
|
56
|
+
assert.deepEqual(rowsOf({ claimable: [{ id: 1 }] }), [{ id: 1 }]);
|
|
57
|
+
assert.deepEqual(rowsOf({ tasks: [{ id: 2 }] }), [{ id: 2 }]);
|
|
58
|
+
assert.deepEqual(rowsOf([{ id: 3 }]), [{ id: 3 }]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
t('rowsOf returns null (not []) for an unrecognized envelope', () => {
|
|
62
|
+
// This distinction is load-bearing: null means "could not check", which
|
|
63
|
+
// haltReasons must treat as silence rather than halting every task.
|
|
64
|
+
assert.equal(rowsOf({ error: 'boom' }), null);
|
|
65
|
+
assert.equal(rowsOf(null), null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------- ordering
|
|
69
|
+
|
|
70
|
+
t('topoOrder puts a dependency before its dependent', () => {
|
|
71
|
+
const { order, cycle } = topoOrder([
|
|
72
|
+
task({ id: 2, dependencies: [1] }),
|
|
73
|
+
task({ id: 1 }),
|
|
74
|
+
]);
|
|
75
|
+
assert.deepEqual(order.map((x) => x.id), [1, 2]);
|
|
76
|
+
assert.deepEqual(cycle, []);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
t('topoOrder handles a three-deep chain', () => {
|
|
80
|
+
const { order } = topoOrder([
|
|
81
|
+
task({ id: 3, dependencies: [2] }),
|
|
82
|
+
task({ id: 1 }),
|
|
83
|
+
task({ id: 2, dependencies: [1] }),
|
|
84
|
+
]);
|
|
85
|
+
assert.deepEqual(order.map((x) => x.id), [1, 2, 3]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
t('topoOrder reports a cycle instead of silently dropping it', () => {
|
|
89
|
+
const { order, cycle } = topoOrder([
|
|
90
|
+
task({ id: 1, dependencies: [2] }),
|
|
91
|
+
task({ id: 2, dependencies: [1] }),
|
|
92
|
+
]);
|
|
93
|
+
assert.deepEqual(order, []);
|
|
94
|
+
assert.deepEqual(cycle, [1, 2]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
t('topoOrder keeps the acyclic part and reports only the cyclic remainder', () => {
|
|
98
|
+
const { order, cycle } = topoOrder([
|
|
99
|
+
task({ id: 1 }),
|
|
100
|
+
task({ id: 2, dependencies: [3] }),
|
|
101
|
+
task({ id: 3, dependencies: [2] }),
|
|
102
|
+
]);
|
|
103
|
+
assert.deepEqual(order.map((x) => x.id), [1]);
|
|
104
|
+
assert.deepEqual(cycle, [2, 3]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
t('topoOrder ignores dependencies pointing OUTSIDE the scope', () => {
|
|
108
|
+
// An out-of-scope dep must not stall ordering — it surfaces later as a
|
|
109
|
+
// deps_unmet halt, which is the honest place for it.
|
|
110
|
+
const { order, cycle } = topoOrder([task({ id: 5, dependencies: [999] })]);
|
|
111
|
+
assert.deepEqual(order.map((x) => x.id), [5]);
|
|
112
|
+
assert.deepEqual(cycle, []);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
t('topoOrder breaks ties on priority, then estimate, then id', () => {
|
|
116
|
+
const { order } = topoOrder([
|
|
117
|
+
task({ id: 10, priority: 3, est_minutes: 5 }),
|
|
118
|
+
task({ id: 11, priority: 1, est_minutes: 90 }),
|
|
119
|
+
task({ id: 12, priority: 1, est_minutes: 10 }),
|
|
120
|
+
]);
|
|
121
|
+
assert.deepEqual(order.map((x) => x.id), [11, 12, 10].sort((a, b) => {
|
|
122
|
+
const p = { 10: 3, 11: 1, 12: 1 }, m = { 10: 5, 11: 90, 12: 10 };
|
|
123
|
+
return p[a] - p[b] || m[a] - m[b] || a - b;
|
|
124
|
+
}));
|
|
125
|
+
assert.equal(order[0].id, 12, 'priority 1 + shortest estimate leads');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------- chain scope
|
|
129
|
+
|
|
130
|
+
t('chainFrom walks dependents transitively', () => {
|
|
131
|
+
const rows = [
|
|
132
|
+
task({ id: 1, dependents: [2] }),
|
|
133
|
+
task({ id: 2, dependents: [3], status: 'backlog' }),
|
|
134
|
+
task({ id: 3, status: 'backlog' }),
|
|
135
|
+
];
|
|
136
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
137
|
+
assert.deepEqual(chainFrom(1, byId).map((x) => x.id), [1, 2, 3]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
t('chainFrom walks THROUGH a shipped task but excludes it from the plan', () => {
|
|
141
|
+
const rows = [
|
|
142
|
+
task({ id: 1, status: 'shipped', dependents: [2] }),
|
|
143
|
+
task({ id: 2, status: 'ready' }),
|
|
144
|
+
];
|
|
145
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
146
|
+
assert.deepEqual(chainFrom(1, byId).map((x) => x.id), [2]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
t('chainFrom does not loop forever on a cyclic dependents graph', () => {
|
|
150
|
+
const rows = [
|
|
151
|
+
task({ id: 1, dependents: [2] }),
|
|
152
|
+
task({ id: 2, dependents: [1] }),
|
|
153
|
+
];
|
|
154
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
155
|
+
assert.deepEqual(chainFrom(1, byId).map((x) => x.id).sort(), [1, 2]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
t('chainFrom honours the limit', () => {
|
|
159
|
+
const rows = [1, 2, 3, 4, 5].map((id) => task({ id, dependents: id < 5 ? [id + 1] : [] }));
|
|
160
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
161
|
+
assert.equal(chainFrom(1, byId, { limit: 3 }).length, 3);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
t('chainFrom stays inside its scope — an unrelated task is not pulled in', () => {
|
|
165
|
+
const rows = [task({ id: 1, dependents: [2] }), task({ id: 2 }), task({ id: 99 })];
|
|
166
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
167
|
+
assert.deepEqual(chainFrom(1, byId).map((x) => x.id), [1, 2]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------- halt matrix
|
|
171
|
+
|
|
172
|
+
const clean = { rank: 'archon', claimableIds: new Set([1]) };
|
|
173
|
+
const codes = (tk, opts = clean) => haltReasons(tk, opts).map((r) => r.code);
|
|
174
|
+
|
|
175
|
+
t('a clean ready task produces no halt reasons', () => {
|
|
176
|
+
assert.deepEqual(codes(task()), []);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
t('halts on needs_migration', () => {
|
|
180
|
+
assert.ok(codes(task({ needs_migration: true })).includes('needs_migration'));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
t('halts on security_sensitive', () => {
|
|
184
|
+
assert.ok(codes(task({ security_sensitive: true })).includes('security_sensitive'));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
t('halts when touches[] hits a protected path', () => {
|
|
188
|
+
const c = codes(task({ touches: ['src/bongos/auth.js'] }));
|
|
189
|
+
assert.ok(c.includes('protected_path'), `expected protected_path, got ${c}`);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
t('does NOT flag an ordinary path as protected', () => {
|
|
193
|
+
assert.ok(!codes(task({ touches: ['modules/hall-ui/public/work.js'] })).includes('protected_path'));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
t('halts when the task requires a rank above the builder', () => {
|
|
197
|
+
const c = haltReasons(task({ requires_rank: 'archon' }), { rank: 'xenos', claimableIds: new Set([1]) });
|
|
198
|
+
assert.ok(c.map((r) => r.code).includes('rank_floor'));
|
|
199
|
+
assert.match(c.find((r) => r.code === 'rank_floor').detail, /archon/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
t('does not flag rank_floor when the builder outranks the floor', () => {
|
|
203
|
+
assert.ok(!codes(task({ requires_rank: 'metic' })).includes('rank_floor'));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
t('halts on a judgement kind (decision, spike)', () => {
|
|
207
|
+
assert.ok(codes(task({ kind: 'decision' })).includes('judgement_kind'));
|
|
208
|
+
assert.ok(codes(task({ kind: 'spike' })).includes('judgement_kind'));
|
|
209
|
+
assert.ok(!codes(task({ kind: 'feature' })).includes('judgement_kind'));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
t('halts on a thin spec', () => {
|
|
213
|
+
assert.ok(codes(task({ description: '' })).includes('thin_spec'));
|
|
214
|
+
assert.ok(codes(task({ description: 'do the thing' })).includes('thin_spec'));
|
|
215
|
+
assert.ok(!codes(task({ description: SPEC })).includes('thin_spec'));
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
t('halts when the task is still gated (not ready)', () => {
|
|
219
|
+
assert.ok(codes(task({ status: 'backlog' })).includes('deps_unmet'));
|
|
220
|
+
assert.ok(codes(task({ status: 'blocked' })).includes('deps_unmet'));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
t('halts when the task is already mid-ship, distinctly from deps_unmet', () => {
|
|
224
|
+
for (const status of ['active', 'completed', 'confirmed']) {
|
|
225
|
+
const c = codes(task({ status }));
|
|
226
|
+
assert.ok(c.includes('in_flight'), `${status} should read in_flight`);
|
|
227
|
+
assert.ok(!c.includes('deps_unmet'), `${status} should not read deps_unmet`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
t('halts when ready but absent from the server claimable feed', () => {
|
|
232
|
+
assert.ok(haltReasons(task(), { rank: 'archon', claimableIds: new Set([42]) })
|
|
233
|
+
.map((r) => r.code).includes('not_claimable'));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
t('stays quiet about claimability when the feed could not be read', () => {
|
|
237
|
+
// The regression this guards: reading the claimable envelope under the wrong
|
|
238
|
+
// key yielded an empty set, which halted every task with not_claimable.
|
|
239
|
+
assert.deepEqual(haltReasons(task(), { rank: 'archon', claimableIds: null }), []);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
t('reports every applicable reason, not just the first', () => {
|
|
243
|
+
const c = codes(task({ needs_migration: true, security_sensitive: true, description: '' }));
|
|
244
|
+
assert.ok(c.includes('needs_migration') && c.includes('security_sensitive') && c.includes('thin_spec'));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
t('a missing task reports not_found rather than throwing', () => {
|
|
248
|
+
assert.deepEqual(haltReasons(null, clean).map((r) => r.code), ['not_found']);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------- totals
|
|
252
|
+
|
|
253
|
+
t('summarize totals count, credits and calibrated minutes', () => {
|
|
254
|
+
const s = summarize([
|
|
255
|
+
task({ id: 1, credits_reward: 30, est_minutes: 20 }),
|
|
256
|
+
task({ id: 2, credits_reward: 15, est_minutes_calibrated: 7, est_minutes: 99 }),
|
|
257
|
+
]);
|
|
258
|
+
assert.deepEqual(s, { count: 2, credits: 45, minutes: 27 });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
t('summarize tolerates missing numbers', () => {
|
|
262
|
+
assert.deepEqual(summarize([task({ credits_reward: null, est_minutes: null })]),
|
|
263
|
+
{ count: 1, credits: 0, minutes: 0 });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
console.log(`\nsequence: ${passed} passed, ${failed} failed`);
|
|
267
|
+
process.exit(failed ? 1 : 0);
|