@1aboveio/skills 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +3 -0
- package/README.md +90 -0
- package/bin/1aboveio-skills.mjs +18 -0
- package/package.json +28 -0
- package/runtime/skills/distribution/generated/recipes.json +1189 -0
- package/runtime/skills/distribution/scripts/bundles.mjs +280 -0
- package/runtime/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
- package/skills/backend/airflow-dag-develop/LICENSE +3 -0
- package/skills/backend/airflow-dag-develop/SKILL.md +111 -0
- package/skills/backend/app-debug/LICENSE +3 -0
- package/skills/backend/app-debug/SKILL.md +109 -0
- package/skills/backend/app-debug/references/common-errors.md +128 -0
- package/skills/backend/python-backend/LICENSE +3 -0
- package/skills/backend/python-backend/SKILL.md +326 -0
- package/skills/cicd-pipeline/cloud-build/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-build/SKILL.md +707 -0
- package/skills/cicd-pipeline/cloud-debug/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-debug/SKILL.md +316 -0
- package/skills/cicd-pipeline/cloud-debug/references/build-failures.md +238 -0
- package/skills/cicd-pipeline/cloud-debug/references/deploy-failures.md +376 -0
- package/skills/cicd-pipeline/cloud-debug/references/pipeline-failures.md +378 -0
- package/skills/cicd-pipeline/cloud-deploy/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-deploy/SKILL.md +229 -0
- package/skills/cicd-pipeline/cloud-deploy/references/config-templates.md +257 -0
- package/skills/cicd-pipeline/docker/LICENSE +3 -0
- package/skills/cicd-pipeline/docker/SKILL.md +126 -0
- package/skills/cicd-pipeline/google-cloud/LICENSE +3 -0
- package/skills/cicd-pipeline/google-cloud/SKILL.md +118 -0
- package/skills/cicd-pipeline/google-cloud/references/gcs.md +469 -0
- package/skills/cicd-pipeline/google-cloud/references/iam.md +451 -0
- package/skills/cicd-pipeline/google-cloud/references/project.md +349 -0
- package/skills/cicd-pipeline/google-cloud/references/secrets.md +336 -0
- package/skills/cicd-pipeline/google-cloud/references/vpc.md +312 -0
- package/skills/cicd-pipeline/google-cloud/scripts/create-sa.sh +36 -0
- package/skills/cicd-pipeline/google-cloud/scripts/gcp-config.sh +31 -0
- package/skills/cicd-pipeline/google-cloud/scripts/grant-iap.sh +41 -0
- package/skills/cicd-pipeline/google-cloud/scripts/setup-secrets.sh +48 -0
- package/skills/cicd-pipeline/mergify/LICENSE +3 -0
- package/skills/cicd-pipeline/mergify/SKILL.md +138 -0
- package/skills/cicd-pipeline/mergify/assets/templates/mergify.yml +237 -0
- package/skills/cicd-pipeline/mergify/assets/templates/ruleset.json +46 -0
- package/skills/cicd-pipeline/mergify/references/branch-protection.md +277 -0
- package/skills/cicd-pipeline/mergify/references/configuration.md +183 -0
- package/skills/cicd-pipeline/mergify/references/diagnosis.md +73 -0
- package/skills/cicd-pipeline/mergify/references/traps.md +78 -0
- package/skills/cicd-pipeline/mergify/references/watch-contract.md +218 -0
- package/skills/cicd-pipeline/mergify/scripts/audit-core.mjs +131 -0
- package/skills/cicd-pipeline/mergify/scripts/audit.mjs +4 -0
- package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs +663 -0
- package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery.mjs +4 -0
- package/skills/cicd-pipeline/podman/LICENSE +3 -0
- package/skills/cicd-pipeline/podman/SKILL.md +70 -0
- package/skills/cicd-pipeline/podman/agents/openai.yaml +4 -0
- package/skills/cicd-pipeline/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
- package/skills/cicd-pipeline/podman/assets/templates/podman-service-override.conf +3 -0
- package/skills/cicd-pipeline/podman/references/compose-compatibility.md +70 -0
- package/skills/cicd-pipeline/podman/references/networking-and-ports.md +74 -0
- package/skills/cicd-pipeline/podman/references/rootless-services-and-sockets.md +156 -0
- package/skills/cicd-pipeline/podman/references/troubleshooting.md +98 -0
- package/skills/engineering/e2e-test/LICENSE +3 -0
- package/skills/engineering/e2e-test/SKILL.md +156 -0
- package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +272 -0
- package/skills/engineering/e2e-test/assets/ci-gates.github.yml +451 -0
- package/skills/engineering/e2e-test/assets/e2e-workflow.yml +282 -0
- package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-config.md +71 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +219 -0
- package/skills/engineering/e2e-test/references/authoring/test-skipping.md +44 -0
- package/skills/engineering/e2e-test/references/ci-integration.md +121 -0
- package/skills/engineering/e2e-test/references/ci-playwright-container.md +280 -0
- package/skills/engineering/e2e-test/references/debugging.md +36 -0
- package/skills/engineering/e2e-test/references/presentation-sweep.md +131 -0
- package/skills/engineering/e2e-test/references/reviewing.md +39 -0
- package/skills/engineering/e2e-test/references/route-discovery.md +50 -0
- package/skills/engineering/e2e-test/references/route-manifest.md +44 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-fastapi.py +290 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
- package/skills/engineering/e2e-test/scripts/post-visual-evidence.mjs +158 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +148 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
- package/skills/engineering/e2e-test/scripts/presentation-visual.mjs +84 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +75 -0
- package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
- package/skills/engineering/engineering-runtime/LICENSE +3 -0
- package/skills/engineering/engineering-runtime/SKILL.md +10 -0
- package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +553 -0
- package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
- package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
- package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
- package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
- package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +576 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +166 -0
- package/skills/engineering/ensure-coverage/LICENSE +3 -0
- package/skills/engineering/ensure-coverage/SKILL.md +136 -0
- package/skills/engineering/ensure-coverage/evals/evals.json +125 -0
- package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +91 -0
- package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +83 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
- package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
- package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +25 -0
- package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
- package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +87 -0
- package/skills/engineering/ensure-coverage/references/depth/test-smells.md +23 -0
- package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +164 -0
- package/skills/engineering/ensure-coverage/references/enforcement/hooks.md +85 -0
- package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +109 -0
- package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +33 -0
- package/skills/engineering/ensure-coverage/references/presentation/axis.md +78 -0
- package/skills/engineering/ensure-coverage/references/presentation/runner-contract.md +74 -0
- package/skills/engineering/ensure-coverage/references/process/audit-mode.md +33 -0
- package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
- package/skills/engineering/ensure-coverage/references/process/review-contract-template.md +119 -0
- package/skills/engineering/ensure-coverage/references/process/scope-class.md +178 -0
- package/skills/engineering/ensure-coverage/references/process/test-strategy.md +55 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +45 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +93 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +178 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +101 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +358 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-checklist.mjs +494 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +663 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
- package/skills/engineering/ensure-coverage/scripts/evidence-block.mjs +367 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +269 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/ensure-coverage/scripts/package-lock.json +76 -0
- package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
- package/skills/engineering/ensure-coverage/scripts/scope-class.mjs +554 -0
- package/skills/engineering/harness-runtime/LICENSE +3 -0
- package/skills/engineering/harness-runtime/SKILL.md +18 -0
- package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
- package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
- package/skills/engineering/harness-runtime/contracts.md +15 -0
- package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
- package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
- package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
- package/skills/engineering/harness-runtime/index.mjs +767 -0
- package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
- package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
- package/skills/engineering/harness-runtime/references/model-catalog-seed.json +159 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
- package/skills/engineering/implement-and-pr/LICENSE +3 -0
- package/skills/engineering/implement-and-pr/SKILL.md +176 -0
- package/skills/engineering/implement-and-pr/references/ci-iteration.md +10 -0
- package/skills/engineering/implement-and-pr/references/closeout.md +27 -0
- package/skills/engineering/implement-and-pr/references/contract-complete-fix-rounds.md +34 -0
- package/skills/engineering/implement-and-pr/references/evidence-rules.md +39 -0
- package/skills/engineering/implement-and-pr/references/incremental-plan.md +16 -0
- package/skills/engineering/implement-and-pr/references/self-review.md +23 -0
- package/skills/engineering/implement-and-pr/references/tdd-mode.md +18 -0
- package/skills/engineering/resolve-issues/LICENSE +3 -0
- package/skills/engineering/resolve-issues/SKILL.md +167 -0
- package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +448 -0
- package/skills/engineering/resolve-issues/references/breaker.md +82 -0
- package/skills/engineering/resolve-issues/references/deliverables.md +27 -0
- package/skills/engineering/resolve-issues/references/delivery.md +108 -0
- package/skills/engineering/resolve-issues/references/evidence-lane.md +21 -0
- package/skills/engineering/resolve-issues/references/exact-head-ci.md +287 -0
- package/skills/engineering/resolve-issues/references/fan-out.md +33 -0
- package/skills/engineering/resolve-issues/references/finalization.md +68 -0
- package/skills/engineering/resolve-issues/references/guarantees.md +10 -0
- package/skills/engineering/resolve-issues/references/high-risk.md +29 -0
- package/skills/engineering/resolve-issues/references/incidents/848/README.md +156 -0
- package/skills/engineering/resolve-issues/references/intake.md +86 -0
- package/skills/engineering/resolve-issues/references/integration-gate.md +53 -0
- package/skills/engineering/resolve-issues/references/interference.md +87 -0
- package/skills/engineering/resolve-issues/references/loop.md +134 -0
- package/skills/engineering/resolve-issues/references/model-catalog.md +9 -0
- package/skills/engineering/resolve-issues/references/postmortem.md +27 -0
- package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +41 -0
- package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +48 -0
- package/skills/engineering/resolve-issues/references/pre-flight.md +41 -0
- package/skills/engineering/resolve-issues/references/regression-checklist.md +26 -0
- package/skills/engineering/resolve-issues/references/run-state.md +288 -0
- package/skills/engineering/resolve-issues/references/sandboxed-testing.md +48 -0
- package/skills/engineering/resolve-issues/references/spawn-contract.md +96 -0
- package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +40 -0
- package/skills/engineering/resolve-issues/references/why.md +653 -0
- package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +49 -0
- package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +721 -0
- package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +962 -0
- package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +220 -0
- package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +420 -0
- package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +256 -0
- package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +168 -0
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/doctrine.mjs +62 -0
- package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +191 -0
- package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +413 -0
- package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +332 -0
- package/skills/engineering/resolve-issues/scripts/finalize.mjs +488 -0
- package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +3307 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +16 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +51 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +17 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +166 -0
- package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +92 -0
- package/skills/engineering/resolve-issues/scripts/guide-index.mjs +73 -0
- package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +159 -0
- package/skills/engineering/resolve-issues/scripts/interference.mjs +427 -0
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/next-operations.mjs +419 -0
- package/skills/engineering/resolve-issues/scripts/postmortem.mjs +909 -0
- package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +322 -0
- package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +415 -0
- package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +697 -0
- package/skills/engineering/resolve-issues/scripts/report.mjs +494 -0
- package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +172 -0
- package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +79 -0
- package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +334 -0
- package/skills/engineering/resolve-issues/scripts/run-state.mjs +4784 -0
- package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +395 -0
- package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +290 -0
- package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +170 -0
- package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +293 -0
- package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +197 -0
- package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +127 -0
- package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +893 -0
- package/skills/engineering/resolve-issues/scripts/workspaces.mjs +829 -0
- package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +290 -0
- package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +80 -0
- package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +102 -0
- package/skills/engineering/resolve-release/LICENSE +3 -0
- package/skills/engineering/resolve-release/SKILL.md +112 -0
- package/skills/engineering/resolve-release/references/assembly.md +137 -0
- package/skills/engineering/resolve-release/references/auto-when-green.md +56 -0
- package/skills/engineering/resolve-release/references/candidate.md +167 -0
- package/skills/engineering/resolve-release/references/exposure.md +178 -0
- package/skills/engineering/resolve-release/references/handoff.md +24 -0
- package/skills/engineering/resolve-release/references/postmortem.md +230 -0
- package/skills/engineering/resolve-release/references/preflight.md +207 -0
- package/skills/engineering/resolve-release/references/principles.md +94 -0
- package/skills/engineering/resolve-release/references/regression-checklist.md +36 -0
- package/skills/engineering/resolve-release/references/related-skills.md +12 -0
- package/skills/engineering/resolve-release/references/routing.md +149 -0
- package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +285 -0
- package/skills/engineering/resolve-release/references/versioning.md +202 -0
- package/skills/engineering/resolve-release/references/why.md +53 -0
- package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +389 -0
- package/skills/engineering/resolve-release/scripts/build-changes.mjs +209 -0
- package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +407 -0
- package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +904 -0
- package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +81 -0
- package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +612 -0
- package/skills/engineering/resolve-release/scripts/close-attempt.mjs +135 -0
- package/skills/engineering/resolve-release/scripts/closeout-release.mjs +161 -0
- package/skills/engineering/resolve-release/scripts/doctrine.mjs +106 -0
- package/skills/engineering/resolve-release/scripts/durable-processing.mjs +522 -0
- package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +659 -0
- package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +1014 -0
- package/skills/engineering/resolve-release/scripts/finalize-release.mjs +526 -0
- package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +169 -0
- package/skills/engineering/resolve-release/scripts/green-gate.mjs +599 -0
- package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +752 -0
- package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +56 -0
- package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +1538 -0
- package/skills/engineering/resolve-release/scripts/postmortem.mjs +381 -0
- package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +498 -0
- package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +326 -0
- package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +272 -0
- package/skills/engineering/resolve-release/scripts/report.mjs +417 -0
- package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +114 -0
- package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +238 -0
- package/skills/engineering/resolve-release/scripts/review-packet.mjs +503 -0
- package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +263 -0
- package/skills/engineering/resolve-release/scripts/version-assert.mjs +339 -0
- package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +485 -0
- package/skills/engineering/resolve-release/scripts/version.mjs +1199 -0
- package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +449 -0
- package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/README.md +60 -0
- package/skills/engineering/resolve-release/vendor/acorn.mjs +6233 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +341 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +70 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +7 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +2033 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +2033 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +7 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +49 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +5 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +549 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +549 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +36 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +47 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +82 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +22 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +25 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +85 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +85 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +20 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +20 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +61 -0
- package/skills/engineering/review-pr/LICENSE +3 -0
- package/skills/engineering/review-pr/SKILL.md +123 -0
- package/skills/engineering/review-pr/references/adversarial-reviewer-prompt.md +42 -0
- package/skills/engineering/review-pr/references/code-correctness.md +5 -0
- package/skills/engineering/review-pr/references/contract-freshness.md +9 -0
- package/skills/engineering/review-pr/references/coordination.md +18 -0
- package/skills/engineering/review-pr/references/domain-hazards.md +123 -0
- package/skills/engineering/review-pr/references/finding-themes.md +7 -0
- package/skills/engineering/review-pr/references/github-posting.md +98 -0
- package/skills/engineering/review-pr/references/golden-path-smoke.md +5 -0
- package/skills/engineering/review-pr/references/incremental-output.md +16 -0
- package/skills/engineering/review-pr/references/inputs-and-discovery.md +31 -0
- package/skills/engineering/review-pr/references/output-format.md +99 -0
- package/skills/engineering/review-pr/references/over-mock-screen.md +7 -0
- package/skills/engineering/review-pr/references/promotion-prs.md +16 -0
- package/skills/engineering/review-pr/references/re-review.md +18 -0
- package/skills/engineering/review-pr/references/review-method.md +199 -0
- package/skills/engineering/review-pr/references/review-mode.md +30 -0
- package/skills/engineering/review-pr/references/review-posture.md +53 -0
- package/skills/engineering/review-pr/references/round1-depth.md +62 -0
- package/skills/engineering/review-pr/references/scripts.md +17 -0
- package/skills/engineering/review-pr/references/workflow.md +16 -0
- package/skills/engineering/review-pr/schemas/findings.schema.json +282 -0
- package/skills/engineering/review-pr/scripts/finding-contract.mjs +285 -0
- package/skills/engineering/review-pr/scripts/post-review.mjs +405 -0
- package/skills/engineering/review-pr/scripts/pr-context.mjs +207 -0
- package/skills/engineering/review-pr/scripts/scan-diff.mjs +365 -0
- package/skills/engineering/review-pr/scripts/theme-contract.mjs +57 -0
- package/skills/engineering/smoke/LICENSE +3 -0
- package/skills/engineering/smoke/SKILL.md +131 -0
- package/skills/engineering/smoke/assets/smoke.manifest.example.json +53 -0
- package/skills/engineering/smoke/references/manifest.md +192 -0
- package/skills/engineering/smoke/scripts/smoke.mjs +713 -0
- package/skills/fullstack/better-auth/LICENSE +3 -0
- package/skills/fullstack/better-auth/SKILL.md +601 -0
- package/skills/fullstack/better-auth/references/feishu-api.md +270 -0
- package/skills/fullstack/monorepo/LICENSE +3 -0
- package/skills/fullstack/monorepo/SKILL.md +465 -0
- package/skills/fullstack/nextjs-fullstack/LICENSE +3 -0
- package/skills/fullstack/nextjs-fullstack/SKILL.md +210 -0
- package/skills/fullstack/nextjs-fullstack/conventions.md +318 -0
- package/skills/fullstack/nextjs-fullstack/frontend-conventions.md +61 -0
- package/skills/fullstack/nextjs-fullstack/nextjs16.md +287 -0
- package/skills/fullstack/nextjs-fullstack/server-actions.md +409 -0
- package/skills/fullstack/prisma-setup/LICENSE +3 -0
- package/skills/fullstack/prisma-setup/SKILL.md +180 -0
- package/skills/fullstack/prisma-setup/nextjs.md +258 -0
- package/skills/fullstack/prisma-setup/turborepo.md +301 -0
- package/skills/fullstack/shadcn/LICENSE +3 -0
- package/skills/fullstack/shadcn/SKILL.md +119 -0
- package/skills/fullstack/shadcn/assets/shadcn-small.png +0 -0
- package/skills/fullstack/shadcn/assets/shadcn.png +0 -0
- package/skills/fullstack/shadcn/cli.md +411 -0
- package/skills/fullstack/shadcn/customization.md +224 -0
- package/skills/fullstack/shadcn/evals/evals.json +90 -0
- package/skills/fullstack/shadcn/mcp.md +101 -0
- package/skills/fullstack/shadcn/rules/base-vs-radix.md +323 -0
- package/skills/fullstack/shadcn/rules/component-selection.md +67 -0
- package/skills/fullstack/shadcn/rules/composition.md +195 -0
- package/skills/fullstack/shadcn/rules/data-table.md +201 -0
- package/skills/fullstack/shadcn/rules/forms.md +255 -0
- package/skills/fullstack/shadcn/rules/icons.md +103 -0
- package/skills/fullstack/shadcn/rules/styling.md +167 -0
- package/skills/fullstack/zod-v4/LICENSE +3 -0
- package/skills/fullstack/zod-v4/SKILL.md +287 -0
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
// evidence-bundle — the durable release-evidence bundle, emitted once per release attempt (#441).
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS. Principle 5 is that the loop exits on OBSERVED production state, observed
|
|
4
|
+
// twice — the candidate at 0%, then the live service after the shift. Step 4b's proof-of-execution
|
|
5
|
+
// captures those observations, but they land in the RUN MANIFEST, which is session-scoped
|
|
6
|
+
// (`run-state.mjs`'s job), plus whatever the transcript happens to hold. So the evidence that a
|
|
7
|
+
// release was verified — the thing the central principle rests on — does not reliably outlive the
|
|
8
|
+
// session that produced it, and a FAILED attempt is worse: it leaves a deleted revision, a normal
|
|
9
|
+
// issue, and nothing durably linking the two. This script makes the evidence a first-class,
|
|
10
|
+
// immutable artifact with a defined lifetime, keyed to the immutable attempt id AND its terminal
|
|
11
|
+
// state (principle 10), so a post-incident question is answerable months later without the originating
|
|
12
|
+
// session. The state is in the key because one attempt can reach two terminal states in sequence — a
|
|
13
|
+
// lease-holding `blocked-manual` hold that recovers on the SAME attempt and closes `released` — and
|
|
14
|
+
// each of those exits owes its own durable record (see the filename section).
|
|
15
|
+
//
|
|
16
|
+
// KEYED ON THE ATTEMPT ID, NOT THE RC TAG (#599). It used to be the rc tag, and that made the whole
|
|
17
|
+
// artifact unreachable on three of the four declarable trigger shapes: branch push, the legacy final
|
|
18
|
+
// tag, and "no deployed surface" all mint NO rc, so a terminal exit on them had no key to write
|
|
19
|
+
// under — and inventing one would fabricate the very artifact the bundle exists to make trustworthy.
|
|
20
|
+
// The attempt id exists on EVERY shape by construction, because the Release Attempt Lease opens at
|
|
21
|
+
// the promotion authorization rather than at the mint. Two consequences worth stating:
|
|
22
|
+
// - `rcTag` is demoted to an ordinary recorded field. It is still REQUIRED where an rc exists, and
|
|
23
|
+
// an absent one must be DECLARED (`deployTrigger` naming a shape that mints none) rather than
|
|
24
|
+
// merely omitted — otherwise dropping the old `e:missing-rc-tag` would silently let an rc-shaped
|
|
25
|
+
// release emit a durable record that forgot the tag it shipped under.
|
|
26
|
+
// - Bundles written BEFORE #599 are unaffected: one carrying only `rcTag` still keys, validates
|
|
27
|
+
// and locates (the key falls back to it), so `v1.2.0-rc.1--released.json` stays byte-identical.
|
|
28
|
+
// The id itself is `<version>-attempt.<n>` on all four shapes and is minted, not composed — see
|
|
29
|
+
// `attemptIdFor` / `nextAttemptId` below for why the version alone was not a usable key.
|
|
30
|
+
// The final tag was rejected as the key: it exists only after verification, which would leave a
|
|
31
|
+
// FAILED no-rc attempt with no key — precisely the case the bundle exists for.
|
|
32
|
+
//
|
|
33
|
+
// THE MANIFEST IS THE WORKING STATE; THE BUNDLE IS THE DURABLE RECORD. They are different things
|
|
34
|
+
// (#441's AC says so): the manifest is mutable working state the run reads and rewrites at every
|
|
35
|
+
// transition; the bundle is written ONCE, at a terminal exit, and never rewritten. This script
|
|
36
|
+
// does not touch the manifest and `run-state.mjs` does not write bundles.
|
|
37
|
+
//
|
|
38
|
+
// KEYS OFF #432'S ENUM, NEVER A PARALLEL ONE. A bundle is emitted per TERMINAL attempt state, and
|
|
39
|
+
// those states are the Release Attempt Lease's — imported from `run-state.mjs` (ATTEMPT_TERMINAL_STATES,
|
|
40
|
+
// ATTEMPT_CLOSE_CAUSES, ATTEMPT_TAG_DISPOSITIONS). Adding or renaming a terminal state there breaks
|
|
41
|
+
// this module's tests, which is the point: the bundle CONSUMES the state machine, it does not
|
|
42
|
+
// re-implement the enum.
|
|
43
|
+
//
|
|
44
|
+
// EMITTED ON EVERY TERMINAL PATH. `released` (verified), `failed-pre-promotion` (pre-exposure
|
|
45
|
+
// discard), `rolled-back` (post-exposure), `revoked`, `blocked-manual`. A failed attempt is
|
|
46
|
+
// validated exactly as strictly as a successful one — each state carries its OWN required
|
|
47
|
+
// observation set (`REQUIRED_BY_STATE`), so "a failed attempt is as well-evidenced as a successful
|
|
48
|
+
// one" is a schema rule, not an aspiration. What it is NOT: it does not degrade into workflow logs
|
|
49
|
+
// alone, a mutable summary, or final-tag-only evidence (which would lose every failed attempt).
|
|
50
|
+
//
|
|
51
|
+
// FAILS CLOSED. An incomplete or malformed bundle REFUSES — an under-populated durable record that
|
|
52
|
+
// looks complete is worse than none, the same reading candidate-identity.mjs takes on a null field
|
|
53
|
+
// the doctrine later compares against. And a bundle that CLAIMS an observation its state never took
|
|
54
|
+
// (a pre-exposure discard carrying a post-shift live verdict) refuses too: the point of a per-state
|
|
55
|
+
// schema is that the evidence present is evidence that was actually produced.
|
|
56
|
+
//
|
|
57
|
+
// IMMUTABLE ONCE WRITTEN (AC5). A mutable summary is not evidence. `emit` refuses to overwrite an
|
|
58
|
+
// existing bundle for an (attempt id, terminal state) with DIFFERENT content; an evidence-identical
|
|
59
|
+
// re-emit (a resumed session re-running the same close) is a resume-safe no-op that keeps the first
|
|
60
|
+
// write. Immutability is per (id, state): a DIFFERENT terminal state for the same attempt (the
|
|
61
|
+
// recovery case above) is a new, legal bundle, not an overwrite. Only the `emittedAt` write-stamp is
|
|
62
|
+
// exempt from the comparison — it is metadata about the write, not the evidence. The guarantee kept
|
|
63
|
+
// its exact shape across #599; only what identifies the attempt changed.
|
|
64
|
+
//
|
|
65
|
+
// Contract:
|
|
66
|
+
// validate --bundle <file> exit 0 valid · 1 invalid (failing ids in `failed`) · 2 usage
|
|
67
|
+
// emit --bundle <file> --dir <d> validate then write immutably to <d>/<attemptId>--<state>.json;
|
|
68
|
+
// exit 0 written/no-op · 1 invalid-or-immutability-violation · 2 usage
|
|
69
|
+
// path --attempt-id <id> --dir <d> [--state <s>] with --state, print the one file for that
|
|
70
|
+
// (id, state); without it, list every bundle for the attempt
|
|
71
|
+
// (reachable by id alone — one attempt can hold >1 bundle).
|
|
72
|
+
// `--rc-tag <tag>` is the pre-#599 spelling and still resolves,
|
|
73
|
+
// because a pre-#599 bundle IS keyed by its rc tag.
|
|
74
|
+
|
|
75
|
+
import fs from 'node:fs'
|
|
76
|
+
import path from 'node:path'
|
|
77
|
+
|
|
78
|
+
import {
|
|
79
|
+
ATTEMPT_TERMINAL_STATES,
|
|
80
|
+
ATTEMPT_CLOSE_CAUSES,
|
|
81
|
+
ATTEMPT_TAG_DISPOSITIONS,
|
|
82
|
+
ATTEMPT_ID_KEY_SAFE,
|
|
83
|
+
RELEASE_EVIDENCE_FIELDS,
|
|
84
|
+
} from '../../resolve-issues/scripts/release-state-contract.mjs'
|
|
85
|
+
import { shaMatches } from './candidate-identity.mjs'
|
|
86
|
+
import { classifyFailureOwner, classifyFailurePhase } from './version-postmortem.mjs'
|
|
87
|
+
import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
|
|
88
|
+
|
|
89
|
+
// The bundle's terminal-state roster is #432's, re-exported so callers (and the test) key off ONE
|
|
90
|
+
// list. Never redefine the five values here — import them.
|
|
91
|
+
export const BUNDLE_TERMINAL_STATES = ATTEMPT_TERMINAL_STATES
|
|
92
|
+
export const BUNDLE_FIELDS = RELEASE_EVIDENCE_FIELDS
|
|
93
|
+
|
|
94
|
+
// Present on every bundle regardless of terminal state. `attemptId` replaced `rcTag` here in #599 —
|
|
95
|
+
// see `bundleKey` for the pre-#599 fallback that keeps existing bundles keyed and valid.
|
|
96
|
+
export const REQUIRED_ALWAYS = ['attemptId', 'sha', 'buildIdentity', 'terminalState']
|
|
97
|
+
|
|
98
|
+
// The four declarable deploy-trigger shapes (SKILL.md pre-flight item 5 / references/versioning.md
|
|
99
|
+
// §5). The bundle carries the declaration so that an ABSENT rc tag is a decided absence with a
|
|
100
|
+
// checkable reason rather than an undeclared skip — principle 13's own distinction.
|
|
101
|
+
export const DEPLOY_TRIGGER_SHAPES = ['rc-tag', 'branch-push', 'final-tag', 'no-deploy']
|
|
102
|
+
|
|
103
|
+
// The three shapes that mint no rc at step 3. `rc-tag` is the only one that does, and it is the only
|
|
104
|
+
// one whose bundle owes an `rcTag`. Derived from the roster above rather than written out twice, so
|
|
105
|
+
// a fifth shape cannot be added on one side only.
|
|
106
|
+
export const NO_RC_TRIGGER_SHAPES = DEPLOY_TRIGGER_SHAPES.filter((s) => s !== 'rc-tag')
|
|
107
|
+
|
|
108
|
+
// The observation set each terminal state owes ON TOP of REQUIRED_ALWAYS, on a DEPLOYED shape.
|
|
109
|
+
// This table is the whole "a failed attempt is as well-evidenced as a successful one" rule made
|
|
110
|
+
// mechanical. It applies to `rc-tag`, `branch-push` and `final-tag`, and — because an UNDECLARED
|
|
111
|
+
// shape is every pre-#599 bundle, all of which deployed — to an undeclared one too. `no-deploy`
|
|
112
|
+
// has its own table below (#604); see `requiredFor`, which is the only thing that should read
|
|
113
|
+
// either of these:
|
|
114
|
+
// - released — the full verified path: both principle-5 observations (candidateVerdict at
|
|
115
|
+
// 0%, liveVerdict after the shift), the exposure approval, the traffic
|
|
116
|
+
// transition, and the candidate receipt.
|
|
117
|
+
// - failed-pre-promotion — a candidate-validation discard carries the candidate receipt, the
|
|
118
|
+
// 4a verdict (the DEAD/EMPTY/cannot-run that stopped it), and cleanup.
|
|
119
|
+
// A deployed pre-candidate failure carries its verified disposition instead:
|
|
120
|
+
// it has no candidate receipt/verdict to claim. Neither form exposed, so it
|
|
121
|
+
// owes NO liveVerdict and NO trafficTransition — claiming one is refused.
|
|
122
|
+
// - rolled-back — post-exposure: it DID shift, so it owes the transition + the live verdict
|
|
123
|
+
// that went unhealthy + the rollback disposition.
|
|
124
|
+
// - revoked — a deliberately abandoned attempt: the disposition, and tagDisposition
|
|
125
|
+
// "retained" (principle 10 — revocation revokes the attempt, never the tag).
|
|
126
|
+
// It abandoned WITHOUT shipping, so like failed-pre-promotion it owes NO
|
|
127
|
+
// liveVerdict and NO trafficTransition, and claiming one is refused below.
|
|
128
|
+
// - blocked-manual — parked on an operator: a reason and the disposition (what is being waited
|
|
129
|
+
// on). It holds the lease, so unlike the four above it carries NO closedBy.
|
|
130
|
+
export const REQUIRED_BY_STATE = {
|
|
131
|
+
released: ['candidateReceipt', 'candidateVerdict', 'approval', 'trafficTransition', 'liveVerdict'],
|
|
132
|
+
'failed-pre-promotion': ['candidateReceipt', 'candidateVerdict', 'disposition'],
|
|
133
|
+
'rolled-back': ['candidateReceipt', 'candidateVerdict', 'approval', 'trafficTransition', 'liveVerdict', 'disposition'],
|
|
134
|
+
revoked: ['disposition', 'tagDisposition'],
|
|
135
|
+
'blocked-manual': ['reason', 'disposition'],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// A failed pre-candidate attempt has no candidate receipt or candidate verdict by definition.
|
|
139
|
+
// It still binds the attempt, SHA, build identity, close cause, and verified disposition through
|
|
140
|
+
// REQUIRED_ALWAYS plus this state-specific set; any candidate/exposure observation is fabricated.
|
|
141
|
+
export const REQUIRED_BY_STATE_PRE_CANDIDATE = {
|
|
142
|
+
'failed-pre-promotion': ['disposition'],
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- the required set is (terminal state × TRIGGER SHAPE), not the state alone (#604) ----
|
|
146
|
+
//
|
|
147
|
+
// #599 made the KEY reachable on the three shapes that mint no rc. The required CONTENT above
|
|
148
|
+
// stayed keyed to a DEPLOYMENT, and on `no-deploy` — a shape #599 itself added — all five of
|
|
149
|
+
// `released`'s fields are artifacts only a deployment produces: a 0%-traffic candidate, a smoke
|
|
150
|
+
// against it, an exposure authorization, a traffic shift, a live service. None can exist there, so
|
|
151
|
+
// that shape could never reach a valid terminal bundle at all, and this repo's `v0.5.0` shipped
|
|
152
|
+
// with NO bundle for exactly that reason — principle 5's "a durable bundle at every terminal exit"
|
|
153
|
+
// quietly excluding a shape the skill offers at pre-flight, which is the state #599 was filed to end.
|
|
154
|
+
//
|
|
155
|
+
// The principle does not change: a bundle carries the evidence its shape actually produced, and
|
|
156
|
+
// refuses to claim an observation nobody took. What changes is that "what the shape produced" stops
|
|
157
|
+
// being ASSUMED to include a deployment. So the deployed shapes are untouched — `released` still
|
|
158
|
+
// owes all five, and a deployed release that cannot produce them is still not releasable — and
|
|
159
|
+
// `no-deploy` owes what it does produce: the version identity and immutable tag (`releaseTag`), the
|
|
160
|
+
// Release reference (`releaseRef`), and the verification evidence standing in for principle 5's two
|
|
161
|
+
// observations (`verification` — in a no-deploy repo, CI plus the full suite on the tagged SHA, and
|
|
162
|
+
// the consumer-facing read that shows the artifact resolvable at the tag; versioning.md §5).
|
|
163
|
+
//
|
|
164
|
+
// THIS MAKES THE REQUIREMENT SATISFIABLE; IT MUST NOT MAKE IT OPTIONAL. Two halves, and the second
|
|
165
|
+
// is the one a widening would drop: the fields above are required per shape (`requiredFor`), AND the
|
|
166
|
+
// five deployment fields are FORBIDDEN on `no-deploy` (`DEPLOY_EVIDENCE_FIELDS` below) — a no-deploy
|
|
167
|
+
// bundle asserting a traffic transition is the same fabrication `EXPOSURE_FORBIDDEN` already refuses,
|
|
168
|
+
// read per shape instead of per state.
|
|
169
|
+
//
|
|
170
|
+
// Only three states differ, because only three of them named deployment artifacts: `revoked`
|
|
171
|
+
// (disposition + tagDisposition) and `blocked-manual` (reason + disposition) owe nothing a
|
|
172
|
+
// deployment produces, so they are shape-independent and are NOT restated here — `requiredFor`
|
|
173
|
+
// falls through to the table above for them, which is what keeps one rule in one place.
|
|
174
|
+
//
|
|
175
|
+
// The final tag is cut only AFTER verification on this shape (versioning.md §5), so a no-deploy
|
|
176
|
+
// attempt that never got there has no tag and no Release: `failed-pre-promotion` owes the
|
|
177
|
+
// verification that stopped it and the cleanup record, and only the two post-publication states
|
|
178
|
+
// owe `releaseTag`/`releaseRef`.
|
|
179
|
+
export const REQUIRED_BY_STATE_NO_DEPLOY = {
|
|
180
|
+
released: ['releaseTag', 'releaseRef', 'verification'],
|
|
181
|
+
'failed-pre-promotion': ['verification', 'disposition'],
|
|
182
|
+
'rolled-back': ['releaseTag', 'releaseRef', 'verification', 'disposition'],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The five fields that only a deployment can produce. Required on the deployed shapes at the states
|
|
186
|
+
// above; REFUSED on `no-deploy`, at EVERY terminal state including `blocked-manual` — the carve-out
|
|
187
|
+
// that lets a deployed `blocked-manual` carry exposure evidence exists because it can legitimately
|
|
188
|
+
// be parked POST-shift at step 4b, and on a shape with no traffic there is no post-shift to park in.
|
|
189
|
+
export const DEPLOY_EVIDENCE_FIELDS = ['candidateReceipt', 'candidateVerdict', 'approval', 'trafficTransition', 'liveVerdict']
|
|
190
|
+
|
|
191
|
+
// The one reader of both tables. A shape that is absent, null or unknown falls through to the
|
|
192
|
+
// deployed set: an undeclared shape is every pre-#599 bundle (all deployed), and an unknown one is
|
|
193
|
+
// already refused by `e:unknown-deploy-trigger` — neither may take the SMALLER required set, because
|
|
194
|
+
// "unrecognised" must never be the cheap way to owe less.
|
|
195
|
+
export function requiredFor(terminalState, deployTrigger, failurePhase = null) {
|
|
196
|
+
// A no-deploy release never creates a candidate, but it has its own required verification
|
|
197
|
+
// evidence. Keep that shape-specific contract ahead of the deployed pre-candidate failure set.
|
|
198
|
+
if (deployTrigger === 'no-deploy' && REQUIRED_BY_STATE_NO_DEPLOY[terminalState]) {
|
|
199
|
+
return REQUIRED_BY_STATE_NO_DEPLOY[terminalState]
|
|
200
|
+
}
|
|
201
|
+
if (failurePhase === 'pre-candidate' && REQUIRED_BY_STATE_PRE_CANDIDATE[terminalState]) {
|
|
202
|
+
return REQUIRED_BY_STATE_PRE_CANDIDATE[terminalState]
|
|
203
|
+
}
|
|
204
|
+
return REQUIRED_BY_STATE[terminalState]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The states that ended WITHOUT shipping, so a live post-shift verdict or a traffic transition on
|
|
208
|
+
// one is not missing evidence, it is fabricated evidence: `failed-pre-promotion` (discarded before
|
|
209
|
+
// exposure) and `revoked` (deliberately abandoned without shipping — SKILL.md:433). NOT
|
|
210
|
+
// `blocked-manual`: it can legitimately be parked at step 4b, POST-shift, so partial exposure
|
|
211
|
+
// evidence on it is real, not a lie — it must stay allowed.
|
|
212
|
+
const EXPOSURE_FORBIDDEN = {
|
|
213
|
+
'failed-pre-promotion': ['liveVerdict', 'trafficTransition'],
|
|
214
|
+
revoked: ['liveVerdict', 'trafficTransition'],
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// NULL IS ABSENCE, EVERYWHERE IN THIS SCHEMA (#604's second finding, generalised). `"rcTag": null`
|
|
218
|
+
// used to produce `e:unusable-rc-tag` while OMITTING the key validated — and null is the natural
|
|
219
|
+
// thing for a generator to write for "this shape has none". The same trap sat one layer down on the
|
|
220
|
+
// forbidden fields, where `"liveVerdict": null` on a pre-exposure discard read as a CLAIMED
|
|
221
|
+
// observation and refused. One rule, applied at every optional-field decision: a key written null
|
|
222
|
+
// is a key that is not there. It is deliberately NOT applied to the required fields' own content
|
|
223
|
+
// checks, because those already answer correctly — a required object that is null is missing, and
|
|
224
|
+
// `e:missing-*` is exactly what it should say.
|
|
225
|
+
const absent = (v) => v === undefined || v === null
|
|
226
|
+
|
|
227
|
+
const isObject = (v) => v != null && typeof v === 'object' && !Array.isArray(v)
|
|
228
|
+
const nonEmptyString = (v) => typeof v === 'string' && v.trim().length > 0
|
|
229
|
+
// An observation field is a non-empty object; `reason` and `tagDisposition` are strings, handled
|
|
230
|
+
// on their own so the message names what each one is.
|
|
231
|
+
const nonEmptyObject = (v) => isObject(v) && Object.keys(v).length > 0
|
|
232
|
+
|
|
233
|
+
// EVIDENCE A DURABLE RECORD MAY CARRY AS PROSE OR AS A STRUCTURE (#604 fix round). The bundle is a
|
|
234
|
+
// record a human reads months later, and a Release URL and a verification statement are naturally
|
|
235
|
+
// STRINGS — so requiring an object rejected the spelling a human writes, and said
|
|
236
|
+
// `e:missing-release-ref` about a field that was right there. Which spelling is right depends on
|
|
237
|
+
// one question, and only one: does anything CROSS-CHECK a field inside it?
|
|
238
|
+
// - `releaseRef` / `verification` — nothing does. `{ ci: "green" }` is no more checkable than
|
|
239
|
+
// "CI green", so the object requirement bought no soundness and only cost a spelling. Both.
|
|
240
|
+
// - `candidateReceipt` — its `sourceSha` IS cross-checked (`e:receipt-sha-mismatch`), and a
|
|
241
|
+
// string receipt would slip past that guard silently. Object only, and the four fields beside
|
|
242
|
+
// it stay object-only with it rather than splitting the deployment set on a technicality.
|
|
243
|
+
// - `releaseTag` — a NAME that is compared against the attempt's version. String only.
|
|
244
|
+
const proseOrStructure = (v) => nonEmptyString(v) || nonEmptyObject(v)
|
|
245
|
+
const PROSE_OR_STRUCTURE_FIELDS = new Set(['releaseRef', 'verification'])
|
|
246
|
+
|
|
247
|
+
// A SHA the bundle can bind to: git's default abbreviation floor, same as candidate-identity.
|
|
248
|
+
const isSha = (s) => typeof s === 'string' && /^[0-9a-f]{7,40}$/i.test(s)
|
|
249
|
+
|
|
250
|
+
// ---- validation ----
|
|
251
|
+
//
|
|
252
|
+
// Returns { valid, failed: [id...] }. FAIL CLOSED: any missing, malformed, or fabricated field
|
|
253
|
+
// appends an id and flips `valid` false. The ids are kebab e:* strings so a caller can branch on
|
|
254
|
+
// which invariant broke, the same shape candidate-identity.mjs uses.
|
|
255
|
+
|
|
256
|
+
const DURABLE_RECEIPT_SCHEMA = 'resolve-release/durable-processing-receipt@1'
|
|
257
|
+
const DURABLE_PAUSE_ORDER = ['database-admission', 'worker-scheduling']
|
|
258
|
+
const DURABLE_RESUME_ORDER = [...DURABLE_PAUSE_ORDER].reverse()
|
|
259
|
+
|
|
260
|
+
function exactList(actual, expected) {
|
|
261
|
+
return Array.isArray(actual)
|
|
262
|
+
&& actual.length === expected.length
|
|
263
|
+
&& actual.every((value, index) => value === expected[index])
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function validDurableReceipt(receipt) {
|
|
267
|
+
return nonEmptyObject(receipt)
|
|
268
|
+
&& receipt.schema === DURABLE_RECEIPT_SCHEMA
|
|
269
|
+
&& nonEmptyObject(receipt.writer)
|
|
270
|
+
&& nonEmptyString(receipt.writer.id)
|
|
271
|
+
&& nonEmptyString(receipt.writtenAt)
|
|
272
|
+
&& Number.isFinite(Date.parse(receipt.writtenAt))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function validateDurableProcessing(value, add) {
|
|
276
|
+
if (!isObject(value) || typeof value.present !== 'boolean') {
|
|
277
|
+
add('e:unusable-durable-processing')
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
if (value.present === false) {
|
|
281
|
+
if (Object.keys(value).some((field) => field !== 'present')) {
|
|
282
|
+
add('e:stateless-carries-durable-processing-receipt')
|
|
283
|
+
}
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
if (!validDurableReceipt(value.preflight)) {
|
|
287
|
+
add('e:durable-processing-preflight-missing')
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
if (!exactList(value.preflight.pauseOrder, DURABLE_PAUSE_ORDER)) {
|
|
291
|
+
add('e:durable-processing-pause-order-invalid')
|
|
292
|
+
}
|
|
293
|
+
if (!exactList(value.preflight.resumeOrder, DURABLE_RESUME_ORDER)) {
|
|
294
|
+
add('e:durable-processing-preflight-resume-order-invalid')
|
|
295
|
+
}
|
|
296
|
+
if (!Number.isSafeInteger(value.preflight.quiescence?.timeoutMs)
|
|
297
|
+
|| value.preflight.quiescence.timeoutMs <= 0
|
|
298
|
+
|| value.preflight.evidenceSchema !== DURABLE_RECEIPT_SCHEMA) {
|
|
299
|
+
add('e:durable-processing-preflight-contract-incomplete')
|
|
300
|
+
}
|
|
301
|
+
const hasUp = !absent(value.up)
|
|
302
|
+
const hasUnwind = !absent(value.unwind)
|
|
303
|
+
if (hasUp === hasUnwind) {
|
|
304
|
+
add('e:durable-processing-sequence-ambiguous')
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
const sequence = hasUp ? value.up : value.unwind
|
|
308
|
+
if (!validDurableReceipt(value.down) || !validDurableReceipt(sequence)) {
|
|
309
|
+
add('e:durable-processing-sequence-missing')
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
if (!Number.isSafeInteger(value.down.proof?.generation) || !nonEmptyString(value.down.proof?.fence)) {
|
|
313
|
+
add('e:durable-processing-fence-missing')
|
|
314
|
+
}
|
|
315
|
+
if (hasUp) {
|
|
316
|
+
if (['blocked', 'refused'].includes(value.down.state)) {
|
|
317
|
+
add('e:blocked-down-requires-unwind')
|
|
318
|
+
}
|
|
319
|
+
if (value.down.state !== 'ready-to-shift' || value.down.proof?.observed?.blockers !== 0) {
|
|
320
|
+
add('e:durable-processing-not-quiescent')
|
|
321
|
+
}
|
|
322
|
+
if (!nonEmptyObject(value.down.shiftFence)) add('e:durable-processing-shift-fence-missing')
|
|
323
|
+
if (sequence.state !== 'restored') add('e:durable-processing-restoration-unproven')
|
|
324
|
+
const traffic = sequence.trafficProof
|
|
325
|
+
const identitiesDeclared = nonEmptyString(sequence.incumbentRevision)
|
|
326
|
+
&& nonEmptyString(sequence.candidateRevision)
|
|
327
|
+
&& sequence.incumbentRevision !== sequence.candidateRevision
|
|
328
|
+
const candidateRestored = identitiesDeclared
|
|
329
|
+
&& sequence.servingRevisionAtResume === sequence.candidateRevision
|
|
330
|
+
&& isObject(traffic)
|
|
331
|
+
&& traffic.incumbentRevision === sequence.incumbentRevision
|
|
332
|
+
&& traffic.candidateRevision === sequence.candidateRevision
|
|
333
|
+
&& traffic.candidatePercent === 100
|
|
334
|
+
&& traffic.incumbentPercent === 0
|
|
335
|
+
const incumbentRestored = identitiesDeclared
|
|
336
|
+
&& sequence.servingRevisionAtResume === sequence.incumbentRevision
|
|
337
|
+
&& isObject(traffic)
|
|
338
|
+
&& traffic.incumbentRevision === sequence.incumbentRevision
|
|
339
|
+
&& traffic.candidateRevision === sequence.candidateRevision
|
|
340
|
+
&& traffic.incumbentPercent === 100
|
|
341
|
+
&& traffic.candidatePercent === 0
|
|
342
|
+
if (!candidateRestored && !incumbentRestored) {
|
|
343
|
+
add('e:post-shift-traffic-proof-invalid')
|
|
344
|
+
}
|
|
345
|
+
if (nonEmptyObject(value.down.shiftFence)
|
|
346
|
+
&& (value.down.shiftFence.generation !== value.down.proof?.generation
|
|
347
|
+
|| value.down.shiftFence.fence !== value.down.proof?.fence)) {
|
|
348
|
+
add('e:durable-processing-shift-fence-mismatch')
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
if (!['blocked', 'refused'].includes(value.down.state)) add('e:unwind-without-pre-shift-failure')
|
|
352
|
+
if (!absent(value.down.shiftFence)) add('e:blocked-down-carries-shift-fence')
|
|
353
|
+
if (sequence.state !== 'unwound') add('e:blocked-down-requires-unwind')
|
|
354
|
+
const traffic = sequence.trafficProof
|
|
355
|
+
if (!isObject(traffic) || traffic.incumbentPercent !== 100 || traffic.candidatePercent !== 0
|
|
356
|
+
|| traffic.incumbentRevision !== sequence.servingRevisionAtResume) {
|
|
357
|
+
add('e:pre-shift-traffic-proof-invalid')
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (!exactList(sequence.resumeOrder, DURABLE_RESUME_ORDER)) add('e:durable-processing-resume-order-invalid')
|
|
361
|
+
if (!nonEmptyString(sequence.servingRevisionAtResume)) add('e:durable-processing-serving-revision-missing')
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function validateBundle(bundle) {
|
|
365
|
+
const failed = []
|
|
366
|
+
const add = (id) => { if (!failed.includes(id)) failed.push(id) }
|
|
367
|
+
|
|
368
|
+
if (!isObject(bundle)) return { valid: false, failed: ['e:not-an-object'] }
|
|
369
|
+
for (const field of Object.keys(bundle)) {
|
|
370
|
+
if (!BUNDLE_FIELDS.includes(field)) add(`e:unknown-field-${kebab(field)}`)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!absent(bundle.durableProcessing)) {
|
|
374
|
+
validateDurableProcessing(bundle.durableProcessing, add)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
// The key. `attemptId ?? rcTag` (bundleKey) so a pre-#599 bundle — which carries only the rc tag,
|
|
379
|
+
// and whose attempt id was that tag — stays keyed, valid and locatable.
|
|
380
|
+
//
|
|
381
|
+
// VALIDATE AND EMIT MUST AGREE. The key has to be WRITABLE, not merely present: without this
|
|
382
|
+
// second check `validate` exits 0 on a bundle `emit` then refuses with an unsafe-key error, and
|
|
383
|
+
// the operator is told a durable record is good by the only command that can tell them so before
|
|
384
|
+
// the write. Same predicate as `bundleFilename`, applied at the same point in the contract.
|
|
385
|
+
const key = bundleKey(bundle)
|
|
386
|
+
if (!nonEmptyString(key)) add('e:missing-attempt-id')
|
|
387
|
+
else if (!SAFE_KEY.test(key)) add('e:unsafe-attempt-id')
|
|
388
|
+
|
|
389
|
+
// `rcTag` is now an ordinary recorded field, but its ABSENCE must be declared, never implied.
|
|
390
|
+
// Dropping the old `e:missing-rc-tag` outright would be a guard that silently widens: an rc-shaped
|
|
391
|
+
// release could then emit a durable record that forgot the tag it shipped under, and nothing
|
|
392
|
+
// downstream re-asks. So: present -> it must be usable; absent -> `deployTrigger` must name one of
|
|
393
|
+
// the three shapes that mint no rc.
|
|
394
|
+
if (!absent(bundle.deployTrigger) && !DEPLOY_TRIGGER_SHAPES.includes(bundle.deployTrigger)) {
|
|
395
|
+
add('e:unknown-deploy-trigger')
|
|
396
|
+
}
|
|
397
|
+
// ...and the converse, which is the same rule read from the other side: a bundle DECLARING a
|
|
398
|
+
// shape that mints no rc, while carrying one, is a record that contradicts itself. One of the
|
|
399
|
+
// two statements is false and nothing downstream can tell which, so it is refused in the same
|
|
400
|
+
// shape as the other declaration failures rather than accepted as harmless extra evidence. An
|
|
401
|
+
// UNDECLARED shape carrying a tag stays valid — that is every pre-#599 bundle.
|
|
402
|
+
// `rcTag: null` is `rcTag` ABSENT, not a present-but-unusable tag (#604) — see `absent` above.
|
|
403
|
+
if (!absent(bundle.rcTag)) {
|
|
404
|
+
if (!nonEmptyString(bundle.rcTag)) add('e:unusable-rc-tag')
|
|
405
|
+
else if (NO_RC_TRIGGER_SHAPES.includes(bundle.deployTrigger)) add('e:rc-tag-on-no-rc-shape')
|
|
406
|
+
} else if (!NO_RC_TRIGGER_SHAPES.includes(bundle.deployTrigger)) {
|
|
407
|
+
add('e:undeclared-missing-rc-tag')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!isSha(bundle.sha)) add(nonEmptyString(bundle.sha) ? 'e:unusable-sha' : 'e:missing-sha')
|
|
411
|
+
|
|
412
|
+
// buildIdentity is an object; if it carries a sha it must be THIS release's — a build record for
|
|
413
|
+
// another commit bound into this bundle is the same class of error as a stale candidate receipt.
|
|
414
|
+
// Same missing/unusable split as the required loop below: it stays object-only (its `sha` is
|
|
415
|
+
// cross-checked, exactly like the candidate receipt's), but a PRESENT one is never called missing.
|
|
416
|
+
if (absent(bundle.buildIdentity)) add('e:missing-build-identity')
|
|
417
|
+
else if (!nonEmptyObject(bundle.buildIdentity)) add('e:unusable-build-identity')
|
|
418
|
+
else if (!absent(bundle.buildIdentity.sha) && !(isSha(bundle.sha) && shaMatches(bundle.buildIdentity.sha, bundle.sha))) {
|
|
419
|
+
add('e:build-identity-sha-mismatch')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const state = bundle.terminalState
|
|
423
|
+
if (!BUNDLE_TERMINAL_STATES.includes(state)) {
|
|
424
|
+
// Without a known state there is no required set to check — the state IS the router.
|
|
425
|
+
add('e:unknown-terminal-state')
|
|
426
|
+
return { valid: false, failed }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// The four lease-closing states each have exactly ONE accepted close cause (#432's map, reused).
|
|
430
|
+
// blocked-manual holds the lease and carries none — a closedBy on it is a mis-recorded close.
|
|
431
|
+
const expectedCause = ATTEMPT_CLOSE_CAUSES[state]
|
|
432
|
+
if (expectedCause) {
|
|
433
|
+
if (bundle.closedBy !== expectedCause) add('e:close-cause-mismatch')
|
|
434
|
+
} else if (!absent(bundle.closedBy)) {
|
|
435
|
+
// ...and `closedBy: null` on the lease-HOLDING state is a key that is not there (#604), not a
|
|
436
|
+
// mis-recorded close. The four closing states are untouched: a null there still fails the
|
|
437
|
+
// exact-cause check above, because null is not the cause they owe.
|
|
438
|
+
add('e:blocked-manual-carries-close-cause')
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// The required set is (state × shape) — `requiredFor`, never either table directly (#604).
|
|
442
|
+
const failurePhase = classifyFailurePhase(bundle)
|
|
443
|
+
const required = requiredFor(state, bundle.deployTrigger, failurePhase)
|
|
444
|
+
|
|
445
|
+
// A pre-candidate failure has no candidate evidence by definition. Accepting any of these
|
|
446
|
+
// fields would turn a pre-candidate bundle into a contradictory record that a later reader
|
|
447
|
+
// cannot reconcile with its stamped phase.
|
|
448
|
+
if (failurePhase === 'pre-candidate' && bundle.deployTrigger !== 'no-deploy') {
|
|
449
|
+
for (const field of ['candidateReceipt', 'candidateVerdict', 'approval', 'trafficTransition', 'liveVerdict']) {
|
|
450
|
+
if (!absent(bundle[field])) add(`e:${kebab(field)}-on-pre-candidate`)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// MISSING AND UNUSABLE ARE DIFFERENT FAILURES, AND THE ID MUST SAY WHICH (#604 fix round).
|
|
455
|
+
// `e:missing-release-ref` on a key that is right there is a message asserting one thing while the
|
|
456
|
+
// check tests another — it sends the next reader to look for a field they already wrote. Absent
|
|
457
|
+
// (or null, which is absence here) -> `e:missing-*`; present but carrying nothing this schema can
|
|
458
|
+
// use -> `e:unusable-*`. It is the split `sha` and `rcTag` already made, applied to every field
|
|
459
|
+
// the required set names. The VERDICT is unchanged in every case: both refuse.
|
|
460
|
+
for (const field of required) {
|
|
461
|
+
const value = bundle[field]
|
|
462
|
+
if (field === 'tagDisposition') {
|
|
463
|
+
// Its id already names the shape rather than claiming absence, so it keeps its own message.
|
|
464
|
+
if (!ATTEMPT_TAG_DISPOSITIONS.includes(bundle.tagDisposition)) add('e:bad-tag-disposition')
|
|
465
|
+
continue
|
|
466
|
+
}
|
|
467
|
+
if (absent(value)) {
|
|
468
|
+
add(`e:missing-${kebab(field)}`)
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
// `reason` and `releaseTag` are strings; the two prose-or-structure fields take either; every
|
|
472
|
+
// remaining required field is a structured observation (see `proseOrStructure` above for why
|
|
473
|
+
// the deployment five must stay that way).
|
|
474
|
+
const usable = field === 'reason' || field === 'releaseTag'
|
|
475
|
+
? nonEmptyString(value)
|
|
476
|
+
: PROSE_OR_STRUCTURE_FIELDS.has(field)
|
|
477
|
+
? proseOrStructure(value)
|
|
478
|
+
: nonEmptyObject(value)
|
|
479
|
+
if (!usable) add(`e:unusable-${kebab(field)}`)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// The final tag IS the version claim on a no-deploy shape — the only one there is, since nothing
|
|
483
|
+
// was deployed. So bind it to the attempt whose record this is, the same cross-artifact rule the
|
|
484
|
+
// receipt and buildIdentity get bound by: a bundle whose durable record pairs one attempt's id
|
|
485
|
+
// with another release's tag contradicts itself, and nothing downstream can tell which half is
|
|
486
|
+
// true. Compared on the version alone (`v0.5.0-attempt.1` -> `v0.5.0`) and with a leading `v`
|
|
487
|
+
// normalised off BOTH sides, because a repo may spell its version `0.5.0` while its tag is
|
|
488
|
+
// `v0.5.0` — the check is for a DIFFERENT release, not for a different spelling.
|
|
489
|
+
if (required.includes('releaseTag') && nonEmptyString(bundle.releaseTag) && nonEmptyString(key)) {
|
|
490
|
+
if (versionKey(bundle.releaseTag) !== versionKey(attemptVersion(key))) add('e:release-tag-version-mismatch')
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// The candidate receipt is the release.candidateIdentity tuple; where a state requires it, bind
|
|
494
|
+
// it to this release's SHA so a receipt copied from an earlier rc refuses (the cross-artifact
|
|
495
|
+
// rule candidate-identity enforces at every hand-off, applied to the durable record).
|
|
496
|
+
if (required.includes('candidateReceipt') && nonEmptyObject(bundle.candidateReceipt)) {
|
|
497
|
+
if (!isSha(bundle.sha) || !shaMatches(bundle.candidateReceipt.sourceSha, bundle.sha)) add('e:receipt-sha-mismatch')
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// A state that never shipped must not carry exposure evidence — fabricated, not merely spare.
|
|
501
|
+
// The id names the state so failed-pre-promotion and revoked are distinguishable in `failed`.
|
|
502
|
+
// `null` is an absent field here too (#604): writing `"liveVerdict": null` to mean "there was
|
|
503
|
+
// none" is not a claim, and refusing it taught generators to omit keys they had no way to know
|
|
504
|
+
// were forbidden.
|
|
505
|
+
for (const field of EXPOSURE_FORBIDDEN[state] ?? []) {
|
|
506
|
+
if (!absent(bundle[field])) add(`e:exposure-claimed-${state}`)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ...and the same rule read per SHAPE rather than per state (#604). On `no-deploy` there is no
|
|
510
|
+
// candidate, no exposure, no traffic and no live service, so ANY of the five is an observation
|
|
511
|
+
// nobody took — at every terminal state, `blocked-manual` included. The id names the field and
|
|
512
|
+
// the shape, matching `e:rc-tag-on-no-rc-shape`, so a generator is told which key to drop.
|
|
513
|
+
if (bundle.deployTrigger === 'no-deploy') {
|
|
514
|
+
for (const field of DEPLOY_EVIDENCE_FIELDS) {
|
|
515
|
+
if (!absent(bundle[field])) add(`e:${kebab(field)}-on-no-deploy`)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return { valid: failed.length === 0, failed }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const kebab = (s) => s.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase())
|
|
523
|
+
|
|
524
|
+
// The version an attempt id was minted for: `v0.5.0-attempt.1` -> `v0.5.0`. A key with no ordinal
|
|
525
|
+
// (a pre-#599 rc-tag key, or an id composed by hand before the convention) is returned unchanged —
|
|
526
|
+
// this is a comparison helper, not a validator, and the key's own legality is checked above.
|
|
527
|
+
const attemptVersion = (key) => (typeof key === 'string' ? key.replace(ATTEMPT_ORDINAL, '') : key)
|
|
528
|
+
// Plain, v-prefixed, and npm package-scoped tags bind to the same semantic version.
|
|
529
|
+
//
|
|
530
|
+
// Exported because the @1aboveio/skills release gate binds a package-scoped tag to package semver
|
|
531
|
+
// with the same split (#998). It is a COMPARISON helper, not a validator: it normalises a leading
|
|
532
|
+
// `v` away and returns an unparseable input unchanged, so a caller that needs "this tag is legal"
|
|
533
|
+
// must add its own strictness on top rather than read a non-throw as a pass.
|
|
534
|
+
export const versionKey = (v) => {
|
|
535
|
+
if (typeof v !== 'string') return v
|
|
536
|
+
const trimmed = v.trim()
|
|
537
|
+
const packageSeparator = trimmed.lastIndexOf('@')
|
|
538
|
+
const version = packageSeparator > 0 ? trimmed.slice(packageSeparator + 1) : trimmed
|
|
539
|
+
return version.replace(/^v/, '')
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ---- filename keyed by (attempt id, terminal state) ----
|
|
543
|
+
//
|
|
544
|
+
// The key is (attempt id, terminal state), NOT the id alone. WHY THE STATE IS IN THE KEY: one
|
|
545
|
+
// attempt can legitimately reach two terminal states IN SEQUENCE, because #432 makes
|
|
546
|
+
// `blocked-manual` a lease-HOLDING state that clears on the SAME attempt via operator recovery, and
|
|
547
|
+
// the attempt id (like the rc tag it may carry) is immutable throughout (principle 10 — no new rc is
|
|
548
|
+
// minted for a recovery). So an attempt can park at `blocked-manual` (owed its own bundle — AC2
|
|
549
|
+
// lists it), then recover and close `released`; an id-only key would make the `released` emit
|
|
550
|
+
// collide with the stale `blocked-manual` bundle and be refused, losing the attempt's actual final
|
|
551
|
+
// outcome. Keying by (id, state) lets both durable records coexist while immutability still holds
|
|
552
|
+
// per (id, state).
|
|
553
|
+
//
|
|
554
|
+
// Attempt ids are tag-shaped (`v0.5.0-attempt.2`, or on a pre-#599 bundle the rc tag `v1.4.0-rc.2`
|
|
555
|
+
// itself — single dashes) and terminal states are single-kebab like `blocked-manual`, so the `--`
|
|
556
|
+
// separator cannot collide with either half: `v1.4.0-rc.2--released` and `v1.4.0-rc.2--blocked-manual` are two
|
|
557
|
+
// distinct, unambiguous filenames. The job here is to REFUSE anything unsafe (a traversal-shaped or
|
|
558
|
+
// empty id, or a state outside #432's enum) rather than silently sanitizing it into a different key.
|
|
559
|
+
// Imported from run-state.mjs, never re-declared: the manifest refuses an unkeyable attempt id at
|
|
560
|
+
// the WRITE and this module refuses it at the emit, and two regexes for one rule is how those two
|
|
561
|
+
// refusals drift into disagreeing about which attempts have a durable record.
|
|
562
|
+
const SAFE_KEY = ATTEMPT_ID_KEY_SAFE
|
|
563
|
+
|
|
564
|
+
// ---- minting the attempt id: unique per ATTEMPT, not per version ----
|
|
565
|
+
//
|
|
566
|
+
// WHY A CONVENTION AT ALL (#599 fix round 2). Keying on the attempt id only helps if the id is
|
|
567
|
+
// unique per attempt, and step 3's first spelling — "the version being cut" — is not: doctrine
|
|
568
|
+
// deliberately re-cuts the SAME version on a retry, and both failure tiers reuse the terminal
|
|
569
|
+
// state. So attempt 1 of v2.0.0 wrote `v2.0.0--failed-pre-promotion.json`, its retry failed the
|
|
570
|
+
// same way, and the immutability guarantee (working exactly as designed) refused the second emit —
|
|
571
|
+
// the second failure's durable record lost, which is the one thing this artifact exists to
|
|
572
|
+
// prevent. Pre-#599 the key was the rc tag, which carries its own per-attempt ordinal, so the
|
|
573
|
+
// change to an attempt id is what introduced the collision.
|
|
574
|
+
//
|
|
575
|
+
// THE CONVENTION IS `<version>-attempt.<n>`, ON ALL FOUR SHAPES. It is deliberately NOT
|
|
576
|
+
// shape-dependent, and in particular the rc shape does NOT reuse its rc tag: decision 1 opens the
|
|
577
|
+
// lease at the promotion AUTHORIZATION, which is before the mint, so at the moment the id is
|
|
578
|
+
// written no rc tag exists yet on any shape. It mirrors the rc tag's own `-rc.<n>` ordinal so the
|
|
579
|
+
// two read alike, and it is SAFE_KEY-legal by construction rather than by the author remembering.
|
|
580
|
+
//
|
|
581
|
+
// AND THE ORDINAL IS READ, NOT REMEMBERED. `nextAttemptId` derives it from the bundles already in
|
|
582
|
+
// the durable evidence dir — the same "look at what exists, mint the next" move `version.mjs rc`
|
|
583
|
+
// makes for rc tags. A caller composing the id by hand still gets the legality check (below) and
|
|
584
|
+
// run-state's spent-id refusal, but the mint is the path doctrine points at.
|
|
585
|
+
//
|
|
586
|
+
// WHAT THAT DOES AND DOES NOT BUY — stated exactly, because an overclaim here is what let two
|
|
587
|
+
// review rounds each close one route into this class and miss the next. Reading the dir makes the
|
|
588
|
+
// mint free of collision with every attempt that has CLOSED, and only that. It is not a
|
|
589
|
+
// self-sufficient uniqueness guarantee, and the two things it does not cover are covered elsewhere
|
|
590
|
+
// or deliberately deferred:
|
|
591
|
+
// - an attempt still IN FLIGHT has emitted no bundle and does not move the ordinal. Closed by
|
|
592
|
+
// the Release Attempt Lease precondition — step 3 mints only on `free`, and an in-flight
|
|
593
|
+
// attempt reads `held` while a reaped manifest reads `unknown`. Neither authorizes a mint.
|
|
594
|
+
// - two sessions minting concurrently, before either emits. Deferred to #509 with the rest of
|
|
595
|
+
// lease atomicity (the take is a check-then-act read, advisory by design).
|
|
596
|
+
// And it holds at all only if the read actually happened, which is why `nextAttemptId` rethrows
|
|
597
|
+
// every readdir failure but ENOENT instead of treating an unreadable dir as an empty one.
|
|
598
|
+
//
|
|
599
|
+
// Pre-#599 bundles are a DIFFERENT namespace (`v1.4.0-rc.2--released.json`) and cannot collide
|
|
600
|
+
// with `<version>-attempt.<n>`, so they neither shift the ordinal nor are shifted by it.
|
|
601
|
+
const ATTEMPT_ORDINAL = /-attempt\.(\d+)$/
|
|
602
|
+
|
|
603
|
+
export function attemptIdFor(version, ordinal) {
|
|
604
|
+
if (!nonEmptyString(version)) throw new Error('attemptIdFor: a version is required — the attempt id is <version>-attempt.<n>')
|
|
605
|
+
if (ATTEMPT_ORDINAL.test(version)) {
|
|
606
|
+
throw new Error(`attemptIdFor: ${JSON.stringify(version)} already carries an attempt ordinal — pass the version being cut (v2.0.0), not an attempt id`)
|
|
607
|
+
}
|
|
608
|
+
if (!Number.isInteger(ordinal) || ordinal < 1) {
|
|
609
|
+
throw new Error(`attemptIdFor: the ordinal must be a positive integer (attempts count from 1); got ${JSON.stringify(ordinal)}`)
|
|
610
|
+
}
|
|
611
|
+
const id = `${version}-attempt.${ordinal}`
|
|
612
|
+
if (!SAFE_KEY.test(id)) {
|
|
613
|
+
throw new Error(`attemptIdFor: version ${JSON.stringify(version)} yields the unsafe attempt id ${JSON.stringify(id)} — the id keys a file, so it must be a plain name (no slashes, no traversal, no "#")`)
|
|
614
|
+
}
|
|
615
|
+
return id
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// The next free ordinal for this version, read off the durable evidence dir.
|
|
619
|
+
//
|
|
620
|
+
// A MISSING dir (ENOENT) means no attempt has emitted yet, which is ordinal 1 — the one case where
|
|
621
|
+
// absence really is emptiness, because the dir is created by the first emit. An UNREADABLE dir is
|
|
622
|
+
// NOT that case and never was: EACCES, ENOTDIR (a `--dir` typo pointing at a file), ELOOP, EMFILE
|
|
623
|
+
// are a dir that could not be read, and answering "no attempts yet" off one hands back an ordinal
|
|
624
|
+
// that may already be SPENT — two attempts under one key, which is the whole thing the ordinal
|
|
625
|
+
// exists to prevent. It is the same "reading absence as freedom" the lease refuses one file away
|
|
626
|
+
// (`run-state.mjs` attemptLeaseStatus answers `unknown`, NOT `free`, when there is no manifest).
|
|
627
|
+
// So: rethrow anything that is not ENOENT, and let the caller fail closed.
|
|
628
|
+
//
|
|
629
|
+
// What this does and does not guarantee, stated precisely because a claim of more was itself the
|
|
630
|
+
// bug: the ordinal is read off bundles, and a bundle exists only after a TERMINAL exit. So this
|
|
631
|
+
// mint is free of collision with every CLOSED attempt, by construction. An attempt still in flight
|
|
632
|
+
// has emitted nothing and does not move the ordinal — that route is closed by the Release Attempt
|
|
633
|
+
// Lease precondition (step 3 mints only on `free`; an in-flight attempt reads `held` and a reaped
|
|
634
|
+
// manifest reads `unknown`, and neither authorizes a mint), not by this function. Two sessions
|
|
635
|
+
// minting concurrently before either emits is the remaining route, deferred to #509 with the rest
|
|
636
|
+
// of lease atomicity.
|
|
637
|
+
export function nextAttemptId(version, dir) {
|
|
638
|
+
if (!nonEmptyString(version)) throw new Error('nextAttemptId: a version is required')
|
|
639
|
+
if (ATTEMPT_ORDINAL.test(version)) {
|
|
640
|
+
throw new Error(`nextAttemptId: ${JSON.stringify(version)} already carries an attempt ordinal — pass the version being cut (v2.0.0), not an attempt id`)
|
|
641
|
+
}
|
|
642
|
+
let entries
|
|
643
|
+
try {
|
|
644
|
+
entries = fs.readdirSync(dir)
|
|
645
|
+
} catch (e) {
|
|
646
|
+
if (e.code !== 'ENOENT') throw e
|
|
647
|
+
entries = []
|
|
648
|
+
}
|
|
649
|
+
let highest = 0
|
|
650
|
+
for (const entry of entries) {
|
|
651
|
+
if (!entry.endsWith('.json')) continue
|
|
652
|
+
const [key] = entry.slice(0, -'.json'.length).split('--')
|
|
653
|
+
if (!key.startsWith(`${version}-attempt.`)) continue
|
|
654
|
+
const m = ATTEMPT_ORDINAL.exec(key)
|
|
655
|
+
if (m) highest = Math.max(highest, Number(m[1]))
|
|
656
|
+
}
|
|
657
|
+
return attemptIdFor(version, highest + 1)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// The bundle's key. `rcTag` is the pre-#599 fallback and is exactly right for the bundles that used
|
|
661
|
+
// it: they WERE keyed by their rc tag, so an existing `v1.2.0-rc.1--released.json` keeps its
|
|
662
|
+
// filename and stays locatable rather than being re-keyed by a schema change.
|
|
663
|
+
export function bundleKey(bundle) {
|
|
664
|
+
return isObject(bundle) ? (bundle.attemptId ?? bundle.rcTag) : undefined
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function bundleFilename(attemptId, terminalState) {
|
|
668
|
+
if (!nonEmptyString(attemptId)) throw new Error('bundleFilename: an attempt id is required (the bundle is keyed by it; a pre-#599 bundle is keyed by its rc tag)')
|
|
669
|
+
if (!SAFE_KEY.test(attemptId)) {
|
|
670
|
+
throw new Error(`bundleFilename: unsafe attempt id ${JSON.stringify(attemptId)} — it keys a file, so it must be a plain name (no slashes, no traversal)`)
|
|
671
|
+
}
|
|
672
|
+
if (!BUNDLE_TERMINAL_STATES.includes(terminalState)) {
|
|
673
|
+
throw new Error(`bundleFilename: a terminal state is required and must be one of ${BUNDLE_TERMINAL_STATES.join(' | ')} — the bundle is keyed by (attempt id, terminal state) so one attempt's two terminal exits (e.g. a blocked-manual hold and its later released close) do not collide; got ${JSON.stringify(terminalState)}`)
|
|
674
|
+
}
|
|
675
|
+
return `${attemptId}--${terminalState}.json`
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// The evidence content, minus the write-stamp — what "same bundle" means for immutability. Keys are
|
|
679
|
+
// recursively sorted so the comparison is stable across property order (a stored bundle read back
|
|
680
|
+
// from disk vs an in-memory one can differ only in key order for identical evidence). A recursive
|
|
681
|
+
// canonicalizer, NOT JSON.stringify's replacer-array — that filters NESTED keys too, which would
|
|
682
|
+
// silently drop the content of `disposition`, `candidateVerdict`, etc. from the comparison and let
|
|
683
|
+
// a different-content re-emit read as identical.
|
|
684
|
+
function canonical(v) {
|
|
685
|
+
if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`
|
|
686
|
+
if (v != null && typeof v === 'object') {
|
|
687
|
+
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(',')}}`
|
|
688
|
+
}
|
|
689
|
+
return JSON.stringify(v)
|
|
690
|
+
}
|
|
691
|
+
function evidenceCanonical(bundle) {
|
|
692
|
+
const { emittedAt, ...evidence } = bundle
|
|
693
|
+
return canonical(evidence)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ---- emit: immutable once written ----
|
|
697
|
+
//
|
|
698
|
+
// Validates, then writes `<dir>/<attemptId>--<terminalState>.json`. If a bundle already exists for
|
|
699
|
+
// this (attempt id, terminal state):
|
|
700
|
+
// - evidence-identical (differs only in emittedAt) -> resume-safe NO-OP, the first write stands;
|
|
701
|
+
// - different content -> REFUSE (AC5, the bundle is immutable).
|
|
702
|
+
// A DIFFERENT terminal state for the same attempt is a different file (a recovery stays on the same
|
|
703
|
+
// attempt and the same rc — principle 10), so it is a new, legal record rather than a collision.
|
|
704
|
+
// Returns { path, noop, emittedAt }.
|
|
705
|
+
// The close-time classification stamp (#962). A failure bundle leaves the lane with the
|
|
706
|
+
// NORMALIZED failure owner and phase recorded on it — #961's taxonomy, computed at the moment
|
|
707
|
+
// the record is written, so the durable attempt carries the classification rather than every
|
|
708
|
+
// later reader re-deriving it (the version post-mortem and the rc circuit breaker both read it
|
|
709
|
+
// back verbatim; a derived value here is exactly what `classifyFailureOwner`'s rule 1 calls the
|
|
710
|
+
// declared extension point). A bundle whose author already declared `failureOwner` (top-level
|
|
711
|
+
// or in the disposition) keeps the declaration — whoever recorded the failure closest to it
|
|
712
|
+
// knows more than the heuristic — and a non-failure terminal state gets neither field, because
|
|
713
|
+
// there is no failure to own. The stamp is a pure function of the bundle, so a resumed
|
|
714
|
+
// re-emit recomputes the identical record and stays the resume-safe no-op.
|
|
715
|
+
export function stampFailureClassification(bundle) {
|
|
716
|
+
if (!isObject(bundle)) return bundle
|
|
717
|
+
const failure = classifyFailureOwner(bundle)
|
|
718
|
+
if (!failure) return bundle
|
|
719
|
+
const out = { ...bundle }
|
|
720
|
+
const declared = nonEmptyString(out.failureOwner)
|
|
721
|
+
|| (isObject(out.disposition) && nonEmptyString(out.disposition.failureOwner))
|
|
722
|
+
if (!declared) out.failureOwner = failure.owner
|
|
723
|
+
if (!nonEmptyString(out.failurePhase)) {
|
|
724
|
+
const phase = classifyFailurePhase(bundle)
|
|
725
|
+
if (phase) out.failurePhase = phase
|
|
726
|
+
}
|
|
727
|
+
return out
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export function emitBundle({ bundle, dir, at = new Date().toISOString() }) {
|
|
731
|
+
const classified = stampFailureClassification(bundle)
|
|
732
|
+
const { valid, failed } = validateBundle(classified)
|
|
733
|
+
if (!valid) throw new Error(`evidence-bundle: refusing to emit an invalid bundle — ${failed.join(', ')}`)
|
|
734
|
+
if (!nonEmptyString(dir)) throw new Error('evidence-bundle: emit requires a --dir to write the durable record into')
|
|
735
|
+
|
|
736
|
+
const file = path.join(dir, bundleFilename(bundleKey(classified), classified.terminalState))
|
|
737
|
+
const stamped = { ...classified, emittedAt: at }
|
|
738
|
+
|
|
739
|
+
// The existsSync-then-writeFileSync check is not atomic against a concurrent emitter (TOCTOU).
|
|
740
|
+
// That concurrency-safety half is deferred to #509; here the Release Attempt Lease serializes
|
|
741
|
+
// attempts in practice, so this guard stays advisory-immutability. No behaviour change owed here.
|
|
742
|
+
if (fs.existsSync(file)) {
|
|
743
|
+
let existing
|
|
744
|
+
try {
|
|
745
|
+
existing = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
746
|
+
} catch {
|
|
747
|
+
throw new Error(`evidence-bundle: a bundle already exists at ${file} but is unreadable — refusing to overwrite (the bundle is immutable once written)`)
|
|
748
|
+
}
|
|
749
|
+
if (evidenceCanonical(existing) === evidenceCanonical(stamped)) {
|
|
750
|
+
return { path: file, noop: true, emittedAt: existing.emittedAt ?? null }
|
|
751
|
+
}
|
|
752
|
+
throw new Error(`evidence-bundle: a bundle for attempt "${bundleKey(bundle)}" in terminal state "${bundle.terminalState}" already exists at ${file} with different content — the bundle is immutable once written (principle 10 / #441 AC5); a re-emit for the same (attempt id, state) must be byte-identical evidence. A recovery stays on the SAME attempt and closes at a DIFFERENT terminal state, which is its own bundle — it never overwrites this one.`)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
756
|
+
fs.writeFileSync(file, JSON.stringify(stamped, null, 2) + '\n')
|
|
757
|
+
return { path: file, noop: false, emittedAt: at }
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ---- CLI ----
|
|
761
|
+
|
|
762
|
+
const USAGE = `evidence-bundle — the durable release-evidence bundle, one per release attempt (#441).
|
|
763
|
+
|
|
764
|
+
The run manifest is the working state; this bundle is the DURABLE record — written once at a
|
|
765
|
+
terminal exit, keyed to the immutable ATTEMPT ID and its terminal state (principle 10), immutable
|
|
766
|
+
once written per (attempt id, state). One attempt can reach two terminal states in sequence (a
|
|
767
|
+
blocked-manual hold that recovers on the same attempt and closes released), so each exit is its own
|
|
768
|
+
bundle. A failed attempt is validated as strictly as a successful one; each terminal state owes its
|
|
769
|
+
own evidence. The key is the attempt id, not the rc tag (#599): three of the four declarable trigger
|
|
770
|
+
shapes mint no rc, and a terminal exit on those had no key at all. On the rc shape the attempt id IS
|
|
771
|
+
the rc tag, so bundles written before #599 keep their filenames and stay locatable.
|
|
772
|
+
|
|
773
|
+
The attempt id is <version>-attempt.<n> on EVERY shape — unique per ATTEMPT, not per version,
|
|
774
|
+
because a retry re-cuts the same version and both failure tiers reuse the terminal state, so a
|
|
775
|
+
version-keyed retry would collide with the attempt it is retrying and lose its record. It is NOT
|
|
776
|
+
the rc tag on any shape: the lease opens at the promotion authorization, before the mint, so no rc
|
|
777
|
+
tag exists yet when the id is written. Mint it with \`next-attempt-id\` rather than composing it.
|
|
778
|
+
|
|
779
|
+
Schema (JSON): {
|
|
780
|
+
attemptId, sha, buildIdentity, # always required; buildIdentity.sha, if set, must match sha
|
|
781
|
+
# attemptId must be able to key a file — validate
|
|
782
|
+
# refuses one emit could not write
|
|
783
|
+
# (a pre-#599 bundle keys off rcTag instead)
|
|
784
|
+
rcTag, # recorded, not the key; REQUIRED where an rc exists —
|
|
785
|
+
# an absent one must be declared by deployTrigger below,
|
|
786
|
+
# and a declared no-rc shape must NOT carry one
|
|
787
|
+
deployTrigger, # one of ${DEPLOY_TRIGGER_SHAPES.join(' | ')} (pre-flight item 5);
|
|
788
|
+
# ${NO_RC_TRIGGER_SHAPES.join('/')} mint no rc, so they declare the absence
|
|
789
|
+
terminalState, # one of ${BUNDLE_TERMINAL_STATES.join(' | ')} (#432's enum)
|
|
790
|
+
closedBy, # the state's ONE verified close cause (#432); absent on blocked-manual
|
|
791
|
+
# a deployed failed-pre-promotion bundle with no candidate evidence
|
|
792
|
+
# is a pre-candidate failure: it owes disposition only and must
|
|
793
|
+
# carry no candidateReceipt/candidateVerdict/exposure evidence
|
|
794
|
+
candidateReceipt, # release.candidateIdentity tuple; sourceSha bound to sha
|
|
795
|
+
candidateVerdict, liveVerdict, # principle 5's two observations (0% candidate, post-shift live)
|
|
796
|
+
approval, trafficTransition, disposition, # exposure authorization, the 0->100 shift, the cleanup/rollback/revocation record
|
|
797
|
+
# the five above are DEPLOYMENT evidence: required on the
|
|
798
|
+
# deployed shapes, REFUSED on no-deploy (#604)
|
|
799
|
+
releaseTag, # the no-deploy shape's immutable final tag — the only version
|
|
800
|
+
# claim there is; a STRING, because it is compared against the
|
|
801
|
+
# attempt's version (a leading "v" is normalised off both sides)
|
|
802
|
+
releaseRef, verification, # the rest of the no-deploy shape's evidence: the GitHub Release,
|
|
803
|
+
# and the verification standing in for principle 5's two
|
|
804
|
+
# observations (CI + the full suite on the tagged SHA, and the
|
|
805
|
+
# consumer-facing read). EITHER SPELLING — a plain string
|
|
806
|
+
# ("https://…/releases/tag/v0.5.0", "CI green, 1590/1590") or an
|
|
807
|
+
# object ({"url": "…"}); nothing cross-checks a field inside
|
|
808
|
+
# them, so prose is evidence too
|
|
809
|
+
tagDisposition, # "retained" — required for revoked
|
|
810
|
+
reason, # required for blocked-manual
|
|
811
|
+
emittedAt # stamped by emit; excluded from the immutability comparison
|
|
812
|
+
}
|
|
813
|
+
A field written NULL is a field that is ABSENT — "rcTag": null is no rc tag, not an unusable one, and
|
|
814
|
+
"liveVerdict": null on a state that never shipped is not a claimed observation (#604). Null is never
|
|
815
|
+
a way to satisfy a required field: a required object that is null is missing, and says so.
|
|
816
|
+
|
|
817
|
+
MISSING and UNUSABLE are different failures and the id says which: e:missing-<field> means the key is
|
|
818
|
+
not there (or is null); e:unusable-<field> means it IS there and carries nothing this schema can use.
|
|
819
|
+
A present field is never reported missing. The five deployment fields above stay OBJECTS — the
|
|
820
|
+
candidate receipt's sourceSha is cross-checked, and a prose receipt would slip past that guard.
|
|
821
|
+
|
|
822
|
+
Required-by-state x SHAPE (#604) — the table is a property of both, because "what evidence exists"
|
|
823
|
+
is a property of both. On the three DEPLOYED shapes (${DEPLOY_TRIGGER_SHAPES.filter((s) => s !== 'no-deploy').join(', ')}), and on an
|
|
824
|
+
undeclared shape (every pre-#599 bundle deployed):
|
|
825
|
+
${Object.entries(REQUIRED_BY_STATE).map(([s, f]) => `${s} -> ${f.join(',')}`).join('\n ')}
|
|
826
|
+
On no-deploy — a versioned artifact nobody deploys, so there is no candidate, no exposure, no traffic
|
|
827
|
+
and no live service to observe; the states not listed are shape-independent and take the set above:
|
|
828
|
+
${Object.entries(REQUIRED_BY_STATE_NO_DEPLOY).map(([s, f]) => `${s} -> ${f.join(',')}`).join('\n ')}
|
|
829
|
+
and it must carry NONE of ${DEPLOY_EVIDENCE_FIELDS.join(', ')} — at any
|
|
830
|
+
terminal state. This makes the requirement satisfiable on that shape; it does not make it optional.
|
|
831
|
+
|
|
832
|
+
Usage:
|
|
833
|
+
evidence-bundle validate --bundle <file> 0 valid · 1 invalid (ids in \`failed\`) · 2 usage
|
|
834
|
+
evidence-bundle emit --bundle <file> --dir <d> validate then write immutably to
|
|
835
|
+
<d>/<attemptId>--<terminalState>.json
|
|
836
|
+
0 written or resume-safe no-op · 1 invalid or
|
|
837
|
+
immutability violation · 2 usage
|
|
838
|
+
evidence-bundle next-attempt-id --version <v> --dir <d>
|
|
839
|
+
print the attempt id step 3 opens the lease
|
|
840
|
+
under: <version>-attempt.<n>, with n read
|
|
841
|
+
off the bundles already in <d>. Unique per
|
|
842
|
+
ATTEMPT (a retry re-cuts the same version,
|
|
843
|
+
so the version alone collides) and key-legal
|
|
844
|
+
by construction. Free of collision with every
|
|
845
|
+
CLOSED attempt; an IN-FLIGHT one has emitted
|
|
846
|
+
no bundle, so call this only on a FREE lease
|
|
847
|
+
(concurrent mints: #509). Fails closed on a
|
|
848
|
+
durable dir it cannot read —
|
|
849
|
+
only ENOENT (nothing emitted yet) is
|
|
850
|
+
ordinal 1. 0 minted · 2 usage or unreadable
|
|
851
|
+
dir
|
|
852
|
+
evidence-bundle path --attempt-id <id> --dir <d> [--state <s>]
|
|
853
|
+
with --state, print the one file for that
|
|
854
|
+
(id, state); without it, list every bundle
|
|
855
|
+
written for the attempt (reachable by id
|
|
856
|
+
alone, without the originating session).
|
|
857
|
+
Without --state this REFUSES (2) a dir it
|
|
858
|
+
cannot read rather than reporting it empty;
|
|
859
|
+
an absent dir holds no bundles (0, silent).
|
|
860
|
+
--rc-tag <tag> is the pre-#599 spelling and
|
|
861
|
+
still resolves on the rc shape.
|
|
862
|
+
|
|
863
|
+
Doctrine: resolve-release/SKILL.md step 5 (emitted at every terminal exit) and its Definition of
|
|
864
|
+
done (which reports the bundle location). On the verified path the released bundle is attached to the
|
|
865
|
+
GitHub Release; every attempt's bundles are indexed by attempt id (one per terminal state reached).`
|
|
866
|
+
|
|
867
|
+
function parseArgs(argv) {
|
|
868
|
+
const opts = {}
|
|
869
|
+
const positional = []
|
|
870
|
+
for (let i = 0; i < argv.length; i++) {
|
|
871
|
+
const a = argv[i]
|
|
872
|
+
if (a === '--help' || a === '-h') opts.help = true
|
|
873
|
+
else if (a.startsWith('--')) opts[a.slice(2)] = argv[++i]
|
|
874
|
+
else positional.push(a)
|
|
875
|
+
}
|
|
876
|
+
return { opts, positional }
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function readJson(file, what) {
|
|
880
|
+
if (!nonEmptyString(file)) return { error: `usage: --${what} <file> is required` }
|
|
881
|
+
let raw
|
|
882
|
+
try {
|
|
883
|
+
raw = fs.readFileSync(file, 'utf8')
|
|
884
|
+
} catch {
|
|
885
|
+
return { error: `${what} not readable: ${file}` }
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
return { value: JSON.parse(raw) }
|
|
889
|
+
} catch {
|
|
890
|
+
return { error: `${what} is not valid JSON: ${file}` }
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export function main(argv) {
|
|
895
|
+
const { opts, positional } = parseArgs(argv)
|
|
896
|
+
const cmd = positional[0]
|
|
897
|
+
|
|
898
|
+
if (opts.help || cmd === 'help') {
|
|
899
|
+
console.log(USAGE)
|
|
900
|
+
return 0
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (cmd !== 'validate' && cmd !== 'emit' && cmd !== 'path' && cmd !== 'next-attempt-id') {
|
|
904
|
+
console.error(`evidence-bundle: ${cmd ? `unknown command "${cmd}"` : 'no subcommand given'}. Use validate|emit|path|next-attempt-id (see --help).`)
|
|
905
|
+
return 2
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Minting the attempt id is a READ of the durable dir, so it answers at the promotion
|
|
909
|
+
// authorization — before anything has been written for this attempt, which is exactly when
|
|
910
|
+
// step 3 needs it.
|
|
911
|
+
if (cmd === 'next-attempt-id') {
|
|
912
|
+
if (!nonEmptyString(opts.version)) {
|
|
913
|
+
console.error('evidence-bundle: usage: next-attempt-id requires --version <v> (the version being cut)')
|
|
914
|
+
return 2
|
|
915
|
+
}
|
|
916
|
+
if (!nonEmptyString(opts.dir)) {
|
|
917
|
+
console.error('evidence-bundle: usage: next-attempt-id requires --dir <durable-evidence-dir> — the ordinal is read off the bundles already written, never remembered')
|
|
918
|
+
return 2
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
console.log(nextAttemptId(opts.version, opts.dir))
|
|
922
|
+
return 0
|
|
923
|
+
} catch (e) {
|
|
924
|
+
console.error(`evidence-bundle: ${e.message}`)
|
|
925
|
+
return 2
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (cmd === 'path') {
|
|
930
|
+
// `--rc-tag` is the pre-#599 spelling, kept because on the rc shape the attempt id IS the rc
|
|
931
|
+
// tag, so every existing caller and every existing bundle still resolves through it.
|
|
932
|
+
const key = nonEmptyString(opts['attempt-id']) ? opts['attempt-id'] : opts['rc-tag']
|
|
933
|
+
if (!nonEmptyString(key)) {
|
|
934
|
+
console.error('evidence-bundle: usage: path requires --attempt-id <id> (or the pre-#599 --rc-tag <tag>)')
|
|
935
|
+
return 2
|
|
936
|
+
}
|
|
937
|
+
if (!nonEmptyString(opts.dir)) {
|
|
938
|
+
console.error('evidence-bundle: usage: path requires --dir <dir>')
|
|
939
|
+
return 2
|
|
940
|
+
}
|
|
941
|
+
// With --state: print the one deterministic file for that (id, state). It is pure derivation,
|
|
942
|
+
// so it answers even before the bundle is written. Without --state: one attempt can hold more
|
|
943
|
+
// than one bundle (one per terminal state — a recovery stays on the same attempt), so list every
|
|
944
|
+
// bundle that EXISTS for it, which keeps "reachable by the key alone, without the session" (AC4)
|
|
945
|
+
// true when you do not yet know which terminal state the attempt reached.
|
|
946
|
+
if (nonEmptyString(opts.state)) {
|
|
947
|
+
let name
|
|
948
|
+
try {
|
|
949
|
+
name = bundleFilename(key, opts.state)
|
|
950
|
+
} catch (e) {
|
|
951
|
+
console.error(`evidence-bundle: ${e.message}`)
|
|
952
|
+
return 2
|
|
953
|
+
}
|
|
954
|
+
console.log(path.join(opts.dir, name))
|
|
955
|
+
return 0
|
|
956
|
+
}
|
|
957
|
+
if (!SAFE_KEY.test(key)) {
|
|
958
|
+
console.error(`evidence-bundle: unsafe attempt id ${JSON.stringify(key)} — must be a plain name (no slashes, no traversal)`)
|
|
959
|
+
return 2
|
|
960
|
+
}
|
|
961
|
+
const prefix = `${key}--`
|
|
962
|
+
let entries
|
|
963
|
+
// Same rule as the mint above, aimed at the reader: ENOENT is a dir the first emit has not
|
|
964
|
+
// created yet, so it genuinely holds no bundles (exit 0, no output). Any OTHER read failure is
|
|
965
|
+
// a dir nobody read, and listing nothing off it reports "this attempt has no bundles" for an
|
|
966
|
+
// attempt that has them — which is how an operator concludes a spent key is free.
|
|
967
|
+
try {
|
|
968
|
+
entries = fs.readdirSync(opts.dir)
|
|
969
|
+
} catch (e) {
|
|
970
|
+
if (e.code !== 'ENOENT') {
|
|
971
|
+
console.error(`evidence-bundle: ${opts.dir} could not be read (${e.code}) — refusing to report "no bundles" off a directory that was never read; an unreadable dir is not an empty one`)
|
|
972
|
+
return 2
|
|
973
|
+
}
|
|
974
|
+
entries = []
|
|
975
|
+
}
|
|
976
|
+
for (const f of entries.filter((f) => f.startsWith(prefix) && f.endsWith('.json')).sort()) {
|
|
977
|
+
console.log(path.join(opts.dir, f))
|
|
978
|
+
}
|
|
979
|
+
return 0
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const read = readJson(opts.bundle, 'bundle')
|
|
983
|
+
if (read.error) {
|
|
984
|
+
console.error(`evidence-bundle: ${read.error}`)
|
|
985
|
+
return 2
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (cmd === 'validate') {
|
|
989
|
+
const result = validateBundle(read.value)
|
|
990
|
+
console.log(JSON.stringify(result, null, 2))
|
|
991
|
+
if (!result.valid) console.error(`evidence-bundle: INVALID — ${result.failed.join(', ')}`)
|
|
992
|
+
return result.valid ? 0 : 1
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// emit
|
|
996
|
+
if (!nonEmptyString(opts.dir)) {
|
|
997
|
+
console.error('evidence-bundle: usage: emit requires --dir <dir>')
|
|
998
|
+
return 2
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const { path: p, noop } = emitBundle({ bundle: read.value, dir: opts.dir })
|
|
1002
|
+
console.log(JSON.stringify({ path: p, noop }, null, 2))
|
|
1003
|
+
if (noop) console.error(`evidence-bundle: ${p} already holds this exact evidence — resume-safe no-op (immutable once written).`)
|
|
1004
|
+
else console.error(`evidence-bundle: wrote ${p}`)
|
|
1005
|
+
return 0
|
|
1006
|
+
} catch (e) {
|
|
1007
|
+
console.error(`evidence-bundle: ${e.message}`)
|
|
1008
|
+
return 1
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (isMainModule(import.meta.url)) {
|
|
1013
|
+
process.exit(main(process.argv.slice(2)))
|
|
1014
|
+
}
|