@bongos/core 1.17.0
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 +5727 -0
- package/.claude/skills/blocker-review/SKILL.md +91 -0
- package/.claude/skills/blocker-solve/SKILL.md +96 -0
- package/.claude/skills/builder-backup/SKILL.md +54 -0
- package/.claude/skills/builder-box/SKILL.md +45 -0
- package/.claude/skills/builder-claim/SKILL.md +76 -0
- package/.claude/skills/builder-connect/SKILL.md +59 -0
- package/.claude/skills/builder-cost/SKILL.md +47 -0
- package/.claude/skills/builder-end/SKILL.md +81 -0
- package/.claude/skills/builder-exit/SKILL.md +88 -0
- package/.claude/skills/builder-key/SKILL.md +40 -0
- package/.claude/skills/builder-reauth/SKILL.md +50 -0
- package/.claude/skills/builder-redteam/SKILL.md +109 -0
- package/.claude/skills/builder-release/SKILL.md +41 -0
- package/.claude/skills/builder-setup/SKILL.md +62 -0
- package/.claude/skills/builder-ship/SKILL.md +79 -0
- package/.claude/skills/builder-stage/SKILL.md +51 -0
- package/.claude/skills/builder-start/SKILL.md +46 -0
- package/.claude/skills/close-box/SKILL.md +43 -0
- package/.claude/skills/design-sync/SKILL.md +69 -0
- package/.claude/skills/dev/SKILL.md +49 -0
- package/.claude/skills/feedback/SKILL.md +32 -0
- package/.claude/skills/figma-design-sync/SKILL.md +74 -0
- package/.claude/skills/goal-review/SKILL.md +89 -0
- package/.claude/skills/idea-triage/SKILL.md +144 -0
- package/.claude/skills/ideate/SKILL.md +82 -0
- package/.claude/skills/merge-mode/SKILL.md +169 -0
- package/.claude/skills/new-project/SKILL.md +108 -0
- package/.claude/skills/otb-character-review/SKILL.md +71 -0
- package/.claude/skills/otb-design-review/SKILL.md +47 -0
- package/.claude/skills/otb-feedback-capture/SKILL.md +45 -0
- package/.claude/skills/otb-figma-sync/SKILL.md +54 -0
- package/.claude/skills/otb-tile-generate/SKILL.md +45 -0
- package/.claude/skills/paint/SKILL.md +70 -0
- package/.claude/skills/planning-session/SKILL.md +299 -0
- package/.claude/skills/priority-session/SKILL.md +134 -0
- package/.claude/skills/read-session-export/SKILL.md +58 -0
- package/.claude/skills/recall/SKILL.md +52 -0
- package/.claude/skills/session-handoff/SKILL.md +46 -0
- package/.claude/skills/status/SKILL.md +33 -0
- package/.devcontainer/Dockerfile +69 -0
- package/.devcontainer/README.md +73 -0
- package/.devcontainer/devcontainer-lock.json +19 -0
- package/.devcontainer/devcontainer.json +93 -0
- package/.devcontainer/healthcheck.sh +84 -0
- package/.devcontainer/init-firewall.sh +198 -0
- package/.devcontainer/post-create.sh +153 -0
- package/.dockerignore +36 -0
- package/.env.local.example +32 -0
- package/.gitattributes +83 -0
- package/.gitignore +91 -0
- package/CLAUDE.md +242 -0
- package/CODE_OF_CONDUCT.md +46 -0
- package/CONTRIBUTING.md +51 -0
- package/Dockerfile +46 -0
- package/GOVERNANCE.md +44 -0
- package/LICENSE +661 -0
- package/README.md +88 -0
- package/bin/bongos.js +222 -0
- package/clients/bongos-client/README.md +48 -0
- package/clients/bongos-client/bongos-client.global.js +617 -0
- package/clients/bongos-client/examples/hello-world.mjs +62 -0
- package/clients/bongos-client/index.cjs +619 -0
- package/clients/bongos-client/index.d.ts +793 -0
- package/clients/bongos-client/index.mjs +614 -0
- package/clients/bongos-client/package.json +32 -0
- package/config/branding.neutral.json +60 -0
- package/config/design-tokens.neutral.json +51 -0
- package/config/design-tokens.schema.json +96 -0
- package/config/modules.neutral.json +9 -0
- package/docker-compose.yml +52 -0
- package/docker-entrypoint.sh +30 -0
- package/docs/adr/0001-stack-choice-single-vps.md +45 -0
- package/docs/adr/0002-digitalocean-over-hetzner.md +27 -0
- package/docs/adr/0003-cloudflare-origin-cert-over-letsencrypt.md +35 -0
- package/docs/adr/0004-amazonprimea-name-and-trademark-acceptance.md +45 -0
- package/docs/adr/0005-invisible-other-players-in-v1.md +37 -0
- package/docs/adr/0006-singleton-worldroom-plus-queueroom.md +30 -0
- package/docs/adr/0007-persistent-identity-via-localstorage-uuid.md +31 -0
- package/docs/adr/0008-family-based-tile-generation.md +86 -0
- package/docs/adr/0008-google-chat-oauth-user-auth.md +37 -0
- package/docs/adr/0009-autotile-architecture.md +181 -0
- package/docs/adr/0010-pms-architecture.md +70 -0
- package/docs/adr/0011-terrain-id-rendering-model.md +174 -0
- package/docs/adr/0012-ground-object-cell-split.md +208 -0
- package/docs/adr/0013-paths-as-object-overlays-with-autotile-dispatch.md +158 -0
- package/docs/adr/0014-families-canonical-over-vocabulary.md +103 -0
- package/docs/adr/0015-task-dependencies-and-auto-promotion.md +125 -0
- package/docs/adr/0016-trust-boundary-server-enforced-permissions.md +132 -0
- package/docs/adr/0017-code-simplification-and-repo-cleanliness.md +74 -0
- package/docs/adr/0018-three-rank-model-goes-live.md +97 -0
- package/docs/adr/0019-red-team-bounty-payout-table.md +52 -0
- package/docs/adr/0020-security-model-public-stub.md +70 -0
- package/docs/adr/0021-per-builder-skill-model-preferences.md +99 -0
- package/docs/adr/0022-secrets-policy.md +124 -0
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +118 -0
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +277 -0
- package/docs/adr/0024-multi-agent-system-architecture.md +125 -0
- package/docs/adr/0025-offsite-backup-vendor-digitalocean-spaces.md +59 -0
- package/docs/adr/0025-structured-criterion-task-link.md +44 -0
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +117 -0
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +170 -0
- package/docs/adr/0028-honest-llm-spend-accounting.md +109 -0
- package/docs/adr/0029-status-page-outage-resilience.md +59 -0
- package/docs/adr/0030-builders-hall-live-updates.md +59 -0
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +222 -0
- package/docs/adr/0032-discord-mirror-of-gds.md +50 -0
- package/docs/adr/0033-discord-bot-service-principal.md +82 -0
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +53 -0
- package/docs/adr/0035-builder-onboarding-three-paths.md +128 -0
- package/docs/adr/0036-builders-subdomain-and-ui-reorg.md +40 -0
- package/docs/adr/0037-discord-channel-management.md +51 -0
- package/docs/adr/0038-chromebook-ttyd-cloudflare-tunnel.md +111 -0
- package/docs/adr/0039-setup-first-onboarding-ux.md +49 -0
- package/docs/adr/0040-remote-control-default-browser-onramp.md +47 -0
- package/docs/adr/0041-temporary-grader-bypass-killswitch.md +40 -0
- package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +82 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +74 -0
- package/docs/adr/0044-mediterranean-palette-replacement.md +73 -0
- package/docs/adr/0044-per-box-live-game-preview.md +57 -0
- package/docs/adr/0045-devbox-desktop-app.md +133 -0
- package/docs/adr/0046-sandbox-first-review-gate.md +72 -0
- package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +36 -0
- package/docs/adr/0048-works-in-progress-ia-split.md +32 -0
- package/docs/adr/0049-split-parallel-safety-contract.md +46 -0
- package/docs/adr/0050-device-flow-admission-invite-gated-by-default.md +43 -0
- package/docs/adr/0051-full-session-transcript-corpus.md +57 -0
- package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +70 -0
- package/docs/adr/0053-scoped-dev-box-session.md +55 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +63 -0
- package/docs/adr/0055-server-mediated-branch-publish.md +47 -0
- package/docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md +62 -0
- package/docs/adr/0057-container-cost-ledger.md +28 -0
- package/docs/adr/0057-discord-archon-approval-channels.md +64 -0
- package/docs/adr/0058-automatic-gate-surface-gatekeeper.md +64 -0
- package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +37 -0
- package/docs/adr/0060-gds-retrieval-layer.md +97 -0
- package/docs/adr/0061-context-layer-decomposition.md +61 -0
- package/docs/adr/0062-medusa-instance-model-decoupling.md +107 -0
- package/docs/adr/0063-generated-repo-map-symbol-skeleton.md +44 -0
- package/docs/adr/0064-rename-medusa-to-cloud-bongos.md +49 -0
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +69 -0
- package/docs/adr/0066-generated-file-map-sections.md +51 -0
- package/docs/adr/0067-session-start-fast-forward-pull.md +43 -0
- package/docs/adr/0068-shared-art-key-for-newcomers.md +38 -0
- package/docs/adr/0069-hall-live-channel-sse-decouple.md +35 -0
- package/docs/adr/0070-deterministic-session-card-delivery.md +44 -0
- package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +59 -0
- package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +35 -0
- package/docs/adr/0072-dev-box-code-staleness-visibility.md +39 -0
- package/docs/adr/0073-builder-needs-signal-and-byok-gemini-key.md +64 -0
- package/docs/adr/0073-secrets-scan-exclude-uri-detector.md +37 -0
- package/docs/adr/0074-memory-map-stays-light.md +32 -0
- package/docs/adr/0075-per-system-cost-attribution.md +37 -0
- package/docs/adr/0076-in-session-deterministic-compaction.md +58 -0
- package/docs/adr/0077-content-addressed-llm-cache.md +32 -0
- package/docs/adr/0078-deterministic-first-cron.md +48 -0
- package/docs/adr/0079-value-gated-autonomous-engagement.md +39 -0
- package/docs/adr/0080-llm-cache-api-transport-and-trust-model.md +46 -0
- package/docs/adr/0081-tool-agnostic-design-layer.md +62 -0
- package/docs/adr/0082-productized-key-storage-provisioning.md +32 -0
- package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +55 -0
- package/docs/adr/0083-modular-architecture-strangler-build-plan.md +143 -0
- package/docs/adr/0084-auto-derived-per-task-rank-floor.md +86 -0
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +39 -0
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +123 -0
- package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +49 -0
- package/docs/adr/0087-compete-on-governance-not-tooling-cursor.md +65 -0
- package/docs/adr/0088-consistent-character-generative-animation.md +169 -0
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +179 -0
- package/docs/adr/0090-metic-task-authoring.md +73 -0
- package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +106 -0
- package/docs/adr/0092-hosting-module-and-on-prem-target.md +117 -0
- package/docs/adr/0093-tranche-2-core-carve-sequence.md +72 -0
- package/docs/adr/0094-repo-atlas-self-drawing-codebase-map.md +49 -0
- package/docs/adr/0095-borrowed-memory-and-retrieval-concepts.md +65 -0
- package/docs/adr/0095-cross-agent-context-management.md +58 -0
- package/docs/adr/0096-require-reward-before-workable.md +209 -0
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +46 -0
- package/docs/adr/0097-retroactive-reward-backfill.md +124 -0
- package/docs/adr/0098-public-mirror-publish-manifest.md +92 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +55 -0
- package/docs/adr/0100-two-repo-instance-model.md +60 -0
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +90 -0
- package/docs/adr/0102-finish-bounding-the-kernel.md +36 -0
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +70 -0
- package/docs/adr/0103-gdsv4-bongos-consolidation.md +28 -0
- package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +42 -0
- package/docs/adr/0105-instance-seed-migrations-out-of-core.md +47 -0
- package/docs/adr/0106-goal-owner-manager-authority.md +84 -0
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +83 -0
- package/docs/adr/0108-instance-composition-configurable-root.md +87 -0
- package/docs/adr/0109-self-describing-openapi-and-hosted-docs.md +44 -0
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +49 -0
- package/docs/adr/0111-instance-hosting-provisioning-module.md +135 -0
- package/docs/adr/0112-private-goals-invitations-join-requests.md +72 -0
- package/docs/adr/0113-sandbox-studio-descope-superseded-by-0044-0052.md +41 -0
- package/docs/adr/0114-instance-docs-discovery-contract.md +32 -0
- package/docs/adr/0115-scheduled-routines-portability.md +31 -0
- package/docs/adr/0116-api-error-envelope-and-code-taxonomy.md +62 -0
- package/docs/adr/0117-api-versioning-and-deprecation-policy.md +50 -0
- package/docs/adr/0118-api-contract-source-of-truth-code-first.md +47 -0
- package/docs/adr/0119-api-pagination-contract.md +34 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +108 -0
- package/docs/adr/0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md +97 -0
- package/docs/adr/0122-onboarding-diagram-renders-deploy-rendered-off-git.md +43 -0
- package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +36 -0
- package/docs/adr/0124-typed-api-response-bodies.md +37 -0
- package/docs/adr/0125-control-plane-scaffolds-standalone-instance-repo.md +36 -0
- package/docs/adr/0126-dedicated-cloudbongos-control-plane-droplet.md +30 -0
- package/docs/adr/0127-cross-box-fleet-observability.md +64 -0
- package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +46 -0
- package/docs/adr/0129-cloudbongos-control-plane-pull-deploy.md +34 -0
- package/docs/adr/0130-provision-remote-exec-over-ssh.md +61 -0
- package/docs/adr/0131-rank-scoped-skill-visibility.md +47 -0
- package/docs/adr/0132-cohost-fleet-control-plane-migration.md +54 -0
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +42 -0
- package/docs/adr/0134-private-first-npm-distribution.md +38 -0
- package/docs/adr/README.md +208 -0
- package/docs/api/openapi.json +13720 -0
- package/docs/api-reference.md +437 -0
- package/docs/architecture.md +244 -0
- package/docs/branding-contract.md +66 -0
- package/docs/branding-fill-prompt.md +25 -0
- package/docs/canonical-permissions.md +241 -0
- package/docs/design/container-exterior-tiles.md +49 -0
- package/docs/design/container-exterior.md +81 -0
- package/docs/design/container-interior.md +77 -0
- package/docs/design/modular-architecture/00-research-report.md +107 -0
- package/docs/design/modular-architecture/01-architecture.md +125 -0
- package/docs/design/modular-architecture/02-module-map.md +116 -0
- package/docs/design/modular-architecture/03-builder-flows.md +104 -0
- package/docs/design/modular-architecture/04-module-lifecycle.md +62 -0
- package/docs/design/modular-architecture/README.md +27 -0
- package/docs/design/vanilla-hall-ui-redesign-scope.md +284 -0
- package/docs/design/world-map-scroll.md +41 -0
- package/docs/design-contract.md +320 -0
- package/docs/file-map.md +418 -0
- package/docs/handoff-template.md +92 -0
- package/docs/modules-contract.md +293 -0
- package/docs/onboarding/browser-terminal-guide.md +73 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +28 -0
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +54 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +37 -0
- package/docs/onboarding/diagrams/04-architecture.mmd +51 -0
- package/docs/onboarding/diagrams/05-versioning.mmd +29 -0
- package/docs/onboarding/diagrams/README.md +94 -0
- package/docs/onboarding/diagrams/assertions.json +170 -0
- package/docs/onboarding/diagrams/puppeteer-config.json +1 -0
- package/docs/onboarding/diagrams/render-config.json +9 -0
- package/docs/onboarding/drachmae-and-karma.md +178 -0
- package/docs/onboarding/primer.md +202 -0
- package/docs/project-context.template.md +67 -0
- package/docs/recipes/README.md +35 -0
- package/docs/recipes/audit-replay.md +100 -0
- package/docs/recipes/bongos-cli-release.md +66 -0
- package/docs/recipes/builder-box-lifecycle.md +152 -0
- package/docs/recipes/claude-design-loop.md +143 -0
- package/docs/recipes/claude-gemini-bongos-doors.md +155 -0
- package/docs/recipes/cloudbongos-staging-gate.md +139 -0
- package/docs/recipes/cloudbongos-standup.md +111 -0
- package/docs/recipes/devbox-app-release.md +93 -0
- package/docs/recipes/devbox-app-windows-test.md +128 -0
- package/docs/recipes/failed-ship-recovery-proof.md +102 -0
- package/docs/recipes/figma-design-loop.md +129 -0
- package/docs/recipes/gds-db-backup.md +255 -0
- package/docs/recipes/instance-deploy.md +37 -0
- package/docs/recipes/managed-settings-remote-control.md +416 -0
- package/docs/recipes/memory-backup.md +71 -0
- package/docs/recipes/memory-sync-conflicts.md +98 -0
- package/docs/recipes/multi-builder-merge.md +78 -0
- package/docs/recipes/ops-gotchas.md +212 -0
- package/docs/recipes/overnight-builder.md +53 -0
- package/docs/recipes/packaging-the-core.md +49 -0
- package/docs/recipes/parallel-load-results-2026-05-30.md +63 -0
- package/docs/recipes/parallel-load-test.md +32 -0
- package/docs/recipes/pixel-art-pipeline.md +411 -0
- package/docs/recipes/private-npm-distribution.md +79 -0
- package/docs/recipes/search-index-ingestion.md +67 -0
- package/docs/recipes/self-host.md +60 -0
- package/docs/recipes/ship-and-regrade.md +72 -0
- package/docs/recipes/staging-data.md +50 -0
- package/docs/recipes/staging-environment.md +215 -0
- package/docs/recipes/upgrading-the-core.md +58 -0
- package/docs/research/cursor-competitive-analysis.md +255 -0
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +227 -0
- package/migrations/003_pms.sql +284 -0
- package/migrations/004_pms_cost_idempotency.sql +13 -0
- package/migrations/005_pms_smoke_filter.sql +44 -0
- package/migrations/006_pms_v2.sql +378 -0
- package/migrations/007_pms_v2_hierarchy_trigger.sql +59 -0
- package/migrations/008_pms_v2_migration_allocator.sql +27 -0
- package/migrations/009_pms_v2_blockers_table.sql +68 -0
- package/migrations/010_uptime.sql +29 -0
- package/migrations/011_backfill_v2_v3_done_when.sql +20 -0
- package/migrations/012_pms_v2_task_lifecycle_states.sql +82 -0
- package/migrations/013_claim_locking.sql +22 -0
- package/migrations/014_pms_task_discipline.sql +86 -0
- package/migrations/015_pms_estimation_calibration.sql +140 -0
- package/migrations/016_pms_task_grades.sql +60 -0
- package/migrations/017_version_progress_exclude_abandoned.sql +49 -0
- package/migrations/019_rename_pms_v3_to_gds_v3.sql +65 -0
- package/migrations/020_task_dependencies.sql +99 -0
- package/migrations/021_v3_renumber_and_deps.sql +224 -0
- package/migrations/022_v3_reconcile_dep_gated_status.sql +41 -0
- package/migrations/023_pms_rank.sql +30 -0
- package/migrations/025_builder_exit_status.sql +15 -0
- package/migrations/027_builders_preferred_disciplines.sql +46 -0
- package/migrations/028_tasks_parallel_safe.sql +71 -0
- package/migrations/029_rank_three_live.sql +32 -0
- package/migrations/030_vulnerability_reports.sql +61 -0
- package/migrations/031_reward_simplification.sql +57 -0
- package/migrations/032_repo_health_snapshots.sql +56 -0
- package/migrations/033_audit_log.sql +64 -0
- package/migrations/034_lifecycle_breakdown_leaf_only.sql +62 -0
- package/migrations/035_auth_token_hygiene.sql +66 -0
- package/migrations/037_builder_skill_model_prefs.sql +44 -0
- package/migrations/038_task_override_requests.sql +51 -0
- package/migrations/039_newcomer_friendly_column.sql +124 -0
- package/migrations/040_builder_karma.sql +58 -0
- package/migrations/041_kind_multipliers_and_idea_bonus.sql +42 -0
- package/migrations/042_criterion_proposal_workflow.sql +78 -0
- package/migrations/043_builder_onboarding_observability.sql +66 -0
- package/migrations/044_tasks_requires_rank.sql +97 -0
- package/migrations/045_builder_onboarding_state.sql +147 -0
- package/migrations/046_cost_log_builder_attribution.sql +42 -0
- package/migrations/054_builder_memory.sql +108 -0
- package/migrations/055_task_criteria.sql +90 -0
- package/migrations/058_peer_votes.sql +41 -0
- package/migrations/059_onboarding_pick_disciplines.sql +59 -0
- package/migrations/060_access_requests.sql +46 -0
- package/migrations/061_backfill_override_ship_attribution.sql +29 -0
- package/migrations/062_session_records.sql +34 -0
- package/migrations/063_session_records_tool_errors_idx.sql +11 -0
- package/migrations/064_grade_attempts.sql +18 -0
- package/migrations/065_bfg_principal.sql +58 -0
- package/migrations/066_live_notify_triggers.sql +69 -0
- package/migrations/067_builder_boxes.sql +117 -0
- package/migrations/067_discord_bot_foundation.sql +69 -0
- package/migrations/068_thetes_requires_rank.sql +27 -0
- package/migrations/069_builder_ssh_keys.sql +66 -0
- package/migrations/070_box_intents.sql +101 -0
- package/migrations/071_box_first_provision_approval.sql +46 -0
- package/migrations/072_onboarding_ship_three_remove_primer.sql +80 -0
- package/migrations/073_box_terminal_access.sql +46 -0
- package/migrations/075_builder_sound_prefs.sql +28 -0
- package/migrations/078_box_claude_active.sql +22 -0
- package/migrations/079_box_first_connected_at.sql +20 -0
- package/migrations/081_tasks_needs_migration.sql +34 -0
- package/migrations/084_tasks_security_sensitive.sql +24 -0
- package/migrations/085_security_engagement_docs.sql +34 -0
- package/migrations/086_rename_disciplines.sql +125 -0
- package/migrations/088_builders_anthropic_link.sql +28 -0
- package/migrations/089_builders_monthly_budget.sql +19 -0
- package/migrations/090_box_scoped_sessions.sql +38 -0
- package/migrations/091_session_token_reward.sql +44 -0
- package/migrations/093_tasks_sandbox_review.sql +22 -0
- package/migrations/102_remove_box_first_provision_approval.sql +58 -0
- package/migrations/103_search_chunks.sql +90 -0
- package/migrations/104_builder_box_blocked.sql +34 -0
- package/migrations/105_box_host_keys.sql +35 -0
- package/migrations/111_medusa_customer0_framing.sql +35 -0
- package/migrations/112_drop_track_check.sql +25 -0
- package/migrations/113_merge_lease.sql +16 -0
- package/migrations/114_cloudbongos_rename_live_strings.sql +32 -0
- package/migrations/118_currency_label_decoupling.sql +23 -0
- package/migrations/119_builder_behavior_prefs.sql +31 -0
- package/migrations/121_box_code_version.sql +37 -0
- package/migrations/122_builder_gemini_key.sql +30 -0
- package/migrations/123_cost_log_tiers.sql +33 -0
- package/migrations/124_cost_baseline.sql +45 -0
- package/migrations/125_llm_cache.sql +54 -0
- package/migrations/126_autonomy_runs.sql +33 -0
- package/migrations/143_cost_calibration.sql +63 -0
- package/migrations/145_backfill_task_requires_rank.sql +84 -0
- package/migrations/160_goal_tier.sql +88 -0
- package/migrations/163_polymorphic_dependencies.sql +187 -0
- package/migrations/164_event_sound_prefs.sql +20 -0
- package/migrations/165_task_module_key.sql +15 -0
- package/migrations/171_claims_published_head_sha.sql +24 -0
- package/migrations/174_close_orphaned_claims_on_terminal_tasks.sql +30 -0
- package/migrations/175_task_grades_diff_hash.sql +9 -0
- package/migrations/176_promotion_respects_open_blockers.sql +58 -0
- package/migrations/176_ui_discipline.sql +40 -0
- package/migrations/177_require_reward_before_workable.sql +76 -0
- package/migrations/178_reward_gate_auto_assign_revert_autopromote.sql +61 -0
- package/migrations/179_search_chunk_usefulness.sql +33 -0
- package/migrations/180_one_active_claim_per_session.sql +54 -0
- package/migrations/181_cost_log_skill_name.sql +33 -0
- package/migrations/184_claim_release_requested.sql +25 -0
- package/migrations/185_module_upstreaming_goal_wiring.sql +59 -0
- package/migrations/187_goal_sort_order.sql +30 -0
- package/migrations/187_tasks_kind_check_add_verify.sql +21 -0
- package/migrations/README.md +88 -0
- package/migrations/core_188_goal_visibility_and_membership_requests.sql +71 -0
- package/migrations/core_189_core_upgrades_ledger.sql +37 -0
- package/migrations/core_189_credit_log_task_id_index.sql +19 -0
- package/migrations/core_190_rebuild_claimable_task_views.sql +71 -0
- package/migrations/instance/018_pms_v3_planning_seed.sql +131 -0
- package/migrations/instance/080_gds_v4_planning_seed.sql +75 -0
- package/migrations/instance/110_medusa_planning_seed.sql +70 -0
- package/migrations/instance/120_bongos_v1_planning_seed.sql +69 -0
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +26 -0
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +29 -0
- package/migrations/instance/172_v2_world_planning_seed.sql +163 -0
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +33 -0
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +132 -0
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +116 -0
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +69 -0
- package/modules/autonomy/CLAUDE.md +49 -0
- package/modules/autonomy/db.js +50 -0
- package/modules/autonomy/module.json +13 -0
- package/modules/autonomy/pollers/routine-timers.js +146 -0
- package/modules/autonomy/routes/autonomy.js +37 -0
- package/modules/builder-settings/CLAUDE.md +58 -0
- package/modules/builder-settings/builder-needs.js +115 -0
- package/modules/builder-settings/db.js +168 -0
- package/modules/builder-settings/display-name.js +84 -0
- package/modules/builder-settings/event-sound-prefs.js +169 -0
- package/modules/builder-settings/interaction-prefs.js +201 -0
- package/modules/builder-settings/model-alloc-prefs.js +147 -0
- package/modules/builder-settings/module.json +10 -0
- package/modules/builder-settings/render-prefs.js +103 -0
- package/modules/builder-settings/routes/settings.js +417 -0
- package/modules/builder-settings/settings.js +52 -0
- package/modules/builder-settings/skill-prefs.js +196 -0
- package/modules/builder-settings/sound-prefs.js +178 -0
- package/modules/builder-settings/wandering-prefs.js +282 -0
- package/modules/dev-box/CLAUDE.md +38 -0
- package/modules/dev-box/box-access.js +275 -0
- package/modules/dev-box/box-credential.js +94 -0
- package/modules/dev-box/box-onboard.js +486 -0
- package/modules/dev-box/boxes.js +842 -0
- package/modules/dev-box/db.js +39 -0
- package/modules/dev-box/module.json +14 -0
- package/modules/dev-box/routes/box.js +977 -0
- package/modules/discord/CLAUDE.md +50 -0
- package/modules/discord/auth.js +54 -0
- package/modules/discord/bug-attachments.js +210 -0
- package/modules/discord/db.js +96 -0
- package/modules/discord/discord-approvals.js +145 -0
- package/modules/discord/discord-audit.js +29 -0
- package/modules/discord/discord-bot.js +545 -0
- package/modules/discord/discord-bugs.js +257 -0
- package/modules/discord/discord-channels.js +412 -0
- package/modules/discord/discord-inbound.js +147 -0
- package/modules/discord/discord-message-buffer.js +54 -0
- package/modules/discord/discord-reaction.js +186 -0
- package/modules/discord/discord-role-sync.js +318 -0
- package/modules/discord/module.json +14 -0
- package/modules/discord/pollers/discord-bot.js +19 -0
- package/modules/discord/routes/bug-attachments.js +44 -0
- package/modules/discord/routes/discord.js +102 -0
- package/modules/discord/ship-broadcast.js +78 -0
- package/modules/discord/webhook.js +74 -0
- package/modules/economy/CLAUDE.md +48 -0
- package/modules/economy/achievements.js +594 -0
- package/modules/economy/cost-classify.js +331 -0
- package/modules/economy/cost-summary-shape.js +90 -0
- package/modules/economy/cost.js +438 -0
- package/modules/economy/credits.js +430 -0
- package/modules/economy/migrations/economy_001_fix_gds_shipper_description.sql +91 -0
- package/modules/economy/module.json +10 -0
- package/modules/economy/reward.js +61 -0
- package/modules/economy/routes/cost.js +97 -0
- package/modules/economy/routes/leaderboard.js +26 -0
- package/modules/economy/routes/reward.js +42 -0
- package/modules/economy/streak.js +285 -0
- package/modules/grading/CLAUDE.md +42 -0
- package/modules/grading/db.js +39 -0
- package/modules/grading/grader-rubric.json +151 -0
- package/modules/grading/grader-workers/efficiency.js +213 -0
- package/modules/grading/grader-workers/extract-json.js +80 -0
- package/modules/grading/grader-workers/fence.js +100 -0
- package/modules/grading/grader-workers/hacker.js +239 -0
- package/modules/grading/grader-workers/index.js +183 -0
- package/modules/grading/grader-workers/narc.js +207 -0
- package/modules/grading/grader-workers/quality.js +198 -0
- package/modules/grading/grader-workers/rubric.js +20 -0
- package/modules/grading/grader-workers/worker-prompt.js +89 -0
- package/modules/grading/grader-workers/worker-verdict.js +50 -0
- package/modules/grading/grader.js +1790 -0
- package/modules/grading/module.json +12 -0
- package/modules/grading/routes/grade.js +51 -0
- package/modules/hall-ui/CLAUDE.md +61 -0
- package/modules/hall-ui/module.json +14 -0
- package/modules/hall-ui/public/atlas.html +180 -0
- package/modules/hall-ui/public/atlas.js +336 -0
- package/modules/hall-ui/public/box-panel.js +423 -0
- package/modules/hall-ui/public/brand-apply.js +117 -0
- package/modules/hall-ui/public/builders.js +3028 -0
- package/modules/hall-ui/public/chime.wav +0 -0
- package/modules/hall-ui/public/copy-on-click.js +174 -0
- package/modules/hall-ui/public/cursors/arrow.png +0 -0
- package/modules/hall-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier-out.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier.png +0 -0
- package/modules/hall-ui/public/diagram-viewer.css +125 -0
- package/modules/hall-ui/public/diagram-viewer.js +330 -0
- package/modules/hall-ui/public/diagrams.html +113 -0
- package/modules/hall-ui/public/dom-utils.js +212 -0
- package/modules/hall-ui/public/drachmae.html +68 -0
- package/modules/hall-ui/public/drachmae.js +136 -0
- package/modules/hall-ui/public/gate.html +143 -0
- package/modules/hall-ui/public/gate.js +228 -0
- package/modules/hall-ui/public/goals-page.js +1297 -0
- package/modules/hall-ui/public/goals.html +376 -0
- package/modules/hall-ui/public/goals.js +119 -0
- package/modules/hall-ui/public/hall-widgets.js +189 -0
- package/modules/hall-ui/public/harbor.html +167 -0
- package/modules/hall-ui/public/harbor.js +335 -0
- package/modules/hall-ui/public/index.html +116 -0
- package/modules/hall-ui/public/instances-panel.js +172 -0
- package/modules/hall-ui/public/md-reader.js +234 -0
- package/modules/hall-ui/public/modules.html +119 -0
- package/modules/hall-ui/public/modules.js +154 -0
- package/modules/hall-ui/public/new-project-wizard.js +186 -0
- package/modules/hall-ui/public/not-ready.html +48 -0
- package/modules/hall-ui/public/pair.html +119 -0
- package/modules/hall-ui/public/pair.js +132 -0
- package/modules/hall-ui/public/primer.css +404 -0
- package/modules/hall-ui/public/primer.html +85 -0
- package/modules/hall-ui/public/primer.js +641 -0
- package/modules/hall-ui/public/profile.html +85 -0
- package/modules/hall-ui/public/profile.js +236 -0
- package/modules/hall-ui/public/ranks.html +69 -0
- package/modules/hall-ui/public/ranks.js +206 -0
- package/modules/hall-ui/public/roadmap.html +209 -0
- package/modules/hall-ui/public/roadmap.js +375 -0
- package/modules/hall-ui/public/sessions.html +92 -0
- package/modules/hall-ui/public/sessions.js +222 -0
- package/modules/hall-ui/public/settings.html +640 -0
- package/modules/hall-ui/public/settings.js +1219 -0
- package/modules/hall-ui/public/shell.js +495 -0
- package/modules/hall-ui/public/style.css +1748 -0
- package/modules/hall-ui/public/task-detail.js +253 -0
- package/modules/hall-ui/public/task.html +83 -0
- package/modules/hall-ui/public/task.js +326 -0
- package/modules/hall-ui/public/watch.html +333 -0
- package/modules/hall-ui/public/watch.js +859 -0
- package/modules/hall-ui/public/work.html +469 -0
- package/modules/hall-ui/public/work.js +1292 -0
- package/modules/ideas/CLAUDE.md +37 -0
- package/modules/ideas/blockers.js +100 -0
- package/modules/ideas/inbox.js +459 -0
- package/modules/ideas/module.json +10 -0
- package/modules/ideas/routes/blockers.js +120 -0
- package/modules/ideas/routes/inbox.js +230 -0
- package/modules/lifecycle/CLAUDE.md +63 -0
- package/modules/lifecycle/conflict-resolve.js +436 -0
- package/modules/lifecycle/db.js +4970 -0
- package/modules/lifecycle/done-when.js +355 -0
- package/modules/lifecycle/github-push.js +969 -0
- package/modules/lifecycle/goal-conflicts.js +133 -0
- package/modules/lifecycle/hierarchy.js +104 -0
- package/modules/lifecycle/lifecycle.js +58 -0
- package/modules/lifecycle/merge-lock.js +261 -0
- package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +28 -0
- package/modules/lifecycle/migrations/lifecycle_002_backfill_orphan_task_goals.sql +36 -0
- package/modules/lifecycle/module.json +13 -0
- package/modules/lifecycle/pollers/publish-reconciler.js +20 -0
- package/modules/lifecycle/publish-reconciler.js +418 -0
- package/modules/lifecycle/routes/analytics.js +63 -0
- package/modules/lifecycle/routes/claims.js +506 -0
- package/modules/lifecycle/routes/dependencies.js +82 -0
- package/modules/lifecycle/routes/done-when.js +136 -0
- package/modules/lifecycle/routes/gate-approvals.js +197 -0
- package/modules/lifecycle/routes/goals.js +1419 -0
- package/modules/lifecycle/routes/lifecycle.js +43 -0
- package/modules/lifecycle/routes/tasks.js +1703 -0
- package/modules/lifecycle/routes/versions.js +128 -0
- package/modules/lifecycle/ship-card.js +505 -0
- package/modules/lifecycle/ship-preflight.js +225 -0
- package/modules/lifecycle/task-classifier.js +512 -0
- package/modules/lifecycle/version-close.js +106 -0
- package/modules/memory/CLAUDE.md +40 -0
- package/modules/memory/learnings.js +275 -0
- package/modules/memory/memory.js +572 -0
- package/modules/memory/module.json +12 -0
- package/modules/memory/routes/learnings.js +90 -0
- package/modules/memory/routes/memory.js +916 -0
- package/modules/memory/routes/search.js +188 -0
- package/modules/memory/search-chunker.js +252 -0
- package/modules/memory/search.js +320 -0
- package/modules/onboarding/CLAUDE.md +55 -0
- package/modules/onboarding/approval-broadcast.js +59 -0
- package/modules/onboarding/db.js +54 -0
- package/modules/onboarding/module.json +10 -0
- package/modules/onboarding/newcomer-restock.js +212 -0
- package/modules/onboarding/onboarding-state.js +532 -0
- package/modules/onboarding/onboarding.js +508 -0
- package/modules/onboarding/port.js +63 -0
- package/modules/onboarding/routes/access-requests.js +208 -0
- package/modules/onboarding/routes/onboarding.js +54 -0
- package/modules/security/CLAUDE.md +44 -0
- package/modules/security/module.json +10 -0
- package/modules/security/routes/reports.js +251 -0
- package/modules/security/secret-scrub.js +201 -0
- package/modules/security/security.js +294 -0
- package/modules/sessions/CLAUDE.md +39 -0
- package/modules/sessions/bfg.js +357 -0
- package/modules/sessions/db.js +326 -0
- package/modules/sessions/module.json +10 -0
- package/modules/sessions/pulse.js +149 -0
- package/modules/sessions/routes/sessions.js +331 -0
- package/modules/status-ui/CLAUDE.md +47 -0
- package/modules/status-ui/module.json +14 -0
- package/modules/status-ui/public/README.md +29 -0
- package/modules/status-ui/public/cursors/arrow.png +0 -0
- package/modules/status-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/status-ui/public/index.html +363 -0
- package/modules/status-ui/public/snapshots/.gitkeep +0 -0
- package/modules/status-ui/public/status.js +1375 -0
- package/modules/status-ui/public/style.css +1342 -0
- package/package-lock.json +975 -0
- package/package.json +28 -0
- package/public-docs/index.html +35 -0
- package/public-docs/redoc.standalone.js +1838 -0
- package/public-docs/redoc.standalone.js.LICENSE.txt +102 -0
- package/public-landing/buddha.png +0 -0
- package/public-landing/create.html +868 -0
- package/public-landing/index.html +168 -0
- package/scripts/bongos-mcp.js +28 -0
- package/scripts/cron/style_guide_audit.sh +134 -0
- package/scripts/discord/README.md +52 -0
- package/scripts/discord/lib.js +80 -0
- package/scripts/discord/post.js +51 -0
- package/scripts/gds/CLAUDE.md +144 -0
- package/scripts/gds/abandon.js +58 -0
- package/scripts/gds/adopt-detect.js +123 -0
- package/scripts/gds/adopt-import.js +111 -0
- package/scripts/gds/adopt-preflight.js +121 -0
- package/scripts/gds/api.js +78 -0
- package/scripts/gds/architect-audit.js +288 -0
- package/scripts/gds/art-stage.js +152 -0
- package/scripts/gds/audit-authorship.sh +243 -0
- package/scripts/gds/audit-deps.js +121 -0
- package/scripts/gds/audit-exports.js +128 -0
- package/scripts/gds/audit-replay.sh +161 -0
- package/scripts/gds/audit-ships.js +251 -0
- package/scripts/gds/autonomy-gate.js +135 -0
- package/scripts/gds/backfill-cost.js +165 -0
- package/scripts/gds/backfill-sessions.js +351 -0
- package/scripts/gds/backfill-task-rewards.js +408 -0
- package/scripts/gds/backup.js +94 -0
- package/scripts/gds/bfg-principal-token.js +65 -0
- package/scripts/gds/box-auth-check.js +160 -0
- package/scripts/gds/box-connect-lib.js +453 -0
- package/scripts/gds/box.js +1258 -0
- package/scripts/gds/build-bongos.js +94 -0
- package/scripts/gds/capture.js +123 -0
- package/scripts/gds/cf-tunnel.js +419 -0
- package/scripts/gds/ci-grade.js +82 -0
- package/scripts/gds/claim-recall.js +112 -0
- package/scripts/gds/claim.js +333 -0
- package/scripts/gds/classify-discipline.js +153 -0
- package/scripts/gds/claude-materialize.js +176 -0
- package/scripts/gds/cleanup-orphans.js +74 -0
- package/scripts/gds/cli-lib.js +518 -0
- package/scripts/gds/code.js +101 -0
- package/scripts/gds/codemod-rename-src-gds.js +145 -0
- package/scripts/gds/conductor-dispatch.js +426 -0
- package/scripts/gds/context-pack.js +228 -0
- package/scripts/gds/coreB-carve-proof.sh +716 -0
- package/scripts/gds/cost.js +47 -0
- package/scripts/gds/db-backup-nightly.sh +160 -0
- package/scripts/gds/db-backup-offsite.sh +152 -0
- package/scripts/gds/demote.js +40 -0
- package/scripts/gds/design-sync.js +62 -0
- package/scripts/gds/diagnose-achievements.js +118 -0
- package/scripts/gds/diagram-facts.js +319 -0
- package/scripts/gds/discipline-modes.json +25 -0
- package/scripts/gds/discord-channels.js +66 -0
- package/scripts/gds/do-api.js +176 -0
- package/scripts/gds/docs-entropy.js +403 -0
- package/scripts/gds/doctor.js +389 -0
- package/scripts/gds/feedback-latest.js +94 -0
- package/scripts/gds/fetch-art-key.js +106 -0
- package/scripts/gds/fetch-sound-prefs.js +88 -0
- package/scripts/gds/figma-design-sync.js +61 -0
- package/scripts/gds/fitness.js +1446 -0
- package/scripts/gds/gate-review.js +322 -0
- package/scripts/gds/gating.js +325 -0
- package/scripts/gds/gds-db-backup.service +24 -0
- package/scripts/gds/gds-db-backup.timer +20 -0
- package/scripts/gds/gds-db-offsite.service +29 -0
- package/scripts/gds/gds-db-offsite.timer +18 -0
- package/scripts/gds/gen-api-client.js +508 -0
- package/scripts/gds/gen-api-docs.js +1037 -0
- package/scripts/gds/gen-atlas.js +366 -0
- package/scripts/gds/gen-diagrams.js +608 -0
- package/scripts/gds/gen-file-map.js +196 -0
- package/scripts/gds/gen-repo-map.js +519 -0
- package/scripts/gds/gen-session-index.js +210 -0
- package/scripts/gds/git-merge-regen.js +147 -0
- package/scripts/gds/grade-smoke.js +205 -0
- package/scripts/gds/hooks/pre-push.js +169 -0
- package/scripts/gds/http-api-client.js +88 -0
- package/scripts/gds/init.js +903 -0
- package/scripts/gds/install-git-hooks.js +114 -0
- package/scripts/gds/key-card.js +213 -0
- package/scripts/gds/key-status.js +126 -0
- package/scripts/gds/land-watch.js +168 -0
- package/scripts/gds/learning-capture.js +145 -0
- package/scripts/gds/lib.sh +44 -0
- package/scripts/gds/linkify-refs.js +473 -0
- package/scripts/gds/local-preview-lib.js +159 -0
- package/scripts/gds/local-preview.js +95 -0
- package/scripts/gds/login.js +225 -0
- package/scripts/gds/main-audit.js +279 -0
- package/scripts/gds/me.js +88 -0
- package/scripts/gds/memory-map.js +174 -0
- package/scripts/gds/merge-lock.js +71 -0
- package/scripts/gds/migration-namespace.js +200 -0
- package/scripts/gds/mirror-lineage-check.js +319 -0
- package/scripts/gds/mirror-redact.js +305 -0
- package/scripts/gds/module.js +482 -0
- package/scripts/gds/newcomer-floor.config.json +13 -0
- package/scripts/gds/newcomer-floor.js +415 -0
- package/scripts/gds/newcomer-floor.templates.json +86 -0
- package/scripts/gds/newcomer-floor.test.js +235 -0
- package/scripts/gds/next-number.js +163 -0
- package/scripts/gds/oauth-secret.js +197 -0
- package/scripts/gds/onboard.js +217 -0
- package/scripts/gds/onboarding-analyze.js +226 -0
- package/scripts/gds/onboarding-config.js +178 -0
- package/scripts/gds/onboarding-ship.js +92 -0
- package/scripts/gds/package-core.js +684 -0
- package/scripts/gds/paste-token.js +131 -0
- package/scripts/gds/portability-smoke-art.sh +85 -0
- package/scripts/gds/portability-smoke-memory.sh +147 -0
- package/scripts/gds/provision.js +1324 -0
- package/scripts/gds/publish-manifest.js +307 -0
- package/scripts/gds/pulse.js +309 -0
- package/scripts/gds/push-main.js +45 -0
- package/scripts/gds/read-session-export.js +359 -0
- package/scripts/gds/reauth.js +149 -0
- package/scripts/gds/recall.js +135 -0
- package/scripts/gds/release.js +71 -0
- package/scripts/gds/render-ideas.js +173 -0
- package/scripts/gds/repo-metrics.js +363 -0
- package/scripts/gds/resolve-skill-model.js +57 -0
- package/scripts/gds/run-routine.js +197 -0
- package/scripts/gds/run-unit-tests.js +178 -0
- package/scripts/gds/sandbox-stage.js +653 -0
- package/scripts/gds/search-ingest-db.js +209 -0
- package/scripts/gds/search-ingest-md.js +225 -0
- package/scripts/gds/security-baseline-check.sh +180 -0
- package/scripts/gds/seed-adopt-tasks.js +75 -0
- package/scripts/gds/seed-api-contract-goal-tasks.js +141 -0
- package/scripts/gds/seed-bongos-app-tasks.js +294 -0
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +81 -0
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +100 -0
- package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +307 -0
- package/scripts/gds/seed-bongos-module-system-tasks.js +346 -0
- package/scripts/gds/seed-bongos-tasks.js +214 -0
- package/scripts/gds/seed-gds-v4-tasks.js +624 -0
- package/scripts/gds/seed-governance-tasks.js +340 -0
- package/scripts/gds/seed-medusa-tasks.js +440 -0
- package/scripts/gds/seed-module-upstreaming.js +219 -0
- package/scripts/gds/seed-productization-tasks.js +235 -0
- package/scripts/gds/seed-provisioning-tasks.js +83 -0
- package/scripts/gds/seed-staging.sh +104 -0
- package/scripts/gds/seed-v2-world-tasks.js +283 -0
- package/scripts/gds/seed-v3-tasks.js +1981 -0
- package/scripts/gds/session-cost.js +393 -0
- package/scripts/gds/session-digest.js +179 -0
- package/scripts/gds/session-eval.js +576 -0
- package/scripts/gds/session.js +404 -0
- package/scripts/gds/setup.js +504 -0
- package/scripts/gds/shell.js +100 -0
- package/scripts/gds/ship.js +3618 -0
- package/scripts/gds/smoke-batch-claim.sh +213 -0
- package/scripts/gds/smoke-batch-release.sh +296 -0
- package/scripts/gds/smoke-box.sh +150 -0
- package/scripts/gds/smoke-claim-consistency.sh +212 -0
- package/scripts/gds/smoke-dependencies.sh +84 -0
- package/scripts/gds/smoke-gds.js +125 -0
- package/scripts/gds/smoke-write.sh +168 -0
- package/scripts/gds/start.js +782 -0
- package/scripts/gds/status.js +365 -0
- package/scripts/gds/triage.js +366 -0
- package/scripts/gds/upgrade.js +421 -0
- package/scripts/gds/validate-design.js +254 -0
- package/scripts/gds/version-close.js +549 -0
- package/scripts/migrate.sh +183 -0
- package/scripts/migrate_cost_ledger.sh +178 -0
- package/scripts/public-mirror-export.js +291 -0
- package/scripts/render-diagrams.sh +88 -0
- package/scripts/security/dep-audit.js +276 -0
- package/scripts/setup-builder.js +274 -0
- package/scripts/setup-builder.sh +28 -0
- package/scripts/snapshots/backfill-history.mjs +292 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +66 -0
- package/scripts/snapshots/render-pre-art-eras.mjs +329 -0
- package/scripts/snapshots/render-world.mjs +322 -0
- package/scripts/status-mirror-sync.js +192 -0
- package/scripts/status-probe.mjs +185 -0
- package/server.js +212 -0
- package/src/asset-version.js +71 -0
- package/src/bongos/CLAUDE.md +64 -0
- package/src/bongos/api-errors.js +281 -0
- package/src/bongos/api-path-404.js +67 -0
- package/src/bongos/api-prefix.js +38 -0
- package/src/bongos/app-pair.js +172 -0
- package/src/bongos/auth.js +979 -0
- package/src/bongos/cascade-dispatch.js +123 -0
- package/src/bongos/db-kernel.js +252 -0
- package/src/bongos/db.js +911 -0
- package/src/bongos/goal-scope-check.js +120 -0
- package/src/bongos/hierarchy-config.js +189 -0
- package/src/bongos/live-channel.js +145 -0
- package/src/bongos/llm-cache.js +260 -0
- package/src/bongos/llm-pricing.js +220 -0
- package/src/bongos/metrics.js +286 -0
- package/src/bongos/middleware/audit.js +274 -0
- package/src/bongos/middleware/error-envelope.js +93 -0
- package/src/bongos/middleware/rate-limit.js +195 -0
- package/src/bongos/module-scope-map.js +271 -0
- package/src/bongos/optimizer.js +166 -0
- package/src/bongos/path-match.js +47 -0
- package/src/bongos/permission-path-check.js +248 -0
- package/src/bongos/pool.js +55 -0
- package/src/bongos/repo-info.js +91 -0
- package/src/bongos/route-rank-check.js +420 -0
- package/src/bongos/routes/CLAUDE.md +43 -0
- package/src/bongos/routes/_helpers.js +369 -0
- package/src/bongos/routes/audit-log.js +59 -0
- package/src/bongos/routes/auth.js +755 -0
- package/src/bongos/routes/backup.js +104 -0
- package/src/bongos/routes/builders.js +526 -0
- package/src/bongos/routes/healthz.js +16 -0
- package/src/bongos/routes/instance.js +129 -0
- package/src/bongos/routes/live.js +125 -0
- package/src/bongos/routes/llm-cache.js +119 -0
- package/src/bongos/routes/me.js +380 -0
- package/src/bongos/routes/modules.js +131 -0
- package/src/bongos/routes/optimizer.js +54 -0
- package/src/bongos/routes/public.js +476 -0
- package/src/bongos/routes/security.js +207 -0
- package/src/bongos/routes.js +166 -0
- package/src/bongos/secret-box.js +177 -0
- package/src/bongos/sensitive-surfaces.js +30 -0
- package/src/bongos/serve-internal.js +889 -0
- package/src/bongos/uptime-poller.js +90 -0
- package/src/bongos-downloads.js +70 -0
- package/src/branding.js +182 -0
- package/src/build-info.js +77 -0
- package/src/devbox-downloads.js +67 -0
- package/src/instance-config.js +200 -0
- package/src/module-api.js +366 -0
- package/src/module-loader/loader.js +268 -0
- package/src/module-loader/manifest-schema.js +140 -0
- package/src/module-loader/provenance.js +28 -0
- package/src/module-loader/semver.js +100 -0
- package/src/module-seams.js +291 -0
- package/src/modules.js +254 -0
- package/src/platform-server.js +135 -0
- package/src/ui/adapters/claude-design/index.js +333 -0
- package/src/ui/adapters/figma/index.js +328 -0
- package/tests/abandon_task.mjs +138 -0
- package/tests/achievements_reconcile.mjs +167 -0
- package/tests/adopt_detect.mjs +141 -0
- package/tests/adopt_import.mjs +132 -0
- package/tests/adopt_preflight.mjs +105 -0
- package/tests/anti_grid.mjs +89 -0
- package/tests/api_client.mjs +231 -0
- package/tests/api_docs.mjs +283 -0
- package/tests/api_error_envelope_buildcheck.mjs +56 -0
- package/tests/api_error_envelope_middleware.mjs +99 -0
- package/tests/api_error_registry.mjs +108 -0
- package/tests/api_pagination_contract.mjs +51 -0
- package/tests/api_path_404.mjs +96 -0
- package/tests/app_pair.mjs +168 -0
- package/tests/architect.mjs +140 -0
- package/tests/art_key.mjs +121 -0
- package/tests/art_stage.mjs +121 -0
- package/tests/asset_version.mjs +94 -0
- package/tests/atlas.mjs +69 -0
- package/tests/audit_deps.mjs +74 -0
- package/tests/audit_middleware.mjs +385 -0
- package/tests/auth_admission_gate.mjs +164 -0
- package/tests/auth_anthropic_identity.mjs +95 -0
- package/tests/auth_duplicate_cookie.mjs +56 -0
- package/tests/auth_error_brand.mjs +77 -0
- package/tests/auth_flow_slice1.mjs +223 -0
- package/tests/auth_inactive_builder.mjs +94 -0
- package/tests/auth_page_gate.mjs +92 -0
- package/tests/auth_session_ttl.mjs +155 -0
- package/tests/autonomous_fit_protected_paths.mjs +115 -0
- package/tests/autonomy_gate.mjs +85 -0
- package/tests/autotile_variants.mjs +256 -0
- package/tests/autotile_xverify.mjs +133 -0
- package/tests/backfill_sessions.mjs +101 -0
- package/tests/backfill_task_rewards.mjs +152 -0
- package/tests/backup_cli.mjs +104 -0
- package/tests/bfg.mjs +327 -0
- package/tests/bfg_write.mjs +423 -0
- package/tests/bongos_box_connect.mjs +89 -0
- package/tests/bongos_dispatch.mjs +74 -0
- package/tests/bongos_downloads.mjs +118 -0
- package/tests/bongos_login.mjs +34 -0
- package/tests/box_access.mjs +186 -0
- package/tests/box_auth_check.mjs +114 -0
- package/tests/box_code_staleness.mjs +116 -0
- package/tests/box_connect_e2e.mjs +196 -0
- package/tests/box_cost.mjs +70 -0
- package/tests/box_dns_repoint.mjs +151 -0
- package/tests/box_host_keys.mjs +102 -0
- package/tests/box_onboard.mjs +317 -0
- package/tests/box_scope_session.mjs +115 -0
- package/tests/box_ship_permissions.mjs +71 -0
- package/tests/box_terminal_ssrf.mjs +90 -0
- package/tests/boxes.mjs +1255 -0
- package/tests/branding.mjs +88 -0
- package/tests/bug_attachments.mjs +189 -0
- package/tests/build_info.mjs +88 -0
- package/tests/builder_needs.mjs +99 -0
- package/tests/cap_newcomer_tasks.mjs +114 -0
- package/tests/cascade-dispatch.mjs +95 -0
- package/tests/cf_tunnel.mjs +459 -0
- package/tests/claim_recall.mjs +150 -0
- package/tests/claims_bind_session.mjs +47 -0
- package/tests/claude_materialize.mjs +148 -0
- package/tests/cli_box_scope.mjs +52 -0
- package/tests/cli_lib_formatters.mjs +87 -0
- package/tests/cli_token_reissue.mjs +147 -0
- package/tests/client_bundle_route.mjs +47 -0
- package/tests/cloud_bongos_pack.mjs +85 -0
- package/tests/conductor.mjs +496 -0
- package/tests/confirm_attribution.mjs +156 -0
- package/tests/conflict_resolve.mjs +462 -0
- package/tests/context_pack.mjs +240 -0
- package/tests/cost.mjs +94 -0
- package/tests/cost_audit.mjs +122 -0
- package/tests/cost_budget_status.mjs +78 -0
- package/tests/cost_reconcile.mjs +102 -0
- package/tests/cost_summary_shape.mjs +94 -0
- package/tests/credit_grant.mjs +376 -0
- package/tests/criterion_progress.mjs +220 -0
- package/tests/criterion_proposal.mjs +109 -0
- package/tests/criterion_ref_parse.mjs +53 -0
- package/tests/currency_label.mjs +119 -0
- package/tests/day_cycle.mjs +197 -0
- package/tests/dependencies_migration.mjs +77 -0
- package/tests/dependencies_polymorphic.mjs +212 -0
- package/tests/deploy_instance.mjs +78 -0
- package/tests/devbox_downloads.mjs +122 -0
- package/tests/diff_derived_signals.mjs +115 -0
- package/tests/discord_approvals.mjs +174 -0
- package/tests/discord_autojoin.mjs +158 -0
- package/tests/discord_bot.mjs +94 -0
- package/tests/discord_bugs.mjs +297 -0
- package/tests/discord_channels.mjs +337 -0
- package/tests/discord_foundation.mjs +76 -0
- package/tests/discord_inbound.mjs +190 -0
- package/tests/discord_message_buffer.mjs +87 -0
- package/tests/discord_oauth.mjs +55 -0
- package/tests/discord_reaction.mjs +170 -0
- package/tests/discord_role_sync.mjs +151 -0
- package/tests/display_name.mjs +123 -0
- package/tests/dns_preflight.mjs +74 -0
- package/tests/event_sound_prefs.mjs +194 -0
- package/tests/eviction_policy.mjs +279 -0
- package/tests/figma_adapter.mjs +199 -0
- package/tests/file_map.mjs +91 -0
- package/tests/first_admin_bootstrap.mjs +40 -0
- package/tests/fitness.mjs +435 -0
- package/tests/fitness_two_root.mjs +104 -0
- package/tests/fixtures/session-506-digest.json +34 -0
- package/tests/gate_approvals.mjs +451 -0
- package/tests/gate_review.mjs +152 -0
- package/tests/git_merge_regen.mjs +151 -0
- package/tests/github_push_ancestry.mjs +119 -0
- package/tests/github_push_land_proof.mjs +127 -0
- package/tests/github_push_merge_driver.mjs +175 -0
- package/tests/goal_abuse_matrix.mjs +187 -0
- package/tests/goal_achievement.mjs +146 -0
- package/tests/goal_conflicts.mjs +305 -0
- package/tests/goal_dependencies_read.mjs +73 -0
- package/tests/goal_member_roles.mjs +420 -0
- package/tests/goal_membership_requests.mjs +332 -0
- package/tests/goal_routes.mjs +203 -0
- package/tests/goal_scope_check.mjs +167 -0
- package/tests/goal_tier.mjs +312 -0
- package/tests/goal_wall_proof.mjs +112 -0
- package/tests/grade_attempts.mjs +73 -0
- package/tests/grade_attribution.mjs +117 -0
- package/tests/grade_diff_hash.mjs +191 -0
- package/tests/grade_server_authoritative.mjs +305 -0
- package/tests/grade_unavailable_display.mjs +66 -0
- package/tests/grader.mjs +1960 -0
- package/tests/grader_bypass.mjs +57 -0
- package/tests/grader_panel.mjs +252 -0
- package/tests/grader_prompt_fencing.mjs +192 -0
- package/tests/helpers.mjs +52 -0
- package/tests/hierarchy-config.mjs +81 -0
- package/tests/init.mjs +463 -0
- package/tests/instance_config.mjs +76 -0
- package/tests/instance_manifest.mjs +110 -0
- package/tests/interaction_prefs.mjs +72 -0
- package/tests/key-card.mjs +110 -0
- package/tests/key-status.mjs +45 -0
- package/tests/land_watch.mjs +115 -0
- package/tests/lifecycle-card.mjs +145 -0
- package/tests/lifecycle_breakdown_view.mjs +236 -0
- package/tests/linkify_refs.mjs +264 -0
- package/tests/live_channel.mjs +128 -0
- package/tests/live_sse.mjs +122 -0
- package/tests/llm-cache-route.mjs +110 -0
- package/tests/llm-cache.mjs +140 -0
- package/tests/llm-pricing.mjs +152 -0
- package/tests/local_preview.mjs +136 -0
- package/tests/main_audit.mjs +137 -0
- package/tests/memory_isolation.mjs +431 -0
- package/tests/memory_map.mjs +107 -0
- package/tests/memory_pull.mjs +429 -0
- package/tests/merge_lease.mjs +100 -0
- package/tests/merge_lock.mjs +312 -0
- package/tests/metrics.mjs +147 -0
- package/tests/migration_allocation.mjs +146 -0
- package/tests/migration_namespace.mjs +185 -0
- package/tests/migrations_fresh_replay_guard.mjs +38 -0
- package/tests/mirror_lineage_fulltree.mjs +45 -0
- package/tests/mirror_lineage_proof.mjs +72 -0
- package/tests/mirror_redact.mjs +193 -0
- package/tests/model_alloc_prefs.mjs +63 -0
- package/tests/module-scope-map.mjs +88 -0
- package/tests/module_api.mjs +110 -0
- package/tests/module_cli.mjs +292 -0
- package/tests/module_contributions.mjs +148 -0
- package/tests/module_loader.mjs +286 -0
- package/tests/module_manifest.mjs +194 -0
- package/tests/module_owned_tests.mjs +105 -0
- package/tests/module_provenance.mjs +34 -0
- package/tests/module_route_rank.mjs +147 -0
- package/tests/module_seams.mjs +68 -0
- package/tests/modules.mjs +156 -0
- package/tests/modules_route.mjs +38 -0
- package/tests/newcomer_floor.mjs +17 -0
- package/tests/newcomer_restock.mjs +80 -0
- package/tests/next_number.mjs +154 -0
- package/tests/oauth_secret.mjs +60 -0
- package/tests/onboard.mjs +70 -0
- package/tests/onboarding_gate.mjs +267 -0
- package/tests/onboarding_observability.mjs +344 -0
- package/tests/onboarding_route_signals.mjs +175 -0
- package/tests/onboarding_state.mjs +346 -0
- package/tests/one_active_claim_per_session.mjs +111 -0
- package/tests/orphaned_claim_ship.mjs +135 -0
- package/tests/override_attribution.mjs +142 -0
- package/tests/override_request_ownership.mjs +235 -0
- package/tests/package_core.mjs +435 -0
- package/tests/parallel-load.mjs +402 -0
- package/tests/path_match.mjs +62 -0
- package/tests/permission_path.mjs +151 -0
- package/tests/platform_boot.mjs +167 -0
- package/tests/pre_compact.mjs +60 -0
- package/tests/preview_server.mjs +127 -0
- package/tests/profile_route.mjs +197 -0
- package/tests/provision.mjs +805 -0
- package/tests/provisioning_github_app_routes.mjs +98 -0
- package/tests/public_mirror_export.mjs +124 -0
- package/tests/public_task_resolver.mjs +112 -0
- package/tests/publish_branch_route.mjs +422 -0
- package/tests/publish_manifest.mjs +184 -0
- package/tests/publish_reconciler.mjs +728 -0
- package/tests/rank_gate.mjs +463 -0
- package/tests/rate_limit_public.mjs +217 -0
- package/tests/read_session_export.mjs +89 -0
- package/tests/recall_cli.mjs +82 -0
- package/tests/recall_hints.mjs +119 -0
- package/tests/recent_shipped_window.mjs +80 -0
- package/tests/regrade_eligibility.mjs +202 -0
- package/tests/render_prefs.mjs +122 -0
- package/tests/repo_map.mjs +92 -0
- package/tests/reward_gate.mjs +284 -0
- package/tests/route_rank_check.mjs +243 -0
- package/tests/route_validation.mjs +490 -0
- package/tests/routine_timers.mjs +72 -0
- package/tests/rubric_fixtures.mjs +140 -0
- package/tests/run_routine.mjs +69 -0
- package/tests/sandbox_stage.mjs +582 -0
- package/tests/scrubber_corpus.mjs +257 -0
- package/tests/search_chunker.mjs +209 -0
- package/tests/search_ingest.mjs +96 -0
- package/tests/search_ingest_db.mjs +88 -0
- package/tests/search_isolation.mjs +204 -0
- package/tests/search_route.mjs +95 -0
- package/tests/secret_box.mjs +189 -0
- package/tests/security_docs.mjs +186 -0
- package/tests/security_route.mjs +108 -0
- package/tests/served_paths_shipped.mjs +118 -0
- package/tests/session-cards.mjs +142 -0
- package/tests/session_506_regression.mjs +69 -0
- package/tests/session_caps.mjs +238 -0
- package/tests/session_cost.mjs +133 -0
- package/tests/session_digest.mjs +73 -0
- package/tests/session_eval.mjs +386 -0
- package/tests/session_index.mjs +105 -0
- package/tests/session_records.mjs +369 -0
- package/tests/session_search.mjs +78 -0
- package/tests/session_start_freshness.mjs +123 -0
- package/tests/setup_smoke.mjs +154 -0
- package/tests/ship_autolinkify.mjs +60 -0
- package/tests/ship_automerge_injection.mjs +174 -0
- package/tests/ship_broadcast_routing.mjs +125 -0
- package/tests/ship_broadcast_server.mjs +175 -0
- package/tests/ship_card.mjs +280 -0
- package/tests/ship_ci_deploy.mjs +374 -0
- package/tests/ship_preflight.mjs +368 -0
- package/tests/ship_premerge.mjs +75 -0
- package/tests/ship_resume.mjs +129 -0
- package/tests/ship_upload_skip.mjs +89 -0
- package/tests/ship_zero_commit.mjs +60 -0
- package/tests/single_host_hall.mjs +98 -0
- package/tests/skill_prefs.mjs +150 -0
- package/tests/smoke_preflight.mjs +66 -0
- package/tests/snapshot_picker.mjs +233 -0
- package/tests/sound_prefs.mjs +198 -0
- package/tests/sprite_facing.mjs +35 -0
- package/tests/status-probe.mjs +89 -0
- package/tests/status_backfill.mjs +94 -0
- package/tests/stop_refcheck.mjs +127 -0
- package/tests/streak.mjs +175 -0
- package/tests/subdomain_auth.mjs +58 -0
- package/tests/task_classifier_vocab.mjs +41 -0
- package/tests/task_create_idempotency.mjs +96 -0
- package/tests/task_goal_link.mjs +93 -0
- package/tests/task_ownership_gate.mjs +125 -0
- package/tests/terrain_picker.mjs +298 -0
- package/tests/touches_overlap.mjs +75 -0
- package/tests/triage_development.mjs +140 -0
- package/tests/turn_in_place.mjs +280 -0
- package/tests/upgrade.mjs +357 -0
- package/tests/verify_task_guard.mjs +64 -0
- package/tests/version_close.mjs +76 -0
- package/tests/versions_route.mjs +93 -0
- package/tests/walkability.mjs +188 -0
- package/tests/wandering_prefs.mjs +176 -0
- package/tests/worktree_disambiguation.mjs +105 -0
- package/tests/worktree_guard.mjs +90 -0
|
@@ -0,0 +1,1446 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// fitness.js — architecture fitness functions (Path C / ADR 0062 §9, task 1225).
|
|
4
|
+
//
|
|
5
|
+
// The ENFORCEMENT layer for the Cloud Bongos decoupling. A dependency-cruiser/ArchUnit-style gate that
|
|
6
|
+
// keeps the architecture from silently rotting back together. Each check is a pure function over
|
|
7
|
+
// the repo on disk returning { name, ok, hardFail, violations[], warnings[], note }. The CLI runs
|
|
8
|
+
// them all, prints a report, and exits non-zero on any hard failure. tests/fitness.mjs asserts
|
|
9
|
+
// each check passes, so the DB-free unit gate (run-unit-tests.js → the `unit` required check)
|
|
10
|
+
// enforces them on every PR — no new workflow needed.
|
|
11
|
+
//
|
|
12
|
+
// RATCHET, not big-bang. The structural epics that physically separate the core (R51 entrypoint
|
|
13
|
+
// #1190, R52 DB carve-out #1191, R54 module system #1193) are not built yet, so the codebase is
|
|
14
|
+
// still fused (every src/bongos/*.js shares the game's pg Pool via require('../db')). These checks
|
|
15
|
+
// therefore lock in the boundary DIRECTION now and fail only on NEW violations beyond a documented
|
|
16
|
+
// grandfather list; as the epics land they SHRINK the grandfather list. Module-level boundary
|
|
17
|
+
// enforcement (§4) tightens once R54 (#1193) gives us a module manifest to check against.
|
|
18
|
+
//
|
|
19
|
+
// Usage:
|
|
20
|
+
// node scripts/gds/fitness.js # run all checks, print report, exit 1 on any hard failure
|
|
21
|
+
// node scripts/gds/fitness.js --json # machine-readable report
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const { execFileSync } = require('node:child_process');
|
|
26
|
+
const { checkRouteRanks } = require('../../src/bongos/route-rank-check');
|
|
27
|
+
const genApiDocs = require('./gen-api-docs'); // R12 (#1999): reuse the route model (readsBody/validated) that drives the OpenAPI spec
|
|
28
|
+
const { parseRoutineFrontmatter } = require('./run-routine'); // System 4 (ADR 0078): deterministic-routine boundary
|
|
29
|
+
const { filenameViolation, stemCollisions, CORE_NS, INSTANCE_NS } = require('./migration-namespace'); // ADR 0108 §5: cross-repo migration namespacing
|
|
30
|
+
const { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-config'); // ADR 0108 §1: configurable-root
|
|
31
|
+
|
|
32
|
+
// ADR 0108 §Consequences #4 (configurable-root W6 / task 1886) — the guardrail walks TWO trees.
|
|
33
|
+
// CORE code + core-owned docs (src/, server.js, scripts/, the src/bongos + scripts/gds nested docs)
|
|
34
|
+
// live under the core PACKAGE; INSTANCE content (the root CLAUDE.md, the .claude/ methodology
|
|
35
|
+
// surface, instance modules + migrations) lives under the host REPO. Modules ship from BOTH.
|
|
36
|
+
// Today both resolve to the same checkout, so every walk below is byte-identical; once the core
|
|
37
|
+
// installs from node_modules they diverge and each check inspects the CORRECT tree. A NEW
|
|
38
|
+
// hardcoded `path.resolve(__dirname,'..','..')` here reintroduces the bug this check exists to stop.
|
|
39
|
+
const CORE_ROOT = resolveCoreRoot();
|
|
40
|
+
const INSTANCE_ROOT = resolveInstanceRoot();
|
|
41
|
+
// REPO_ROOT retained as an alias to the core root for the many purely-core walks below (src/bongos,
|
|
42
|
+
// KERNEL_FILES, scope map, …) — those keep their exact behavior. Instance-owned reads route
|
|
43
|
+
// through INSTANCE_ROOT explicitly, and module reads through MODULE_ROOTS.
|
|
44
|
+
const REPO_ROOT = CORE_ROOT;
|
|
45
|
+
// Deduped module search roots — a module can ship from the core package OR the instance repo (the
|
|
46
|
+
// loader errors on a cross-root key collision, so a key resolves to exactly one root). Deduped so
|
|
47
|
+
// today's single checkout scans each module ONCE (no double-counting).
|
|
48
|
+
const MODULE_ROOTS = [...new Set([CORE_ROOT, INSTANCE_ROOT])];
|
|
49
|
+
|
|
50
|
+
// ---- shared fs helpers -----------------------------------------------------
|
|
51
|
+
|
|
52
|
+
function walkJs(absRoot, out = []) {
|
|
53
|
+
let st;
|
|
54
|
+
try { st = fs.statSync(absRoot); } catch { return out; }
|
|
55
|
+
if (st.isFile()) { if (absRoot.endsWith('.js')) out.push(absRoot); return out; }
|
|
56
|
+
for (const ent of fs.readdirSync(absRoot, { withFileTypes: true })) {
|
|
57
|
+
if (ent.name === 'node_modules' || ent.name === '.git') continue;
|
|
58
|
+
walkJs(path.join(absRoot, ent.name), out);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
// repo-relative POSIX path for an abs file under EITHER root (core or instance). The classifier
|
|
63
|
+
// regexes throughout this file key on these relative strings, which are identical whichever tree
|
|
64
|
+
// a file lives in — so only the WALK roots change under the split, not the classification.
|
|
65
|
+
const rel = (abs) => {
|
|
66
|
+
for (const base of [CORE_ROOT, INSTANCE_ROOT]) {
|
|
67
|
+
const r = path.relative(base, abs);
|
|
68
|
+
if (r && !r.startsWith('..') && !path.isAbsolute(r)) return r.split(path.sep).join('/');
|
|
69
|
+
}
|
|
70
|
+
return path.relative(CORE_ROOT, abs).split(path.sep).join('/');
|
|
71
|
+
};
|
|
72
|
+
// Walk .js under `subpath` (e.g. 'modules') across ALL module roots, deduped by repo-relative
|
|
73
|
+
// path so the same file is never scanned twice when the roots coincide (today).
|
|
74
|
+
function walkJsAllRoots(subpath, roots = MODULE_ROOTS) {
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const root of roots) {
|
|
78
|
+
for (const abs of walkJs(path.join(root, subpath))) {
|
|
79
|
+
const r = rel(abs);
|
|
80
|
+
if (seen.has(r)) continue;
|
|
81
|
+
seen.add(r);
|
|
82
|
+
out.push(abs);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Check 1 — core ↔ host/game import boundary (ADR 0062 §1 cut-line)
|
|
90
|
+
// ============================================================================
|
|
91
|
+
//
|
|
92
|
+
// The portable core (src/bongos/) must not import the HOST GAME runtime. After BV1.R49 the game
|
|
93
|
+
// module lives in modules/game/ (rooms, world, db, seats, public, migrations). The only game-
|
|
94
|
+
// related file that stays in src/ is src/preview-server.js (the game-only sandbox entrypoint).
|
|
95
|
+
// Imports into scripts/ (build tooling / engine) are NOT host-game coupling.
|
|
96
|
+
const HOST_GAME = [
|
|
97
|
+
/^modules\/game\//, // everything in the game module (moved from src/ by BV1.R49)
|
|
98
|
+
/^src\/preview-server\.js$/, // game-only sandbox entrypoint (stays in src/)
|
|
99
|
+
];
|
|
100
|
+
// Empty as of R52/#1191 — the core owns its pool (src/bongos/pool.js) and no longer imports the
|
|
101
|
+
// game's db helper. The boundary now enforces with ZERO grandfathered exceptions.
|
|
102
|
+
const GRANDFATHERED = new Set([]);
|
|
103
|
+
const REQUIRE_RE = /require\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g;
|
|
104
|
+
|
|
105
|
+
function resolvesToHostGame(fromAbsFile, spec) {
|
|
106
|
+
const abs = path.resolve(path.dirname(fromAbsFile), spec);
|
|
107
|
+
const r = rel(abs);
|
|
108
|
+
for (const cand of [r, r + '.js', r.replace(/\/$/, '') + '/index.js']) {
|
|
109
|
+
if (GRANDFATHERED.has(cand)) return null; // allowed (and not a violation)
|
|
110
|
+
if (HOST_GAME.some((re) => re.test(cand))) return cand;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function checkCoreHostBoundary() {
|
|
116
|
+
const coreDir = path.join(REPO_ROOT, 'src', 'bongos');
|
|
117
|
+
const violations = [];
|
|
118
|
+
for (const abs of walkJs(coreDir)) {
|
|
119
|
+
// scan line-by-line, skipping comment lines so a require(...) quoted in a doc comment
|
|
120
|
+
// (e.g. explaining the boundary) is never mistaken for a real import.
|
|
121
|
+
for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
|
|
122
|
+
const trimmed = raw.trim();
|
|
123
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
|
|
124
|
+
let m;
|
|
125
|
+
REQUIRE_RE.lastIndex = 0;
|
|
126
|
+
while ((m = REQUIRE_RE.exec(raw))) {
|
|
127
|
+
const target = resolvesToHostGame(abs, m[1]);
|
|
128
|
+
if (target) violations.push(`${rel(abs)} → require('${m[1]}') reaches host-game code (${target}) — the core must not import the game runtime (ADR 0062 §1).`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
name: 'core↔host import boundary',
|
|
134
|
+
ok: violations.length === 0,
|
|
135
|
+
hardFail: violations.length > 0,
|
|
136
|
+
violations,
|
|
137
|
+
warnings: [],
|
|
138
|
+
note: `src/bongos/ must not import the host game runtime (grandfathered: ${[...GRANDFATHERED].join(', ') || 'none'} — severed by R52/#1191).`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// Check 2 — root CLAUDE.md size budget + charter presence (ADR 0061)
|
|
144
|
+
// ============================================================================
|
|
145
|
+
const ROOT_HARD_MAX = 300; // ceiling: the always-loaded file must never balloon past this. Ratcheted
|
|
146
|
+
// 360→310 when task 1226 generated the §13 session-log index (was a ~58-line
|
|
147
|
+
// hand-maintained list), then 310→300 when task 1283 trimmed the quick-nav
|
|
148
|
+
// table + §2/§8/§9/§10/§12 verbose prose down to ~294 lines. Lower further as
|
|
149
|
+
// more detail relocates to nested docs / ADRs (ADR 0061).
|
|
150
|
+
const ROOT_TARGET = 200; // the charter's stated aim; keep ratcheting toward it.
|
|
151
|
+
|
|
152
|
+
// The line count is a structure/readability proxy; the cost paid EVERY session is
|
|
153
|
+
// characters/tokens. A 600-char quick-nav row is one line but ~150 tokens, so
|
|
154
|
+
// within-line bloat slips past ROOT_HARD_MAX entirely (exactly what task 1283
|
|
155
|
+
// trimmed). Guard the real cost too (task 1286). Ratcheted just above the
|
|
156
|
+
// post-1283 size (~39.5K chars / ~9.9K tok); lower alongside ROOT_HARD_MAX as
|
|
157
|
+
// detail relocates to nested docs / ADRs.
|
|
158
|
+
const ROOT_CHAR_HARD_MAX = 42000; // ~10.5K tokens — the always-loaded file must never balloon past this.
|
|
159
|
+
const ROOT_CHAR_TARGET = 32000; // ~8K tokens — aspirational, mirrors the ~200-line aim.
|
|
160
|
+
const estTokens = (n) => Math.round(n / 4); // ~4 chars/token for markdown prose.
|
|
161
|
+
|
|
162
|
+
function checkClaudeMdBudget(opts = {}) {
|
|
163
|
+
const p = path.join(INSTANCE_ROOT, 'CLAUDE.md'); // the root CLAUDE.md is INSTANCE content (materialized per instance; ADR 0108)
|
|
164
|
+
const text = opts.text != null ? opts.text : fs.readFileSync(p, 'utf8');
|
|
165
|
+
const lines = text.split('\n').length;
|
|
166
|
+
const violations = [];
|
|
167
|
+
const warnings = [];
|
|
168
|
+
if (lines > ROOT_HARD_MAX) {
|
|
169
|
+
violations.push(`CLAUDE.md is ${lines} lines — over the ${ROOT_HARD_MAX}-line hard ceiling. It loads in full every session; relocate detail to a nested CLAUDE.md / ADR / recipe (ADR 0061).`);
|
|
170
|
+
} else if (lines > ROOT_TARGET) {
|
|
171
|
+
warnings.push(`CLAUDE.md is ${lines} lines — over the ~${ROOT_TARGET}-line target (still under the ${ROOT_HARD_MAX} ceiling). Relocate more detail to nested CLAUDE.md / ADRs / recipes (ADR 0061), then lower ROOT_HARD_MAX as it shrinks.`);
|
|
172
|
+
}
|
|
173
|
+
const chars = text.length;
|
|
174
|
+
const tok = estTokens(chars);
|
|
175
|
+
if (chars > ROOT_CHAR_HARD_MAX) {
|
|
176
|
+
violations.push(`CLAUDE.md is ${chars} chars (~${tok} tokens) — over the ${ROOT_CHAR_HARD_MAX}-char (~${estTokens(ROOT_CHAR_HARD_MAX)}-token) hard ceiling. The line count may be fine, but the per-session token cost is not — trim verbose prose WITHIN lines (long quick-nav rows, restated rationale), not just whole lines (ADR 0061).`);
|
|
177
|
+
} else if (chars > ROOT_CHAR_TARGET) {
|
|
178
|
+
warnings.push(`CLAUDE.md is ${chars} chars (~${tok} tokens) — over the ~${ROOT_CHAR_TARGET}-char (~${estTokens(ROOT_CHAR_TARGET)}-token) target. This is the true per-session cost; within-line bloat (a 600-char table row is one line) escapes the line check. Trim prose, then lower ROOT_CHAR_HARD_MAX.`);
|
|
179
|
+
}
|
|
180
|
+
if (!text.includes('## Working with this file (content charter)')) {
|
|
181
|
+
violations.push('CLAUDE.md is missing the content-charter section (ADR 0061) — the per-line test + "what goes where" table must stay.');
|
|
182
|
+
}
|
|
183
|
+
return { name: 'root CLAUDE.md budget + charter', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings, note: `≤ ${ROOT_HARD_MAX} lines / ${ROOT_CHAR_HARD_MAX} chars (~${estTokens(ROOT_CHAR_HARD_MAX)} tok); targets ~${ROOT_TARGET} lines / ~${estTokens(ROOT_CHAR_TARGET)} tok; charter present.` };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// Check 3 — nested per-module CLAUDE.md presence for high-traffic dirs (ADR 0061)
|
|
188
|
+
// ============================================================================
|
|
189
|
+
const HIGH_TRAFFIC = ['src/bongos', 'src/bongos/routes', 'scripts/gds'];
|
|
190
|
+
|
|
191
|
+
function checkNestedDocsPresent() {
|
|
192
|
+
const violations = [];
|
|
193
|
+
for (const dir of HIGH_TRAFFIC) {
|
|
194
|
+
// HIGH_TRAFFIC dirs (src/bongos, src/bongos/routes, scripts/gds) are CORE-package dirs — resolve
|
|
195
|
+
// against the core root, not the instance repo (ADR 0108 §Consequences #4).
|
|
196
|
+
const doc = path.join(CORE_ROOT, dir, 'CLAUDE.md');
|
|
197
|
+
if (!fs.existsSync(doc)) violations.push(`${dir}/CLAUDE.md is missing — high-traffic dirs must carry a nested altitude map (ADR 0061).`);
|
|
198
|
+
}
|
|
199
|
+
return { name: 'nested CLAUDE.md presence', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: `present for: ${HIGH_TRAFFIC.join(', ')}.` };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Check 4 — no route bypasses server-side rank checks (ADR 0016) [reuses route-rank-check]
|
|
204
|
+
// ============================================================================
|
|
205
|
+
function checkRankGuards() {
|
|
206
|
+
const r = checkRouteRanks();
|
|
207
|
+
// hardFail = an ungated WRITE route or a downgraded privileged route. Unclassified read routes
|
|
208
|
+
// are 'concerns' (the Hacker adjudicates intent) — surfaced as warnings, not a hard failure.
|
|
209
|
+
const violations = r.findings.filter((f) => f.severity === 'blocker').map((f) => `${f.route} (${f.file}) — ${f.summary}`);
|
|
210
|
+
const warnings = r.findings.filter((f) => f.severity !== 'blocker').map((f) => `${f.route} (${f.file}) — ${f.summary}`);
|
|
211
|
+
return { name: 'route rank guards (ADR 0016)', ok: !r.hardFail, hardFail: r.hardFail, violations, warnings, note: `${r.routeCount} routes classify to a known rank; no ungated writes, no privileged downgrades.` };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// Check 5 — generated artifacts are fresh (Path C enforces Path B; ADR 0063 + ADR 0062 §8)
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Deterministic file-reads with a `--check` mode (regenerate in-memory, diff, exit non-zero on
|
|
218
|
+
// drift). This gate fails a PR that committed a stale artifact so the "generate, don't hand-maintain"
|
|
219
|
+
// contract can't rot.
|
|
220
|
+
// ADR 0110 (task 1926): docs/repo-map.md + docs/session-log-index.md are now GITIGNORED and
|
|
221
|
+
// regenerated at deploy/box-fetch (the gen-atlas.js model), so parallel branches never collide on
|
|
222
|
+
// them — and there is no committed copy to gate. Their generators are DUAL-DUTY, though: each also
|
|
223
|
+
// refreshes a TRACKED embedded block (the nested CLAUDE.md symbol blocks; the CLAUDE.md §13 snippet).
|
|
224
|
+
// Those blocks stay committed, so we gate the BLOCK bucket HARD via `--check-block`. A naive `--check`
|
|
225
|
+
// here would false-fail every PR (the gitignored whole file is absent from a clean checkout, so it
|
|
226
|
+
// always reads as "would change"); `--check-block` is the exact, non-noisy gate. (docs/file-map.md +
|
|
227
|
+
// the OpenAPI docs are still fully committed and unchanged by ADR 0110 — they keep the plain --check.)
|
|
228
|
+
function checkGeneratedArtifactsFresh() {
|
|
229
|
+
const violations = [];
|
|
230
|
+
try {
|
|
231
|
+
execFileSync(process.execPath, [path.join(__dirname, 'gen-repo-map.js'), '--check-block'], { cwd: REPO_ROOT, stdio: 'pipe' });
|
|
232
|
+
} catch {
|
|
233
|
+
violations.push('The tracked nested-doc symbol blocks (src/bongos/CLAUDE.md, src/bongos/routes/CLAUDE.md, scripts/gds/CLAUDE.md) are STALE — run `node scripts/gds/gen-repo-map.js` and commit (ADR 0063 / ADR 0110). NB docs/repo-map.md itself is gitignored + deploy-regenerated, not gated here.');
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
execFileSync(process.execPath, [path.join(__dirname, 'gen-session-index.js'), '--check-block'], { cwd: REPO_ROOT, stdio: 'pipe' });
|
|
237
|
+
} catch {
|
|
238
|
+
violations.push('The tracked CLAUDE.md §13 recent-sessions snippet is STALE — run `node scripts/gds/gen-session-index.js` and commit (ADR 0062 §8 / ADR 0110). NB docs/session-log-index.md itself is gitignored + deploy-regenerated, not gated here.');
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
execFileSync(process.execPath, [path.join(__dirname, 'gen-file-map.js'), '--check'], { cwd: REPO_ROOT, stdio: 'pipe' });
|
|
242
|
+
} catch {
|
|
243
|
+
violations.push('docs/file-map.md generated sections (.claude/skills/, .claude/scheduled-tasks/) are STALE or a skill/routine has no note in docs/file-map.notes.json — run `node scripts/gds/gen-file-map.js` and add any missing note, then commit (ADR 0066).');
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
// Also structurally validates the spec (exit 1 on invalid OpenAPI 3.1) beyond the drift check (exit 2). Task 1918.
|
|
247
|
+
execFileSync(process.execPath, [path.join(__dirname, 'gen-api-docs.js'), '--check'], { cwd: REPO_ROOT, stdio: 'pipe' });
|
|
248
|
+
} catch {
|
|
249
|
+
violations.push('docs/api/openapi.json + docs/api-reference.md + docs/routes-permissions.md are STALE (or the spec is not valid OpenAPI 3.1) — run `node scripts/gds/gen-api-docs.js` and commit (task 1918).');
|
|
250
|
+
}
|
|
251
|
+
return { name: 'generated artifacts fresh', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: 'Tracked generated content matches source: nested CLAUDE.md symbol blocks (code) + the CLAUDE.md §13 snippet (sessions) via --check-block, plus docs/file-map.md skills/scheduled-tasks sections and docs/api/openapi.json + api-reference.md (API). The gitignored docs/repo-map.md + docs/session-log-index.md are deploy-regenerated (ADR 0110), not gated here.' };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---- deterministic cron routine boundary (System 4 / ADR 0078) -------------
|
|
255
|
+
//
|
|
256
|
+
// A `mode: deterministic` routine runs as plain node via run-routine.js — NO
|
|
257
|
+
// model session reads its SKILL body. So that body must not rot back into a
|
|
258
|
+
// wrapped session: it may not instruct a model to mutate (Edit/Write/git
|
|
259
|
+
// push/commit/deploy) or synthesize free text, and its documented commands must
|
|
260
|
+
// not diverge from its declared steps. Keeps the markdown-graveyard pattern out.
|
|
261
|
+
|
|
262
|
+
const SCHEDULED_TASKS_DIR = path.join(INSTANCE_ROOT, '.claude', 'scheduled-tasks'); // .claude/ is materialized INSTANCE content (ADR 0108 §3)
|
|
263
|
+
|
|
264
|
+
// Judgment/mutation instructions illegal in a deterministic body. Negated
|
|
265
|
+
// mentions ("never pushes", "does NOT edit") are descriptive boundaries — allowed.
|
|
266
|
+
const JUDGMENT_PATTERNS = [
|
|
267
|
+
{ re: /\bgit\s+(push|commit|add)\b/i, label: 'a git push/commit/add' },
|
|
268
|
+
{ re: /deploy\.sh\b/i, label: 'a deploy.sh invocation' },
|
|
269
|
+
{ re: /`(Edit|Write|MultiEdit|NotebookEdit)`/, label: 'an Edit/Write tool instruction' },
|
|
270
|
+
{ re: /\bsynthesi[sz]e\b/i, label: 'free-text synthesis' },
|
|
271
|
+
{ re: /\b(write|compose|draft)\s+(a|an|the)\s+(summary|report|analysis|narrative|note)\b/i, label: 'free-text synthesis' },
|
|
272
|
+
];
|
|
273
|
+
const NEGATION_RE = /\b(never|not|no|without|cannot|can't|don'?t|doesn'?t|won'?t)\b/i;
|
|
274
|
+
|
|
275
|
+
// Pure: scan one routine's parsed (mode, steps, body). Returns violation strings.
|
|
276
|
+
// Only inspects deterministic routines; others return []. Exported for tests.
|
|
277
|
+
function scanDeterministicBody(name, { mode, steps, body }) {
|
|
278
|
+
if (mode !== 'deterministic') return [];
|
|
279
|
+
const violations = [];
|
|
280
|
+
if (!steps || steps.length === 0) {
|
|
281
|
+
violations.push(`${name}: mode: deterministic but no steps: — run-routine.js would launch nothing (ADR 0078).`);
|
|
282
|
+
}
|
|
283
|
+
const text = String(body == null ? '' : body);
|
|
284
|
+
for (const raw of text.split('\n')) {
|
|
285
|
+
const line = raw.trim();
|
|
286
|
+
if (!line || NEGATION_RE.test(line)) continue; // skip blank + negated (boundary) lines
|
|
287
|
+
for (const p of JUDGMENT_PATTERNS) {
|
|
288
|
+
if (p.re.test(line)) {
|
|
289
|
+
violations.push(`${name}: deterministic SKILL body contains ${p.label} ("${line.slice(0, 70)}") — it runs as plain node with no model; move remediation/judgment to a separate claimed task (ADR 0078).`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Divergence: every `node …` command in a fenced code block must be a declared step.
|
|
294
|
+
const stepCmds = (steps || []).map((s) => String(s.command).replace(/\s+/g, ' ').trim());
|
|
295
|
+
for (const fence of text.match(/```[\s\S]*?```/g) || []) {
|
|
296
|
+
for (const fl of fence.split('\n')) {
|
|
297
|
+
const cmd = fl.trim().replace(/\s+/g, ' ');
|
|
298
|
+
if (!cmd.startsWith('node ')) continue;
|
|
299
|
+
if (!stepCmds.some((s) => cmd === s || cmd.startsWith(s) || s.startsWith(cmd))) {
|
|
300
|
+
violations.push(`${name}: deterministic SKILL body documents \`${cmd.slice(0, 70)}\` which is not in its steps: — prose and steps must not diverge (ADR 0078).`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return violations;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function checkDeterministicRoutines() {
|
|
308
|
+
const violations = [];
|
|
309
|
+
let detCount = 0;
|
|
310
|
+
let dirs = [];
|
|
311
|
+
try {
|
|
312
|
+
dirs = fs.readdirSync(SCHEDULED_TASKS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
313
|
+
} catch (_) { /* no scheduled-tasks dir → nothing to check */ }
|
|
314
|
+
for (const name of dirs) {
|
|
315
|
+
let md;
|
|
316
|
+
try { md = fs.readFileSync(path.join(SCHEDULED_TASKS_DIR, name, 'SKILL.md'), 'utf8'); } catch (_) { continue; }
|
|
317
|
+
const { mode, steps } = parseRoutineFrontmatter(md);
|
|
318
|
+
if (mode !== 'deterministic') continue;
|
|
319
|
+
detCount += 1;
|
|
320
|
+
const body = md.replace(/^---\n[\s\S]*?\n---\n?/, ''); // strip frontmatter
|
|
321
|
+
violations.push(...scanDeterministicBody(name, { mode, steps, body }));
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
name: 'deterministic cron routines (ADR 0078)',
|
|
325
|
+
ok: violations.length === 0,
|
|
326
|
+
hardFail: violations.length > 0,
|
|
327
|
+
violations,
|
|
328
|
+
warnings: [],
|
|
329
|
+
note: `${detCount} mode:deterministic routine(s): steps declared, no judgment instructions, prose↔steps aligned.`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ============================================================================
|
|
334
|
+
// Check 7/8 — module boundary (the one-way rule, ADR 0083 / design 04§3)
|
|
335
|
+
// ============================================================================
|
|
336
|
+
//
|
|
337
|
+
// The module system's one-way rule: a module imports ONLY src/module-api.js (the
|
|
338
|
+
// published doorway); the core NEVER imports a module; a module NEVER imports a
|
|
339
|
+
// sibling module. These two checks enforce it.
|
|
340
|
+
//
|
|
341
|
+
// Both were GREEN through the strangler: dev-box + art-pipeline (the M07/M08
|
|
342
|
+
// moves) import only the doorway, and the core imports neither. The boundary
|
|
343
|
+
// shipped in WARN mode so a half-moved module mid-migration couldn't red the
|
|
344
|
+
// build; BV1.R45 (task 1414) flips it to ENFORCING now that the first move
|
|
345
|
+
// (dev-box, M07) proved clean — the monotonic ratchet (ADR 0062 §9 / design
|
|
346
|
+
// 04§3). A NEW cross-boundary import is now a red build; warn-mode is gone.
|
|
347
|
+
let MODULE_BOUNDARY_ENFORCING = true;
|
|
348
|
+
const MODULE_API_REL = 'src/module-api.js';
|
|
349
|
+
|
|
350
|
+
// If `spec` (a relative require) from `fromAbsFile` resolves into modules/<key>/,
|
|
351
|
+
// return <key>; else null. Pure path math — no fs.
|
|
352
|
+
function resolvesToModule(fromAbsFile, spec) {
|
|
353
|
+
const abs = path.resolve(path.dirname(fromAbsFile), spec);
|
|
354
|
+
const r = rel(abs);
|
|
355
|
+
const m = /^modules\/([^/]+)(\/|$)/.exec(r);
|
|
356
|
+
return m ? m[1] : null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// The module a core/module file belongs to, or null if it isn't under modules/.
|
|
360
|
+
function moduleOwnerOf(absFile) {
|
|
361
|
+
const m = /^modules\/([^/]+)\//.exec(rel(absFile));
|
|
362
|
+
return m ? m[1] : null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// For a require `spec` inside a MODULE file, return a violation string or null.
|
|
366
|
+
// Allowed: src/module-api.js (the doorway), intra-module relatives, and bare
|
|
367
|
+
// (npm/node) specs. Forbidden: deep core imports + sibling-module imports.
|
|
368
|
+
function moduleImportViolation(fromAbsFile, spec) {
|
|
369
|
+
const ownKey = moduleOwnerOf(fromAbsFile);
|
|
370
|
+
if (!ownKey) return null; // not a module file
|
|
371
|
+
const absTarget = path.resolve(path.dirname(fromAbsFile), spec);
|
|
372
|
+
const r = rel(absTarget);
|
|
373
|
+
// resolve possible file forms
|
|
374
|
+
const forms = [r, r + '.js', r.replace(/\/$/, '') + '/index.js'];
|
|
375
|
+
if (forms.includes(MODULE_API_REL)) return null; // the doorway — allowed
|
|
376
|
+
const otherKey = (() => { const m = /^modules\/([^/]+)(\/|$)/.exec(r); return m ? m[1] : null; })();
|
|
377
|
+
if (otherKey) {
|
|
378
|
+
if (otherKey === ownKey) return null; // intra-module — allowed
|
|
379
|
+
return `${rel(fromAbsFile)} → require('${spec}') reaches sibling module "${otherKey}" — modules must not import each other (use a kernel seam; ADR 0083).`;
|
|
380
|
+
}
|
|
381
|
+
if (/^src\//.test(r)) {
|
|
382
|
+
return `${rel(fromAbsFile)} → require('${spec}') reaches core internals (${r}) — a module may import only the published doorway src/module-api.js (ADR 0083).`;
|
|
383
|
+
}
|
|
384
|
+
return null; // outside the repo (node/npm) — not our concern
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function checkCoreNeverImportsModule() {
|
|
388
|
+
const findings = [];
|
|
389
|
+
const roots = [path.join(REPO_ROOT, 'src'), path.join(REPO_ROOT, 'server.js')];
|
|
390
|
+
for (const root of roots) {
|
|
391
|
+
for (const abs of walkJs(root)) {
|
|
392
|
+
if (moduleOwnerOf(abs)) continue; // module files are scanned by the other check
|
|
393
|
+
if (HOST_GAME.some((re) => re.test(rel(abs)))) continue; // game-owned files that live in src/ (e.g. preview-server.js) may import game module
|
|
394
|
+
for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
|
|
395
|
+
const t = raw.trim();
|
|
396
|
+
if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) continue;
|
|
397
|
+
let m; REQUIRE_RE.lastIndex = 0;
|
|
398
|
+
while ((m = REQUIRE_RE.exec(raw))) {
|
|
399
|
+
const key = resolvesToModule(abs, m[1]);
|
|
400
|
+
if (key) findings.push(`${rel(abs)} → require('${m[1]}') reaches module "${key}" — the core must never import a module (ADR 0083).`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const enforcing = MODULE_BOUNDARY_ENFORCING;
|
|
406
|
+
return {
|
|
407
|
+
name: 'core never imports a module',
|
|
408
|
+
ok: findings.length === 0,
|
|
409
|
+
hardFail: enforcing && findings.length > 0,
|
|
410
|
+
violations: enforcing ? findings : [],
|
|
411
|
+
warnings: enforcing ? [] : findings,
|
|
412
|
+
note: `core (src/, server.js) must not require modules/* — ${enforcing ? 'ENFORCING' : 'warn-mode until BV1.R45'} (ADR 0083).`,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function checkModulesImportOnlyApi() {
|
|
417
|
+
const findings = [];
|
|
418
|
+
for (const abs of walkJsAllRoots('modules')) { // modules ship from BOTH roots (ADR 0108 §Consequences #4)
|
|
419
|
+
for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
|
|
420
|
+
const t = raw.trim();
|
|
421
|
+
if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) continue;
|
|
422
|
+
let m; REQUIRE_RE.lastIndex = 0;
|
|
423
|
+
while ((m = REQUIRE_RE.exec(raw))) {
|
|
424
|
+
const v = moduleImportViolation(abs, m[1]);
|
|
425
|
+
if (v) findings.push(v);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const enforcing = MODULE_BOUNDARY_ENFORCING;
|
|
430
|
+
return {
|
|
431
|
+
name: 'modules import only the published API',
|
|
432
|
+
ok: findings.length === 0,
|
|
433
|
+
hardFail: enforcing && findings.length > 0,
|
|
434
|
+
violations: enforcing ? findings : [],
|
|
435
|
+
warnings: enforcing ? [] : findings,
|
|
436
|
+
note: `modules/** may require only src/module-api.js (no deep core, no sibling) — ${enforcing ? 'ENFORCING' : 'warn-mode until BV1.R45'} (ADR 0083).`,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ============================================================================
|
|
441
|
+
// Check 9 — the goal scope map imports PROTECTED_GLOBS, never re-declares it
|
|
442
|
+
// (ADR 0086 §7; the SEC #863 "second list that drifts" failure mode)
|
|
443
|
+
// ============================================================================
|
|
444
|
+
//
|
|
445
|
+
// src/bongos/module-scope-map.js (BV1.R54) derives each module's `protected` flag
|
|
446
|
+
// from PROTECTED_GLOBS. That derivation is only trustworthy if it reads the ONE
|
|
447
|
+
// source (permission-path-check.js) — a hand-copied second list would silently
|
|
448
|
+
// drift from the real protected set and quietly de-protect a module. This check
|
|
449
|
+
// reds the build if the scope map stops importing PROTECTED_GLOBS, or copies it.
|
|
450
|
+
const SCOPE_MAP_REL = 'src/bongos/module-scope-map.js';
|
|
451
|
+
|
|
452
|
+
// Pure: scan the scope-map source for the import contract. Exported for the
|
|
453
|
+
// "actually catches a violation" test (mirrors scanDeterministicBody).
|
|
454
|
+
function scanScopeMapSource(src) {
|
|
455
|
+
const violations = [];
|
|
456
|
+
const text = String(src || '');
|
|
457
|
+
const importsFrom = /require\(\s*['"]\.\/permission-path-check['"]\s*\)/.test(text);
|
|
458
|
+
const namesIt = /\bPROTECTED_GLOBS\b/.test(text);
|
|
459
|
+
if (!importsFrom || !namesIt) {
|
|
460
|
+
violations.push(`${SCOPE_MAP_REL} must IMPORT PROTECTED_GLOBS from ./permission-path-check and derive \`protected\` from it — never hand-maintain a second protected list (ADR 0086 §7).`);
|
|
461
|
+
}
|
|
462
|
+
if (/\b(?:const|let|var)\s+PROTECTED_GLOBS\s*=/.test(text)) {
|
|
463
|
+
violations.push(`${SCOPE_MAP_REL} re-declares PROTECTED_GLOBS — it must import the single source from permission-path-check.js, not copy it (ADR 0086 §7; the SEC #863 second-list failure mode).`);
|
|
464
|
+
}
|
|
465
|
+
return violations;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function checkScopeMapImportsProtected() {
|
|
469
|
+
const abs = path.join(REPO_ROOT, SCOPE_MAP_REL);
|
|
470
|
+
let src = null;
|
|
471
|
+
try { src = fs.readFileSync(abs, 'utf8'); } catch (_) { /* absent → skip */ }
|
|
472
|
+
// Absent → clean pass: the scope map is a BV1.R54 deliverable; until it exists
|
|
473
|
+
// there is no import contract to enforce (its own unit test asserts existence).
|
|
474
|
+
const violations = src == null ? [] : scanScopeMapSource(src);
|
|
475
|
+
return {
|
|
476
|
+
name: 'scope map imports PROTECTED_GLOBS (ADR 0086)',
|
|
477
|
+
ok: violations.length === 0,
|
|
478
|
+
hardFail: violations.length > 0,
|
|
479
|
+
violations,
|
|
480
|
+
warnings: [],
|
|
481
|
+
note: src == null
|
|
482
|
+
? `${SCOPE_MAP_REL} absent — skipped (created by BV1.R54).`
|
|
483
|
+
: `${SCOPE_MAP_REL} derives \`protected\` from the imported PROTECTED_GLOBS (single source; no copied list).`,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ============================================================================
|
|
488
|
+
// Check 10 — core carries no module-specific code (ADR 0083 §Decision #4 / design 04§3)
|
|
489
|
+
// ============================================================================
|
|
490
|
+
//
|
|
491
|
+
// The module-system MACHINERY (the doorway + loader + seam registry) must stay
|
|
492
|
+
// module-AGNOSTIC: it discovers, validates, and mounts modules uniformly and may
|
|
493
|
+
// never special-case one by name. The import checks (Check 7/8) stop a module's
|
|
494
|
+
// CODE from being reached by `require`; this stops a module's IDENTITY from being
|
|
495
|
+
// hardcoded into the kernel (`if (key === 'dev-box') …`, a copied module helper,
|
|
496
|
+
// a registry entry that should have moved out). "No module-specific code" is
|
|
497
|
+
// operationalized as the one drift-free signal — the kernel names no discovered
|
|
498
|
+
// module KEY (the canonical id from modules/<key>/module.json). Domain terms are
|
|
499
|
+
// deliberately NOT pattern-matched: they're non-deterministic and the doorway
|
|
500
|
+
// legitimately re-exports capability accessors (getBuilderGeminiKeyRow, …); the
|
|
501
|
+
// KEY is the precise thing a generic kernel never needs to write.
|
|
502
|
+
//
|
|
503
|
+
// Scope (BV1.R70 / ADR 0091 §1): the real ~3.5k-LOC TRUST KERNEL — the module-
|
|
504
|
+
// system machinery PLUS the trust primitives + data substrate + config/identity
|
|
505
|
+
// the rest of core depends on. This replaces the original 6-file machinery-only
|
|
506
|
+
// roster (the kernel was unbounded before — the audit's finding). Deliberately
|
|
507
|
+
// EXCLUDED: the still-fused monolith remnants (src/bongos/db.js, routes/me.js,
|
|
508
|
+
// routes.js, server.js) still carry un-stranglered module seams by design
|
|
509
|
+
// (lifecycle + tranche-2 domains leave later) — scanning them would red the
|
|
510
|
+
// build on intentional state. Comment lines are skipped (a key named in a
|
|
511
|
+
// doc/example comment is documentation, not code), and the scope-map registry is
|
|
512
|
+
// exempt from the no-module-keys scan (see KERNEL_NAMES_MODULES_OK).
|
|
513
|
+
const KERNEL_FILES = [
|
|
514
|
+
// module-system machinery (the original roster)
|
|
515
|
+
'src/module-api.js', // the published doorway
|
|
516
|
+
'src/modules.js', // the registry/resolver facade
|
|
517
|
+
'src/module-seams.js', // the seam registry (ports/events/contributions)
|
|
518
|
+
'src/module-loader/loader.js', // the disk loader (discover → validate → mount)
|
|
519
|
+
'src/module-loader/manifest-schema.js', // the manifest validator
|
|
520
|
+
'src/module-loader/semver.js', // coreVersion compatibility
|
|
521
|
+
'src/bongos/module-scope-map.js', // the module→path registry (it NAMES every module — exempt below)
|
|
522
|
+
// trust primitives (ADR 0091 §1) — the authz boundary + data substrate
|
|
523
|
+
'src/bongos/auth.js', // authz gates (requireBuilder/requireRank, ADR 0016)
|
|
524
|
+
'src/bongos/route-rank-check.js', // deterministic route→rank classification
|
|
525
|
+
'src/bongos/permission-path-check.js', // protected-path matcher (PROTECTED_GLOBS, ADR 0043)
|
|
526
|
+
'src/bongos/goal-scope-check.js', // goal create-gate scope wall (composes the 3 matchers above; ADR 0086 §3, BV1.R60)
|
|
527
|
+
'src/bongos/secret-box.js', // AES-256-GCM per-builder secret box
|
|
528
|
+
'src/bongos/pool.js', // the singleton Postgres pool accessor
|
|
529
|
+
'src/bongos/db-kernel.js', // the kernel db slice — identity/audit/rank/withTx (BV1.R69)
|
|
530
|
+
'src/bongos/routes/_helpers.js', // shared request validators the doorway re-exports
|
|
531
|
+
// config / identity (host context a module reads, never hardcodes)
|
|
532
|
+
'src/instance-config.js',
|
|
533
|
+
'src/branding.js',
|
|
534
|
+
'src/build-info.js',
|
|
535
|
+
// BV1.R86: the git-remote/slug + publish-target identity the doorway exposes
|
|
536
|
+
// (api.repoInfo) for the carved lifecycle github-push/conflict-resolve. It is
|
|
537
|
+
// KERNEL (already in the module-scope-map kernel key) and imports only the
|
|
538
|
+
// kernel (instance-config) + node built-ins, so it joins the roster cleanly.
|
|
539
|
+
'src/bongos/repo-info.js',
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
// Kernel files exempt from the no-module-keys scan (Check 10) because naming a
|
|
543
|
+
// module is, for them, the JOB — they stay in KERNEL_FILES for the import check
|
|
544
|
+
// (Check 12), which guards dependency *direction* (a much stronger guarantee than
|
|
545
|
+
// "never write the string 'dev-box'"). Two legitimate reasons a kernel file names
|
|
546
|
+
// a module:
|
|
547
|
+
// • the REGISTRY — module-scope-map.js IS the module→path map (and
|
|
548
|
+
// checkScopeMapImportsProtected already guards how it derives `protected`).
|
|
549
|
+
// • the TRUST BOUNDARY — auth.js carries the Discord OAuth account-LINK flow
|
|
550
|
+
// (core by design — see modules/discord/CLAUDE.md "NOT in this module"); and
|
|
551
|
+
// permission-path-check.js / route-rank-check.js MUST name module path
|
|
552
|
+
// subtrees ('modules/dev-box/', …) to protect + rank-gate them. Forbidding
|
|
553
|
+
// these the string would force the trust layer to stop protecting module
|
|
554
|
+
// surfaces — exactly backwards. The generic machinery + data + config files
|
|
555
|
+
// (module-api, the loader, db-kernel, secret-box, pool, _helpers, branding,
|
|
556
|
+
// instance-config, build-info) are NOT exempt and must stay module-agnostic.
|
|
557
|
+
const KERNEL_NAMES_MODULES_OK = new Set([
|
|
558
|
+
'src/bongos/module-scope-map.js', // the module→path registry
|
|
559
|
+
'src/bongos/auth.js', // Discord OAuth account-link flow (core by design)
|
|
560
|
+
'src/bongos/permission-path-check.js', // names module path subtrees to PROTECT them
|
|
561
|
+
'src/bongos/route-rank-check.js', // names module route dirs to rank-gate them
|
|
562
|
+
]);
|
|
563
|
+
|
|
564
|
+
// The discovered module keys = the directory names under modules/ (a manifest's
|
|
565
|
+
// `key` must equal its dir name — manifest-schema). Empty before any move. With no explicit
|
|
566
|
+
// `modulesDir` it UNIONS every module root (core package + instance repo, ADR 0108 §Consequences
|
|
567
|
+
// #4); an explicit dir keeps the old single-dir behavior (tests). Deduped — a key resolves to
|
|
568
|
+
// exactly one root (the loader errors on a cross-root collision), and today the roots coincide.
|
|
569
|
+
function _moduleKeysIn(modulesDir) {
|
|
570
|
+
try {
|
|
571
|
+
return fs.readdirSync(modulesDir, { withFileTypes: true })
|
|
572
|
+
.filter((d) => d.isDirectory())
|
|
573
|
+
.map((d) => d.name);
|
|
574
|
+
} catch { return []; }
|
|
575
|
+
}
|
|
576
|
+
function discoveredModuleKeys(modulesDir) {
|
|
577
|
+
if (modulesDir) return _moduleKeysIn(modulesDir);
|
|
578
|
+
const keys = new Set();
|
|
579
|
+
for (const root of MODULE_ROOTS) for (const k of _moduleKeysIn(path.join(root, 'modules'))) keys.add(k);
|
|
580
|
+
return [...keys];
|
|
581
|
+
}
|
|
582
|
+
// Absolute path to a discovered module's dir, resolving WHICH root holds it (core package or
|
|
583
|
+
// instance repo). Today both roots coincide → the single checkout.
|
|
584
|
+
function moduleDir(key) {
|
|
585
|
+
for (const root of MODULE_ROOTS) {
|
|
586
|
+
const d = path.join(root, 'modules', key);
|
|
587
|
+
if (fs.existsSync(d)) return d;
|
|
588
|
+
}
|
|
589
|
+
return path.join(CORE_ROOT, 'modules', key);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Match a module key as a DELIMITED token so `dev-box` hits in `'dev-box'` /
|
|
593
|
+
// `mount('dev-box')` but never inside `dev-boxes` or `xdev-box`. Hyphens are
|
|
594
|
+
// non-word chars, so the boundary guards against word-chars AND hyphens on both
|
|
595
|
+
// sides (no lookbehind — portable on every node the unit gate runs).
|
|
596
|
+
function keyTokenRe(key) {
|
|
597
|
+
const esc = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
598
|
+
return new RegExp(`(^|[^\\w-])${esc}($|[^\\w-])`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Pure: scan kernel source text for any hardcoded module key, skipping comment
|
|
602
|
+
// lines. Returns violation strings (one per hit). Exported for the negative test
|
|
603
|
+
// (mirrors scanScopeMapSource / scanDeterministicBody).
|
|
604
|
+
function scanKernelForModuleKeys(text, keys) {
|
|
605
|
+
const violations = [];
|
|
606
|
+
if (!keys || keys.length === 0) return violations;
|
|
607
|
+
const probes = keys.map((k) => ({ key: k, re: keyTokenRe(k) }));
|
|
608
|
+
String(text == null ? '' : text).split('\n').forEach((raw, i) => {
|
|
609
|
+
const t = raw.trim();
|
|
610
|
+
if (!t || t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) return;
|
|
611
|
+
for (const { key, re } of probes) {
|
|
612
|
+
if (re.test(raw)) {
|
|
613
|
+
violations.push(`names module "${key}" at line ${i + 1} — the kernel/module-system machinery must stay module-agnostic; no module key may be hardcoded in core (ADR 0083 §Decision #4).`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
return violations;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function checkCoreCarriesNoModuleCode() {
|
|
621
|
+
const keys = discoveredModuleKeys();
|
|
622
|
+
const violations = [];
|
|
623
|
+
for (const relFile of KERNEL_FILES) {
|
|
624
|
+
if (KERNEL_NAMES_MODULES_OK.has(relFile)) continue; // the registry legitimately names every module
|
|
625
|
+
let text;
|
|
626
|
+
try { text = fs.readFileSync(path.join(REPO_ROOT, relFile), 'utf8'); } catch { continue; }
|
|
627
|
+
for (const v of scanKernelForModuleKeys(text, keys)) violations.push(`${relFile} ${v}`);
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
name: 'core carries no module code',
|
|
631
|
+
ok: violations.length === 0,
|
|
632
|
+
hardFail: violations.length > 0,
|
|
633
|
+
violations,
|
|
634
|
+
warnings: [],
|
|
635
|
+
note: `kernel machinery (${KERNEL_FILES.length} files) names no discovered module key${keys.length ? ` (keys: ${keys.join(', ')})` : ' (none yet)'} (ADR 0083 §Decision #4).`,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ============================================================================
|
|
640
|
+
// Check 12 — the kernel imports no domain (ADR 0091 §1 / BV1.R70)
|
|
641
|
+
// ============================================================================
|
|
642
|
+
//
|
|
643
|
+
// A bounded kernel is only real if it DEPENDS on nothing but itself. This check
|
|
644
|
+
// walks every KERNEL_FILE's relative requires and flags any that reach a
|
|
645
|
+
// non-kernel core file (a soon-to-be-module domain). Kernel→kernel is fine;
|
|
646
|
+
// kernel→grandfathered-infra (the raw base pg pool, the pure path-match util) is
|
|
647
|
+
// fine; a bare npm/node spec is fine. Everything else is a tracked leak.
|
|
648
|
+
//
|
|
649
|
+
// ENFORCING (ADR 0102): the tranche-2 ratchet reached zero, so this is now a hard
|
|
650
|
+
// gate — the kernel imports only itself + grandfathered infra, and any NEW
|
|
651
|
+
// kernel→domain import fails the build (violations). The last leaks cleared were
|
|
652
|
+
// auth.js→db (carved to db-kernel), the doorway's Gemini-key re-exports (→ the
|
|
653
|
+
// art-pipeline 'art.getBuilderGeminiKeyRow' port) and builderRollingAvgGrade (→ the
|
|
654
|
+
// grading 'grade.builderRollingAvgGrade' port), plus reclassifying llm-cache /
|
|
655
|
+
// llm-pricing as kernel-adjacent infra. Adding a leak back reds the build.
|
|
656
|
+
|
|
657
|
+
// Grandfathered kernel-adjacent targets a KERNEL_FILE may import without it
|
|
658
|
+
// counting as a domain leak. The still-fused src/bongos/db.js monolith is NOT here:
|
|
659
|
+
// a kernel file reaching into it IS a real (tracked) leak until the identity/
|
|
660
|
+
// session slices finish carving into db-kernel.js.
|
|
661
|
+
const KERNEL_IMPORT_ALLOWLIST = new Set([
|
|
662
|
+
'src/db.js', // the raw shared pg Pool (base infra; pool.js wraps it)
|
|
663
|
+
'src/bongos/path-match.js', // pure touches-overlap matcher (no domain logic)
|
|
664
|
+
// Cross-cutting LLM-cost infra (ADR 0102): a stateless token→USD pricing table
|
|
665
|
+
// (llm-pricing) + a best-effort content-addressed cache (llm-cache), consumed by
|
|
666
|
+
// core, the grading module, and the CLI cost tooling ALIKE — no domain logic and
|
|
667
|
+
// no domain state beyond a cache table. Same category as the pg pool / path-match,
|
|
668
|
+
// not a domain module core would have to depend on anyway. The doorway may import
|
|
669
|
+
// them; they never flip the kernel→domain leak.
|
|
670
|
+
'src/bongos/llm-cache.js',
|
|
671
|
+
'src/bongos/llm-pricing.js',
|
|
672
|
+
]);
|
|
673
|
+
|
|
674
|
+
// Pure: for relative require `spec` from kernel file `fromRel` (repo-relative),
|
|
675
|
+
// return the offending non-kernel target relpath, or null if the import is clean
|
|
676
|
+
// (kernel→kernel, kernel→allowlisted-infra, or a bare npm/node spec). Exported
|
|
677
|
+
// for the "actually catches a leak" negative test (mirrors scanScopeMapSource).
|
|
678
|
+
function kernelDomainImport(fromRel, spec, kernelSet = new Set(KERNEL_FILES), allow = KERNEL_IMPORT_ALLOWLIST) {
|
|
679
|
+
if (!/^\.\.?\//.test(spec)) return null; // bare npm/node spec — not our concern
|
|
680
|
+
const absTarget = path.resolve(path.dirname(path.join(REPO_ROOT, fromRel)), spec);
|
|
681
|
+
const r = rel(absTarget);
|
|
682
|
+
const forms = [r, r + '.js', r.replace(/\/$/, '') + '/index.js'];
|
|
683
|
+
if (forms.some((f) => kernelSet.has(f))) return null; // kernel → kernel
|
|
684
|
+
if (forms.some((f) => allow.has(f))) return null; // grandfathered infra/util
|
|
685
|
+
if (!/^src\//.test(r)) return null; // only flag app-source targets
|
|
686
|
+
return r.endsWith('.js') || r.endsWith('.json') ? r : `${r}.js`; // name the real file
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function checkKernelImportsNoDomain() {
|
|
690
|
+
const kernelSet = new Set(KERNEL_FILES);
|
|
691
|
+
const findings = [];
|
|
692
|
+
for (const relFile of KERNEL_FILES) {
|
|
693
|
+
let text;
|
|
694
|
+
try { text = fs.readFileSync(path.join(REPO_ROOT, relFile), 'utf8'); } catch { continue; }
|
|
695
|
+
for (const raw of text.split('\n')) {
|
|
696
|
+
const t = raw.trim();
|
|
697
|
+
if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) continue;
|
|
698
|
+
let m; REQUIRE_RE.lastIndex = 0;
|
|
699
|
+
while ((m = REQUIRE_RE.exec(raw))) {
|
|
700
|
+
const target = kernelDomainImport(relFile, m[1], kernelSet);
|
|
701
|
+
if (target) findings.push(`${relFile} → require('${m[1]}') reaches non-kernel core file ${target} — a kernel file must depend only on the kernel (ADR 0091 §1). Carve ${target}'s domain out (or promote it into the kernel) to clear this.`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
name: 'kernel imports no domain',
|
|
707
|
+
ok: findings.length === 0,
|
|
708
|
+
hardFail: findings.length > 0, // ENFORCING (ADR 0102): the ratchet reached zero — any new kernel→domain import now reds the build
|
|
709
|
+
violations: findings,
|
|
710
|
+
warnings: [],
|
|
711
|
+
note: `every KERNEL_FILE (${KERNEL_FILES.length}) must import only the kernel + grandfathered infra — ENFORCING (ADR 0091 §1 / ADR 0102).${findings.length ? ` ${findings.length} leak(s).` : ' Boundary clean.'}`,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ============================================================================
|
|
716
|
+
// Check 11 — module migrations are additive + namespaced (ADR 0083 §Decision #5 / design 04§3)
|
|
717
|
+
// ============================================================================
|
|
718
|
+
//
|
|
719
|
+
// A module owns its DB slice through migrations under modules/<key>/migrations/.
|
|
720
|
+
// Two invariants keep that slice from clobbering core or a sibling:
|
|
721
|
+
// • ADDITIVE — no destructive DDL. "Disable a module" means "stop applying its
|
|
722
|
+
// migrations," never "drop its tables" (ADR 0083 §Module-owned migrations);
|
|
723
|
+
// there is no down path, so a DROP/TRUNCATE can only be an accident.
|
|
724
|
+
// • NAMESPACED — every object it CREATEs lives under the <key>_ prefix
|
|
725
|
+
// (dev-box → dev_box_), and it may only ALTER its own namespaced tables;
|
|
726
|
+
// core/sibling state is reached through the doorway/seams, never raw DDL.
|
|
727
|
+
//
|
|
728
|
+
// LEGACY_NAMESPACED: tables that predate the <key>_-prefix convention (created before the module
|
|
729
|
+
// system existed). Grandfathered per-module to avoid requiring a rename migration for legacy prod
|
|
730
|
+
// data. The fitness rule applies in full to any NEW migration a module ships.
|
|
731
|
+
const LEGACY_NAMESPACED = {
|
|
732
|
+
// game module: world_events + players were created before the module system (V1 schema). The
|
|
733
|
+
// schema_migrations table is a core tracking table that every migration may re-create idempotently.
|
|
734
|
+
game: new Set(['world_events', 'players', 'schema_migrations']),
|
|
735
|
+
};
|
|
736
|
+
const DESTRUCTIVE_DDL = [
|
|
737
|
+
{ re: /\bdrop\s+table\b/i, label: 'DROP TABLE' },
|
|
738
|
+
{ re: /\balter\s+table\b[\s\S]*?\bdrop\s+(?:column|constraint)\b/i, label: 'ALTER TABLE … DROP COLUMN/CONSTRAINT' },
|
|
739
|
+
{ re: /\bdrop\s+(?:column|constraint)\b/i, label: 'DROP COLUMN/CONSTRAINT' },
|
|
740
|
+
{ re: /\bdrop\s+(?:index|type|view|sequence|schema|materialized\s+view)\b/i, label: 'DROP <object>' },
|
|
741
|
+
{ re: /\btruncate\b/i, label: 'TRUNCATE' },
|
|
742
|
+
];
|
|
743
|
+
const CREATE_OBJ_RE = /\bcreate\s+(?:or\s+replace\s+)?(?:unique\s+)?(?:materialized\s+)?(table|view|sequence|type)\s+(?:if\s+not\s+exists\s+)?("?[\w.]+"?)/gi;
|
|
744
|
+
const CREATE_INDEX_RE = /\bcreate\s+(?:unique\s+)?index\s+(?:concurrently\s+)?(?:if\s+not\s+exists\s+)?(?:"?[\w.]+"?)\s+on\s+(?:only\s+)?("?[\w.]+"?)/gi;
|
|
745
|
+
const ALTER_TABLE_RE = /\balter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?("?[\w.]+"?)/gi;
|
|
746
|
+
|
|
747
|
+
// SQL object-namespace prefix for a module key: dev-box → dev_box (SQL-safe).
|
|
748
|
+
function moduleNamespace(key) { return key.replace(/-/g, '_'); }
|
|
749
|
+
|
|
750
|
+
// strip SQL comments so a commented-out statement is never scanned
|
|
751
|
+
function stripSqlComments(sql) {
|
|
752
|
+
return String(sql == null ? '' : sql)
|
|
753
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
754
|
+
.replace(/--[^\n]*/g, ' ');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// is `rawName` (possibly quoted / schema-qualified) under the <ns>_ namespace?
|
|
758
|
+
function inNamespace(rawName, ns, legacySet) {
|
|
759
|
+
const name = String(rawName).replace(/"/g, '').replace(/^[A-Za-z_]\w*\./, '');
|
|
760
|
+
return name.startsWith(ns + '_') || (legacySet && legacySet.has(name));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// A CHECK-enum WIDENING is additive-in-spirit: `DROP CONSTRAINT IF EXISTS <c>` PAIRED
|
|
764
|
+
// with a re-`ADD CONSTRAINT <c>` (SAME name) in the SAME migration only PERMITS more
|
|
765
|
+
// values — no existing row changes, no data is lost (task 1978: provisioning gains a
|
|
766
|
+
// 'standalone' hosting_shape). Allow that exact pattern; a BARE `DROP CONSTRAINT` (no
|
|
767
|
+
// re-add), or any DROP TABLE/COLUMN/TRUNCATE, stays destructive. PURE.
|
|
768
|
+
function reAddedConstraintNames(sql) {
|
|
769
|
+
const names = new Set();
|
|
770
|
+
const re = /\badd\s+constraint\s+("?[\w.]+"?)/gi;
|
|
771
|
+
let m;
|
|
772
|
+
while ((m = re.exec(sql))) names.add(m[1].replace(/"/g, ''));
|
|
773
|
+
return names;
|
|
774
|
+
}
|
|
775
|
+
function isConstraintWidening(stmt, reAdded) {
|
|
776
|
+
const m = /\bdrop\s+constraint\s+(?:if\s+exists\s+)?("?[\w.]+"?)/i.exec(stmt);
|
|
777
|
+
if (!m) return false; // not a DROP CONSTRAINT at all
|
|
778
|
+
// never let a DROP CONSTRAINT statement smuggle a table/column drop past the gate
|
|
779
|
+
if (/\bdrop\s+(?:table|column|index|type|view|sequence|schema|materialized\s+view)\b/i.test(stmt)) return false;
|
|
780
|
+
if (/\btruncate\b/i.test(stmt)) return false;
|
|
781
|
+
return reAdded.has(m[1].replace(/"/g, '')); // re-added same-named constraint → widening
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Pure: violations for ONE module's migration SQL. Exported for the negative test.
|
|
785
|
+
function moduleMigrationViolations(key, sql) {
|
|
786
|
+
const ns = moduleNamespace(key);
|
|
787
|
+
const legacy = LEGACY_NAMESPACED[key] || null;
|
|
788
|
+
const clean = stripSqlComments(sql);
|
|
789
|
+
const violations = [];
|
|
790
|
+
// additive — scan per statement so an ALTER…DROP never spans a `;`
|
|
791
|
+
const reAdded = reAddedConstraintNames(clean);
|
|
792
|
+
for (const stmt of clean.split(';')) {
|
|
793
|
+
if (isConstraintWidening(stmt, reAdded)) continue; // additive CHECK-enum widening (task 1978)
|
|
794
|
+
for (const d of DESTRUCTIVE_DDL) {
|
|
795
|
+
if (d.re.test(stmt)) {
|
|
796
|
+
violations.push(`module "${key}" migration uses destructive DDL (${d.label}) — module migrations must be ADDITIVE; disabling a module stops applying, it never drops (ADR 0083 §Decision #5).`);
|
|
797
|
+
break; // one destructive flag per statement is enough
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// namespaced — CREATEd objects + ALTER targets must live under <ns>_ (or be grandfathered)
|
|
802
|
+
let m;
|
|
803
|
+
CREATE_OBJ_RE.lastIndex = 0;
|
|
804
|
+
while ((m = CREATE_OBJ_RE.exec(clean))) {
|
|
805
|
+
if (!inNamespace(m[2], ns, legacy)) {
|
|
806
|
+
violations.push(`module "${key}" migration creates ${m[1].toUpperCase()} ${m[2]} outside its namespace — module objects must be ${ns}_-prefixed (ADR 0083 §Decision #5).`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
CREATE_INDEX_RE.lastIndex = 0;
|
|
810
|
+
while ((m = CREATE_INDEX_RE.exec(clean))) {
|
|
811
|
+
if (!inNamespace(m[1], ns, legacy)) {
|
|
812
|
+
violations.push(`module "${key}" migration indexes a non-namespaced table ${m[1]} — a module migration may only touch its own ${ns}_ tables (ADR 0083 §Decision #5).`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
ALTER_TABLE_RE.lastIndex = 0;
|
|
816
|
+
while ((m = ALTER_TABLE_RE.exec(clean))) {
|
|
817
|
+
if (!inNamespace(m[1], ns, legacy)) {
|
|
818
|
+
violations.push(`module "${key}" migration alters a non-namespaced table ${m[1]} — a module migration may only touch its own ${ns}_ tables; reach core state through the doorway/seams, not DDL (ADR 0083 §Decision #5).`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return violations;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function checkModuleMigrationsAdditive() {
|
|
825
|
+
const violations = [];
|
|
826
|
+
let scanned = 0;
|
|
827
|
+
for (const key of discoveredModuleKeys()) { // union across module roots (ADR 0108 §Consequences #4)
|
|
828
|
+
const migDir = path.join(moduleDir(key), 'migrations');
|
|
829
|
+
let files = [];
|
|
830
|
+
try { files = fs.readdirSync(migDir).filter((f) => f.endsWith('.sql')); } catch { continue; }
|
|
831
|
+
for (const f of files.sort()) {
|
|
832
|
+
scanned += 1;
|
|
833
|
+
const sql = fs.readFileSync(path.join(migDir, f), 'utf8');
|
|
834
|
+
for (const v of moduleMigrationViolations(key, sql)) {
|
|
835
|
+
violations.push(`modules/${key}/migrations/${f}: ${v}`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
name: 'module migrations additive + namespaced',
|
|
841
|
+
ok: violations.length === 0,
|
|
842
|
+
hardFail: violations.length > 0,
|
|
843
|
+
violations,
|
|
844
|
+
warnings: [],
|
|
845
|
+
note: `${scanned} module migration file(s): additive (no destructive DDL) + <key>_-namespaced objects (ADR 0083 §Decision #5).`,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ============================================================================
|
|
850
|
+
// Check 11b — migration FILENAMES are namespaced + no cross-root stem collision
|
|
851
|
+
// (ADR 0108 §5; extends Check 11 to the cross-repo layer)
|
|
852
|
+
// ============================================================================
|
|
853
|
+
//
|
|
854
|
+
// Check 11 (above) guards a module migration's OBJECTS (additive + <key>_-namespaced
|
|
855
|
+
// tables). THIS check guards migration FILENAMES across all three roots — the
|
|
856
|
+
// discipline that lets the core repo and the instance repo version independently
|
|
857
|
+
// (ADR 0108) without their migrations colliding:
|
|
858
|
+
// • migrations/*.sql (CORE) → NEW files must be core_-prefixed
|
|
859
|
+
// • migrations/instance/*.sql (INSTANCE) → stay bare (or instance_-prefixed)
|
|
860
|
+
// • modules/<key>/migrations/ (MODULE) → NEW files must be <key>_-prefixed
|
|
861
|
+
// Existing bare files are grandfathered (migration-namespace.js LEGACY_BARE_*). The
|
|
862
|
+
// second invariant is the real bug guard: migrate.sh keys schema_migrations by
|
|
863
|
+
// filename STEM, so two migrations from different sources sharing a stem would apply
|
|
864
|
+
// ONCE and the other be SILENTLY SKIPPED (never applied). Single-root today; W6/task
|
|
865
|
+
// 1886 makes the root resolution two-root-aware once the core installs from node_modules.
|
|
866
|
+
function sqlFilesIn(absDir) {
|
|
867
|
+
try { return fs.readdirSync(absDir).filter((f) => f.endsWith('.sql')).sort(); }
|
|
868
|
+
catch { return []; } // dir absent (e.g. no migrations/instance in a vanilla instance) → nothing to scan
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function checkMigrationNamespacing() {
|
|
872
|
+
const violations = [];
|
|
873
|
+
const files = []; // { source, moduleKey?, basename } — fed to the stem-collision scan
|
|
874
|
+
|
|
875
|
+
const coreFiles = sqlFilesIn(path.join(CORE_ROOT, 'migrations')); // core migrations ship in the core package
|
|
876
|
+
for (const f of coreFiles) {
|
|
877
|
+
const v = filenameViolation(CORE_NS, f);
|
|
878
|
+
if (v) violations.push(`migrations/${f}: ${v}`);
|
|
879
|
+
files.push({ source: CORE_NS, basename: f });
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const instFiles = sqlFilesIn(path.join(INSTANCE_ROOT, 'migrations', 'instance')); // instance seeds live in the host repo
|
|
883
|
+
for (const f of instFiles) {
|
|
884
|
+
const v = filenameViolation(INSTANCE_NS, f);
|
|
885
|
+
if (v) violations.push(`migrations/instance/${f}: ${v}`);
|
|
886
|
+
files.push({ source: INSTANCE_NS, basename: f });
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
let moduleCount = 0;
|
|
890
|
+
for (const key of discoveredModuleKeys()) {
|
|
891
|
+
for (const f of sqlFilesIn(path.join(moduleDir(key), 'migrations'))) {
|
|
892
|
+
moduleCount += 1;
|
|
893
|
+
const v = filenameViolation('module', f, { moduleKey: key });
|
|
894
|
+
if (v) violations.push(`modules/${key}/migrations/${f}: ${v}`);
|
|
895
|
+
files.push({ source: 'module', moduleKey: key, basename: f });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
for (const c of stemCollisions(files)) {
|
|
900
|
+
violations.push(`migration stem "${c.stem}" is produced by more than one source (${c.files.join(', ')}) — schema_migrations is stem-keyed, so one applies and the rest are SILENTLY SKIPPED; namespace them (ADR 0108 §5).`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
name: 'migration filenames namespaced + no stem collision (ADR 0108 §5)',
|
|
905
|
+
ok: violations.length === 0,
|
|
906
|
+
hardFail: violations.length > 0,
|
|
907
|
+
violations,
|
|
908
|
+
warnings: [],
|
|
909
|
+
note: `${coreFiles.length} core + ${instFiles.length} instance + ${moduleCount} module migration file(s): new core→core_, new module→<key>_, instance bare; no cross-root stem collision.`,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ============================================================================
|
|
914
|
+
// Check 13 — portable code carries no hardcoded host-brand identity (BV1.R76 + task 1705)
|
|
915
|
+
// ============================================================================
|
|
916
|
+
//
|
|
917
|
+
// The portable core (src/bongos/**) and every feature MODULE (modules/**) must read
|
|
918
|
+
// the instance's product/world name + domains from branding() / window.__BRANDING__,
|
|
919
|
+
// never a baked-in "Off the Boats" / "Amazonprimea" / apex domain (ADR 0062 §3/§5;
|
|
920
|
+
// the branding contract). R76 shipped this as an 8-FILE allowlist over the kernel
|
|
921
|
+
// LLM-prompt headers; task 1705 WIDENS it to a directory-wide scan, so ANY new brand
|
|
922
|
+
// leak in portable code reds the build — not just the nine files someone remembered
|
|
923
|
+
// to list. (The 2026-07-02 separation audit found five leaks the old allowlist missed:
|
|
924
|
+
// the rank/graduation broadcasts, the auth error-page title, the apex-origin default,
|
|
925
|
+
// and ~124 served-UI strings.) Comments are stripped first (block /* */, HTML
|
|
926
|
+
// <!-- -->, and JS // line comments via a `://`-safe strip so a URL is never
|
|
927
|
+
// truncated) because the de-brand rationale legitimately names the old literals in
|
|
928
|
+
// prose. Instance-injected placeholders ({{worldName}} etc.) carry no literal, so
|
|
929
|
+
// they pass untouched.
|
|
930
|
+
//
|
|
931
|
+
// EXCLUDED — the small instance-1 / ops allowlist (task 1705):
|
|
932
|
+
// - modules/game/** — the INSTANCE'S OWN PRODUCT. A product's identity is
|
|
933
|
+
// instance-specific by definition (a vanilla instance ships a different product,
|
|
934
|
+
// or none); it is not portable-platform code the branding pack can neutralize.
|
|
935
|
+
// A documented carve-out, not an oversight.
|
|
936
|
+
// - generated / build-output dirs (no authored source).
|
|
937
|
+
const PROMPT_IDENTITY_LITERALS = [
|
|
938
|
+
/off the boats/i,
|
|
939
|
+
/amazonprimea/i,
|
|
940
|
+
];
|
|
941
|
+
// Scan roots (recursive) + kernel-prompt CLIs that live outside them.
|
|
942
|
+
const IDENTITY_SCAN_ROOTS = ['src/bongos', 'modules'];
|
|
943
|
+
const IDENTITY_SCAN_EXTRA_FILES = ['scripts/gds/architect-audit.js'];
|
|
944
|
+
const IDENTITY_SCAN_EXTS = new Set(['.js', '.html', '.css']);
|
|
945
|
+
// Path-prefix carve-outs (see the header note). Kept SMALL and documented.
|
|
946
|
+
const IDENTITY_SCAN_EXCLUDE = ['modules/game/'];
|
|
947
|
+
// Directories with no authored source to gate.
|
|
948
|
+
const IDENTITY_SCAN_SKIP_DIRS = new Set(['node_modules', '.git', 'generated', 'iterations', 'references']);
|
|
949
|
+
|
|
950
|
+
// Strip comments so a rationale comment naming the old brand never trips the scan.
|
|
951
|
+
// The JS // strip is `://`-safe (keeps the char before `//` unless it's `:`), so a
|
|
952
|
+
// hardcoded "https://amazonprimea.com" URL is NOT truncated and still gets caught.
|
|
953
|
+
function stripCommentsForIdentity(text, ext) {
|
|
954
|
+
let t = String(text == null ? '' : text);
|
|
955
|
+
if (ext === '.html') return t.replace(/<!--[\s\S]*?-->/g, '');
|
|
956
|
+
t = t.replace(/\/\*[\s\S]*?\*\//g, ''); // block comments (js + css)
|
|
957
|
+
if (ext === '.js') t = t.split('\n').map((l) => l.replace(/(^|[^:])\/\/.*$/, '$1')).join('\n');
|
|
958
|
+
return t;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Pure: scan one file's comment-stripped source for a hardcoded host-brand literal.
|
|
962
|
+
// Returns violation strings. Exported for the "actually catches a re-hardcode" test.
|
|
963
|
+
function scanPromptIdentity(relFile, text, ext = path.extname(relFile)) {
|
|
964
|
+
const violations = [];
|
|
965
|
+
stripCommentsForIdentity(text, ext).split('\n').forEach((raw, i) => {
|
|
966
|
+
if (!raw.trim()) return;
|
|
967
|
+
for (const re of PROMPT_IDENTITY_LITERALS) {
|
|
968
|
+
const m = raw.match(re);
|
|
969
|
+
if (m) {
|
|
970
|
+
violations.push(`${relFile}:${i + 1} hardcodes the host-brand identity "${m[0]}" — read it from branding()/window.__BRANDING__ (identity/domains/currency), or use a {{worldName}}-style token, so a non-OTB instance renders ITS OWN brand (ADR 0062 §3/§5; BV1.R76 / task 1705).`);
|
|
971
|
+
break; // one hit per line is enough
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
return violations;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Recursively collect scannable files under one root, honoring the skip-dir + carve-out lists.
|
|
979
|
+
function collectIdentityFiles(absRoot, relRoot) {
|
|
980
|
+
const out = [];
|
|
981
|
+
let entries;
|
|
982
|
+
try { entries = fs.readdirSync(absRoot, { withFileTypes: true }); }
|
|
983
|
+
catch { return out; }
|
|
984
|
+
for (const e of entries) {
|
|
985
|
+
const rel = relRoot ? `${relRoot}/${e.name}` : e.name;
|
|
986
|
+
if (e.isDirectory()) {
|
|
987
|
+
if (IDENTITY_SCAN_SKIP_DIRS.has(e.name)) continue;
|
|
988
|
+
if (IDENTITY_SCAN_EXCLUDE.some((p) => `${rel}/`.startsWith(p))) continue;
|
|
989
|
+
out.push(...collectIdentityFiles(path.join(absRoot, e.name), rel));
|
|
990
|
+
} else if (IDENTITY_SCAN_EXTS.has(path.extname(e.name))
|
|
991
|
+
&& !IDENTITY_SCAN_EXCLUDE.some((p) => rel.startsWith(p))) {
|
|
992
|
+
out.push(rel);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return out;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function checkKernelPromptsNoHardcodedIdentity() {
|
|
999
|
+
const violations = [];
|
|
1000
|
+
let scanned = 0;
|
|
1001
|
+
// Collect {abs, rel}: src/bongos is CORE; modules ship from BOTH roots (ADR 0108 §Consequences #4).
|
|
1002
|
+
// Dedup by repo-relative path so a shared checkout never scans a file twice, and read each file
|
|
1003
|
+
// from the root it was actually found under (a module may live in the instance repo post-split).
|
|
1004
|
+
const seen = new Set();
|
|
1005
|
+
const entries = [];
|
|
1006
|
+
const addFrom = (rootAbs, sub) => {
|
|
1007
|
+
for (const relFromRoot of collectIdentityFiles(path.join(rootAbs, sub), sub)) {
|
|
1008
|
+
if (seen.has(relFromRoot)) continue;
|
|
1009
|
+
seen.add(relFromRoot);
|
|
1010
|
+
entries.push({ abs: path.join(rootAbs, relFromRoot), rel: relFromRoot });
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
for (const root of IDENTITY_SCAN_ROOTS) {
|
|
1014
|
+
for (const rootAbs of (root === 'modules' ? MODULE_ROOTS : [CORE_ROOT])) addFrom(rootAbs, root);
|
|
1015
|
+
}
|
|
1016
|
+
for (const extra of IDENTITY_SCAN_EXTRA_FILES) {
|
|
1017
|
+
if (!seen.has(extra)) { seen.add(extra); entries.push({ abs: path.join(CORE_ROOT, extra), rel: extra }); }
|
|
1018
|
+
}
|
|
1019
|
+
for (const { abs, rel: relFile } of entries) {
|
|
1020
|
+
let text;
|
|
1021
|
+
try { text = fs.readFileSync(abs, 'utf8'); }
|
|
1022
|
+
catch { continue; } // a file moved/carved away is not a failure of this gate
|
|
1023
|
+
scanned += 1;
|
|
1024
|
+
violations.push(...scanPromptIdentity(relFile, text));
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
name: 'portable code carries no hardcoded host identity (BV1.R76 / task 1705)',
|
|
1028
|
+
ok: violations.length === 0,
|
|
1029
|
+
hardFail: violations.length > 0,
|
|
1030
|
+
violations,
|
|
1031
|
+
warnings: [],
|
|
1032
|
+
note: `${scanned} portable source file(s) scanned (src/bongos/** + modules/** + kernel-prompt CLIs, minus the instance-1 product + generated dirs) — none may hardcode the host brand; identity comes from branding()/window.__BRANDING__ (ADR 0062 §3/§5).`,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// ---- design contract (ADR 0081 / task 1377) --------------------------------
|
|
1037
|
+
|
|
1038
|
+
function checkDesignContract() {
|
|
1039
|
+
const { spawnSync } = require('node:child_process');
|
|
1040
|
+
const validatorPath = path.join(REPO_ROOT, 'scripts', 'gds', 'validate-design.js');
|
|
1041
|
+
const adaptersDir = path.join(REPO_ROOT, 'src', 'ui', 'adapters');
|
|
1042
|
+
const hasAdapters = (() => {
|
|
1043
|
+
try { return fs.readdirSync(adaptersDir).some((e) => fs.statSync(path.join(adaptersDir, e)).isDirectory()); }
|
|
1044
|
+
catch { return false; }
|
|
1045
|
+
})();
|
|
1046
|
+
|
|
1047
|
+
const result = spawnSync(process.execPath, [validatorPath, '--json'], { encoding: 'utf8' });
|
|
1048
|
+
let report = { ok: true, errors: [], warnings: [] };
|
|
1049
|
+
try { report = JSON.parse(result.stdout || '{}'); } catch { /* fall through with defaults */ }
|
|
1050
|
+
if (result.status !== 0 && !report.errors.length) {
|
|
1051
|
+
report.errors.push(result.stderr ? result.stderr.trim() : 'validate-design.js exited non-zero');
|
|
1052
|
+
report.ok = false;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
return {
|
|
1056
|
+
name: 'design contract valid (docs/design-contract.md / ADR 0081)',
|
|
1057
|
+
ok: report.ok,
|
|
1058
|
+
hardFail: !report.ok && hasAdapters,
|
|
1059
|
+
violations: report.errors || [],
|
|
1060
|
+
warnings: report.warnings || [],
|
|
1061
|
+
note: hasAdapters
|
|
1062
|
+
? 'Hard-fail: adapters present — contract must be valid. Run: node scripts/gds/validate-design.js'
|
|
1063
|
+
: 'Warn-only until first adapter lands (task 1378 / task 1379). Run: node scripts/gds/validate-design.js',
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// ============================================================================
|
|
1068
|
+
// Check — hall sections are registry-owned, not hardcoded (BV1.R75 / ADR 0062 §4)
|
|
1069
|
+
// ============================================================================
|
|
1070
|
+
// The builders'-hall widgets are now a REGISTRY (modules/hall-ui/public/dom-utils.js
|
|
1071
|
+
// contributeHallWidget; the core set in modules/hall-ui/public/hall-widgets.js, modules
|
|
1072
|
+
// via their own client script). The hall's <main> is filled by builders.js from that
|
|
1073
|
+
// registry. So no widget may be hardcoded as a literal <section class="scroll"> inside
|
|
1074
|
+
// <main> in index.html — that was the pre-R75 second copy this carve removed. This
|
|
1075
|
+
// gate fails if one creeps back, keeping the registry the single source of truth (so a
|
|
1076
|
+
// reskin/module-toggle reaches every widget). Self-contained: a pure scan of
|
|
1077
|
+
// index.html's <main>, no module/registry state needed. Exported for the unit test.
|
|
1078
|
+
// (The hall surface itself carved to modules/hall-ui/public/ in BV1.R85.)
|
|
1079
|
+
const HALL_INDEX = path.join(moduleDir('hall-ui'), 'public', 'index.html'); // hall-ui is a module — resolve its root (ADR 0108)
|
|
1080
|
+
|
|
1081
|
+
function hallMainSlice(html) {
|
|
1082
|
+
// Strip HTML comments first so a comment that merely MENTIONS <main> or a
|
|
1083
|
+
// <section class="scroll"> (e.g. the carve's own explanatory note) never trips
|
|
1084
|
+
// the scan — only real, rendered markup counts.
|
|
1085
|
+
const stripped = html.replace(/<!--[\s\S]*?-->/g, '');
|
|
1086
|
+
const open = stripped.indexOf('<main');
|
|
1087
|
+
if (open === -1) return '';
|
|
1088
|
+
const start = stripped.indexOf('>', open);
|
|
1089
|
+
const close = stripped.indexOf('</main>', start === -1 ? open : start);
|
|
1090
|
+
if (start === -1 || close === -1) return '';
|
|
1091
|
+
return stripped.slice(start + 1, close);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function scanHallSectionRegistry(html) {
|
|
1095
|
+
const main = hallMainSlice(html);
|
|
1096
|
+
// A hall widget section carries class="scroll" (possibly with modifiers, e.g.
|
|
1097
|
+
// "scroll scroll--welcome"). Catch any <section ... class="...scroll..."> in <main>.
|
|
1098
|
+
const re = /<section\b[^>]*\bclass\s*=\s*"(?:[^"]*\b)?scroll\b[^"]*"/gi;
|
|
1099
|
+
const hits = main.match(re) || [];
|
|
1100
|
+
return hits.map((h) => h.replace(/\s+/g, ' ').trim());
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function checkHallSectionRegistry() {
|
|
1104
|
+
let html = '';
|
|
1105
|
+
try { html = fs.readFileSync(HALL_INDEX, 'utf8'); }
|
|
1106
|
+
catch { return { name: 'hall widgets are registry-owned (ADR 0062 §4)', ok: true, hardFail: false, violations: [], warnings: ['modules/hall-ui/public/index.html not found — skipped'], note: 'index.html absent.' }; }
|
|
1107
|
+
const hardcoded = scanHallSectionRegistry(html);
|
|
1108
|
+
const violations = hardcoded.map((s) =>
|
|
1109
|
+
`modules/hall-ui/public/index.html <main> hardcodes a hall section "${s}" — register it via window.OTB.contributeHallWidget (modules/hall-ui/public/hall-widgets.js for core, a module's client script otherwise), don't inline the markup (BV1.R75 / ADR 0062 §4).`);
|
|
1110
|
+
return {
|
|
1111
|
+
name: 'hall widgets are registry-owned (ADR 0062 §4)',
|
|
1112
|
+
ok: violations.length === 0,
|
|
1113
|
+
hardFail: violations.length > 0,
|
|
1114
|
+
violations,
|
|
1115
|
+
warnings: [],
|
|
1116
|
+
note: 'modules/hall-ui/public/index.html <main> contains no hardcoded <section class="scroll"> — the hall-widget registry is the single source of truth.',
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ============================================================================
|
|
1121
|
+
// Check 16 — core code must not HARD-require an instance-excluded config/*.json
|
|
1122
|
+
// ============================================================================
|
|
1123
|
+
//
|
|
1124
|
+
// A vanilla self-host boot ships only the PUBLISHABLE tree (publish-manifest.js). An
|
|
1125
|
+
// instance-only config (config/devbox-app.json, config/bongos-cli.json, config/branding.json,
|
|
1126
|
+
// config/modules.json, …) is boundary-EXCLUDED, so a bare `require('../../config/<excluded>.json')`
|
|
1127
|
+
// in core code throws MODULE_NOT_FOUND and CRASHES the boot on a fresh instance — the exact
|
|
1128
|
+
// class that shipped in R88 (two hard-requires in src/bongos/serve-internal.js; task 1824). The
|
|
1129
|
+
// cure is to guard the read (tryRequireConfig / fs.existsSync → fail-to-vanilla), which also
|
|
1130
|
+
// makes the require a non-literal so it no longer matches here. Check 1 (core↔host) scans
|
|
1131
|
+
// code→code IMPORTS and misses this code→config class. Only an EXCLUDED config is a violation;
|
|
1132
|
+
// a publishable neutral.*/schema config (which ships with the core) is fine.
|
|
1133
|
+
const CONFIG_REQUIRE_RE = /require\(\s*['"]([^'"]*config\/[^'"]*\.json)['"]\s*\)/g;
|
|
1134
|
+
|
|
1135
|
+
function checkCoreRequiresExcludedConfig() {
|
|
1136
|
+
const { isPublishable } = require('./publish-manifest');
|
|
1137
|
+
const violations = [];
|
|
1138
|
+
// src/ + scripts/ are CORE; modules ship from BOTH roots (ADR 0108 §Consequences #4). Dedup by
|
|
1139
|
+
// repo-relative path so a shared checkout scans each file once.
|
|
1140
|
+
const seen = new Set();
|
|
1141
|
+
const allAbs = [
|
|
1142
|
+
...['src', 'scripts'].flatMap((d) => walkJs(path.join(CORE_ROOT, d))),
|
|
1143
|
+
...walkJsAllRoots('modules'),
|
|
1144
|
+
];
|
|
1145
|
+
for (const abs of allAbs) {
|
|
1146
|
+
const relFile = rel(abs);
|
|
1147
|
+
if (seen.has(relFile)) continue;
|
|
1148
|
+
seen.add(relFile);
|
|
1149
|
+
if (!isPublishable(relFile)) continue; // only CORE (publishable) code can crash a vanilla boot
|
|
1150
|
+
for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
|
|
1151
|
+
const trimmed = raw.trim();
|
|
1152
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
|
|
1153
|
+
let m;
|
|
1154
|
+
CONFIG_REQUIRE_RE.lastIndex = 0;
|
|
1155
|
+
while ((m = CONFIG_REQUIRE_RE.exec(raw))) {
|
|
1156
|
+
const relCfg = rel(path.resolve(path.dirname(abs), m[1]));
|
|
1157
|
+
if (!isPublishable(relCfg)) {
|
|
1158
|
+
violations.push(`${relFile} → require('${m[1]}') hard-requires an instance-excluded config (${relCfg}) — guard the read (tryRequireConfig / fs.existsSync, fail-to-vanilla) so a fresh core boots (task 1824).`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
name: 'core never hard-requires an excluded config (task 1824)',
|
|
1165
|
+
ok: violations.length === 0,
|
|
1166
|
+
hardFail: violations.length > 0,
|
|
1167
|
+
violations,
|
|
1168
|
+
warnings: [],
|
|
1169
|
+
note: "a publishable src/·scripts/·modules/ .js must not require('…config/X.json') when X.json is boundary-excluded — guard the read so a vanilla core boots.",
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// ============================================================================
|
|
1174
|
+
// Check 17 — no route hand-rolls a FLAT error (BV1 R03 / task 1990 / ADR 0116)
|
|
1175
|
+
// ============================================================================
|
|
1176
|
+
//
|
|
1177
|
+
// The error-envelope contract (ADR 0116) says every failure is the ONE envelope
|
|
1178
|
+
// `{error:{code,message,details}}`, emitted via res.fail() or a thrown ApiError
|
|
1179
|
+
// (the R02 middleware, task 1989). A hand-rolled flat error —
|
|
1180
|
+
// `res.status(n).json({ error: 'x' })` or `.json({ error })` — breaks the
|
|
1181
|
+
// contract: the caller gets a bare string where the envelope should be.
|
|
1182
|
+
//
|
|
1183
|
+
// RATCHET, not big-bang. The R04 sweep (task 1991) migrates the ~562 existing
|
|
1184
|
+
// sites; until it finishes they'd all "fail" a strict check, so this HOLDS THE
|
|
1185
|
+
// LINE: it fails only when the count rises ABOVE the committed baseline (a NEW
|
|
1186
|
+
// hand-rolled flat error), and nudges the baseline DOWN as the sweep shrinks it.
|
|
1187
|
+
// When the sweep reaches 0, FLAT_ERROR_BASELINE is 0 and any new flat error
|
|
1188
|
+
// hard-fails immediately. Detector is shared with the sweep + its test.
|
|
1189
|
+
const FLAT_ERROR_BASELINE = 0; // R04 sweep (task 1991) landed the surface at 0 — the gate is now hard-zero: any new hand-rolled flat error fails the build immediately.
|
|
1190
|
+
// A flat error is a `.json({ … })` whose FIRST key is `error` with a value that
|
|
1191
|
+
// is NOT an object literal (a string/var/shorthand). The envelope's nested
|
|
1192
|
+
// `error: { … }` and `.json(envelope(…))` deliberately do NOT match.
|
|
1193
|
+
// The `(?!\s*\{)` lookahead is whitespace-aware: without the inner `\s*` the
|
|
1194
|
+
// preceding `\s*` backtracks to zero and the lookahead is tested at the space in
|
|
1195
|
+
// `error: {`, so the compliant nested envelope was wrongly flagged (task 2029).
|
|
1196
|
+
const FLAT_ERROR_VALUE_RE = /\.json\(\s*\{\s*error\s*:\s*(?!\s*\{)/g; // error: 'x' | error: code
|
|
1197
|
+
const FLAT_ERROR_SHORTHAND_RE = /\.json\(\s*\{\s*error\s*[,}]/g; // { error } | { error, … }
|
|
1198
|
+
|
|
1199
|
+
// Strip // line comments + /* */ block comments AND blank out string/template
|
|
1200
|
+
// contents (keeping the delimiters) so neither a DOC example of the anti-pattern
|
|
1201
|
+
// — e.g. the res.fail() usage note in middleware/error-envelope.js — nor a
|
|
1202
|
+
// pattern quoted inside a string literal is counted as a real flat error once
|
|
1203
|
+
// the gate is hard-zero (task 1991). Real flat errors are unaffected: in
|
|
1204
|
+
// `res.status(404).json({ error: 'x' })` the `.json({ error:` the detector keys
|
|
1205
|
+
// on is CODE (outside the string), so blanking the `'x'` content still matches.
|
|
1206
|
+
function stripCommentsForFlat(src) {
|
|
1207
|
+
let out = '';
|
|
1208
|
+
let i = 0;
|
|
1209
|
+
let quote = null;
|
|
1210
|
+
while (i < src.length) {
|
|
1211
|
+
const c = src[i];
|
|
1212
|
+
const d = src[i + 1];
|
|
1213
|
+
if (quote) {
|
|
1214
|
+
// inside a string/template: drop content, keep only the closing delimiter
|
|
1215
|
+
if (c === '\\') { i += 2; continue; }
|
|
1216
|
+
if (c === quote) { out += c; quote = null; }
|
|
1217
|
+
i++; continue;
|
|
1218
|
+
}
|
|
1219
|
+
if (c === '"' || c === "'" || c === '`') { quote = c; out += c; i++; continue; }
|
|
1220
|
+
if (c === '/' && d === '/') { while (i < src.length && src[i] !== '\n') i++; continue; }
|
|
1221
|
+
if (c === '/' && d === '*') { i += 2; while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++; i += 2; continue; }
|
|
1222
|
+
out += c; i++;
|
|
1223
|
+
}
|
|
1224
|
+
return out;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function countFlatErrors(text) {
|
|
1228
|
+
const code = stripCommentsForFlat(text);
|
|
1229
|
+
return (code.match(FLAT_ERROR_VALUE_RE) || []).length
|
|
1230
|
+
+ (code.match(FLAT_ERROR_SHORTHAND_RE) || []).length;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// scanFlatErrorSites() — per-file flat-error counts across the API surface
|
|
1234
|
+
// (core src/bongos + every module), skipping tests. Shared by the check below, the
|
|
1235
|
+
// R04 codemod, and tests/api_error_envelope_buildcheck.mjs.
|
|
1236
|
+
function scanFlatErrorSites() {
|
|
1237
|
+
const seen = new Set();
|
|
1238
|
+
const byFile = {};
|
|
1239
|
+
let total = 0;
|
|
1240
|
+
const files = [
|
|
1241
|
+
...walkJs(path.join(CORE_ROOT, 'src', 'bongos')),
|
|
1242
|
+
...walkJsAllRoots('modules'),
|
|
1243
|
+
];
|
|
1244
|
+
for (const abs of files) {
|
|
1245
|
+
const relFile = rel(abs);
|
|
1246
|
+
if (seen.has(relFile)) continue;
|
|
1247
|
+
seen.add(relFile);
|
|
1248
|
+
if (relFile.includes('/tests/') || relFile.endsWith('.test.js')) continue;
|
|
1249
|
+
const n = countFlatErrors(fs.readFileSync(abs, 'utf8'));
|
|
1250
|
+
if (n) { byFile[relFile] = n; total += n; }
|
|
1251
|
+
}
|
|
1252
|
+
return { total, byFile };
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function checkErrorEnvelope() {
|
|
1256
|
+
const { total, byFile } = scanFlatErrorSites();
|
|
1257
|
+
const violations = [];
|
|
1258
|
+
const warnings = [];
|
|
1259
|
+
if (total > FLAT_ERROR_BASELINE) {
|
|
1260
|
+
const top = Object.entries(byFile).sort((a, b) => b[1] - a[1]).slice(0, 8)
|
|
1261
|
+
.map(([f, n]) => `${f} (${n})`).join(', ');
|
|
1262
|
+
violations.push(
|
|
1263
|
+
`${total} hand-rolled flat-error sites — ABOVE the ${FLAT_ERROR_BASELINE} baseline. A new route emitted a flat \`res.status(n).json({error:'x'})\` instead of \`res.fail('x', n)\` / \`throw new ApiError('x')\` (ADR 0116). Top files: ${top}. Use the envelope helper (src/bongos/middleware/error-envelope.js).`
|
|
1264
|
+
);
|
|
1265
|
+
} else if (total < FLAT_ERROR_BASELINE) {
|
|
1266
|
+
warnings.push(
|
|
1267
|
+
`${total} flat-error sites — BELOW the ${FLAT_ERROR_BASELINE} baseline. The sweep tightened; lower FLAT_ERROR_BASELINE to ${total} in fitness.js so the ratchet can't slip back (BV1 R04 / task 1991).`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
name: 'no route hand-rolls a flat error (ADR 0116 / R03)',
|
|
1272
|
+
ok: violations.length === 0,
|
|
1273
|
+
hardFail: violations.length > 0,
|
|
1274
|
+
violations,
|
|
1275
|
+
warnings,
|
|
1276
|
+
note: `≤ ${FLAT_ERROR_BASELINE} flat \`.json({error:'…'})\` sites (ratchet → 0 as the R04 sweep lands); currently ${total}. New failures must use res.fail()/ApiError (the R02 envelope).`,
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// ============================================================================
|
|
1281
|
+
// Check 18 — every write route validates its body (BV1 R12 / task 1999 / ADR 0118)
|
|
1282
|
+
// ============================================================================
|
|
1283
|
+
//
|
|
1284
|
+
// Criterion api-validation: "the build fails on an unvalidated write route." R11
|
|
1285
|
+
// (#1998) gave every body-reading write route a strict validateOrRespond schema
|
|
1286
|
+
// (code-first, spec-driven — ADR 0118); this LOCKS that in. Reusing the SAME
|
|
1287
|
+
// route model that generates the OpenAPI spec (gen-api-docs.extractModel —
|
|
1288
|
+
// readsBody + validated), a POST/PUT/PATCH/DELETE that reads req.body must carry
|
|
1289
|
+
// a validateOrRespond()/validate(req.body) schema. Hard-zero (no ratchet — the
|
|
1290
|
+
// R11 sweep already reached 100%): a NEW write route that reads a body but
|
|
1291
|
+
// declares no schema fails the build immediately, so unknown/typo'd fields can
|
|
1292
|
+
// never again be silently dropped.
|
|
1293
|
+
//
|
|
1294
|
+
// EXEMPT — the builder-settings /me/*-prefs routes. Their bodies are DYNAMIC-KEY
|
|
1295
|
+
// maps ({ "<skill|sound|event-id>": <value> }, or a single-key toggle) that a
|
|
1296
|
+
// FIXED field schema cannot express; they validate via their module lib's
|
|
1297
|
+
// validatePatch()/validate() (which already rejects unknown keys with a 422). A
|
|
1298
|
+
// fixed strict schema would wrongly reject the valid dynamic keys — so these are
|
|
1299
|
+
// validated-by-lib, not by validateOrRespond. Documented per-route below; see
|
|
1300
|
+
// learning #133 / ADR 0118. Adding a route here is a deliberate, reviewable act.
|
|
1301
|
+
const WRITE_VALIDATION_EXEMPT = new Set([
|
|
1302
|
+
'PATCH /me/skill-prefs', // { "<skill>": "<model>"|null } — dynamic keys; skillPrefs.validatePatch
|
|
1303
|
+
'PATCH /me/sound-prefs', // { "<sound>": <bool>|null } — dynamic keys; soundPrefs.validatePatch
|
|
1304
|
+
'PATCH /me/event-sound-prefs', // { "<event>": "<variant>"|null } — dynamic keys; eventSoundPrefs.validatePatch
|
|
1305
|
+
'PATCH /me/wandering', // { wandering: 0..3|null } — rank-capped; wanderingPrefs.validatePatch
|
|
1306
|
+
'PATCH /me/interaction', // interaction profile — interactionPrefs.validatePatch
|
|
1307
|
+
'PATCH /me/model-allocation', // per-slot tiers — modelAllocPrefs.validatePatch
|
|
1308
|
+
'PATCH /me/render', // { widgets: <bool>|null } — renderPrefs.validatePatch
|
|
1309
|
+
'PATCH /me/display-name', // { display_name } — displayName.validate
|
|
1310
|
+
]);
|
|
1311
|
+
|
|
1312
|
+
function scanUnvalidatedWriteRoutes(model = genApiDocs.extractModel()) {
|
|
1313
|
+
const unvalidated = [];
|
|
1314
|
+
for (const r of model) {
|
|
1315
|
+
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(r.method)) continue;
|
|
1316
|
+
// Only routes that actually read a body have an input surface to validate;
|
|
1317
|
+
// a write that ignores req.body has nothing to declare.
|
|
1318
|
+
if (!r.body || !r.body.readsBody || r.body.validated) continue;
|
|
1319
|
+
if (WRITE_VALIDATION_EXEMPT.has(`${r.method} ${r.route}`)) continue;
|
|
1320
|
+
unvalidated.push(`${r.method} ${r.route} [${r.file}]`);
|
|
1321
|
+
}
|
|
1322
|
+
return unvalidated;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function checkWriteRoutesValidated() {
|
|
1326
|
+
const unvalidated = scanUnvalidatedWriteRoutes();
|
|
1327
|
+
const violations = [];
|
|
1328
|
+
if (unvalidated.length) {
|
|
1329
|
+
violations.push(
|
|
1330
|
+
`${unvalidated.length} write route(s) read req.body but declare NO validateOrRespond/validate schema (ADR 0118 / R11 requires one so unknown fields are rejected via the ADR-0116 envelope): ${unvalidated.join('; ')}. Add a strict validateOrRespond(req, res, { … }) listing every field the handler reads (see learning #133). A dynamic-key body validated by a module lib belongs in WRITE_VALIDATION_EXEMPT with a one-line reason.`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
name: 'every write route validates its body (ADR 0118 / R12)',
|
|
1335
|
+
ok: violations.length === 0,
|
|
1336
|
+
hardFail: violations.length > 0,
|
|
1337
|
+
violations,
|
|
1338
|
+
warnings: [],
|
|
1339
|
+
note: `body-reading POST/PUT/PATCH/DELETE routes must call validateOrRespond/validate (${WRITE_VALIDATION_EXEMPT.size} dynamic-key prefs routes validate by lib); ${unvalidated.length} unvalidated.`,
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// ---- runner ----------------------------------------------------------------
|
|
1344
|
+
|
|
1345
|
+
const CHECKS = [
|
|
1346
|
+
checkCoreHostBoundary,
|
|
1347
|
+
checkClaudeMdBudget,
|
|
1348
|
+
checkNestedDocsPresent,
|
|
1349
|
+
checkRankGuards,
|
|
1350
|
+
checkGeneratedArtifactsFresh,
|
|
1351
|
+
checkDeterministicRoutines,
|
|
1352
|
+
checkCoreNeverImportsModule,
|
|
1353
|
+
checkModulesImportOnlyApi,
|
|
1354
|
+
checkScopeMapImportsProtected,
|
|
1355
|
+
checkCoreCarriesNoModuleCode,
|
|
1356
|
+
checkKernelImportsNoDomain,
|
|
1357
|
+
checkModuleMigrationsAdditive,
|
|
1358
|
+
checkMigrationNamespacing, // Check 11b — ADR 0108 §5 cross-repo migration filenames + stem collision
|
|
1359
|
+
checkDesignContract,
|
|
1360
|
+
checkKernelPromptsNoHardcodedIdentity, // BV1.R76 — kernel prompts read identity from branding()
|
|
1361
|
+
checkHallSectionRegistry,
|
|
1362
|
+
checkCoreRequiresExcludedConfig, // task 1824 — core→instance-config require guard
|
|
1363
|
+
checkErrorEnvelope, // Check 17 — BV1 R03 / task 1990: no route hand-rolls a flat error (ADR 0116)
|
|
1364
|
+
checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
|
|
1365
|
+
];
|
|
1366
|
+
|
|
1367
|
+
function runAll() {
|
|
1368
|
+
return CHECKS.map((fn) => fn());
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function main() {
|
|
1372
|
+
const results = runAll();
|
|
1373
|
+
if (process.argv.includes('--json')) {
|
|
1374
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
1375
|
+
} else {
|
|
1376
|
+
console.log('Architecture fitness functions (Path C / ADR 0062 §9)\n');
|
|
1377
|
+
for (const r of results) {
|
|
1378
|
+
const mark = r.hardFail ? '✗ FAIL' : (r.warnings.length ? '! WARN' : '✓ PASS');
|
|
1379
|
+
console.log(`${mark} ${r.name}`);
|
|
1380
|
+
console.log(` ${r.note}`);
|
|
1381
|
+
for (const v of r.violations) console.log(` ✗ ${v}`);
|
|
1382
|
+
for (const w of r.warnings) console.log(` ! ${w}`);
|
|
1383
|
+
}
|
|
1384
|
+
const failed = results.filter((r) => r.hardFail).length;
|
|
1385
|
+
console.log(`\n${failed ? `${failed} check(s) FAILED` : 'all checks passed'}.`);
|
|
1386
|
+
}
|
|
1387
|
+
process.exit(results.some((r) => r.hardFail) ? 1 : 0);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (require.main === module) main();
|
|
1391
|
+
|
|
1392
|
+
module.exports = {
|
|
1393
|
+
runAll,
|
|
1394
|
+
resolvesToHostGame,
|
|
1395
|
+
checkCoreHostBoundary,
|
|
1396
|
+
checkClaudeMdBudget,
|
|
1397
|
+
checkNestedDocsPresent,
|
|
1398
|
+
checkRankGuards,
|
|
1399
|
+
checkGeneratedArtifactsFresh,
|
|
1400
|
+
checkDeterministicRoutines,
|
|
1401
|
+
checkCoreNeverImportsModule,
|
|
1402
|
+
checkModulesImportOnlyApi,
|
|
1403
|
+
checkScopeMapImportsProtected,
|
|
1404
|
+
checkCoreCarriesNoModuleCode,
|
|
1405
|
+
checkKernelImportsNoDomain,
|
|
1406
|
+
checkModuleMigrationsAdditive,
|
|
1407
|
+
checkMigrationNamespacing, // Check 11b — ADR 0108 §5 cross-repo migration filenames
|
|
1408
|
+
checkDesignContract,
|
|
1409
|
+
checkKernelPromptsNoHardcodedIdentity, // BV1.R76 / task 1705
|
|
1410
|
+
scanPromptIdentity, // BV1.R76 — negative-test hook
|
|
1411
|
+
stripCommentsForIdentity, // task 1705 — comment-strip helper (tested)
|
|
1412
|
+
checkHallSectionRegistry,
|
|
1413
|
+
scanHallSectionRegistry,
|
|
1414
|
+
hallMainSlice,
|
|
1415
|
+
checkErrorEnvelope, // Check 17 — BV1 R03 / task 1990 (ADR 0116)
|
|
1416
|
+
scanFlatErrorSites, // shared with the R04 sweep + its test
|
|
1417
|
+
countFlatErrors, // per-text detector (tested)
|
|
1418
|
+
FLAT_ERROR_BASELINE,
|
|
1419
|
+
checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999 (ADR 0118)
|
|
1420
|
+
scanUnvalidatedWriteRoutes, // shared with its test
|
|
1421
|
+
WRITE_VALIDATION_EXEMPT,
|
|
1422
|
+
scanScopeMapSource,
|
|
1423
|
+
scanKernelForModuleKeys,
|
|
1424
|
+
kernelDomainImport,
|
|
1425
|
+
KERNEL_FILES,
|
|
1426
|
+
discoveredModuleKeys,
|
|
1427
|
+
moduleMigrationViolations,
|
|
1428
|
+
moduleNamespace,
|
|
1429
|
+
resolvesToModule,
|
|
1430
|
+
moduleImportViolation,
|
|
1431
|
+
moduleOwnerOf,
|
|
1432
|
+
// test/ratchet hook: toggle the module boundary enforce flag. Default is now
|
|
1433
|
+
// ENFORCING (BV1.R45 / task 1414); tests that need warn-mode restore the
|
|
1434
|
+
// default in a finally.
|
|
1435
|
+
_setModuleBoundaryEnforcing: (v) => { MODULE_BOUNDARY_ENFORCING = !!v; },
|
|
1436
|
+
scanDeterministicBody,
|
|
1437
|
+
HOST_GAME,
|
|
1438
|
+
GRANDFATHERED,
|
|
1439
|
+
LEGACY_NAMESPACED,
|
|
1440
|
+
KERNEL_FILES,
|
|
1441
|
+
ROOT_HARD_MAX,
|
|
1442
|
+
ROOT_TARGET,
|
|
1443
|
+
ROOT_CHAR_HARD_MAX,
|
|
1444
|
+
ROOT_CHAR_TARGET,
|
|
1445
|
+
HIGH_TRAFFIC,
|
|
1446
|
+
};
|