@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,141 @@
|
|
|
1
|
+
// tests/doctor_hooks.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for doctor's git-hooks verdict (task 1002524). The pure core
|
|
4
|
+
// `classifyHooksState` decides whether an unset/dangling core.hooksPath is a
|
|
5
|
+
// FAIL the builder must fix or an honest WARN about absent instance topology.
|
|
6
|
+
//
|
|
7
|
+
// The incident this locks down: on a portable-core checkout `.husky/` does not
|
|
8
|
+
// exist (README "Secrets scanning" declares it instance topology), yet doctor
|
|
9
|
+
// hard-failed with "set it to .husky" and then, once set, failed again with
|
|
10
|
+
// "resolves to a non-existent path" — and repeated the SAME command. An
|
|
11
|
+
// unbreakable red loop for anyone who followed the advice.
|
|
12
|
+
//
|
|
13
|
+
// The rule: never recommend a hooks dir that is not there, and never report a
|
|
14
|
+
// missing ADR 0043 layer-1 gate as green. Absent .husky is a WARN that names
|
|
15
|
+
// the enforcing layers; a hooksPath pointing at nothing is a real FAIL.
|
|
16
|
+
//
|
|
17
|
+
// Importing doctor.js must NOT run its checks (which shell out to git and hit
|
|
18
|
+
// the network) — doctor.js guards its main IIFE behind `require.main === module`.
|
|
19
|
+
// This test proves that holds.
|
|
20
|
+
//
|
|
21
|
+
// Run: node tests/doctor_hooks.mjs
|
|
22
|
+
|
|
23
|
+
import { strict as assert } from 'node:assert';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const { classifyHooksState } = require('../scripts/gds/doctor.js');
|
|
29
|
+
|
|
30
|
+
let passed = 0;
|
|
31
|
+
let failed = 0;
|
|
32
|
+
function test(name, fn) {
|
|
33
|
+
try {
|
|
34
|
+
fn();
|
|
35
|
+
passed++;
|
|
36
|
+
console.log(` ok ${name}`);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
failed++;
|
|
39
|
+
console.log(` FAIL ${name}`);
|
|
40
|
+
console.log(` ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ROOT = path.join(path.sep, 'repo');
|
|
45
|
+
// exists() stub: give it the set of paths that are present. Built with
|
|
46
|
+
// path.join so the separators match what the implementation produces on both
|
|
47
|
+
// POSIX and Windows.
|
|
48
|
+
const has = (...paths) => {
|
|
49
|
+
const set = new Set(paths);
|
|
50
|
+
return (p) => set.has(p);
|
|
51
|
+
};
|
|
52
|
+
const HUSKY = path.join(ROOT, '.husky');
|
|
53
|
+
|
|
54
|
+
console.log('classifyHooksState');
|
|
55
|
+
|
|
56
|
+
test('hooksPath set and the dir exists → ok', () => {
|
|
57
|
+
const r = classifyHooksState({ hooksPath: '.husky', worktreeRoot: ROOT, exists: has(HUSKY) });
|
|
58
|
+
assert.equal(r.status, 'ok');
|
|
59
|
+
assert.equal(r.resolved, HUSKY);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('an absolute hooksPath that exists is honoured, not overridden', () => {
|
|
63
|
+
const r = classifyHooksState({
|
|
64
|
+
hooksPath: '/shared/hooks', worktreeRoot: ROOT, exists: has('/shared/hooks'),
|
|
65
|
+
});
|
|
66
|
+
assert.equal(r.status, 'ok');
|
|
67
|
+
assert.equal(r.resolved, '/shared/hooks');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// THE REGRESSION. Portable core: no .husky anywhere, hooksPath unset.
|
|
71
|
+
test('unset hooksPath + no .husky → WARN, not fail (portable core)', () => {
|
|
72
|
+
const r = classifyHooksState({ hooksPath: '', worktreeRoot: ROOT, exists: has() });
|
|
73
|
+
assert.equal(r.status, 'warn', 'a core checkout must not be a hard fail');
|
|
74
|
+
assert.equal(r.code, 'no-hooks-dir');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('the no-.husky warning never tells you to set hooksPath to .husky', () => {
|
|
78
|
+
const r = classifyHooksState({ hooksPath: '', worktreeRoot: ROOT, exists: has() });
|
|
79
|
+
assert.ok(
|
|
80
|
+
!/config\s+core\.hooksPath\s+\.husky/.test(r.message),
|
|
81
|
+
`must not recommend a dir that is not there — got: ${r.message}`
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('the no-.husky warning names the layers that DO enforce', () => {
|
|
86
|
+
const r = classifyHooksState({ hooksPath: '', worktreeRoot: ROOT, exists: has() });
|
|
87
|
+
assert.ok(/CI|audit/i.test(r.message), `must report honestly, got: ${r.message}`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Instance checkout: .husky is present but not switched on — a real, fixable gap.
|
|
91
|
+
test('unset hooksPath + .husky present → fail (a real inactive gate)', () => {
|
|
92
|
+
const r = classifyHooksState({ hooksPath: '', worktreeRoot: ROOT, exists: has(HUSKY) });
|
|
93
|
+
assert.equal(r.status, 'fail');
|
|
94
|
+
assert.equal(r.code, 'inactive');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('the inactive-gate fix points at the installer, which is itself guarded', () => {
|
|
98
|
+
const r = classifyHooksState({ hooksPath: '', worktreeRoot: ROOT, exists: has(HUSKY) });
|
|
99
|
+
assert.ok(/install-git-hooks/.test(r.message), `got: ${r.message}`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Dangling hooksPath — the second half of the old loop.
|
|
103
|
+
test('hooksPath set but resolving nowhere → fail', () => {
|
|
104
|
+
const r = classifyHooksState({ hooksPath: '.husky', worktreeRoot: ROOT, exists: has() });
|
|
105
|
+
assert.equal(r.status, 'fail');
|
|
106
|
+
assert.equal(r.code, 'dangling');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('a dangling hooksPath with no .husky advises UNSET, not the same failing command', () => {
|
|
110
|
+
const r = classifyHooksState({ hooksPath: '.husky', worktreeRoot: ROOT, exists: has() });
|
|
111
|
+
assert.ok(/--unset/.test(r.message), `must break the loop — got: ${r.message}`);
|
|
112
|
+
assert.ok(
|
|
113
|
+
!/config\s+core\.hooksPath\s+\.husky\s*$/.test(r.message.trim()),
|
|
114
|
+
'must not repeat the command that just failed'
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('a dangling hooksPath WITH .husky on disk advises the installer instead', () => {
|
|
119
|
+
// 'nope' resolves to ROOT/nope, which is absent — while .husky IS present.
|
|
120
|
+
const r = classifyHooksState({ hooksPath: 'nope', worktreeRoot: ROOT, exists: has(HUSKY) });
|
|
121
|
+
assert.equal(r.status, 'fail');
|
|
122
|
+
assert.equal(r.code, 'dangling');
|
|
123
|
+
assert.ok(/install-git-hooks/.test(r.message), `got: ${r.message}`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('every verdict carries a non-empty message', () => {
|
|
127
|
+
for (const c of [
|
|
128
|
+
{ hooksPath: '', exists: has() },
|
|
129
|
+
{ hooksPath: '', exists: has(HUSKY) },
|
|
130
|
+
{ hooksPath: '.husky', exists: has() },
|
|
131
|
+
{ hooksPath: '.husky', exists: has(HUSKY) },
|
|
132
|
+
]) {
|
|
133
|
+
const r = classifyHooksState({ ...c, worktreeRoot: ROOT });
|
|
134
|
+
assert.ok(r.message && r.message.length > 0, `empty message for ${JSON.stringify(c)}`);
|
|
135
|
+
assert.ok(['ok', 'warn', 'fail'].includes(r.status), `bad status ${r.status}`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log(`${passed} passed, ${failed} failed`);
|
|
141
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// tests/extract_json.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit test for grader-workers/extract-json.js (task 1001678) — the PURE, leaf
|
|
4
|
+
// JSON extractor every grader worker delegates to when pulling a verdict object
|
|
5
|
+
// out of raw LLM output. It is load-bearing for the quality gate: if extraction
|
|
6
|
+
// returns null the worker reports parse_error and the panel retries, which is
|
|
7
|
+
// the exact flake the balanced-brace scan was written to kill.
|
|
8
|
+
//
|
|
9
|
+
// Three strategies are tried in order (whole-string → ```json fence → balanced
|
|
10
|
+
// brace scan), so the cases below are grouped by which one they exercise, plus
|
|
11
|
+
// the two rules that decide between multiple candidates: objects only, and a
|
|
12
|
+
// real SCORE key wins over an incidental prose object.
|
|
13
|
+
//
|
|
14
|
+
// Pure: no DB, no network, no filesystem, no live server.
|
|
15
|
+
// Run: node tests/extract_json.mjs
|
|
16
|
+
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const { extractJson, GRADE_SHAPE_KEYS } = require('../modules/grading/grader-workers/extract-json.js');
|
|
22
|
+
|
|
23
|
+
let passed = 0;
|
|
24
|
+
let failed = 0;
|
|
25
|
+
function test(name, fn) {
|
|
26
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
27
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('non-string input is null, never a throw', () => {
|
|
31
|
+
// Workers hand this whatever the LLM layer produced — a timeout can make that
|
|
32
|
+
// undefined, so a non-string must degrade to null rather than crash the panel.
|
|
33
|
+
assert.equal(extractJson(42), null);
|
|
34
|
+
assert.equal(extractJson(null), null);
|
|
35
|
+
assert.equal(extractJson(undefined), null);
|
|
36
|
+
assert.equal(extractJson({ already: 'parsed' }), null);
|
|
37
|
+
assert.equal(extractJson(''), null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('strategy 1 — a clean JSON-only response parses whole', () => {
|
|
41
|
+
assert.deepEqual(extractJson('{"a":1}'), { a: 1 });
|
|
42
|
+
assert.deepEqual(extractJson('\n\n {"a":1} \n'), { a: 1 });
|
|
43
|
+
assert.deepEqual(extractJson('{"outer":{"inner":1}}'), { outer: { inner: 1 } });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('only OBJECTS count — a valid array or scalar is not a verdict', () => {
|
|
47
|
+
// isObj rejects arrays/scalars, so valid-but-wrong-shaped JSON must not be
|
|
48
|
+
// mistaken for a grade. '[1,2,3]' contains no '{' at all, so every strategy
|
|
49
|
+
// misses and the result is null.
|
|
50
|
+
assert.equal(extractJson('[1,2,3]'), null);
|
|
51
|
+
assert.equal(extractJson('"hello"'), null);
|
|
52
|
+
// ...but an object later in the same string is still found.
|
|
53
|
+
assert.deepEqual(extractJson('[1,2] then {"a":1}'), { a: 1 });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('strategy 2 — a fenced block wins over an earlier prose object', () => {
|
|
57
|
+
// The decoy matters: {"a":0} appears BEFORE the fence, so if the fenced branch
|
|
58
|
+
// were removed the brace scan would return the decoy instead. Without a decoy
|
|
59
|
+
// this case would pass either way and prove nothing about strategy 2.
|
|
60
|
+
assert.deepEqual(extractJson('Consider {"a":0} first.\n```json\n{"a":1}\n```'), { a: 1 });
|
|
61
|
+
assert.deepEqual(extractJson('Consider {"a":0} first.\n```\n{"a":1}\n```'), { a: 1 });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('strategy 3 — prose around the object still extracts', () => {
|
|
65
|
+
assert.deepEqual(extractJson('Here is my grade:\n{"a":1}\nThanks!'), { a: 1 });
|
|
66
|
+
// The #465 flake: trailing prose carrying a stray unbalanced brace. The naive
|
|
67
|
+
// first-'{'-to-last-'}' slice this replaced would have swallowed the tail and
|
|
68
|
+
// returned a spurious parse_error.
|
|
69
|
+
assert.deepEqual(extractJson('{"a":1}\nNote: the } char is fine'), { a: 1 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the brace scan is string- and escape-aware', () => {
|
|
73
|
+
// Both inputs are wrapped in prose ON PURPOSE. The bare object is valid JSON,
|
|
74
|
+
// so strategy 1 would return it whole and the scan under test would never run
|
|
75
|
+
// -- the assertion would then hold even with the scan's string handling gone.
|
|
76
|
+
// Prose forces strategy 3 to be the code path that produces the result.
|
|
77
|
+
//
|
|
78
|
+
// A '}' inside a string literal must not close the object early...
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
extractJson('Grade: {"note":"a } brace here","a":1} -- done'),
|
|
81
|
+
{ note: 'a } brace here', a: 1 },
|
|
82
|
+
);
|
|
83
|
+
// ...and an escaped quote must not be read as the end of that string.
|
|
84
|
+
assert.deepEqual(
|
|
85
|
+
extractJson('Grade: {"note":"q \\" then } brace","a":1} -- done'),
|
|
86
|
+
{ note: 'q " then } brace', a: 1 },
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('a "{" that only appears inside a string yields nothing', () => {
|
|
91
|
+
// Pins the real behaviour: no candidate, so null -- not a throw, not a partial
|
|
92
|
+
// parse. NB this does not prove the outer loop's string-skip is present: that
|
|
93
|
+
// skip avoids launching a doomed scan (a cost saving), and a scan launched
|
|
94
|
+
// from the mid-string brace finds no balanced object either. The observable
|
|
95
|
+
// result is null with or without it, so treat this as a null-safety pin.
|
|
96
|
+
assert.equal(extractJson('text "a { b" more'), null);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('a real SCORE key beats an incidental prose object', () => {
|
|
100
|
+
// The tie-break that stops chatty output from winning: an earlier
|
|
101
|
+
// {"verdict":...} in prose must lose to the object carrying a grade key.
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
extractJson('I think {"verdict":"pass"} and the grade is {"functional_fidelity":9}'),
|
|
104
|
+
{ functional_fidelity: 9 },
|
|
105
|
+
);
|
|
106
|
+
// Order-independent -- it prefers by shape, not by position.
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
extractJson('{"functional_fidelity":9} also {"verdict":"pass"}'),
|
|
109
|
+
{ functional_fidelity: 9 },
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('with no grade-shaped candidate, the FIRST balanced object wins', () => {
|
|
114
|
+
assert.deepEqual(extractJson('{"a":1} and {"b":2}'), { a: 1 });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('an invalid fence falls through to the brace scan', () => {
|
|
118
|
+
// Strategy 2 matches the fence but its body does not parse, so extraction
|
|
119
|
+
// must continue to strategy 3 rather than giving up at the fence.
|
|
120
|
+
assert.deepEqual(extractJson('```json\n{bad}\n```\n{"a":1}'), { a: 1 });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('nothing parseable is null', () => {
|
|
124
|
+
assert.equal(extractJson('no json here at all'), null);
|
|
125
|
+
assert.equal(extractJson('{"a":'), null);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('GRADE_SHAPE_KEYS is the exported score-key list', () => {
|
|
129
|
+
// Exported so workers and grader.js agree on what "grade-shaped" means; the
|
|
130
|
+
// preference test above depends on functional_fidelity being in this list.
|
|
131
|
+
assert.deepEqual(GRADE_SHAPE_KEYS, [
|
|
132
|
+
'functional_fidelity',
|
|
133
|
+
'code_quality',
|
|
134
|
+
'security_score',
|
|
135
|
+
'efficiency_score',
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log(`\nextract-json (task 1001678) tests: ${passed} passed, ${failed} failed`);
|
|
140
|
+
process.exit(failed > 0 ? 1 : 0);
|
package/tests/fitness.mjs
CHANGED
|
@@ -338,6 +338,68 @@ test('hall section registry check actually CATCHES a hardcoded <section class="s
|
|
|
338
338
|
assert.deepEqual(otherSection, [], 'a non-widget <section> must not be flagged');
|
|
339
339
|
});
|
|
340
340
|
|
|
341
|
+
// --- hall Home zone budgets (task 2177) ---------------------------------------
|
|
342
|
+
|
|
343
|
+
test('hall zone budgets: the live tree is within every declared budget', () => {
|
|
344
|
+
assert.deepEqual(fitness.checkHallZoneBudgets(), [], 'the live hall must be within budget');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('hall zone budgets: ZONE_BUDGETS is parsed from builders.js, not copied here', () => {
|
|
348
|
+
const budgets = fitness.parseZoneBudgets("const ZONE_BUDGETS = { 'home-stats': 1, 'home-band': 2, 'home-main': 3 };");
|
|
349
|
+
assert.deepEqual(budgets, { 'home-stats': 1, 'home-band': 2, 'home-main': 3 });
|
|
350
|
+
// no literal → null, so the check can report "budgets are unenforced" rather
|
|
351
|
+
// than silently passing on a builders.js that no longer declares them.
|
|
352
|
+
assert.equal(fitness.parseZoneBudgets('const SOMETHING_ELSE = {};'), null);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('hall zone budgets check actually CATCHES a breach (not a no-op)', () => {
|
|
356
|
+
const budgets = "const ZONE_BUDGETS = { 'home-stats': 1, 'home-main': 2 };";
|
|
357
|
+
// Stub fs: builders.js supplies the budgets, the widget files supply the counts.
|
|
358
|
+
const stub = (widgetFiles) => ({
|
|
359
|
+
readdirSync: () => Object.keys(widgetFiles),
|
|
360
|
+
readFileSync: (p) => (String(p).endsWith('builders.js')
|
|
361
|
+
? budgets
|
|
362
|
+
: widgetFiles[Object.keys(widgetFiles).find((f) => String(p).endsWith(f))] ?? ''),
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// within budget → clean
|
|
366
|
+
assert.deepEqual(
|
|
367
|
+
fitness.checkHallZoneBudgets(stub({
|
|
368
|
+
'a.js': "contributeHallWidget({ zone: 'home-main' })",
|
|
369
|
+
'b.js': "contributeHallWidget({ zone: 'home-main' })",
|
|
370
|
+
})),
|
|
371
|
+
[],
|
|
372
|
+
'2 home-main widgets against a budget of 2 must pass',
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// one over → flagged, naming the zone, the count and the budget
|
|
376
|
+
const over = fitness.checkHallZoneBudgets(stub({
|
|
377
|
+
'a.js': "contributeHallWidget({ zone: 'home-main' })",
|
|
378
|
+
'b.js': "contributeHallWidget({ zone: 'home-main' })",
|
|
379
|
+
'c.js': "contributeHallWidget({ zone: 'home-main', summary: true })",
|
|
380
|
+
}));
|
|
381
|
+
assert.equal(over.length, 1, 'a 3rd home-main widget must be caught');
|
|
382
|
+
assert.match(over[0], /home-main.*3 registered.*budget is 2/s);
|
|
383
|
+
|
|
384
|
+
// a file that never calls contributeHallWidget is not counted (a `zone:` key
|
|
385
|
+
// in some unrelated object must not consume budget).
|
|
386
|
+
assert.deepEqual(
|
|
387
|
+
fitness.checkHallZoneBudgets(stub({
|
|
388
|
+
'a.js': "contributeHallWidget({ zone: 'home-main' })",
|
|
389
|
+
'unrelated.js': "const cfg = { zone: 'home-main' };",
|
|
390
|
+
})),
|
|
391
|
+
[],
|
|
392
|
+
'only real registrations count against the budget',
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// builders.js without a parseable ZONE_BUDGETS → reported, never silently green
|
|
396
|
+
const unenforced = fitness.checkHallZoneBudgets({
|
|
397
|
+
readdirSync: () => [], readFileSync: () => 'const OTHER = 1;',
|
|
398
|
+
});
|
|
399
|
+
assert.equal(unenforced.length, 1);
|
|
400
|
+
assert.match(unenforced[0], /no longer declares a parseable ZONE_BUDGETS/);
|
|
401
|
+
});
|
|
402
|
+
|
|
341
403
|
// --- the demo proof module: BV1.R52 / task 1424 (ADR 0083) ----------------------
|
|
342
404
|
// The net-new `modules/demo/` module is the live acceptance proof for the module
|
|
343
405
|
// system. These tests lock in BOTH directions: the live demo module is clean, AND
|
|
@@ -433,3 +495,30 @@ test('write-route validation exemptions are the known dynamic-key prefs routes o
|
|
|
433
495
|
const exempt = [...fitness.WRITE_VALIDATION_EXEMPT];
|
|
434
496
|
assert.ok(exempt.length >= 1 && exempt.every((e) => e.startsWith('PATCH /me/')), 'exemptions are all /me/* prefs patches: ' + exempt.join(', '));
|
|
435
497
|
});
|
|
498
|
+
|
|
499
|
+
test('identity gate strips comments on CRLF checkouts too (task 1002428)', () => {
|
|
500
|
+
// Regression: stripCommentsForIdentity split on '\n', leaving a trailing '\r'
|
|
501
|
+
// on every line of a CRLF checkout. `.` does not match '\r', so the `.*$` in
|
|
502
|
+
// the line-comment pattern could never reach end-of-string and NO line comment
|
|
503
|
+
// was stripped — every rationale comment naming the old brand was scanned as
|
|
504
|
+
// if it were code. The gate's whole "comments never trip the scan" contract
|
|
505
|
+
// silently failed on Windows, and disagreed with Linux CI on the same tree.
|
|
506
|
+
// The pre-existing cases above are all LF-only, which is why this hid.
|
|
507
|
+
const { scanPromptIdentity } = fitness;
|
|
508
|
+
assert.equal(
|
|
509
|
+
scanPromptIdentity('x.js', '// see amazonprimea.com for context\r\nconst ok = 1;\r\n').length,
|
|
510
|
+
0,
|
|
511
|
+
'a CRLF full-line comment must be skipped'
|
|
512
|
+
);
|
|
513
|
+
assert.equal(
|
|
514
|
+
scanPromptIdentity('x.js', 'doThing(); // amazonprimea note\r\n').length,
|
|
515
|
+
0,
|
|
516
|
+
'a CRLF trailing comment must be skipped'
|
|
517
|
+
);
|
|
518
|
+
// ...and the strip must still not hide REAL code on CRLF (the gate keeps biting).
|
|
519
|
+
assert.equal(
|
|
520
|
+
scanPromptIdentity('x.js', 'const u = "https://amazonprimea.com/x";\r\n').length,
|
|
521
|
+
1,
|
|
522
|
+
'a CRLF code literal must still be caught'
|
|
523
|
+
);
|
|
524
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// tests/gen_freshness.mjs — the doc generators' freshness comparison (task 1002445).
|
|
2
|
+
//
|
|
3
|
+
// Regression guard for a bug that made a HARD CI gate permanently red on Windows:
|
|
4
|
+
// the generators render blocks with join('\n') (LF) but read the working copy with
|
|
5
|
+
// readFileSync, which on a core.autocrlf=true checkout returns CRLF. The old
|
|
6
|
+
// byte-wise `next !== cur` therefore reported STALE forever, and regenerating never
|
|
7
|
+
// cleared it. A gate that cannot pass trains builders to ignore it AND hides real
|
|
8
|
+
// drift behind the permanent failure.
|
|
9
|
+
//
|
|
10
|
+
// Two things must hold, and the second is what stops an over-broad "fix":
|
|
11
|
+
// 1. line endings alone must NOT read as drift,
|
|
12
|
+
// 2. a genuine content change must STILL read as drift.
|
|
13
|
+
//
|
|
14
|
+
// Pure file-read + require (no DB/network) — runs in the DB-free unit gate.
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { test } from 'node:test';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
+
const { sameContent, normalizeEol } = require(path.join(ROOT, 'scripts', 'gds', 'gen-freshness.js'));
|
|
25
|
+
|
|
26
|
+
const LF = '# Title\n\n- one\n- two\n';
|
|
27
|
+
const CRLF = '# Title\r\n\r\n- one\r\n- two\r\n';
|
|
28
|
+
|
|
29
|
+
test('a CRLF working copy is not drift against an LF-rendered block', () => {
|
|
30
|
+
assert.equal(sameContent(CRLF, LF), true, 'this is the bug: LF vs CRLF must not read as STALE');
|
|
31
|
+
assert.equal(sameContent(LF, CRLF), true, 'and it is symmetric');
|
|
32
|
+
assert.equal(sameContent(CRLF, CRLF), true);
|
|
33
|
+
assert.equal(sameContent(LF, LF), true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('mixed line endings (an LF block spliced into a CRLF doc) are not drift', () => {
|
|
37
|
+
// Exactly what injectBlock/injectSection/injectSnippet produce on Windows.
|
|
38
|
+
const mixed = '# Title\r\n\r\n- one\n- two\r\n';
|
|
39
|
+
assert.equal(sameContent(mixed, LF), true);
|
|
40
|
+
assert.equal(sameContent(mixed, CRLF), true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a REAL content change is still drift (the fix does not blind the gate)', () => {
|
|
44
|
+
assert.equal(sameContent(LF, '# Title\n\n- one\n- three\n'), false, 'changed list item');
|
|
45
|
+
assert.equal(sameContent(CRLF, '# Title\r\n\r\n- one\r\n- two\r\n- three\r\n'), false, 'added line');
|
|
46
|
+
assert.equal(sameContent(LF, ''), false, 'emptied file');
|
|
47
|
+
assert.equal(sameContent(LF, '# Title\n\n- one\n- two'), false, 'dropped trailing newline');
|
|
48
|
+
assert.equal(sameContent(LF, '# Title\n\n- one\n\n- two\n'), false, 'added blank line');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('whitespace that is not a line ending is still content', () => {
|
|
52
|
+
assert.equal(sameContent('a\tb', 'a b'), false, 'tab vs space is a real difference');
|
|
53
|
+
assert.equal(sameContent('a \n', 'a\n'), false, 'trailing space is a real difference');
|
|
54
|
+
assert.equal(sameContent('\r', '\n'), false, 'a lone CR is not normalized — no generator emits it');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('a missing file (null/undefined) reads as drift against real content', () => {
|
|
58
|
+
assert.equal(sameContent(null, LF), false, 'absent → must regenerate');
|
|
59
|
+
assert.equal(sameContent(undefined, LF), false);
|
|
60
|
+
assert.equal(normalizeEol(null), '', 'null normalizes to empty, not the string "null"');
|
|
61
|
+
assert.equal(normalizeEol(undefined), '');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// The guard that keeps the fix from rotting: every generator behind the
|
|
65
|
+
// `generated artifacts fresh` gate must route its freshness comparison through
|
|
66
|
+
// sameContent(). A reintroduced `cur !== next` on file bytes is the original bug.
|
|
67
|
+
const GENERATORS = ['gen-repo-map.js', 'gen-session-index.js', 'gen-file-map.js', 'gen-api-docs.js'];
|
|
68
|
+
|
|
69
|
+
test('every gated generator compares freshness through sameContent()', () => {
|
|
70
|
+
for (const g of GENERATORS) {
|
|
71
|
+
const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', g), 'utf8');
|
|
72
|
+
assert.match(src, /require\('\.\/gen-freshness'\)/, `${g} must import the shared comparison`);
|
|
73
|
+
assert.match(src, /sameContent\(/, `${g} must use sameContent() for freshness`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('no gated generator byte-compares a file read against rendered content', () => {
|
|
78
|
+
// Catches the exact shape of the original bug: `cur !== next` / `next !== cur`
|
|
79
|
+
// (and the idx*/map* variants) directly on strings read from disk.
|
|
80
|
+
const BYTEWISE = /\b(cur|next|idxCur|idxNext|mapCur|mapNext)\s*!==\s*(cur|next|idxCur|idxNext|mapCur|mapNext)\b/;
|
|
81
|
+
for (const g of GENERATORS) {
|
|
82
|
+
const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', g), 'utf8');
|
|
83
|
+
const offenders = src.split('\n')
|
|
84
|
+
.map((line, i) => ({ line: line.trim(), n: i + 1 }))
|
|
85
|
+
.filter(({ line }) => BYTEWISE.test(line));
|
|
86
|
+
assert.deepEqual(offenders, [], `${g} byte-compares generated content — use sameContent()`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('the byte-compare guard actually CATCHES the original bug shape', () => {
|
|
91
|
+
const BYTEWISE = /\b(cur|next|idxCur|idxNext|mapCur|mapNext)\s*!==\s*(cur|next|idxCur|idxNext|mapCur|mapNext)\b/;
|
|
92
|
+
assert.equal(BYTEWISE.test('if (next !== cur) writes.push({});'), true, 'the gen-file-map shape');
|
|
93
|
+
assert.equal(BYTEWISE.test('if (idxCur !== idxNext) writes.push({});'), true, 'the gen-session-index shape');
|
|
94
|
+
assert.equal(BYTEWISE.test('if (mapCur !== mapNext) writes.push({});'), true, 'the gen-repo-map shape');
|
|
95
|
+
assert.equal(BYTEWISE.test('if (!sameContent(next, cur)) writes.push({});'), false, 'the fixed shape passes');
|
|
96
|
+
});
|