@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,234 @@
|
|
|
1
|
+
// tests/artist_gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// The artist gate (ADR 0241, task 1003575) — src/bongos/artist-gate.js.
|
|
4
|
+
//
|
|
5
|
+
// WHAT IS ACTUALLY WORTH PINNING HERE is not that the filter filters. It is the
|
|
6
|
+
// four properties ADR 0241 spends its whole argument on, each of which is a way
|
|
7
|
+
// this gate could quietly become the gate ADR 0162 retired:
|
|
8
|
+
//
|
|
9
|
+
// 1. IT READS A STATE, NEVER A VERDICT. Nothing here may consult who reviewed,
|
|
10
|
+
// what they thought, or whether they approved — only whether the review task
|
|
11
|
+
// is still open. A verdict-reading gate is pinned to a person and stalls when
|
|
12
|
+
// that person is away.
|
|
13
|
+
// 2. IT NEVER HOLDS ANYTHING BUT AT 'strict'. `off` and `advisory` must be
|
|
14
|
+
// incapable of producing a halt, however many reviews are open.
|
|
15
|
+
// 3. TIGHTENING IS NOT RETROACTIVE (§5). A review filed before the gate was
|
|
16
|
+
// raised must not hold, or raising the setting means inheriting a backlog for
|
|
17
|
+
// work nobody remembers — which is what makes a setting frightening to raise.
|
|
18
|
+
// 4. THE REFUSAL EXPLAINS ITSELF (§4). It names what holds the deploy and which
|
|
19
|
+
// control releases it. ADR 0162's incident was made worse by a blocked state
|
|
20
|
+
// that did not say either.
|
|
21
|
+
//
|
|
22
|
+
// Pure: the module's read half is injectable, so nothing here needs a branding
|
|
23
|
+
// pack, an env or a database.
|
|
24
|
+
//
|
|
25
|
+
// Run: node --test tests/artist_gate.mjs
|
|
26
|
+
|
|
27
|
+
import { test } from 'node:test';
|
|
28
|
+
import { strict as assert } from 'node:assert';
|
|
29
|
+
// Static named imports, not createRequire: the same shape tests/project_join_door.mjs
|
|
30
|
+
// uses on the sibling kernel classifier. It reads better, and it is also what lets the
|
|
31
|
+
// dead-code ratchet SEE these exports being exercised — a createRequire call is opaque
|
|
32
|
+
// to it, so a fully-tested module reads as ten unused exports.
|
|
33
|
+
import {
|
|
34
|
+
ARTIST_GATE_LEVELS,
|
|
35
|
+
RESOLVED_REVIEW_STATUSES,
|
|
36
|
+
ARTIST_REVIEW_SOURCE,
|
|
37
|
+
ARTIST_REVIEW_REF_PREFIX,
|
|
38
|
+
isArtistReview,
|
|
39
|
+
isReviewResolved,
|
|
40
|
+
unresolvedArtistReviews,
|
|
41
|
+
artistGateHalt,
|
|
42
|
+
artistGateMode,
|
|
43
|
+
artistGateSince,
|
|
44
|
+
} from '../src/bongos/artist-gate.js';
|
|
45
|
+
|
|
46
|
+
const gate = {
|
|
47
|
+
ARTIST_GATE_LEVELS,
|
|
48
|
+
RESOLVED_REVIEW_STATUSES,
|
|
49
|
+
ARTIST_REVIEW_SOURCE,
|
|
50
|
+
ARTIST_REVIEW_REF_PREFIX,
|
|
51
|
+
isArtistReview,
|
|
52
|
+
isReviewResolved,
|
|
53
|
+
unresolvedArtistReviews,
|
|
54
|
+
artistGateHalt,
|
|
55
|
+
artistGateMode,
|
|
56
|
+
artistGateSince,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// A cascade-filed artist review row, as the board holds it.
|
|
60
|
+
const review = (over = {}) => ({
|
|
61
|
+
id: '9001',
|
|
62
|
+
title: 'Artist review: the copy and visuals that shipped in task 7001',
|
|
63
|
+
status: 'backlog',
|
|
64
|
+
source: 'cascade',
|
|
65
|
+
source_ref: 'cascade-artist-review-on-ship-7001',
|
|
66
|
+
created_at: '2026-09-10T00:00:00.000Z',
|
|
67
|
+
merged_into_task_id: null,
|
|
68
|
+
...over,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// A pack reader, as branding().project would answer.
|
|
72
|
+
const pack = (project) => () => ({ project });
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// What counts as a review at all
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
test('only a CASCADE-filed artist review counts — a hand-filed task with a similar title does not', () => {
|
|
79
|
+
assert.equal(gate.isArtistReview(review()), true);
|
|
80
|
+
// Same source, different rule: another cascade must never hold a deploy this
|
|
81
|
+
// gate was not given authority over.
|
|
82
|
+
assert.equal(gate.isArtistReview(review({ source_ref: 'cascade-some-other-rule-7001' })), false);
|
|
83
|
+
// Same ref shape, not the cascade's source.
|
|
84
|
+
assert.equal(gate.isArtistReview(review({ source: 'manual' })), false);
|
|
85
|
+
assert.equal(gate.isArtistReview({ title: 'Artist review: please look at this' }), false);
|
|
86
|
+
assert.equal(gate.isArtistReview(null), false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('RESOLVED is a status, never a judgement — the two ways a task leaves the board, plus a merge', () => {
|
|
90
|
+
assert.equal(gate.isReviewResolved(review({ status: 'shipped' })), true);
|
|
91
|
+
assert.equal(gate.isReviewResolved(review({ status: 'abandoned' })), true, "triage's dismiss");
|
|
92
|
+
// A row merged into another is resolved even at an open status: its successor
|
|
93
|
+
// carries the work, and holding a deploy on a row nobody will action again is
|
|
94
|
+
// exactly the indefinite stall this gate must not have.
|
|
95
|
+
assert.equal(gate.isReviewResolved(review({ status: 'backlog', merged_into_task_id: '9100' })), true);
|
|
96
|
+
for (const status of ['backlog', 'ready', 'active', 'completed', 'confirmed']) {
|
|
97
|
+
assert.equal(gate.isReviewResolved(review({ status })), false, status);
|
|
98
|
+
}
|
|
99
|
+
assert.equal(gate.isReviewResolved(null), true, 'nothing is not an open request');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// §5 — tightening is not retroactive
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
test('a review filed BEFORE the gate was raised does not hold; one filed after does', () => {
|
|
107
|
+
const older = review({ id: '9001', created_at: '2026-09-01T00:00:00.000Z' });
|
|
108
|
+
const newer = review({ id: '9002', created_at: '2026-09-12T00:00:00.000Z' });
|
|
109
|
+
const held = gate.unresolvedArtistReviews([older, newer], { since: '2026-09-10T00:00:00.000Z' });
|
|
110
|
+
assert.deepEqual(held.map((t) => t.id), ['9002']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('no cutoff means every open review counts — absent must not read as a licence to ignore', () => {
|
|
114
|
+
const rows = [review({ id: '9001' }), review({ id: '9002', created_at: '2020-01-01T00:00:00.000Z' })];
|
|
115
|
+
assert.equal(gate.unresolvedArtistReviews(rows, {}).length, 2);
|
|
116
|
+
assert.equal(gate.unresolvedArtistReviews(rows, { since: null }).length, 2);
|
|
117
|
+
assert.equal(gate.unresolvedArtistReviews(rows, { since: 'not a date' }).length, 2, 'junk is no cutoff, not no gate');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('a review with an unreadable filing date is KEPT — the cutoff spares work, it does not excuse it', () => {
|
|
121
|
+
const held = gate.unresolvedArtistReviews([review({ created_at: null })], { since: '2026-09-10T00:00:00.000Z' });
|
|
122
|
+
assert.equal(held.length, 1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('resolved reviews are filtered out before the cutoff is even consulted', () => {
|
|
126
|
+
const rows = [review({ id: '9001', status: 'shipped' }), review({ id: '9002', status: 'abandoned' })];
|
|
127
|
+
assert.deepEqual(gate.unresolvedArtistReviews(rows, {}), []);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// §2 / §4 — what holds, and what the refusal says
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
test("only 'strict' can hold — off and advisory are incapable of it, however many are open", () => {
|
|
135
|
+
const open = [review({ id: '9001' }), review({ id: '9002' })];
|
|
136
|
+
assert.equal(gate.artistGateHalt({ mode: 'off', reviews: open }), null);
|
|
137
|
+
assert.equal(gate.artistGateHalt({ mode: 'advisory', reviews: open }), null);
|
|
138
|
+
assert.ok(gate.artistGateHalt({ mode: 'strict', reviews: open }));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("'strict' with nothing open holds nothing — the gate reads state, so no state is no hold", () => {
|
|
142
|
+
assert.equal(gate.artistGateHalt({ mode: 'strict', reviews: [] }), null);
|
|
143
|
+
assert.equal(gate.artistGateHalt({ mode: 'strict' }), null);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('the refusal names WHAT holds the deploy and WHICH control releases it', () => {
|
|
147
|
+
const halt = gate.artistGateHalt({ mode: 'strict', reviews: [review({ id: '9001' }), review({ id: '9002' })] });
|
|
148
|
+
assert.equal(halt.code, 'ARTIST_REVIEW_UNRESOLVED');
|
|
149
|
+
assert.match(halt.message, /2 unresolved artist reviews/);
|
|
150
|
+
assert.match(halt.message, /strict/, 'and why it is holding at all');
|
|
151
|
+
// The two real ways out, and neither is "wait for someone".
|
|
152
|
+
assert.match(halt.detail, /task 9001, task 9002/);
|
|
153
|
+
assert.match(halt.detail, /resolve or dismiss/i);
|
|
154
|
+
assert.match(halt.detail, /override-request/);
|
|
155
|
+
assert.match(halt.detail, /OWNER/, 'the escape hatch is the owner, not any Metic+ (ADR 0241 §3)');
|
|
156
|
+
assert.match(halt.detail, /advisory/, 'and the setting that stops this happening again');
|
|
157
|
+
assert.deepEqual(halt.reviewIds, ['9001', '9002']);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('the refusal never writes a bare #id — that autolinks to a GitHub issue that does not exist', () => {
|
|
161
|
+
const halt = gate.artistGateHalt({ mode: 'strict', reviews: [review()] });
|
|
162
|
+
assert.doesNotMatch(`${halt.message} ${halt.detail}`, /#\d/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('a TRUNCATED read reports a floor, and truncation can never change the verdict', () => {
|
|
166
|
+
const many = Array.from({ length: 3 }, (_, i) => review({ id: String(9000 + i) }));
|
|
167
|
+
const halt = artistGateHalt({ mode: 'strict', reviews: many, truncated: true });
|
|
168
|
+
assert.match(halt.message, /^3\+ unresolved artist reviews/, 'a floor, not a count it cannot stand behind');
|
|
169
|
+
// The verdict is identical either way: every row handed in is a holder, so one
|
|
170
|
+
// is enough and a capped read still holds.
|
|
171
|
+
assert.equal(artistGateHalt({ mode: 'strict', reviews: many }).code, halt.code);
|
|
172
|
+
// A single truncated row is still plural — "1+ review" would read as exactly one.
|
|
173
|
+
assert.match(artistGateHalt({ mode: 'strict', reviews: [review()], truncated: true }).message, /^1\+ unresolved artist reviews/);
|
|
174
|
+
assert.match(artistGateHalt({ mode: 'strict', reviews: [review()] }).message, /^1 unresolved artist review /);
|
|
175
|
+
// And truncation cannot manufacture a hold out of nothing.
|
|
176
|
+
assert.equal(artistGateHalt({ mode: 'strict', reviews: [], truncated: true }), null);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('a long queue is summarised rather than dumped — five named, the rest counted', () => {
|
|
180
|
+
const many = Array.from({ length: 9 }, (_, i) => review({ id: String(9000 + i) }));
|
|
181
|
+
const halt = gate.artistGateHalt({ mode: 'strict', reviews: many });
|
|
182
|
+
assert.match(halt.detail, /and 4 more/);
|
|
183
|
+
assert.equal(halt.reviewIds.length, 9, 'but the machine-readable list is complete');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// The reader — fail-closed in the direction the default already points
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
test('the level is read from the pack, and only the three declared levels are accepted', () => {
|
|
191
|
+
assert.equal(gate.artistGateMode(pack({ artistGate: 'off' })), 'off');
|
|
192
|
+
assert.equal(gate.artistGateMode(pack({ artistGate: 'advisory' })), 'advisory');
|
|
193
|
+
assert.equal(gate.artistGateMode(pack({ artistGate: 'strict' })), 'strict');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("an absent, unreadable or unrecognised level resolves to 'strict' — the platform default", () => {
|
|
197
|
+
// Absent is the honest case for a self-hosted instance nobody ever pushed a
|
|
198
|
+
// setting to; it takes the default rather than dropping the policy.
|
|
199
|
+
assert.equal(gate.artistGateMode(pack({})), 'strict');
|
|
200
|
+
assert.equal(gate.artistGateMode(pack({ artistGate: '' })), 'strict');
|
|
201
|
+
// A value this core cannot read is a policy it does not know — and the safe
|
|
202
|
+
// direction here is the strict one, because strict only ever delays a pin move.
|
|
203
|
+
assert.equal(gate.artistGateMode(pack({ artistGate: 'Strict' })), 'strict');
|
|
204
|
+
assert.equal(gate.artistGateMode(pack({ artistGate: 'sort-of' })), 'strict');
|
|
205
|
+
assert.equal(gate.artistGateMode(() => { throw new Error('no pack'); }), 'strict');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('the cutoff is read from the pack too, and junk reads as no cutoff', () => {
|
|
209
|
+
assert.equal(gate.artistGateSince(pack({ artistGateSince: '2026-09-10T00:00:00.000Z' })), '2026-09-10T00:00:00.000Z');
|
|
210
|
+
assert.equal(gate.artistGateSince(pack({ artistGateSince: '' })), null, 'the empty env value the transport always writes');
|
|
211
|
+
assert.equal(gate.artistGateSince(pack({ artistGateSince: 'whenever' })), null);
|
|
212
|
+
assert.equal(gate.artistGateSince(pack({})), null);
|
|
213
|
+
assert.equal(gate.artistGateSince(() => { throw new Error('no pack'); }), null);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// The property the whole ADR turns on
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
test('NOTHING in the gate reads a verdict — only source, ref, status, merge and date', () => {
|
|
221
|
+
// A row carrying every judgement-shaped field the board could hand it must
|
|
222
|
+
// behave identically to one carrying none: if this ever fails, the gate has
|
|
223
|
+
// started reading an opinion and ADR 0162's objection applies again.
|
|
224
|
+
const opinionated = review({
|
|
225
|
+
grade: 'fail', verdict: 'rejected', approved_by: null, review_outcome: 'needs-work', artist_says: 'this is slop',
|
|
226
|
+
});
|
|
227
|
+
assert.deepEqual(
|
|
228
|
+
gate.unresolvedArtistReviews([opinionated], {}).map((t) => t.id),
|
|
229
|
+
gate.unresolvedArtistReviews([review()], {}).map((t) => t.id)
|
|
230
|
+
);
|
|
231
|
+
const a = gate.artistGateHalt({ mode: 'strict', reviews: [opinionated] });
|
|
232
|
+
const b = gate.artistGateHalt({ mode: 'strict', reviews: [review()] });
|
|
233
|
+
assert.deepEqual(a, b, 'an approving artist releases nothing; a disapproving one holds nothing extra');
|
|
234
|
+
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// tests/artist_gate_release.mjs
|
|
2
|
+
//
|
|
3
|
+
// THE ESCAPE HATCH (task 1003575, ADR 0241 §3) — releasing a deploy that an
|
|
4
|
+
// unresolved artist review is holding.
|
|
5
|
+
//
|
|
6
|
+
// ADR 0241 was explicit that this must be the mechanism that already exists —
|
|
7
|
+
// the override request — and NOT a second authority path. So a release is the
|
|
8
|
+
// same table, the same request flow and the same decide endpoint as the grader
|
|
9
|
+
// bypass, and everything worth pinning here is where the two DIVERGE:
|
|
10
|
+
//
|
|
11
|
+
// • a release may be opened on a task that is NOT 'completed'. An artist review
|
|
12
|
+
// sits at `backlog`, and ADR 0241 guard #4 calls a strict project with no
|
|
13
|
+
// artist "the normal case" for a new instance — so a completed-only gate
|
|
14
|
+
// would make the one documented hatch unusable for exactly the case it was
|
|
15
|
+
// written for. (This is what the first cut of this task got wrong.)
|
|
16
|
+
// • only the PROJECT OWNER may decide it — narrower than the grader bypass's
|
|
17
|
+
// Archon floor, and enforced in the data layer so no second caller inherits
|
|
18
|
+
// the wider one.
|
|
19
|
+
// • approving it CONFIRMS NOTHING and PAYS NOTHING. The review stays open on
|
|
20
|
+
// the board; what stops is its hold on the pin.
|
|
21
|
+
// • it is never quiet: a self-release is permitted (a solo project would
|
|
22
|
+
// otherwise deadlock) and recorded as a self-override.
|
|
23
|
+
//
|
|
24
|
+
// DB-free: decideOverrideRequest's #538 `deps.pool` seam drives the txn with a
|
|
25
|
+
// SQL-aware fake, and the module pool is stubbed on the doorway for the reads.
|
|
26
|
+
//
|
|
27
|
+
// Run: node --test tests/artist_gate_release.mjs
|
|
28
|
+
|
|
29
|
+
import { strict as assert } from 'node:assert';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
|
|
32
|
+
|
|
33
|
+
process.env.NODE_ENV = 'test';
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
|
|
37
|
+
// The reward port must be registered before decideOverrideRequest is driven
|
|
38
|
+
// (the grader-bypass branch resolves it) — the override_attribution.mjs setup.
|
|
39
|
+
const achievements = require('../modules/economy/achievements.js');
|
|
40
|
+
achievements.evaluateAfterShip = async () => [];
|
|
41
|
+
achievements.evaluateAfterCreditAward = async () => [];
|
|
42
|
+
achievements.evaluateAfterClaim = async () => [];
|
|
43
|
+
achievements.reconcileBuilderAchievements = async () => [];
|
|
44
|
+
const seams = require('../src/module-seams.js');
|
|
45
|
+
if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
|
|
46
|
+
|
|
47
|
+
const api = require('../src/module-api.js');
|
|
48
|
+
|
|
49
|
+
// The module-level pool createOverrideRequest reads through. Stubbed BEFORE the
|
|
50
|
+
// db facade is required, because db-overrides.js destructures it at load.
|
|
51
|
+
let poolRows = [];
|
|
52
|
+
let poolQueries = [];
|
|
53
|
+
api.pool = { query: async (sql, params) => { poolQueries.push({ sql, params }); return { rows: poolRows }; } };
|
|
54
|
+
|
|
55
|
+
const db = require('../modules/lifecycle/db.js');
|
|
56
|
+
const overrides = require('../modules/lifecycle/db-overrides.js');
|
|
57
|
+
|
|
58
|
+
const { test, summary } = makeRunner();
|
|
59
|
+
|
|
60
|
+
const OWNER = 7;
|
|
61
|
+
const ARCHON = 42;
|
|
62
|
+
const ARTIST = 99;
|
|
63
|
+
|
|
64
|
+
const review = (over = {}) => ({
|
|
65
|
+
id: 9001, status: 'backlog', source: 'cascade',
|
|
66
|
+
source_ref: 'cascade-artist-review-on-ship-7001', merged_into_task_id: null, ...over,
|
|
67
|
+
});
|
|
68
|
+
const ordinary = (over = {}) => ({ id: 555, status: 'backlog', source: 'manual', source_ref: null, merged_into_task_id: null, ...over });
|
|
69
|
+
|
|
70
|
+
const flat = (q) => String(q.sql).replace(/\s+/g, ' ').trim();
|
|
71
|
+
const find = (queries, re) => queries.find((q) => re.test(flat(q)));
|
|
72
|
+
const all = (queries, re) => queries.filter((q) => re.test(flat(q)));
|
|
73
|
+
|
|
74
|
+
// =========================================================================
|
|
75
|
+
// Which tasks are in the release class
|
|
76
|
+
// =========================================================================
|
|
77
|
+
|
|
78
|
+
await test('the release class is exactly "an artist review that is still open" — asked of core\'s one reader', () => {
|
|
79
|
+
assert.equal(overrides.gateReleaseTarget(review()), true);
|
|
80
|
+
// Resolved reviews are not held, so there is nothing to release.
|
|
81
|
+
assert.equal(overrides.gateReleaseTarget(review({ status: 'shipped' })), false);
|
|
82
|
+
assert.equal(overrides.gateReleaseTarget(review({ status: 'abandoned' })), false);
|
|
83
|
+
assert.equal(overrides.gateReleaseTarget(review({ merged_into_task_id: 9100 })), false);
|
|
84
|
+
// And an ordinary task never is — a release must not become a second way to
|
|
85
|
+
// wave a graded ship through at any status.
|
|
86
|
+
assert.equal(overrides.gateReleaseTarget(ordinary()), false);
|
|
87
|
+
assert.equal(overrides.gateReleaseTarget(ordinary({ status: 'completed' })), false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// =========================================================================
|
|
91
|
+
// Opening one
|
|
92
|
+
// =========================================================================
|
|
93
|
+
|
|
94
|
+
await test('a request MAY be opened on a backlog artist review — the case the hatch exists for', async () => {
|
|
95
|
+
poolRows = [review()]; poolQueries = [];
|
|
96
|
+
// The INSERT returns the created row; the fake answers every statement with
|
|
97
|
+
// poolRows, which is enough to prove the completed-check did not refuse.
|
|
98
|
+
const row = await db.createOverrideRequest({ taskId: 9001, requesterId: ARTIST, rationale: 'x'.repeat(30) });
|
|
99
|
+
assert.ok(row, 'the request was created rather than refused');
|
|
100
|
+
assert.ok(find(poolQueries, /^INSERT INTO task_override_requests/), 'it reached the insert');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await test('an ordinary task at backlog is STILL refused — the class is narrow, not an open door', async () => {
|
|
104
|
+
poolRows = [ordinary()]; poolQueries = [];
|
|
105
|
+
await assert.rejects(
|
|
106
|
+
() => db.createOverrideRequest({ taskId: 555, requesterId: ARTIST, rationale: 'x'.repeat(30) }),
|
|
107
|
+
(e) => e.code === 'TASK_NOT_COMPLETED' && e.currentStatus === 'backlog'
|
|
108
|
+
);
|
|
109
|
+
assert.equal(all(poolQueries, /^INSERT INTO task_override_requests/).length, 0, 'nothing was written');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await test('a RESOLVED artist review is refused too — a dismissed review holds nothing to release', async () => {
|
|
113
|
+
poolRows = [review({ status: 'shipped' })]; poolQueries = [];
|
|
114
|
+
await assert.rejects(
|
|
115
|
+
() => db.createOverrideRequest({ taskId: 9001, requesterId: ARTIST, rationale: 'x'.repeat(30) }),
|
|
116
|
+
(e) => e.code === 'TASK_NOT_COMPLETED'
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// =========================================================================
|
|
121
|
+
// Deciding one
|
|
122
|
+
// =========================================================================
|
|
123
|
+
|
|
124
|
+
// The txn fake: answers the target-task SELECT with `target`, the request UPDATE
|
|
125
|
+
// with an approved row, and everything else with nothing.
|
|
126
|
+
function txPool({ target, requesterId = ARTIST, decision = 'approve' }) {
|
|
127
|
+
return makeSqlAwareClient((s) => {
|
|
128
|
+
if (/FROM tasks WHERE id = \(SELECT task_id/.test(s)) return { rows: target ? [target] : [] };
|
|
129
|
+
if (/^UPDATE task_override_requests/.test(s)) {
|
|
130
|
+
return { rows: [{ id: 1, task_id: target ? target.id : 0, status: decision === 'approve' ? 'approved' : 'denied', requested_by_builder_id: requesterId }] };
|
|
131
|
+
}
|
|
132
|
+
if (/^UPDATE tasks/.test(s)) return { rows: [{ id: target.id, title: 't', credits_reward: 10, kind: 'feature' }] };
|
|
133
|
+
return { rows: [] };
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const owner = async () => ({ id: OWNER });
|
|
138
|
+
|
|
139
|
+
await test('an ARCHON who is not the owner cannot release — and the refusal costs the review nothing', async () => {
|
|
140
|
+
const pool = txPool({ target: review() });
|
|
141
|
+
await assert.rejects(
|
|
142
|
+
() => db.decideOverrideRequest(
|
|
143
|
+
{ requestId: 1, archonId: ARCHON, decision: 'approve' },
|
|
144
|
+
{ pool, getFoundingBuilder: owner, insertAuditLog: async () => {} }
|
|
145
|
+
),
|
|
146
|
+
(e) => e.code === 'OVERRIDE_OWNER_ONLY'
|
|
147
|
+
);
|
|
148
|
+
assert.equal(all(pool._client.queries, /^UPDATE task_override_requests/).length, 0, 'the request stays open');
|
|
149
|
+
assert.ok(find(pool._client.queries, /^ROLLBACK/), 'and the txn rolls back');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await test('the OWNER releases it — and the release confirms nothing and pays nothing', async () => {
|
|
153
|
+
const audits = [];
|
|
154
|
+
const pool = txPool({ target: review() });
|
|
155
|
+
const out = await db.decideOverrideRequest(
|
|
156
|
+
{ requestId: 1, archonId: OWNER, decision: 'approve' },
|
|
157
|
+
{ pool, getFoundingBuilder: owner, insertAuditLog: async (a) => { audits.push(a); } }
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
assert.equal(out.request.status, 'approved');
|
|
161
|
+
assert.equal(out.gateRelease.taskId, '9001');
|
|
162
|
+
// The three things a grader bypass does that a release must NOT.
|
|
163
|
+
assert.equal(out.confirmResult, null, 'no confirm result — nothing was confirmed');
|
|
164
|
+
assert.equal(all(pool._client.queries, /^UPDATE tasks/).length, 0, 'the review is NOT flipped to confirmed');
|
|
165
|
+
assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'and nothing is paid for work nobody did');
|
|
166
|
+
assert.ok(find(pool._client.queries, /^COMMIT/), 'the release commits');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await test('a SELF-release is permitted and is recorded as a self-override — never quiet (ADR 0241 §3)', async () => {
|
|
170
|
+
const audits = [];
|
|
171
|
+
const pool = txPool({ target: review(), requesterId: OWNER });
|
|
172
|
+
const out = await db.decideOverrideRequest(
|
|
173
|
+
{ requestId: 1, archonId: OWNER, decision: 'approve' },
|
|
174
|
+
{ pool, getFoundingBuilder: owner, insertAuditLog: async (a) => { audits.push(a); } }
|
|
175
|
+
);
|
|
176
|
+
// Permitted: a solo instance where the owner is also the builder and the artist
|
|
177
|
+
// would otherwise deadlock, which ADR 0241 §3 rules out explicitly.
|
|
178
|
+
assert.equal(out.gateRelease.selfOverride, true);
|
|
179
|
+
assert.equal(audits.length, 1, 'exactly one audit line for the act');
|
|
180
|
+
assert.equal(audits[0].builderId, OWNER);
|
|
181
|
+
assert.equal(audits[0].requestBodyRedacted.override_kind, 'artist_gate_release');
|
|
182
|
+
assert.equal(audits[0].requestBodyRedacted.self_override, true, 'the ledger says WHO waved through their OWN work');
|
|
183
|
+
assert.equal(audits[0].requestBodyRedacted.task_id, '9001');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await test('a release of SOMEONE ELSE\'S review is audited too, and marked as not a self-override', async () => {
|
|
187
|
+
const audits = [];
|
|
188
|
+
const pool = txPool({ target: review(), requesterId: ARTIST });
|
|
189
|
+
const out = await db.decideOverrideRequest(
|
|
190
|
+
{ requestId: 1, archonId: OWNER, decision: 'approve' },
|
|
191
|
+
{ pool, getFoundingBuilder: owner, insertAuditLog: async (a) => { audits.push(a); } }
|
|
192
|
+
);
|
|
193
|
+
assert.equal(out.gateRelease.selfOverride, false);
|
|
194
|
+
assert.equal(audits.length, 1, 'every release is on the record, self or not');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await test('an audit write that FAILS does not roll back a decision the owner made', async () => {
|
|
198
|
+
const pool = txPool({ target: review() });
|
|
199
|
+
const out = await db.decideOverrideRequest(
|
|
200
|
+
{ requestId: 1, archonId: OWNER, decision: 'approve' },
|
|
201
|
+
{ pool, getFoundingBuilder: owner, insertAuditLog: async () => { throw new Error('audit table down'); } }
|
|
202
|
+
);
|
|
203
|
+
assert.equal(out.request.status, 'approved', 'the release is durable in the request row');
|
|
204
|
+
assert.ok(find(pool._client.queries, /^COMMIT/));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await test('DENYING a release changes nothing — the review goes on holding the pin', async () => {
|
|
208
|
+
const pool = txPool({ target: review(), decision: 'deny' });
|
|
209
|
+
const out = await db.decideOverrideRequest(
|
|
210
|
+
{ requestId: 1, archonId: OWNER, decision: 'deny' },
|
|
211
|
+
{ pool, getFoundingBuilder: owner, insertAuditLog: async () => {} }
|
|
212
|
+
);
|
|
213
|
+
assert.equal(out.request.status, 'denied');
|
|
214
|
+
assert.equal(out.gateRelease, null, 'nothing was released');
|
|
215
|
+
assert.equal(all(pool._client.queries, /^UPDATE tasks/).length, 0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await test('the GRADER BYPASS is untouched: a completed ordinary task still needs only an Archon', async () => {
|
|
219
|
+
// The owner check must not have leaked onto the path it does not belong to —
|
|
220
|
+
// that would be a silent tightening of an unrelated authority.
|
|
221
|
+
const pool = txPool({ target: ordinary({ status: 'completed' }) });
|
|
222
|
+
const out = await db.decideOverrideRequest(
|
|
223
|
+
{ requestId: 1, archonId: ARCHON, decision: 'approve' },
|
|
224
|
+
{ pool, getFoundingBuilder: owner, insertAuditLog: async () => {} }
|
|
225
|
+
);
|
|
226
|
+
assert.equal(out.gateRelease, null, 'not a release');
|
|
227
|
+
assert.ok(out.confirmResult, 'the ordinary bypass still confirms');
|
|
228
|
+
assert.ok(find(pool._client.queries, /^UPDATE tasks/), 'and still flips the task');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// =========================================================================
|
|
232
|
+
// The gate READS the release
|
|
233
|
+
// =========================================================================
|
|
234
|
+
|
|
235
|
+
await test('an approved override is excluded by the gate\'s own read — a released review stops holding', async () => {
|
|
236
|
+
poolRows = []; poolQueries = [];
|
|
237
|
+
await db.listOpenArtistReviews({ source: 'cascade', refPrefix: 'cascade-artist-review-on-ship-', resolvedStatuses: ['shipped', 'abandoned'] });
|
|
238
|
+
const q = flat(poolQueries[0]);
|
|
239
|
+
// The exclusion has to be IN THE QUERY: the release is not a status change, so
|
|
240
|
+
// nothing downstream would notice it otherwise — which is precisely how the
|
|
241
|
+
// first cut of this task shipped an escape hatch that released nothing.
|
|
242
|
+
assert.match(q, /NOT EXISTS \( SELECT 1 FROM task_override_requests r WHERE r\.task_id = t\.id AND r\.status = 'approved'\)/);
|
|
243
|
+
assert.deepEqual(poolQueries[0].params.slice(0, 3), ['cascade', 'cascade-artist-review-on-ship-%', ['shipped', 'abandoned']]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await test('the scan asks for ONE row past its cap, so "there are more" needs no second COUNT', async () => {
|
|
247
|
+
poolRows = []; poolQueries = [];
|
|
248
|
+
const out = await db.listOpenArtistReviews({ source: 'cascade', refPrefix: 'p-', resolvedStatuses: [], limit: 3 });
|
|
249
|
+
assert.equal(poolQueries[0].params[3], 4, 'cap + 1');
|
|
250
|
+
assert.deepEqual(out, { reviews: [], truncated: false });
|
|
251
|
+
|
|
252
|
+
// With cap+1 rows back, the extra one is dropped and truncation is declared —
|
|
253
|
+
// a COUNT that could disagree with the page it describes is what this avoids.
|
|
254
|
+
poolRows = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; poolQueries = [];
|
|
255
|
+
const full = await db.listOpenArtistReviews({ source: 'cascade', refPrefix: 'p-', resolvedStatuses: [], limit: 3 });
|
|
256
|
+
assert.equal(full.reviews.length, 3);
|
|
257
|
+
assert.equal(full.truncated, true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await test('the owner-check seam is NODE_ENV-gated like deps.pool — it cannot be swapped in production', () => {
|
|
261
|
+
// The one check deciding who may release a deploy-blocking review must not be
|
|
262
|
+
// overridable by an options object that happens to carry the key: a batch decide
|
|
263
|
+
// path or a careless options-forwarding refactor would otherwise hand an
|
|
264
|
+
// attacker-shaped `{ getFoundingBuilder: () => me }` straight past the gate.
|
|
265
|
+
//
|
|
266
|
+
// Asserted on the RESOLVER rather than through decideOverrideRequest, because
|
|
267
|
+
// the deps.pool seam is gated the same way — so a "production" run of the whole
|
|
268
|
+
// function fails on a real database connection long before it reaches this
|
|
269
|
+
// check, and a test that passed on THAT would be proving nothing.
|
|
270
|
+
const injected = async () => ({ id: ARCHON });
|
|
271
|
+
const doorway = api.getFoundingBuilder;
|
|
272
|
+
|
|
273
|
+
assert.equal(overrides.resolveOwnerReader({ getFoundingBuilder: injected }, { NODE_ENV: 'test' }), injected,
|
|
274
|
+
'a test keeps its seam');
|
|
275
|
+
for (const NODE_ENV of ['production', 'development', undefined]) {
|
|
276
|
+
assert.equal(overrides.resolveOwnerReader({ getFoundingBuilder: injected }, { NODE_ENV }), doorway,
|
|
277
|
+
`NODE_ENV=${NODE_ENV}: the injected reader is ignored and the doorway decides`);
|
|
278
|
+
}
|
|
279
|
+
// And with nothing injected it is the doorway either way.
|
|
280
|
+
assert.equal(overrides.resolveOwnerReader({}, { NODE_ENV: 'test' }), doorway);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
summary();
|