@bongos/core 1.19.714 → 1.19.724
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 +488 -413
- package/.claude/skills/backlog-review/SKILL.md +3 -3
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/blocker-solve/SKILL.md +2 -2
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/design/SKILL.md +2 -2
- package/.claude/skills/feedback/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/goal-review/SKILL.md +2 -2
- package/.claude/skills/idea-triage/SKILL.md +5 -5
- package/.claude/skills/merge-mode/SKILL.md +4 -4
- package/.claude/skills/new-project/SKILL.md +17 -17
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/recall/SKILL.md +2 -2
- package/.claude/skills/strand-fix/SKILL.md +1 -1
- package/.devcontainer/README.md +2 -2
- package/CONTRIBUTING.md +2 -2
- package/GOVERNANCE.md +4 -4
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/config/branding.neutral.json +4 -2
- package/docs/adr/0001-stack-choice-single-vps.md +2 -2
- package/docs/adr/0002-digitalocean-over-hetzner.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0009-autotile-architecture.md +1 -1
- package/docs/adr/0011-terrain-id-rendering-model.md +3 -3
- package/docs/adr/0013-paths-as-object-overlays-with-autotile-dispatch.md +1 -1
- package/docs/adr/0014-families-canonical-over-vocabulary.md +1 -1
- package/docs/adr/0015-task-dependencies-and-auto-promotion.md +1 -1
- package/docs/adr/0016-trust-boundary-server-enforced-permissions.md +1 -1
- package/docs/adr/0018-three-rank-model-goes-live.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +9 -9
- package/docs/adr/0021-per-builder-skill-model-preferences.md +2 -2
- package/docs/adr/0022-secrets-policy.md +5 -5
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +3 -3
- package/docs/adr/0024-multi-agent-system-architecture.md +10 -10
- package/docs/adr/0025-offsite-backup-vendor-digitalocean-spaces.md +1 -1
- package/docs/adr/0025-structured-criterion-task-link.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +13 -13
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +14 -14
- package/docs/adr/0029-status-page-outage-resilience.md +1 -1
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +7 -7
- package/docs/adr/0032-discord-mirror-of-gds.md +3 -3
- package/docs/adr/0033-discord-bot-service-principal.md +4 -4
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0035-builder-onboarding-three-paths.md +4 -4
- package/docs/adr/0036-builders-subdomain-and-ui-reorg.md +1 -1
- package/docs/adr/0037-discord-channel-management.md +1 -1
- package/docs/adr/0038-chromebook-ttyd-cloudflare-tunnel.md +5 -5
- package/docs/adr/0039-setup-first-onboarding-ux.md +6 -6
- package/docs/adr/0040-remote-control-default-browser-onramp.md +3 -3
- package/docs/adr/0041-temporary-grader-bypass-killswitch.md +2 -2
- package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +5 -5
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +2 -2
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0046-sandbox-first-review-gate.md +1 -1
- package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +1 -1
- package/docs/adr/0049-split-parallel-safety-contract.md +1 -1
- package/docs/adr/0050-device-flow-admission-invite-gated-by-default.md +2 -2
- package/docs/adr/0051-full-session-transcript-corpus.md +2 -2
- package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +1 -1
- package/docs/adr/0053-scoped-dev-box-session.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +7 -7
- package/docs/adr/0055-server-mediated-branch-publish.md +2 -2
- package/docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md +3 -3
- package/docs/adr/0057-container-cost-ledger.md +1 -1
- package/docs/adr/0057-discord-archon-approval-channels.md +2 -2
- package/docs/adr/0058-automatic-gate-surface-gatekeeper.md +6 -6
- package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +11 -11
- package/docs/adr/0060-gds-retrieval-layer.md +3 -3
- package/docs/adr/0061-context-layer-decomposition.md +2 -2
- package/docs/adr/0062-medusa-instance-model-decoupling.md +5 -5
- package/docs/adr/0063-generated-repo-map-symbol-skeleton.md +2 -2
- package/docs/adr/0064-rename-medusa-to-cloud-bongos.md +3 -3
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +5 -5
- package/docs/adr/0066-generated-file-map-sections.md +2 -2
- package/docs/adr/0067-session-start-fast-forward-pull.md +1 -1
- package/docs/adr/0068-shared-art-key-for-newcomers.md +1 -1
- package/docs/adr/0069-hall-live-channel-sse-decouple.md +3 -3
- package/docs/adr/0070-deterministic-session-card-delivery.md +1 -1
- package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +2 -2
- package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +2 -2
- package/docs/adr/0072-dev-box-code-staleness-visibility.md +2 -2
- package/docs/adr/0073-builder-needs-signal-and-byok-gemini-key.md +3 -3
- package/docs/adr/0073-secrets-scan-exclude-uri-detector.md +2 -2
- package/docs/adr/0076-in-session-deterministic-compaction.md +2 -2
- package/docs/adr/0077-content-addressed-llm-cache.md +2 -2
- package/docs/adr/0078-deterministic-first-cron.md +3 -3
- package/docs/adr/0079-value-gated-autonomous-engagement.md +2 -2
- package/docs/adr/0080-llm-cache-api-transport-and-trust-model.md +3 -3
- package/docs/adr/0081-tool-agnostic-design-layer.md +3 -3
- package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +2 -2
- package/docs/adr/0083-modular-architecture-strangler-build-plan.md +3 -3
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +2 -2
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +15 -15
- package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +5 -5
- package/docs/adr/0087-compete-on-governance-not-tooling-cursor.md +1 -1
- package/docs/adr/0088-consistent-character-generative-animation.md +5 -5
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -15
- package/docs/adr/0090-metic-task-authoring.md +8 -8
- package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +3 -3
- package/docs/adr/0092-hosting-module-and-on-prem-target.md +11 -11
- package/docs/adr/0093-tranche-2-core-carve-sequence.md +5 -5
- package/docs/adr/0094-repo-atlas-self-drawing-codebase-map.md +1 -1
- package/docs/adr/0095-borrowed-memory-and-retrieval-concepts.md +6 -6
- package/docs/adr/0095-cross-agent-context-management.md +5 -5
- package/docs/adr/0096-require-reward-before-workable.md +5 -5
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -2
- package/docs/adr/0097-retroactive-reward-backfill.md +4 -4
- package/docs/adr/0098-public-mirror-publish-manifest.md +3 -3
- package/docs/adr/0099-delayed-redacted-mirror-export.md +1 -1
- package/docs/adr/0100-two-repo-instance-model.md +6 -6
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +9 -9
- package/docs/adr/0102-finish-bounding-the-kernel.md +1 -1
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +6 -6
- package/docs/adr/0103-gdsv4-bongos-consolidation.md +3 -3
- package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +3 -3
- package/docs/adr/0105-instance-seed-migrations-out-of-core.md +2 -2
- package/docs/adr/0106-goal-owner-manager-authority.md +2 -2
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +6 -6
- package/docs/adr/0108-instance-composition-configurable-root.md +6 -6
- package/docs/adr/0109-self-describing-openapi-and-hosted-docs.md +3 -3
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +1 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +18 -18
- package/docs/adr/0112-private-goals-invitations-join-requests.md +7 -7
- package/docs/adr/0113-sandbox-studio-descope-superseded-by-0044-0052.md +1 -1
- package/docs/adr/0114-instance-docs-discovery-contract.md +4 -4
- package/docs/adr/0115-scheduled-routines-portability.md +1 -1
- package/docs/adr/0117-api-versioning-and-deprecation-policy.md +1 -1
- package/docs/adr/0118-api-contract-source-of-truth-code-first.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +2 -2
- package/docs/adr/0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md +1 -1
- package/docs/adr/0122-onboarding-diagram-renders-deploy-rendered-off-git.md +3 -3
- package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +1 -1
- package/docs/adr/0124-typed-api-response-bodies.md +2 -2
- package/docs/adr/0125-control-plane-scaffolds-standalone-instance-repo.md +2 -2
- package/docs/adr/0126-dedicated-cloudbongos-control-plane-droplet.md +3 -3
- package/docs/adr/0127-cross-box-fleet-observability.md +1 -1
- package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +3 -3
- package/docs/adr/0129-cloudbongos-control-plane-pull-deploy.md +4 -4
- package/docs/adr/0130-provision-remote-exec-over-ssh.md +4 -4
- package/docs/adr/0131-rank-scoped-skill-visibility.md +2 -2
- package/docs/adr/0132-cohost-fleet-control-plane-migration.md +2 -2
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +1 -1
- package/docs/adr/0134-private-first-npm-distribution.md +3 -3
- package/docs/adr/0135-module-upstream-submission-interim-queue.md +1 -1
- package/docs/adr/0136-update-channel-subscription-policy.md +2 -2
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +6 -6
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +4 -4
- package/docs/adr/0142-project-context-is-prose-only.md +2 -2
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +2 -2
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +2 -2
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +2 -2
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +4 -4
- package/docs/adr/0147-standalone-live-docs-regen.md +1 -1
- package/docs/adr/0149-bongos-dev-local-launcher.md +1 -1
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +3 -3
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +20 -20
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +2 -2
- package/docs/adr/0152-metic-task-abandonment.md +7 -7
- package/docs/adr/0153-sequential-chain-runner.md +1 -1
- package/docs/adr/0154-goal-owner-criterion-authoring.md +7 -7
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +2 -2
- package/docs/adr/0156-user-set-display-name-wins.md +1 -1
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +10 -10
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +1 -1
- package/docs/adr/0159-merge-gate-reconciliation.md +1 -1
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +1 -1
- package/docs/adr/0161-publish-on-merge.md +2 -2
- package/docs/adr/0163-legacy-display-name-resolution.md +1 -1
- package/docs/adr/0165-instances-hookless-by-default.md +1 -1
- package/docs/adr/0166-module-maintenance-and-deprecation-policy.md +3 -3
- package/docs/adr/0167-gate-trust-link-agpl-default.md +2 -2
- package/docs/adr/0167-module-catalog-source-vs-provenance.md +3 -3
- package/docs/adr/0169-bongos-serve-server-bring-up.md +1 -1
- package/docs/adr/0171-profile-rollup-join-and-consent.md +1 -1
- package/docs/adr/0172-editable-rank-roles-substrate-axis.md +2 -2
- package/docs/adr/0172-per-craft-compensation-ideator-credit-lane.md +4 -4
- package/docs/adr/0173-legacy-multi-ship-shortfall-not-backfilled.md +1 -1
- package/docs/adr/0174-government-terminology-ranks-not-roles.md +1 -1
- package/docs/adr/0177-goal-membership-kind.md +3 -3
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +1 -1
- package/docs/adr/0179-board-membership-predicate-grammar.md +1 -1
- package/docs/adr/0181-teardown-state-gate-and-address-retention.md +1 -1
- package/docs/adr/0182-project-visibility-two-axes.md +1 -1
- package/docs/adr/0183-criteria-close-themselves.md +1 -1
- package/docs/adr/0184-ship-requires-an-assurance.md +1 -1
- package/docs/adr/0185-spark-handoff-credit-split.md +1 -1
- package/docs/adr/0186-per-project-idea-templates.md +1 -1
- package/docs/adr/0187-collab-four-decisions.md +1 -1
- package/docs/adr/0187-oauth-handshake-cookie-lifetime.md +1 -1
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +2 -2
- package/docs/adr/0191-majority-and-the-author-who-may-not-decide-alone.md +1 -1
- package/docs/adr/0192-a-category-orients-and-authorises-nothing.md +2 -2
- package/docs/adr/0193-pause-task-scoped-box-slices.md +1 -1
- package/docs/adr/0195-adr-numbers-are-checked-like-migration-numbers.md +2 -2
- package/docs/adr/0196-chrome-world-token-contract-and-the-landing-sheets.md +2 -2
- package/docs/adr/0197-the-ui-design-module.md +1 -1
- package/docs/adr/0198-third-party-skill-vendoring-policy.md +1 -1
- package/docs/adr/0203-doorway-changelog-out-of-module-api.md +2 -2
- package/docs/adr/0204-the-apex-is-dark-only.md +2 -2
- package/docs/adr/0205-federated-checkin-needs-the-hubs-own-signin-witness.md +1 -1
- package/docs/adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md +1 -1
- package/docs/adr/0207-connections-implementation-departures-and-honest-limits.md +1 -1
- package/docs/adr/0208-application-lifecycle-the-handle-waits-and-the-vouch-has-no-minter.md +2 -2
- package/docs/adr/0209-one-budget-for-the-account-existence-oracle.md +1 -1
- package/docs/adr/0210-scouting-graduates-on-an-instance-asserted-owner-claim.md +1 -1
- package/docs/adr/0212-connections-on-the-profile-surface-and-addressing.md +1 -1
- package/docs/adr/0213-platform-terms-acceptance-is-the-hubs-own-witness.md +2 -2
- package/docs/adr/0215-the-recruiter-sliver-is-a-ceiling-not-a-step.md +6 -6
- package/docs/adr/0216-a-fake-pool-interpreter-must-fail-loudly-not-silently-ignore.md +2 -2
- package/docs/adr/0217-rank-gates-inviting-not-viewing.md +1 -1
- package/docs/adr/0218-the-apex-root-web-surface.md +2 -2
- package/docs/adr/0219-a-look-is-a-branding-pack-the-style-library.md +1 -1
- package/docs/adr/0220-an-in-house-rebuilt-skill-is-a-first-party-skill.md +1 -1
- package/docs/adr/0221-the-impeccable-method-is-one-skill-with-sub-command-playbooks.md +2 -2
- package/docs/adr/0222-the-image-family-under-the-hero-only-rule.md +2 -2
- package/docs/adr/0223-the-detector-is-two-tiers-in-the-kit.md +3 -3
- package/docs/adr/0224-module-skills-reach-the-core-checkout-as-untracked-self-ignored-copies.md +1 -1
- package/docs/adr/0225-privacy-suppresses-cross-project-surfaces-only.md +1 -1
- package/docs/adr/0226-one-stale-timer-reporting-degrades-enforcement-refuses.md +1 -1
- package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +1 -1
- package/docs/adr/0230-a-look-skill-is-a-composition-grammar-the-palette-is-the-pack.md +1 -1
- package/docs/adr/0231-the-style-session-an-owner-authors-a-look-inside-the-contract.md +2 -2
- package/docs/adr/0232-rot-is-derived-not-swept-and-water-is-the-only-new-verb.md +2 -2
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +2 -2
- package/docs/adr/0234-idea-routing-capture-time-promotion-landing-matrix-homeless-inbox.md +2 -2
- package/docs/adr/0235-a-fix-category-is-kindbug-under-a-goal-no-column-required.md +11 -11
- package/docs/adr/0236-many-ideas-one-task-the-lineage-key-is-the-pair.md +5 -5
- package/docs/adr/0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md +5 -5
- package/docs/adr/0238-the-join-door-is-a-provisioning-settings-policy-key.md +1 -1
- package/docs/adr/0239-a-recommendation-is-a-table-that-carries-its-own-reason.md +4 -4
- package/docs/adr/0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md +4 -4
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +1 -1
- package/docs/adr/0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md +2 -2
- package/docs/adr/0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md +4 -4
- package/docs/adr/0244-a-catalog-search-is-a-view-over-the-rows-never-an-edit-to-the-selection.md +3 -3
- package/docs/adr/0245-the-way-back-into-an-optional-step-is-that-step-s-own-answer-route.md +7 -7
- package/docs/adr/0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md +1 -1
- package/docs/adr/0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md +2 -2
- package/docs/adr/0248-a-hub-project-s-owner-is-the-provisioning-row-carried-on-a-port.md +3 -3
- package/docs/adr/0249-the-invite-step-is-the-done-panel-s-first-act-not-a-rail-step.md +3 -3
- package/docs/adr/0250-strict-versioning-the-version-boundary-is-the-scope-gate.md +6 -6
- package/docs/adr/0251-a-suggestion-of-who-to-invite-is-a-recruiting-surface-and-owes-the-opt-out.md +5 -5
- package/docs/adr/0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md +19 -19
- package/docs/adr/0253-the-takedown-lever-has-two-writers-and-provenance-is-a-column.md +2 -2
- package/docs/adr/0254-a-relayed-link-is-contained-downward-or-it-is-not-published.md +6 -6
- package/docs/adr/0255-a-public-lists-ordering-is-part-of-its-payload.md +2 -2
- package/docs/adr/0256-the-visibility-ceiling-is-the-sign-in-gate-too-and-the-reader-moves-to-the-composer.md +3 -3
- package/docs/adr/0257-auth-resolves-before-the-hall-mounts-anything.md +1 -1
- package/docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md +1 -1
- package/docs/adr/0259-a-projects-departure-from-the-public-list-is-public.md +7 -7
- package/docs/adr/0260-the-application-is-the-consent-and-the-echo-is-the-gate.md +4 -4
- package/docs/adr/0261-a-preselect-always-carries-a-reason-the-bundle-summary-is-the-floor.md +1 -1
- package/docs/adr/0262-a-bug-never-lands-in-the-inbox.md +1 -1
- package/docs/adr/0263-how-a-version-closes.md +3 -3
- package/docs/adr/0264-the-ten-working-areas.md +3 -3
- package/docs/adr/0265-work-with-no-goal-and-the-order-the-cut-needs.md +5 -5
- package/docs/adr/0266-the-board-room-is-its-own-surface-reachable-by-whoever-may-vote.md +2 -2
- package/docs/adr/0267-unanimity-and-the-revise-and-re-sit-loop.md +1 -1
- package/docs/adr/0268-the-constitution-comes-from-two-roots.md +3 -3
- package/docs/adr/0269-the-cli-session-store-is-host-keyed-at-a-fixed-anchor.md +1 -1
- package/docs/adr/0270-rank-floor-derives-without-a-prediction.md +2 -2
- package/docs/adr/0271-the-ideator-surface-is-two-halves.md +3 -3
- package/docs/adr/0272-a-craft-is-an-identity-not-a-toolchain.md +4 -4
- package/docs/adr/0273-hall-nav-can-gate-on-a-permission.md +1 -1
- package/docs/adr/0274-one-kernel-three-role-packs.md +1 -1
- package/docs/adr/0275-one-source-for-a-written-role-responsibility.md +1 -1
- package/docs/adr/0276-the-skill-listing-budget-cannot-hold-every-trigger.md +1 -1
- package/docs/adr/0277-a-box-is-in-use-only-while-a-human-is-attached.md +1 -1
- package/docs/adr/0278-a-gated-project-still-takes-applications.md +2 -2
- package/docs/adr/0279-an-upgrade-is-proven-by-the-served-version-not-the-health-check.md +1 -1
- package/docs/adr/0280-the-unattended-lane-may-drive-a-co-tenant-because-the-roster-is-the-decision.md +1 -1
- package/docs/adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md +2 -2
- package/docs/adr/README.md +294 -294
- package/docs/api/openapi.json +72 -5
- package/docs/api-reference.md +8 -2
- package/docs/architecture.md +22 -22
- package/docs/branding-contract.md +5 -5
- package/docs/canonical-permissions.md +22 -22
- package/docs/copy-inventory.md +15 -13
- package/docs/copy-registry.json +35 -17
- package/docs/design/hall-direction-v2.md +3 -3
- package/docs/design/landing-page-direction-v2.md +1 -1
- package/docs/design/projects-hub-direction-v2.md +1 -1
- package/docs/file-map.md +3 -1
- package/docs/module-api-changelog.md +36 -1
- package/docs/modules-contract.md +25 -25
- package/docs/onboarding/browser-terminal-guide.md +1 -1
- package/docs/onboarding/diagrams/README.md +3 -3
- package/docs/onboarding/drachmae-and-karma.md +13 -13
- package/docs/onboarding/primer.md +7 -7
- package/docs/packs/artist.md +1 -1
- package/docs/packs/ideator.md +1 -1
- package/docs/project-context.template.md +3 -3
- package/docs/recipes/builder-box-lifecycle.md +5 -5
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/core-release-pipeline.md +55 -14
- package/docs/recipes/figma-design-loop.md +1 -1
- package/docs/recipes/gds-db-backup.md +2 -2
- package/docs/recipes/instance-deploy.md +2 -2
- package/docs/recipes/instance-service-restart.md +3 -3
- package/docs/recipes/local-dev.md +2 -2
- package/docs/recipes/managed-settings-remote-control.md +3 -3
- package/docs/recipes/memory-backup.md +4 -4
- package/docs/recipes/memory-sync-conflicts.md +2 -2
- package/docs/recipes/multi-builder-merge.md +2 -2
- package/docs/recipes/ops-gotchas.md +4 -4
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/packaging-the-core.md +6 -6
- package/docs/recipes/private-npm-distribution.md +5 -5
- package/docs/recipes/search-index-ingestion.md +3 -3
- package/docs/recipes/self-host.md +5 -5
- package/docs/recipes/server-bring-up.md +1 -1
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +1 -1
- package/docs/recipes/ui-look-before-you-ship.md +1 -1
- package/docs/recipes/upgrading-the-core.md +2 -2
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +6 -6
- package/docs/research/cursor-competitive-analysis.md +3 -3
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +7 -7
- package/migrations/core_240_artist_gate_indexes.sql +48 -0
- package/migrations/core_240_goal_members_drop_legacy_role.sql +79 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/hall-ui/public/watch.css +6 -0
- package/modules/hall-ui/public/watch.js +52 -6
- package/modules/lifecycle/cascade.js +37 -9
- package/modules/lifecycle/db-claims.js +5 -2
- package/modules/lifecycle/db-goals.js +4 -3
- package/modules/lifecycle/db-overrides.js +117 -4
- package/modules/lifecycle/db-tasks.js +63 -0
- package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +12 -4
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/pg-constraint-fail.js +49 -0
- package/modules/lifecycle/routes/artist-gate.js +101 -0
- package/modules/lifecycle/routes/claims.js +13 -1
- package/modules/lifecycle/routes/lifecycle.js +6 -0
- package/modules/lifecycle/routes/task-write-routes.js +7 -0
- package/modules/lifecycle/routes/tasks.js +24 -0
- package/modules/lifecycle/task-classifier.js +10 -1
- package/modules/lifecycle/tests/cascade.mjs +43 -0
- package/modules/provisioning/provisioning.js +79 -3
- package/modules/provisioning/routes/provisioning.js +6 -1
- package/modules/provisioning/tests/provisioning.mjs +8 -8
- package/modules/security/secret-scrub.js +40 -4
- package/modules/status-ui/public/README.md +1 -1
- package/modules/ui-design/docs/design-contract.md +2 -2
- package/modules/ui-design/kit/README.md +1 -1
- package/modules/ui-design/kit/serve.js +1 -0
- package/modules/ui-design/skills/README.md +6 -6
- package/modules/ui-design/skills/brandkit/SKILL.md +1 -1
- package/modules/ui-design/skills/design-taste-frontend/SKILL.md +1 -1
- package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +1 -1
- package/modules/ui-design/skills/gpt-taste/SKILL.md +1 -1
- package/modules/ui-design/skills/high-end-visual-design/SKILL.md +1 -1
- package/modules/ui-design/skills/image-to-code/SKILL.md +1 -1
- package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +1 -1
- package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +1 -1
- package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
- package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +1 -1
- package/modules/ui-design/skills/minimalist-ui/SKILL.md +1 -1
- package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +1 -1
- package/modules/ui-design/skills/stitch-design-taste/SKILL.md +1 -1
- package/modules/ui-design/skills/style/SKILL.md +2 -2
- package/modules/ui-design/styles/README.md +3 -3
- package/modules/ui-design/styles/blueprint/DESIGN.md +1 -1
- package/modules/ui-design/styles/chrome-world/DESIGN.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/audit-rank-roles.js +172 -0
- package/scripts/gds/backfill-task-rewards.js +9 -4
- package/scripts/gds/build-cli-package.js +1 -0
- package/scripts/gds/claim.js +94 -9
- package/scripts/gds/fitness.js +8 -8
- package/scripts/gds/go-live.js +139 -5
- package/scripts/gds/land-watch.js +13 -5
- package/scripts/gds/mirror-redact.js +13 -2
- package/scripts/gds/package-core.js +11 -3
- package/scripts/gds/release.js +55 -0
- package/scripts/gds/ship-land.js +10 -2
- package/scripts/gds/ship-regen.js +41 -8
- package/scripts/gds/start.js +7 -4
- package/scripts/gds/worktree-claim-guard.js +27 -1
- package/scripts/public-mirror-export.js +17 -4
- package/src/bongos/api-errors.js +9 -0
- package/src/bongos/artist-gate.js +204 -0
- package/src/branding.js +15 -0
- package/src/module-api.js +34 -1
- package/tests/api_path_404.mjs +28 -0
- package/tests/artist_gate.mjs +234 -0
- package/tests/artist_gate_release.mjs +283 -0
- package/tests/artist_gate_route.mjs +174 -0
- package/tests/box_scope_predicate.mjs +117 -0
- package/tests/claim_error_surface.mjs +163 -1
- package/tests/cli_exit_no_abort.mjs +256 -3
- package/tests/credit_log_single_writer.mjs +94 -0
- package/tests/currency_label.mjs +11 -4
- package/tests/go_live.mjs +144 -0
- package/tests/helpers.mjs +8 -0
- package/tests/main_worktree_busy_reason.mjs +167 -0
- package/tests/membership_kind_lockstep.mjs +88 -0
- package/tests/mirror_redact.mjs +14 -0
- package/tests/module_api.mjs +6 -0
- package/tests/provision_settings_apply.mjs +13 -5
- package/tests/provisioning_settings.mjs +11 -7
- package/tests/provisioning_settings_apply.mjs +4 -4
- package/tests/provisioning_settings_env.mjs +59 -7
- package/tests/public_mirror_export.mjs +12 -0
- package/tests/rank_roles_audit.mjs +114 -0
- package/tests/scrubber_corpus.mjs +70 -2
- package/tests/start_rebase_warning.mjs +50 -0
- package/tests/task_field_bounds_match_db.mjs +140 -0
- package/tests/watch_roster_self_row.mjs +289 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// tests/main_worktree_busy_reason.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002871 — every bail that touches the shared main checkout must identify
|
|
4
|
+
// the OCCUPANT, not just the symptom.
|
|
5
|
+
//
|
|
6
|
+
// THE INCIDENT (idea 1000604, 2026-07-28). A shared main checkout sat parked on
|
|
7
|
+
// `task-1002503-backfill-recovery` with 2 unpushed commits and 4 modified
|
|
8
|
+
// generated files, and blocked the auto-merge leg for NINE confirmed tasks. Most
|
|
9
|
+
// of that idea was fixed: ship-merge.js now detects a main checkout on a feature
|
|
10
|
+
// branch, names it, and states the cure.
|
|
11
|
+
//
|
|
12
|
+
// What was NOT fixed is the exact shape of the incident. mainWorktreeBusy() runs
|
|
13
|
+
// FIRST, and only ever said 'a merge is already in progress' or 'the working tree
|
|
14
|
+
// is dirty or has unmerged files'. A checkout that is BOTH parked AND dirty — the
|
|
15
|
+
// 2026-07-28 state precisely — short-circuits there, so the named-branch
|
|
16
|
+
// diagnostic was reachable only when the tree was clean: the one case where
|
|
17
|
+
// nobody needs telling twice.
|
|
18
|
+
//
|
|
19
|
+
// The decision is pure now so the exact string is pinned rather than eyeballed.
|
|
20
|
+
// This is the ship hot path and the string IS the deliverable.
|
|
21
|
+
//
|
|
22
|
+
// Run: node tests/main_worktree_busy_reason.mjs
|
|
23
|
+
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
+
const { describeMainWorktreeBusy } = require(path.join(ROOT, 'scripts', 'gds', 'ship-regen.js'));
|
|
33
|
+
const { parkedMainCheckoutWarning } = require(path.join(ROOT, 'scripts', 'gds', 'worktree-claim-guard.js'));
|
|
34
|
+
|
|
35
|
+
let passed = 0;
|
|
36
|
+
let failed = 0;
|
|
37
|
+
function t(name, fn) {
|
|
38
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
39
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- describeMainWorktreeBusy ------------------------------------------------
|
|
43
|
+
|
|
44
|
+
t('a clean main checkout on main is not busy', () => {
|
|
45
|
+
assert.equal(describeMainWorktreeBusy({ mergeInProgress: false, dirty: false, branch: 'main' }), null);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
t('THE INCIDENT: dirty AND parked on a feature branch names the branch', () => {
|
|
49
|
+
const reason = describeMainWorktreeBusy({
|
|
50
|
+
mergeInProgress: false, dirty: true, branch: 'task-1002503-backfill-recovery',
|
|
51
|
+
});
|
|
52
|
+
assert.match(reason, /dirty or has unmerged files/, 'keeps the original symptom');
|
|
53
|
+
assert.match(reason, /task-1002503-backfill-recovery/, 'and NAMES the squatting branch — the whole point');
|
|
54
|
+
assert.match(reason, /not `main`/, 'and says what it should have been');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
t('a merge in progress on a feature branch also names it', () => {
|
|
58
|
+
const reason = describeMainWorktreeBusy({ mergeInProgress: true, dirty: true, branch: 'task-999' });
|
|
59
|
+
assert.match(reason, /merge is already in progress/);
|
|
60
|
+
assert.match(reason, /task-999/, 'the branch is named on this arm too, not just the dirty one');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
t('merge-in-progress wins over dirty — one reason, the most specific', () => {
|
|
64
|
+
const reason = describeMainWorktreeBusy({ mergeInProgress: true, dirty: true, branch: 'main' });
|
|
65
|
+
assert.match(reason, /merge is already in progress/);
|
|
66
|
+
assert.ok(!/dirty/.test(reason), 'must not concatenate both symptoms into one run-on reason');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
t('on `main` itself the branch is NOT named — that line would be pure noise', () => {
|
|
70
|
+
const reason = describeMainWorktreeBusy({ mergeInProgress: false, dirty: true, branch: 'main' });
|
|
71
|
+
assert.equal(reason, 'the working tree is dirty or has unmerged files',
|
|
72
|
+
'saying "(on main, not main)" in every bail trains the reader to skip the line');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
t('an unreadable branch degrades to the old wording rather than inventing one', () => {
|
|
76
|
+
for (const branch of [null, undefined, '', 'HEAD']) {
|
|
77
|
+
const reason = describeMainWorktreeBusy({ mergeInProgress: false, dirty: true, branch });
|
|
78
|
+
assert.equal(reason, 'the working tree is dirty or has unmerged files',
|
|
79
|
+
`branch=${JSON.stringify(branch)} must not produce a half-formed name`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
t('a non-default defaultBranch is honoured', () => {
|
|
84
|
+
assert.equal(describeMainWorktreeBusy({ dirty: true, branch: 'trunk', defaultBranch: 'trunk' }),
|
|
85
|
+
'the working tree is dirty or has unmerged files', 'the repo default is not hardcoded to main');
|
|
86
|
+
assert.match(describeMainWorktreeBusy({ dirty: true, branch: 'main', defaultBranch: 'trunk' }),
|
|
87
|
+
/on `main`, not `trunk`/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
t('no facts at all is a quiet null, never a throw', () => {
|
|
91
|
+
assert.equal(describeMainWorktreeBusy(), null);
|
|
92
|
+
assert.equal(describeMainWorktreeBusy({}), null);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// --- parkedMainCheckoutWarning (the release.js half) -------------------------
|
|
96
|
+
|
|
97
|
+
t('release warning is silent when the main checkout is on main', () => {
|
|
98
|
+
assert.deepEqual(parkedMainCheckoutWarning({ branch: 'main' }), []);
|
|
99
|
+
assert.deepEqual(parkedMainCheckoutWarning({ branch: null }), [], 'an unreadable branch says nothing');
|
|
100
|
+
assert.deepEqual(parkedMainCheckoutWarning({}), []);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
t('release warning names the parked branch, the cost, and the cure', () => {
|
|
104
|
+
const out = parkedMainCheckoutWarning({ branch: 'task-1002503-backfill-recovery' }).join('\n');
|
|
105
|
+
assert.match(out, /task-1002503-backfill-recovery/, 'names the occupant');
|
|
106
|
+
assert.match(out, /nowhere to merge INTO/, 'states the cost, not just the fact');
|
|
107
|
+
assert.match(out, /someone else/, 'and that the cost lands on other builders too');
|
|
108
|
+
assert.match(out, /checkout main/, 'and gives the cure');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
t('release warning NEVER offers to restore the tree — on EITHER branch of the message', () => {
|
|
112
|
+
// Both arms, deliberately. A probe that made only the clean-tree line offer a
|
|
113
|
+
// restore kept this test green when it checked the dirty arm alone: the
|
|
114
|
+
// "we will not touch it" promise has two renderings and both must keep it.
|
|
115
|
+
for (const dirty of [true, false]) {
|
|
116
|
+
const out = parkedMainCheckoutWarning({ branch: 'task-42', dirty }).join('\n');
|
|
117
|
+
assert.match(out, /nothing here will touch it/i,
|
|
118
|
+
`dirty=${dirty}: switching the tree back would be a destructive guess — the branch may hold real work`);
|
|
119
|
+
assert.ok(!/\brestore it\b/i.test(out),
|
|
120
|
+
`dirty=${dirty}: must not offer to restore the tree on the builder's behalf`);
|
|
121
|
+
}
|
|
122
|
+
const dirtyOut = parkedMainCheckoutWarning({ branch: 'task-42', dirty: true }).join('\n');
|
|
123
|
+
assert.match(dirtyOut, /uncommitted changes/i, 'and when it IS dirty, say so — that is why we do not touch it');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// --- the probes that gather the facts must not build shell strings -----------
|
|
127
|
+
//
|
|
128
|
+
// Caught by the grader on this task: the first cut ran
|
|
129
|
+
// `git -C "${root}" rev-parse …` through execSync, which spawns a shell. A
|
|
130
|
+
// resolved filesystem path is NOT trusted input — a directory containing a quote,
|
|
131
|
+
// backtick or `$()` breaks the quoting and executes as shell. The repo's
|
|
132
|
+
// established pattern passes the directory to child_process instead of
|
|
133
|
+
// interpolating it into a command string, and release.js now goes further and
|
|
134
|
+
// uses an argv array with no shell at all.
|
|
135
|
+
t('release.js never interpolates a path into a git command string', () => {
|
|
136
|
+
const raw = readFileSync(path.join(ROOT, 'scripts', 'gds', 'release.js'), 'utf8');
|
|
137
|
+
// Strip comments first: the file DESCRIBES the unsafe form on purpose, to stop
|
|
138
|
+
// the next reader reintroducing it. Scanning prose as if it were code is how a
|
|
139
|
+
// guard reds on its own documentation — which this one did, first run.
|
|
140
|
+
const src = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
|
|
141
|
+
assert.ok(!/git\s+-C\s+["'`]?\$\{/.test(src),
|
|
142
|
+
'a `git -C "${path}"` command string is shell-interpolated — pass the directory to child_process instead');
|
|
143
|
+
assert.ok(!/execSync\s*\(\s*[`'"]git /.test(src),
|
|
144
|
+
'a git command built as a shell string re-opens the injection this test exists to close');
|
|
145
|
+
assert.match(src, /execFileSync\('git', args/,
|
|
146
|
+
'git must be invoked with an argv array, which never reaches a shell');
|
|
147
|
+
assert.match(src, /cwd,/, 'and the directory must travel as cwd, not inside the command');
|
|
148
|
+
// The stripper must not have eaten the file: a guard over an empty string
|
|
149
|
+
// passes every negative assertion above and proves nothing.
|
|
150
|
+
assert.ok(src.length > 500, `comment-stripping left only ${src.length} chars — the scan is reading nothing`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
t('release.js imports nothing outside the published CLI package', () => {
|
|
154
|
+
const src = readFileSync(path.join(ROOT, 'scripts', 'gds', 'release.js'), 'utf8');
|
|
155
|
+
const manifest = readFileSync(path.join(ROOT, 'scripts', 'gds', 'build-cli-package.js'), 'utf8');
|
|
156
|
+
const locals = [...src.matchAll(/require\('\.\/([A-Za-z0-9._-]+)'\)/g)].map((m) => m[1]);
|
|
157
|
+
assert.ok(locals.length > 0, 'no local requires found — this guard is reading the wrong shape');
|
|
158
|
+
for (const name of locals) {
|
|
159
|
+
const file = name.endsWith('.js') ? name : `${name}.js`;
|
|
160
|
+
assert.ok(manifest.includes(`scripts/gds/${file}`),
|
|
161
|
+
`release.js requires ./${name}, which is not in the CLI package FILES list — `
|
|
162
|
+
+ '`bongos release` would crash at require time in the published package (it did once already)');
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
console.log(`\nmain_worktree_busy_reason: ${passed} passed, ${failed} failed`);
|
|
167
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -35,9 +35,13 @@ import assert from 'node:assert/strict';
|
|
|
35
35
|
import { test } from 'node:test';
|
|
36
36
|
import fs from 'node:fs';
|
|
37
37
|
import path from 'node:path';
|
|
38
|
+
import { createRequire } from 'node:module';
|
|
38
39
|
import { fileURLToPath } from 'node:url';
|
|
39
40
|
import { authSource } from './helpers.mjs';
|
|
40
41
|
|
|
42
|
+
const require = createRequire(import.meta.url);
|
|
43
|
+
const lint = require('../scripts/gds/lint-migrations.js');
|
|
44
|
+
|
|
41
45
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
42
46
|
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
43
47
|
|
|
@@ -111,3 +115,87 @@ test('the sender writes BOTH spellings, computing the kind once (the cross-versi
|
|
|
111
115
|
assert.equal((body.match(/membershipKind\(\{ githubLogin \}\)/g) || []).length, 1,
|
|
112
116
|
'the kind is computed ONCE per check-in, not per field');
|
|
113
117
|
});
|
|
118
|
+
|
|
119
|
+
// ── THE CONTRACT PHASE — the test of the shim's own removal (task 1003089) ─────
|
|
120
|
+
//
|
|
121
|
+
// ADR 0174's Lineage lesson: a time-boxed shim outlives its release unless its
|
|
122
|
+
// removal ships with the test that says it is gone. core_240 closed the GOAL
|
|
123
|
+
// membership window — the trigger, the function and `goal_members.role` are all
|
|
124
|
+
// dropped, and `membership_kind` carries NOT NULL + DEFAULT itself rather than
|
|
125
|
+
// resting on a trigger surviving.
|
|
126
|
+
//
|
|
127
|
+
// The PROJECT membership half (platform_identity_010) is deliberately NOT here:
|
|
128
|
+
// it is still inside its cross-version federation window, its contract phase is
|
|
129
|
+
// task 1003092, and the expand-phase block above is what still guards it. Do not
|
|
130
|
+
// merge the two — the whole point of the pair is that they close on different
|
|
131
|
+
// schedules.
|
|
132
|
+
const CONTRACT = 'migrations/core_240_goal_members_drop_legacy_role.sql';
|
|
133
|
+
|
|
134
|
+
test('core_240: the contract phase drops the trigger, the function AND the column', () => {
|
|
135
|
+
const sql = read(CONTRACT);
|
|
136
|
+
assert.match(sql, /DROP TRIGGER IF EXISTS goal_members_membership_kind_lockstep ON goal_members/);
|
|
137
|
+
assert.match(sql, /DROP FUNCTION IF EXISTS goal_members_membership_kind_lockstep\(\)/,
|
|
138
|
+
'the FUNCTION outlives the trigger unless it is dropped by name — dropping only the trigger '
|
|
139
|
+
+ 'leaves a dangling plpgsql body naming a column that no longer exists');
|
|
140
|
+
assert.match(sql, /ALTER TABLE goal_members\s+DROP COLUMN IF EXISTS role/);
|
|
141
|
+
assert.match(sql, /ALTER COLUMN membership_kind SET NOT NULL/,
|
|
142
|
+
'the guarantee core_220 deferred to step 2 arrives here, or the column never gets one');
|
|
143
|
+
assert.match(sql, /ALTER COLUMN membership_kind SET DEFAULT 'member'/,
|
|
144
|
+
'safe only NOW: a default while the legacy column was still written is the task-1003093 bug');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('core_240: the forward-unsafe pragma is present, and its FIRST line carries the reason', () => {
|
|
148
|
+
const scan = lint.scanMigrationSql(read(CONTRACT));
|
|
149
|
+
assert.deepEqual([...scan.unsafe].sort(), ['drop-column', 'set-not-null'],
|
|
150
|
+
'the linter must still SEE both operations — an acknowledged migration is not an exempt one');
|
|
151
|
+
assert.ok(scan.acknowledged,
|
|
152
|
+
'a drop-column with no pragma reds tests/migrations_forward_safe.mjs, which is the point');
|
|
153
|
+
// ACK_RE captures a SINGLE line, so a reason spread over a paragraph records
|
|
154
|
+
// nothing in the lint output. The release a rollback could restore is the one
|
|
155
|
+
// fact that makes the drop safe, so it belongs on that line.
|
|
156
|
+
assert.match(scan.ackReason, /1\.19\.238/,
|
|
157
|
+
'the pragma must name the release that stopped reading the column, not merely assert safety');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('the FRESH-INSTALL seed writes the new name — the writer that is not application code', () => {
|
|
161
|
+
// The last writer of `goal_members.role` was a MODULE migration, not a query:
|
|
162
|
+
// lifecycle_001_goal_first_leads.sql named it in the INSERT list AND the
|
|
163
|
+
// ON CONFLICT arm. Module migrations run AFTER the core ones on every fresh
|
|
164
|
+
// install, so a stale name here survives the DROP invisibly on an existing
|
|
165
|
+
// instance (already in schema_migrations, never re-run) and breaks
|
|
166
|
+
// `cloudbongos init` for every new one. Zero rows on a fresh DB does not save
|
|
167
|
+
// it: a missing column is a plan-time error, not a row-time one.
|
|
168
|
+
const code = lint.stripSqlComments(read('modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql'));
|
|
169
|
+
assert.match(code, /INSERT INTO goal_members \(goal_id, builder_id, membership_kind\)/);
|
|
170
|
+
assert.match(code, /DO UPDATE SET membership_kind = 'lead'/,
|
|
171
|
+
'the ON CONFLICT arm names the column too — renaming only the INSERT list still fails to plan');
|
|
172
|
+
assert.ok(!/\brole\b/.test(code),
|
|
173
|
+
'no statement in the fresh-install seed may name the dropped column');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('no migration outside the three that own its lifecycle still names goal_members.role', () => {
|
|
177
|
+
// Comments are stripped first: the history is allowed to DISCUSS the old column,
|
|
178
|
+
// and this rule is about statements. The three exemptions are the migrations that
|
|
179
|
+
// created the column, expanded away from it, and dropped it.
|
|
180
|
+
const OWNS_ITS_LIFECYCLE = new Set([
|
|
181
|
+
'160_goal_tier.sql', // created it
|
|
182
|
+
'core_220_goal_members_membership_kind.sql', // expand: read it to backfill; the trigger kept it in step
|
|
183
|
+
'core_240_goal_members_drop_legacy_role.sql', // contract: dropped it
|
|
184
|
+
]);
|
|
185
|
+
const dirs = [path.join(ROOT, 'migrations')];
|
|
186
|
+
for (const d of fs.readdirSync(path.join(ROOT, 'modules'), { withFileTypes: true })) {
|
|
187
|
+
if (!d.isDirectory()) continue;
|
|
188
|
+
const md = path.join(ROOT, 'modules', d.name, 'migrations');
|
|
189
|
+
if (fs.existsSync(md)) dirs.push(md);
|
|
190
|
+
}
|
|
191
|
+
const offenders = [];
|
|
192
|
+
for (const dir of dirs) {
|
|
193
|
+
for (const f of fs.readdirSync(dir)) {
|
|
194
|
+
if (!f.endsWith('.sql') || OWNS_ITS_LIFECYCLE.has(f)) continue;
|
|
195
|
+
const code = lint.stripSqlComments(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
196
|
+
if (/goal_members/.test(code) && /\brole\b/.test(code)) offenders.push(f);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
assert.deepEqual(offenders, [],
|
|
200
|
+
`these migrations touch goal_members and still name the dropped column: ${offenders.join(', ')}`);
|
|
201
|
+
});
|
package/tests/mirror_redact.mjs
CHANGED
|
@@ -161,6 +161,20 @@ t('findSecretLeak detects a secret shape, clean text passes', () => {
|
|
|
161
161
|
assert.equal(findSecretLeak('token ghp_' + 'a'.repeat(36)), true);
|
|
162
162
|
assert.equal(findSecretLeak('just ordinary prose here'), false);
|
|
163
163
|
});
|
|
164
|
+
// Regression (task 1003873). findSecretLeak is a DIFFERENTIAL audit — it re-scrubs and asks
|
|
165
|
+
// "did anything change?" — so it must run with the same options as the scrub it is auditing.
|
|
166
|
+
// Given none, it re-redacts the long ADR filename that task 1002460's allowlist deliberately
|
|
167
|
+
// spared, mistakes its own disagreement for a leak, and (the gate being fail-closed) reports
|
|
168
|
+
// a phantom secret in every published doc.
|
|
169
|
+
t('findSecretLeak forwards scrub options, so it can audit a scrub that used them', () => {
|
|
170
|
+
const name = '0281-differential-audit-allowlist-symmetry.md';
|
|
171
|
+
const doc = `see docs/adr/${name} for the rationale`;
|
|
172
|
+
const knownDocNames = new Set([name]);
|
|
173
|
+
assert.equal(findSecretLeak(doc), true, 'without the allowlist the filename reads as a secret');
|
|
174
|
+
assert.equal(findSecretLeak(doc, [], { knownDocNames }), false, 'with it, a file is a file');
|
|
175
|
+
// …and the allowlist is not a smuggling channel: a real shape beside it still trips.
|
|
176
|
+
assert.equal(findSecretLeak(`ghp_${'a'.repeat(36)} in docs/adr/${name}`, [], { knownDocNames }), true);
|
|
177
|
+
});
|
|
164
178
|
|
|
165
179
|
// ── vanilla instance (empty branding) — IP floor still works, no token/owner rules ────
|
|
166
180
|
t('empty branding derives no tokens/owners but the IP floor still redacts', () => {
|
package/tests/module_api.mjs
CHANGED
|
@@ -23,6 +23,11 @@ const PUBLISHED_SURFACE = [
|
|
|
23
23
|
'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope', 'resolveSession',
|
|
24
24
|
'requirePermission', // BV1.R105: the authority-atom gate module routes migrate onto (1.19.0; ADR 0151)
|
|
25
25
|
'pool', 'getPool', 'instanceDbName', 'withTx', 'getBuilderById',
|
|
26
|
+
// added by task 1003575 (ADR 0241 §3): the instance's PROJECT OWNER — its first
|
|
27
|
+
// admitted human builder. Kernel identity like getBuilderById beside it, and
|
|
28
|
+
// published because the artist gate's one escape hatch is the owner's alone, and
|
|
29
|
+
// a module cannot enforce that without being able to ask who the owner is.
|
|
30
|
+
'getFoundingBuilder',
|
|
26
31
|
'secretBox', // Gemini-key storage relocated to modules/art-pipeline + its 'art.getBuilderGeminiKeyRow' port (ADR 0102)
|
|
27
32
|
'grantedScopeCoversRepos', // the create-route credential preflight's scope read (task 1002909); kernel (auth-config.js), unlike db/github-repos which the module reaches via `pool` + its own client
|
|
28
33
|
'accountExistenceReadRateLimit', // task 1003339 (ADR 0209): the shared per-IP budget both account-existence probes spend from — one instance, not a factory, so the budget is genuinely shared
|
|
@@ -40,6 +45,7 @@ const PUBLISHED_SURFACE = [
|
|
|
40
45
|
'resolveRotDays', 'projectSettings', // added by task 1003279 (ADR 0232; goal 1000072): the ROT timer + the live knob store behind it. A DIFFERENT question from the stale-claim timer above — days not hours, DB not env, work nobody picked up rather than a builder who went quiet, and it only ever ASKS a human where that one ACTS. Both ride the doorway so no module hardcodes either.
|
|
41
46
|
'joinabilityMode', // added by task 1003044 (ADR 0194; the carrier after 1.19.374): the project's membership door, one reader for the sign-in gate AND the onboarding module's public POST
|
|
42
47
|
'projectJoinDoor', // added by task 1002331 (ADR 0182 D6 / ADR 0247): the COMPOSED join/apply door — visibility × join grant × joinability, one answer, so a module reading a single knob cannot re-open a door the other two closed
|
|
48
|
+
'artistGate', // added by task 1003575 (ADR 0241): the artist gate — off | advisory | strict, plus the pure verdict helpers over it. Two modules ask it two different questions ("file this review?" and "does an open one hold the deploy?") and both must resolve the knob through this one reader, never branding().project
|
|
43
49
|
'readApplicantProfileFromHub', // added by task 1002972 (privacy spec D7): the reviewer queue's LIVE per-render read of one applicant's hub profile view. NARROW by design — the underlying call is authenticated with this instance's hub client secret, and loadIdpConfig stays OFF the doorway so no module ever holds the credential that speaks for the whole project
|
|
44
50
|
'enabledDisciplines', // BV1.R81: instance offered-disciplines (1.9.0; onboarding restock backstop)
|
|
45
51
|
'registerProvider', 'hasProvider', 'resolve', 'resolveOptional', 'listPorts', 'verifyPortsSatisfied',
|
|
@@ -61,6 +61,8 @@ ${NEUTRAL_PREFIX}_PLATFORM_VISIBILITY=public
|
|
|
61
61
|
${NEUTRAL_PREFIX}_JOINABILITY=apply
|
|
62
62
|
${NEUTRAL_PREFIX}_VISIBILITY=public
|
|
63
63
|
${NEUTRAL_PREFIX}_JOIN_GRANT=full
|
|
64
|
+
${NEUTRAL_PREFIX}_ARTIST_GATE=strict
|
|
65
|
+
${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE=
|
|
64
66
|
`;
|
|
65
67
|
|
|
66
68
|
t('replaces an existing KEY= line where it stands; every other line survives byte-for-byte', () => {
|
|
@@ -113,7 +115,13 @@ t('is PURE: same input, same output; empty body yields just the lines', () => {
|
|
|
113
115
|
t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at standup, under the pack prefix', () => {
|
|
114
116
|
const inst = { slug: 'spike', port: 3010, db_name: 'spike', hosting_shape: 'standalone', target_ref: 'jaxri/spike', settings: { platform_visibility: 'gated' } };
|
|
115
117
|
const vars = P.settingsEnvVarsFor(inst);
|
|
116
|
-
assert.deepEqual(vars, {
|
|
118
|
+
assert.deepEqual(vars, {
|
|
119
|
+
[`${NEUTRAL_PREFIX}_PLATFORM_VISIBILITY`]: 'gated', [`${NEUTRAL_PREFIX}_JOINABILITY`]: 'apply',
|
|
120
|
+
[`${NEUTRAL_PREFIX}_VISIBILITY`]: 'public', [`${NEUTRAL_PREFIX}_JOIN_GRANT`]: 'full',
|
|
121
|
+
// the artist gate + its non-retroactivity stamp (task 1003575, ADR 0241) ride
|
|
122
|
+
// the same transport; the stamp is empty until the gate is actually RAISED
|
|
123
|
+
[`${NEUTRAL_PREFIX}_ARTIST_GATE`]: 'strict', [`${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE`]: '',
|
|
124
|
+
});
|
|
117
125
|
const body = P.webEnvBody(inst, {});
|
|
118
126
|
for (const [k, v] of Object.entries(vars)) assert.match(body, new RegExp(`^${k}=${v}$`, 'm'), `${k} at standup`);
|
|
119
127
|
});
|
|
@@ -124,10 +132,10 @@ const GATED = { settings: { platform_visibility: 'gated', joinability: 'invite_o
|
|
|
124
132
|
const manifestWith = (project) => JSON.stringify({ schema: 'instance-manifest/v1', brand: project === undefined ? {} : { project } });
|
|
125
133
|
|
|
126
134
|
t('true when every pushed key matches brand.project (camelCase, the ENV_OVERRIDES targets)', () => {
|
|
127
|
-
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full' }), GATED), true);
|
|
135
|
+
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' }), GATED), true);
|
|
128
136
|
});
|
|
129
137
|
t('false when the instance still reports a different value', () => {
|
|
130
|
-
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public', joinGrant: 'full' }), GATED), false);
|
|
138
|
+
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' }), GATED), false);
|
|
131
139
|
assert.equal(P.settingsConsumed(manifestWith({ joinability: 'invite_only' }), GATED), false, 'a missing key is a mismatch, not a pass');
|
|
132
140
|
});
|
|
133
141
|
t('null (unknown) when the manifest carries no project block — a core that predates task 1003139 — or does not parse', () => {
|
|
@@ -145,7 +153,7 @@ t('instanceManifestCmd asks the instance on loopback, like healthzCmd', () => {
|
|
|
145
153
|
function recorder({ apply = false, envBody = LIVE_ENV, readFails = false, healthzFails = false, privileged = false,
|
|
146
154
|
manifest = 'match', restartThrows = false, writeThrows = false } = {}) {
|
|
147
155
|
const cmds = [], writes = [], events = [], probes = [], cleared = [], logs = [];
|
|
148
|
-
const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full' })
|
|
156
|
+
const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' })
|
|
149
157
|
: manifest === 'mismatch' ? manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public' })
|
|
150
158
|
: manifest === 'none' ? manifestWith(undefined) : 'nope';
|
|
151
159
|
const exec = (cmd, opts = {}) => {
|
|
@@ -214,7 +222,7 @@ await ta('dry-run PLANS the patch: reads nothing real, writes NOTHING (no synthe
|
|
|
214
222
|
assert.equal(r.ok, true);
|
|
215
223
|
assert.equal(r.healthy, null, 'nothing ran, nothing observed');
|
|
216
224
|
assert.equal(writes.length, 0, 'a dry-run must not plan a two-line overwrite of web.env');
|
|
217
|
-
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
225
|
+
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_ARTIST_GATE_SINCE in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
218
226
|
assert.ok(cmds.some((c) => /restart spike/.test(c)), 'the restart is still in the plan');
|
|
219
227
|
assert.ok(logs.some((l) => /dry-run — nothing ran/.test(l)), 'the closing line does not claim a restart happened');
|
|
220
228
|
});
|
|
@@ -164,7 +164,7 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
|
|
|
164
164
|
reset();
|
|
165
165
|
const r = await get('/provisioning/instances/18/settings');
|
|
166
166
|
assert.equal(r.status, 200, r.raw);
|
|
167
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full' });
|
|
167
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
168
168
|
// the planet rides beside the settings (task 1003347): no default — null = the platform's own pick
|
|
169
169
|
assert.deepEqual(r.body.planet, { planet_template: null, planet_accent: null, card_backdrop: null });
|
|
170
170
|
assert.deepEqual(r.body.options, {
|
|
@@ -175,6 +175,10 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
|
|
|
175
175
|
// what a PUBLIC join grants (task 1002331, ADR 0247) — the fourth policy
|
|
176
176
|
// axis, offered to the form the same way
|
|
177
177
|
join_grant: ['view', 'apply', 'full'],
|
|
178
|
+
// the artist gate (task 1003575, ADR 0241) — the fifth policy axis. It is
|
|
179
|
+
// offered to the form like the rest; what makes it different is only its
|
|
180
|
+
// default, which is the owner's ruling rather than today's behaviour.
|
|
181
|
+
artist_gate: ['off', 'advisory', 'strict'],
|
|
178
182
|
planet_template: [...provisioning.PLANET_TEMPLATES], planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
|
|
179
183
|
// the card's backdrop (task 1003352) is the same kind of key: platform-drawn,
|
|
180
184
|
// closed list, no default, no push — so it rides the same vocab
|
|
@@ -187,14 +191,14 @@ await ta('a stored planet reads back; a name that left the vocabulary reads null
|
|
|
187
191
|
const r = await get('/provisioning/instances/18/settings');
|
|
188
192
|
assert.equal(r.status, 200, r.raw);
|
|
189
193
|
assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: null, card_backdrop: null });
|
|
190
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full' }, 'the planet is not a setting: the policy read is untouched');
|
|
194
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the planet is not a setting: the policy read is untouched');
|
|
191
195
|
});
|
|
192
196
|
|
|
193
197
|
await ta('stored keys overlay the defaults', async () => {
|
|
194
198
|
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', settings: { joinability: 'invite_only' } } });
|
|
195
199
|
const r = await get('/provisioning/instances/18/settings');
|
|
196
200
|
assert.equal(r.status, 200, r.raw);
|
|
197
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public', join_grant: 'full' });
|
|
201
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
198
202
|
});
|
|
199
203
|
|
|
200
204
|
await ta('ANOTHER builder\'s GET → 404 (no existence leak)', async () => {
|
|
@@ -222,7 +226,7 @@ await ta('owner changes one key — merged, returned effective, audited api:self
|
|
|
222
226
|
reset();
|
|
223
227
|
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
|
|
224
228
|
assert.equal(r.status, 200, r.raw);
|
|
225
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
|
|
229
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
|
|
226
230
|
assert.equal(r.body.push.queued, true, 'an active co-tenant gets the live push (task 1003140)');
|
|
227
231
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { joinability: 'open' } }], 'exactly the sent key, nothing else');
|
|
228
232
|
assert.equal(events.length, 1);
|
|
@@ -235,7 +239,7 @@ await ta('owner changes both keys in one call', async () => {
|
|
|
235
239
|
reset();
|
|
236
240
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'invite_only' });
|
|
237
241
|
assert.equal(r.status, 200, r.raw);
|
|
238
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public', join_grant: 'full' });
|
|
242
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
239
243
|
});
|
|
240
244
|
|
|
241
245
|
await ta('the OWN-SCOPE WALL: another builder\'s PATCH → 404 and the write path is never reached', async () => {
|
|
@@ -274,7 +278,7 @@ await ta('the owner sets the door to stealth — stored, echoed effective, audit
|
|
|
274
278
|
reset();
|
|
275
279
|
const r = await patch('/provisioning/instances/18/settings', { visibility: 'stealth' });
|
|
276
280
|
assert.equal(r.status, 200, r.raw);
|
|
277
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth', join_grant: 'full' });
|
|
281
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth', join_grant: 'full', artist_gate: 'strict' });
|
|
278
282
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { visibility: 'stealth' } }], 'exactly the sent key, nothing else');
|
|
279
283
|
assert.equal(events[0].actor, 'api:self');
|
|
280
284
|
assert.match(events[0].detail, /visibility=stealth/);
|
|
@@ -334,7 +338,7 @@ await ta('the owner picks a planet: saved, echoed as `planet`, audited — and N
|
|
|
334
338
|
const r = await patch('/provisioning/instances/18/settings', { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' });
|
|
335
339
|
assert.equal(r.status, 200, r.raw);
|
|
336
340
|
assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: 'sea', card_backdrop: null });
|
|
337
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full' }, 'the policy half is untouched');
|
|
341
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the policy half is untouched');
|
|
338
342
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' } }]);
|
|
339
343
|
assert.equal(r.body.push.queued, false);
|
|
340
344
|
assert.equal(r.body.push.restart, false, 'an active co-tenant would get a push for a POLICY change — never for the planet');
|
|
@@ -225,7 +225,7 @@ await ta('ACTIVE standalone: the row is written FIRST, then a settings-apply int
|
|
|
225
225
|
reset();
|
|
226
226
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated' });
|
|
227
227
|
assert.equal(r.status, 200, r.raw);
|
|
228
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public', join_grant: 'full' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
|
|
228
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
|
|
229
229
|
assert.equal(r.body.settings, undefined);
|
|
230
230
|
assert.deepEqual(updateCalls.map((c) => c.patch), [{ platform_visibility: 'gated' }]);
|
|
231
231
|
assert.equal(updateCalls[0].enqueuedSoFar, 0, 'the runner reads the row — the write must precede the intent');
|
|
@@ -274,7 +274,7 @@ await ta('an enqueue that THROWS still answers 200 "saved" — never a 500 that
|
|
|
274
274
|
enqueueThrows = new Error('db down');
|
|
275
275
|
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
|
|
276
276
|
assert.equal(r.status, 200, r.raw);
|
|
277
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full' });
|
|
277
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
278
278
|
assert.equal(updateCalls.length, 1, 'the write landed');
|
|
279
279
|
receiptShape(r.body.push, { queued: false, restart: true, reachable: true });
|
|
280
280
|
assert.match(r.body.push.message, /Saved, but the restart could not be queued/);
|
|
@@ -287,7 +287,7 @@ await ta('same values as already stored → no write, no event, no intent, "No c
|
|
|
287
287
|
reset({ inst: { ...ACTIVE, settings: { platform_visibility: 'gated', joinability: 'open' } } });
|
|
288
288
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'open' });
|
|
289
289
|
assert.equal(r.status, 200, r.raw);
|
|
290
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full' });
|
|
290
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
291
291
|
assert.equal(updateCalls.length, 0, 'nothing written (no updated_at reshuffle)');
|
|
292
292
|
assert.equal(events.length, 0, 'no "settings changed" event for a change that is not one');
|
|
293
293
|
assert.equal(enqueueCalls.length, 0, 'a whole-form save must not bounce a live project');
|
|
@@ -404,7 +404,7 @@ await ta('save A queues → save B 409s while A RUNS → A settles → the UNCHA
|
|
|
404
404
|
assert.equal(enqueueCalls.length, enqueuedBefore + 1, 'exactly one new intent');
|
|
405
405
|
assert.equal(updateCalls.length, writesBefore, 'nothing re-written — the value was already stored in step 2');
|
|
406
406
|
assert.deepEqual(owedCalls[owedCalls.length - 1], { id: 18, owed: false }, 'and the debt is settled');
|
|
407
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full' },
|
|
407
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' },
|
|
408
408
|
'the value that finally ships is the one the owner saved');
|
|
409
409
|
});
|
|
410
410
|
|
|
@@ -59,8 +59,17 @@ t('a row with NO stored settings emits today\'s behavior EXPLICITLY, never nothi
|
|
|
59
59
|
assert.equal(v.CLOUDBONGOS_VISIBILITY, 'public');
|
|
60
60
|
// what a public join grants rides it too (task 1002331, ADR 0182 D6)
|
|
61
61
|
assert.equal(v.CLOUDBONGOS_JOIN_GRANT, 'full');
|
|
62
|
+
// The artist gate is the one key whose default is NOT today's behaviour — it is
|
|
63
|
+
// the owner's ruling (ADR 0241 §4), and it is safe as a default because strict
|
|
64
|
+
// holds only a pin move, never a builder's ship.
|
|
65
|
+
assert.equal(v.CLOUDBONGOS_ARTIST_GATE, 'strict');
|
|
66
|
+
// Its companion stamp is emitted EMPTY rather than omitted: an absent variable is
|
|
67
|
+
// a policy the instance would have to infer, and empty already means "no cutoff".
|
|
68
|
+
assert.equal(v.CLOUDBONGOS_ARTIST_GATE_SINCE, '', 'present and empty — never absent');
|
|
69
|
+
assert.ok(linesOf(provisioning.settingsEnvLines({}, 'X')).some((l) => l === 'X_ARTIST_GATE_SINCE='));
|
|
62
70
|
assert.equal(linesOf(provisioning.settingsEnvLines({}, 'X')).length,
|
|
63
|
-
Object.keys(provisioning.SETTINGS_VOCAB).length
|
|
71
|
+
Object.keys(provisioning.SETTINGS_VOCAB).length + Object.keys(provisioning.SETTINGS_COMPANION_ENV).length,
|
|
72
|
+
'every policy key and every declared companion always present');
|
|
64
73
|
});
|
|
65
74
|
|
|
66
75
|
t('an off-vocabulary stored value falls back to the default, never reaches the instance', () => {
|
|
@@ -82,10 +91,14 @@ t('the planet never rides the env transport (task 1003347): it is the platform\'
|
|
|
82
91
|
assert.doesNotMatch(provisioning.settingsEnvLines({ settings: { planet_template: 'bead-obsidian-tint' } }, 'CB'), /planet|PLANET/);
|
|
83
92
|
});
|
|
84
93
|
|
|
85
|
-
t('every emitted key is
|
|
94
|
+
t('every emitted key is a vocabulary key or a DECLARED companion — the lists cannot drift', () => {
|
|
86
95
|
const keys = linesOf(provisioning.settingsEnvLines({}, 'X')).map((l) => l.split('=')[0].replace(/^X_/, '').toLowerCase());
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
const expected = [
|
|
97
|
+
...Object.keys(provisioning.SETTINGS_VOCAB),
|
|
98
|
+
...Object.keys(provisioning.SETTINGS_COMPANION_ENV).map((k) => k.toLowerCase()),
|
|
99
|
+
];
|
|
100
|
+
assert.deepEqual(keys.sort(), expected.sort(),
|
|
101
|
+
'a key added to the vocabulary must be carried, or the instance never learns it — and an env var the instance reads must be a DECLARED companion, never an ad-hoc extra');
|
|
89
102
|
});
|
|
90
103
|
|
|
91
104
|
console.log('\nThe generated file tells the truth about what it is:');
|
|
@@ -99,11 +112,50 @@ t('it names the live push by the intent the runner actually dispatches (task 100
|
|
|
99
112
|
});
|
|
100
113
|
|
|
101
114
|
t('settingsEnvVars is the single source both legs derive from — same keys, same values as the lines', () => {
|
|
102
|
-
const row = { settings: { platform_visibility: 'gated', joinability: 'open', visibility: 'stealth', join_grant: 'view' } };
|
|
115
|
+
const row = { settings: { platform_visibility: 'gated', joinability: 'open', visibility: 'stealth', join_grant: 'view', artist_gate: 'advisory' } };
|
|
103
116
|
const vars = provisioning.settingsEnvVars(row, 'CLOUDBONGOS');
|
|
104
|
-
assert.deepEqual(vars, {
|
|
117
|
+
assert.deepEqual(vars, {
|
|
118
|
+
CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'open', CLOUDBONGOS_VISIBILITY: 'stealth',
|
|
119
|
+
CLOUDBONGOS_JOIN_GRANT: 'view', CLOUDBONGOS_ARTIST_GATE: 'advisory', CLOUDBONGOS_ARTIST_GATE_SINCE: '',
|
|
120
|
+
});
|
|
121
|
+
// varsOf splits on '=', so an empty value reads as '' on both sides — the point
|
|
122
|
+
// of the comparison is that the standup lines carry exactly the same key set.
|
|
105
123
|
assert.deepEqual(varsOf(provisioning.settingsEnvLines(row, 'CLOUDBONGOS')), vars, 'the standup lines ARE the vars');
|
|
106
|
-
assert.deepEqual(provisioning.settingsEnvVars({}, 'X'), {
|
|
124
|
+
assert.deepEqual(provisioning.settingsEnvVars({}, 'X'), {
|
|
125
|
+
X_PLATFORM_VISIBILITY: 'public', X_JOINABILITY: 'apply', X_VISIBILITY: 'public', X_JOIN_GRANT: 'full',
|
|
126
|
+
X_ARTIST_GATE: 'strict', X_ARTIST_GATE_SINCE: '',
|
|
127
|
+
}, 'defaults, explicitly');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
console.log('\nThe artist gate rides the same transport (ADR 0241, task 1003575):');
|
|
131
|
+
|
|
132
|
+
t('the stamp is carried only when the gate was actually RAISED, and it is the row\'s own value', () => {
|
|
133
|
+
const stamped = { settings: { artist_gate: 'strict', artist_gate_strict_since: '2026-09-13T00:00:00.000Z' } };
|
|
134
|
+
assert.equal(provisioning.settingsEnvVars(stamped, 'X').X_ARTIST_GATE_SINCE, '2026-09-13T00:00:00.000Z');
|
|
135
|
+
// An unparseable stamp is NOT transmitted as a date nobody can read: it reads as
|
|
136
|
+
// absent, which is the safe direction — no cutoff, so every open review counts.
|
|
137
|
+
assert.equal(provisioning.settingsEnvVars({ settings: { artist_gate_strict_since: 'whenever' } }, 'X').X_ARTIST_GATE_SINCE, '');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
t('artistGateStamp writes the cutoff on a RAISE only — no other save may move it (ADR 0241 §5)', () => {
|
|
141
|
+
const now = new Date('2026-09-13T12:00:00.000Z');
|
|
142
|
+
const key = provisioning.ARTIST_GATE_STRICT_SINCE_KEY;
|
|
143
|
+
// advisory → strict is the tightening the stamp exists for.
|
|
144
|
+
assert.deepEqual(provisioning.artistGateStamp({ settings: { artist_gate: 'advisory' } }, { artist_gate: 'strict' }, now),
|
|
145
|
+
{ [key]: '2026-09-13T12:00:00.000Z' });
|
|
146
|
+
// A row with NO stored gate is already 'strict' by default — re-sending 'strict'
|
|
147
|
+
// is not a raise, and stamping it would silently un-hold reviews already held.
|
|
148
|
+
assert.deepEqual(provisioning.artistGateStamp({}, { artist_gate: 'strict' }, now), {});
|
|
149
|
+
// Lowering, and any save that does not touch the gate at all, move nothing.
|
|
150
|
+
assert.deepEqual(provisioning.artistGateStamp({ settings: { artist_gate: 'strict' } }, { artist_gate: 'off' }, now), {});
|
|
151
|
+
assert.deepEqual(provisioning.artistGateStamp({ settings: { artist_gate: 'advisory' } }, { joinability: 'open' }, now), {});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
t('the stamp can neither leak as a setting nor be written by a client patch', () => {
|
|
155
|
+
const row = { settings: { artist_gate: 'strict', artist_gate_strict_since: '2026-09-13T00:00:00.000Z' } };
|
|
156
|
+
assert.ok(!(provisioning.ARTIST_GATE_STRICT_SINCE_KEY in provisioning.SETTINGS_VOCAB));
|
|
157
|
+
assert.ok(!(provisioning.ARTIST_GATE_STRICT_SINCE_KEY in provisioning.PLANET_VOCAB));
|
|
158
|
+
assert.ok(!(provisioning.ARTIST_GATE_STRICT_SINCE_KEY in provisioning.effectiveSettings(row)));
|
|
107
159
|
});
|
|
108
160
|
|
|
109
161
|
console.log('\nwebEnvBody — the settings ride the real per-instance env file:');
|
|
@@ -103,6 +103,18 @@ t('redactBlob SCRUBS a secret shape from a text blob (handled, not leaked)', ()
|
|
|
103
103
|
assert.ok(!r.buf.toString('utf8').includes('AKIAABCDEFGHIJKLMNOP'), 'the secret must be scrubbed from the published text');
|
|
104
104
|
assert.equal(r.secret, false, 'a scrubbed text secret is handled, so it is not a residual leak');
|
|
105
105
|
});
|
|
106
|
+
// Regression (task 1003873). The secret verdict audits THIS call's own scrub, so it has to
|
|
107
|
+
// share the scrub's knownDocNames allowlist. When it did not, the audit re-redacted the ADR
|
|
108
|
+
// filename the scrub had just spared, read the difference as a leak, and failed the packager
|
|
109
|
+
// closed on every doc in the tree — the publish lane stayed red for seven core releases.
|
|
110
|
+
t('redactBlob reports no secret for an allowlisted doc filename (the gate agrees with its own scrub)', () => {
|
|
111
|
+
const name = '0281-differential-audit-allowlist-symmetry.md';
|
|
112
|
+
const knownDocNames = new Set([name]);
|
|
113
|
+
const r = redactBlob({ buf: Buffer.from(`see [ADR 0281](docs/adr/${name}) for why`), rules, sensitiveLiterals, envSecrets: [], knownDocNames });
|
|
114
|
+
assert.ok(r.buf.toString('utf8').includes(name), 'the allowlisted filename must survive the scrub');
|
|
115
|
+
assert.equal(r.secret, false, 'and must not then be reported as a residual secret');
|
|
116
|
+
assert.deepEqual(r.leaks, []);
|
|
117
|
+
});
|
|
106
118
|
t('redactBlob FAILS CLOSED on a secret inside a BINARY blob (cannot safely rewrite it)', () => {
|
|
107
119
|
const bin = Buffer.concat([Buffer.from('BIN\x00'), Buffer.from('AKIA' + 'ABCDEFGHIJKLMNOP')]);
|
|
108
120
|
const r = redactBlob({ buf: bin, rules, sensitiveLiterals, envSecrets: [] });
|