@bongos/core 1.19.715 → 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 +472 -407
- 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/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 +34 -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/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/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/currency_label.mjs +11 -4
- package/tests/go_live.mjs +144 -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/go_live.mjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { test } from 'node:test';
|
|
26
26
|
import assert from 'node:assert/strict';
|
|
27
27
|
import { createRequire } from 'node:module';
|
|
28
|
+
import fs from 'node:fs';
|
|
28
29
|
import path from 'node:path';
|
|
29
30
|
import { fileURLToPath } from 'node:url';
|
|
30
31
|
|
|
@@ -109,6 +110,149 @@ test('an undetermined divergence halts and does not fall through to a bump', ()
|
|
|
109
110
|
assert.equal(p.from, null, 'must not invent a from-version');
|
|
110
111
|
});
|
|
111
112
|
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// The artist gate (ADR 0241, task 1003575)
|
|
115
|
+
//
|
|
116
|
+
// This is the one halt that comes from a PERSON'S queue rather than from git, so
|
|
117
|
+
// what these pin is the boundary around it: it stops a PIN MOVE, it names its own
|
|
118
|
+
// remedy, and it never fires on anything but a definite 'strict'-with-open-reviews
|
|
119
|
+
// answer. A gate that halted on "I could not tell" would brick deploys on an
|
|
120
|
+
// unrelated outage with no task id to release against — the blocked-state-that-
|
|
121
|
+
// does-not-explain-itself failure ADR 0241 §4 was written against.
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
// The shape readArtistGate() resolves to, as the live instance reports it.
|
|
125
|
+
const gate = (over = {}) => ({ mode: 'strict', since: null, holds: false, reviews: [], halt: null, ...over });
|
|
126
|
+
const heldGate = (n = 1) => gate({
|
|
127
|
+
holds: true,
|
|
128
|
+
reviews: Array.from({ length: n }, (_, i) => ({ id: String(9000 + i), title: 'Artist review: …' })),
|
|
129
|
+
halt: {
|
|
130
|
+
code: 'ARTIST_REVIEW_UNRESOLVED',
|
|
131
|
+
message: `${n} unresolved artist review${n === 1 ? '' : 's'} hold this deploy — the project runs artist_gate 'strict'`,
|
|
132
|
+
detail: 'Held by task 9000. Two ways out … POST /tasks/:id/override-request …',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('an unresolved artist review HALTS the pin move', () => {
|
|
137
|
+
const p = G.planGoLive({ div: div(), target: okTarget, targetVersion: '1.19.306', artistGate: heldGate(2) });
|
|
138
|
+
const h = p.halts.find((x) => x.code === 'ARTIST_REVIEW_UNRESOLVED');
|
|
139
|
+
assert.ok(h, 'expected ARTIST_REVIEW_UNRESOLVED');
|
|
140
|
+
// The refusal has to carry its own way out, or it is the stall ADR 0162 retired.
|
|
141
|
+
assert.match(h.detail, /override-request/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a gate that holds nothing halts nothing — at any level', () => {
|
|
145
|
+
for (const mode of ['off', 'advisory', 'strict']) {
|
|
146
|
+
const p = G.planGoLive({ div: div(), target: okTarget, targetVersion: '1.19.306', artistGate: gate({ mode }) });
|
|
147
|
+
assert.equal(p.halts.length, 0, `${mode}: ${JSON.stringify(p.halts)}`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('an UNREAD gate is a note, never a halt — and never renders as "nothing holds"', () => {
|
|
152
|
+
// No session, or an instance too old to serve the route: we do not know the
|
|
153
|
+
// gate applies at all, and halting every deploy on that would brick the pin
|
|
154
|
+
// with no task id to release against.
|
|
155
|
+
const p = G.planGoLive({
|
|
156
|
+
div: div(), target: okTarget, targetVersion: '1.19.306',
|
|
157
|
+
artistGate: { unread: 'this instance does not serve /artist-gate — it predates ADR 0241' },
|
|
158
|
+
});
|
|
159
|
+
assert.equal(p.halts.length, 0, 'not knowing must not brick the pin');
|
|
160
|
+
const n = p.notes.find((x) => x.code === 'ARTIST_GATE_UNREAD');
|
|
161
|
+
assert.ok(n, 'but it must be REPORTED — "could not tell" is not "nothing holds"');
|
|
162
|
+
assert.match(n.message, /may be holding/, 'the wording must not claim the gate is clear');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('a gate that is THERE and BROKEN halts — the fail-closed half', () => {
|
|
166
|
+
// The other side of the line: the route exists and 500s. We know the gate
|
|
167
|
+
// applies and cannot get an answer from it, so deploying past would be a
|
|
168
|
+
// quieter way through than the owner's own override — the asymmetry to refuse.
|
|
169
|
+
const p = G.planGoLive({
|
|
170
|
+
div: div(), target: okTarget, targetVersion: '1.19.306',
|
|
171
|
+
artistGate: { broken: 'GET /artist-gate returned 500' },
|
|
172
|
+
});
|
|
173
|
+
const h = p.halts.find((x) => x.code === 'ARTIST_GATE_UNREADABLE');
|
|
174
|
+
assert.ok(h, 'expected ARTIST_GATE_UNREADABLE');
|
|
175
|
+
assert.match(h.detail, /fault on the instance/, 'and it must point at the instance, not at an artist');
|
|
176
|
+
assert.equal(p.notes.find((x) => x.code === 'ARTIST_GATE_UNREAD'), undefined, 'one verdict, not two');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('artistGateEntries is the shared composer, so the gitless lane cannot skip the gate', () => {
|
|
180
|
+
// The on-box lane builds its plan without planGoLive; it reaches the same halt
|
|
181
|
+
// through this, which is what stops "no git checkout" from meaning "no gate".
|
|
182
|
+
assert.equal(G.artistGateEntries(heldGate()).halts[0].code, 'ARTIST_REVIEW_UNRESOLVED');
|
|
183
|
+
assert.deepEqual(G.artistGateEntries(null), { halts: [], notes: [] });
|
|
184
|
+
assert.equal(G.artistGateEntries({ unread: 'no session' }).notes[0].code, 'ARTIST_GATE_UNREAD');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// readArtistGate — the network leg.
|
|
189
|
+
//
|
|
190
|
+
// This is tested rather than left to the file's ssh/spawnSync boundary because a
|
|
191
|
+
// mistake here is INVISIBLE: cli-lib's apiCall composes `${origin}${path}`, so a
|
|
192
|
+
// path missing its `/api/bongos` prefix lands on the origin's catch-all and the
|
|
193
|
+
// gate reports itself unread on every instance — including the ones that fully
|
|
194
|
+
// support it — while the plan reads exactly like a clean run. It shipped that way
|
|
195
|
+
// once. The URL is pinned against the route's REAL mount point in the generated
|
|
196
|
+
// OpenAPI spec, so a rename on either side reds this rather than going quiet.
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
test('the path it requests is the one the server actually mounts', () => {
|
|
200
|
+
// The spec's paths are relative to its server base, and apiCall prepends
|
|
201
|
+
// nothing — so the URL go-live builds must be base + path, assembled from the
|
|
202
|
+
// spec rather than retyped. A rename on either side reds this.
|
|
203
|
+
const spec = JSON.parse(fs.readFileSync(new URL('../docs/api/openapi.json', import.meta.url), 'utf8'));
|
|
204
|
+
const bases = (spec.servers || []).map((s) => s.url);
|
|
205
|
+
const full = new Set(bases.flatMap((b) => Object.keys(spec.paths || {}).map((p) => `${b}${p}`)));
|
|
206
|
+
assert.ok(full.has(G.ARTIST_GATE_PATH),
|
|
207
|
+
`go-live requests ${G.ARTIST_GATE_PATH}, which the API spec does not serve — it would hit the origin's catch-all and read as "unread" forever`);
|
|
208
|
+
assert.match(G.ARTIST_GATE_PATH, /^\/api\//, 'apiCall does not prepend an API prefix — the path must carry it');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('a real answer is parsed; a 200 that is NOT this route reads as unread, never as "nothing holds"', async () => {
|
|
212
|
+
const seen = [];
|
|
213
|
+
const call = async (method, path, body, opts) => { seen.push({ method, path, opts }); return call._res; };
|
|
214
|
+
|
|
215
|
+
call._res = { status: 200, ok: true, data: { mode: 'strict', since: null, holds: true, reviews: [{ id: '9001' }], halt: { message: 'm', detail: 'd' } } };
|
|
216
|
+
const got = await G.readArtistGate(call);
|
|
217
|
+
assert.deepEqual(seen[0].method, 'GET');
|
|
218
|
+
assert.equal(seen[0].path, G.ARTIST_GATE_PATH);
|
|
219
|
+
assert.equal(got.mode, 'strict');
|
|
220
|
+
assert.ok(got.halt, 'a real halt survives the read');
|
|
221
|
+
|
|
222
|
+
// An SPA index.html from a catch-all, or a proxy's own page: 200, no gate. The
|
|
223
|
+
// one failure that would wave a deploy through in silence.
|
|
224
|
+
call._res = { status: 200, ok: true, data: { raw: '<!doctype html><html>…' } };
|
|
225
|
+
const spa = await G.readArtistGate(call);
|
|
226
|
+
assert.ok(spa.unread, 'a 200 from something else is NOT a gate that holds nothing');
|
|
227
|
+
assert.equal(spa.halt, undefined);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('every failure shape resolves — 404 and a lost connection are notes, a 5xx is the halt', async () => {
|
|
231
|
+
const res = (r) => async () => r;
|
|
232
|
+
assert.match((await G.readArtistGate(res({ status: 404, ok: false, data: {} }))).unread, /predates ADR 0241/);
|
|
233
|
+
assert.match((await G.readArtistGate(res({ status: 401, ok: false, data: {} }))).unread, /401/);
|
|
234
|
+
assert.match((await G.readArtistGate(res({ status: 500, ok: false, data: {} }))).broken, /500/);
|
|
235
|
+
assert.match((await G.readArtistGate(res({ status: 503, ok: false, data: {} }))).broken, /503/);
|
|
236
|
+
// It must never throw: a go-live that crashes on a gate read is worse than one
|
|
237
|
+
// that halts on it.
|
|
238
|
+
const thrower = async () => { throw new Error('ECONNREFUSED'); };
|
|
239
|
+
assert.match((await G.readArtistGate(thrower)).unread, /ECONNREFUSED/);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('the level vocabulary go-live checks against is the gate\'s own', async () => {
|
|
243
|
+
const gate = await import('../src/bongos/artist-gate.js');
|
|
244
|
+
assert.deepEqual([...G.ARTIST_GATE_LEVELS].sort(), [...gate.ARTIST_GATE_LEVELS].sort(),
|
|
245
|
+
'the mirrored list must not drift from src/bongos/artist-gate.js');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('the plan SHOWS the gate level even when nothing is held', () => {
|
|
249
|
+
const out = G.renderPlan(
|
|
250
|
+
{ ...G.planGoLive({ div: div(), target: okTarget, targetVersion: '1.19.306', artistGate: gate({ mode: 'advisory' }) }), artistGate: gate({ mode: 'advisory' }) },
|
|
251
|
+
{ targetName: 'web' }
|
|
252
|
+
);
|
|
253
|
+
assert.match(out, /artist gate\s+advisory/, '"why did nothing stop?" must be answerable from the plan');
|
|
254
|
+
});
|
|
255
|
+
|
|
112
256
|
// ---------------------------------------------------------------------------
|
|
113
257
|
// Shell safety — this builds a command that runs as root over ssh
|
|
114
258
|
// ---------------------------------------------------------------------------
|
|
@@ -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');
|