@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,904 @@
|
|
|
1
|
+
// candidate-identity — deterministic, fail-closed candidate discovery for the release lane (issue #433).
|
|
2
|
+
//
|
|
3
|
+
// Principle 12's "out" clause used to end at "revision name and URL discoverable", and step 3
|
|
4
|
+
// then had to WARN that the `candidate` tag URL means "the newest candidate", not "the one I
|
|
5
|
+
// built" — a warning an agent must remember to act on, at exactly the point where a wrong
|
|
6
|
+
// answer validates one candidate under another's identity. This script replaces the warning
|
|
7
|
+
// with a comparison: the tag is keyed to the SHA, the revision and URL come from ONE traffic
|
|
8
|
+
// entry, and the running process attests the tuple. A disagreement becomes an absent or
|
|
9
|
+
// ambiguous read the script REFUSES, not a discrepancy a reader has to notice.
|
|
10
|
+
//
|
|
11
|
+
// Three subcommands, because the sequence really has three steps and the middle one is a
|
|
12
|
+
// network read this script must not take on (principle 12 assigns platform reads to the
|
|
13
|
+
// release lane; a script that shells out to gcloud/curl has taken on deploy mechanics):
|
|
14
|
+
//
|
|
15
|
+
// candidate-identity.mjs locate --sha <shipped> --traffic <flattened-traffic>
|
|
16
|
+
// Selects THE candidate traffic entry — the row tagged `candidate-<short-sha>` for the
|
|
17
|
+
// promoted SHA — and emits its tag, url, revision and percent. Missing, ambiguous, or a
|
|
18
|
+
// foreign candidate tag (a second release in flight, principle 7) all REFUSE.
|
|
19
|
+
// candidate-identity.mjs attest --sha <shipped> --locate <locate.json> --health <health.json>
|
|
20
|
+
// Binds the located coordinates to the identity the RUNNING PROCESS attests: source SHA,
|
|
21
|
+
// revision name, image digest. Emits `release.candidateIdentity` for the manifest.
|
|
22
|
+
// candidate-identity.mjs serving --sha <shipped> --identity <candidateIdentity.json> --health <live-health.json>
|
|
23
|
+
// Step 4b's post-shift confirmation: the live service attests the SAME tuple. This is what
|
|
24
|
+
// replaced comparing image-tag strings — the tuple carries the SHA, the revision AND the
|
|
25
|
+
// digest, and it comes from the process rather than from the revision's spec.
|
|
26
|
+
//
|
|
27
|
+
// Every subcommand after the first is handed an artifact the previous one wrote, and each one
|
|
28
|
+
// cross-checks that artifact against the identity it CLAIMS to describe before using it — see
|
|
29
|
+
// `tagBacksGuarantee`. Without that, "the record and the live process agree" is a true statement
|
|
30
|
+
// about the wrong release.
|
|
31
|
+
//
|
|
32
|
+
// Contract:
|
|
33
|
+
// exit 0 = ACCEPT — identity established (or established under a DECLARED weakening).
|
|
34
|
+
// exit 1 = REFUSE — the release stops here; the specific failing ids are named in `failed`.
|
|
35
|
+
// exit 2 = usage / unreadable-input error — bad flags, unknown subcommand, missing required file.
|
|
36
|
+
//
|
|
37
|
+
// FAIL CLOSED is the invariant, and it has a floor: identity rests on two legs — a SHA-keyed
|
|
38
|
+
// tag and a health attestation — and a repo whose pipeline cannot provide one DECLARES that
|
|
39
|
+
// (`--declare-shared-candidate-tag` / `--declare-no-attestation`, pre-flight item 3), which
|
|
40
|
+
// records the weakened guarantee in the artifact instead of leaving it assumed. An UNdeclared
|
|
41
|
+
// missing leg refuses. BOTH legs missing refuses even when both are declared: nothing keys
|
|
42
|
+
// identity at all then, and a declaration is a statement about a weaker guarantee, never a
|
|
43
|
+
// waiver of the whole check.
|
|
44
|
+
//
|
|
45
|
+
// And a declaration owes a SUBSTITUTE, not just a reason — see `evaluateAttest`'s
|
|
46
|
+
// `e:no-fallback-digest`. A declared weakening has to leave the operator with a check they can
|
|
47
|
+
// actually run; a branch that accepts and records a field the doctrine then sends someone to
|
|
48
|
+
// compare against, having set it to `null`, is worse than one that refuses, because it looks
|
|
49
|
+
// complete. That is the rule this file holds in every branch: **every field of a recorded
|
|
50
|
+
// identity is populated in every branch that accepts, or that branch refuses.**
|
|
51
|
+
//
|
|
52
|
+
// Its other half, and the one that was missing: a field is not made trustworthy by the branch
|
|
53
|
+
// that WROTE it, because the artifact carrying it can be stale, truncated or hand-edited by the
|
|
54
|
+
// time the next subcommand reads it. So **every field an identity record carries is re-derived or
|
|
55
|
+
// revalidated at the hand-off that carries it** — see the field walk below `tagBacksGuarantee`.
|
|
56
|
+
// Re-deriving only the SHA and the tag left an accepted locate artifact with `url: null` minting
|
|
57
|
+
// a candidateIdentity with a hole in it, at exit 0.
|
|
58
|
+
|
|
59
|
+
import fs from 'node:fs'
|
|
60
|
+
import { parseTraffic, normalizeTrafficPercents, str } from './candidate-traffic.mjs'
|
|
61
|
+
|
|
62
|
+
export { parseTraffic, normalizeTrafficPercents } from './candidate-traffic.mjs'
|
|
63
|
+
import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
|
|
64
|
+
|
|
65
|
+
// A short SHA is 7 hex chars (Cloud Build's $SHORT_SHA, git's default abbreviation). Below
|
|
66
|
+
// that, a "prefix match" stops being evidence of anything — 4 hex chars collide across a
|
|
67
|
+
// day's builds — so a shorter tag suffix is not treated as SHA-keyed at all.
|
|
68
|
+
export const MIN_SHORT_SHA = 7
|
|
69
|
+
|
|
70
|
+
// The floor lives HERE and nowhere else: every SHA this script reads — the shipped SHA, a tag
|
|
71
|
+
// suffix, an attested `sourceSha` — goes through this predicate, so the minimum is enforced
|
|
72
|
+
// once rather than re-asserted per call site. (It was re-asserted in shaMatches at first, where
|
|
73
|
+
// it was unreachable — both operands are already ≥ MIN_SHORT_SHA by the time they get there —
|
|
74
|
+
// and a guard that cannot fire reads as protection while providing none.)
|
|
75
|
+
const SHA_RE = new RegExp(`^[0-9a-f]{${MIN_SHORT_SHA},40}$`, 'i')
|
|
76
|
+
const isSha = (s) => typeof s === 'string' && SHA_RE.test(s)
|
|
77
|
+
|
|
78
|
+
// Two SHAs identify the same commit when one is a prefix of the other. Health endpoints
|
|
79
|
+
// commonly report $SHORT_SHA while the release lane carries the full one, so an equality-only
|
|
80
|
+
// comparison would fail closed on every healthy release — the wrong kind of strict.
|
|
81
|
+
export function shaMatches(a, b) {
|
|
82
|
+
if (!isSha(a) || !isSha(b)) return false
|
|
83
|
+
const [x, y] = [a.toLowerCase(), b.toLowerCase()]
|
|
84
|
+
const short = x.length <= y.length ? x : y
|
|
85
|
+
const long = short === x ? y : x
|
|
86
|
+
return long.startsWith(short)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// An image DIGEST, not an image tag. `algorithm:hex` is the registry-wide form, and requiring
|
|
90
|
+
// it is the point of the field: `myapp:latest` and `myapp:$SHA` are tags — mutable pointers a
|
|
91
|
+
// registry can re-point — and accepting one here would re-admit exactly the image-TAG string
|
|
92
|
+
// comparison the attested tuple replaced.
|
|
93
|
+
const isDigest = (s) => typeof s === 'string' && /^[a-z0-9][a-z0-9+.-]*:[0-9a-f]{32,}$/i.test(s)
|
|
94
|
+
|
|
95
|
+
// Digest hex is case-insensitive, and `isDigest` accepts either case — so the comparison must
|
|
96
|
+
// too. An endpoint echoing `SHA256:AB…` is naming the SAME artifact, and a strict `!==` would
|
|
97
|
+
// refuse a healthy release on a rendering choice. It fails closed, so it is safe; it is still
|
|
98
|
+
// wrong, and a fail-closed bug spends a release cycle to discover.
|
|
99
|
+
const digestMatches = (a, b) =>
|
|
100
|
+
typeof a === 'string' && typeof b === 'string' && a.toLowerCase() === b.toLowerCase()
|
|
101
|
+
|
|
102
|
+
// The candidate's tagged endpoint. A shape, not mere non-emptiness: this is the field the
|
|
103
|
+
// doctrine sends the operator to curl, and `""`, `"n/a"` and `"-"` are all truthy-or-nearly so
|
|
104
|
+
// while naming nothing. A field that cannot be dereferenced is the same defect as a null one —
|
|
105
|
+
// it just survives a truthiness check.
|
|
106
|
+
const isUrl = (s) => typeof s === 'string' && /^https?:\/\/\S+$/i.test(s.trim())
|
|
107
|
+
|
|
108
|
+
// ---- locate ----
|
|
109
|
+
|
|
110
|
+
// Does this tag key the candidate to a specific commit, and is that commit ours?
|
|
111
|
+
const DEFAULT_CANDIDATE_TAG_PREFIX = 'candidate-'
|
|
112
|
+
|
|
113
|
+
function validCandidateTagPrefix(prefix) {
|
|
114
|
+
return typeof prefix === 'string' && /^[a-z][a-z0-9-]{0,30}-$/.test(prefix)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function classifyTag(tag, sha, candidateTagPrefix = DEFAULT_CANDIDATE_TAG_PREFIX) {
|
|
118
|
+
if (tag === 'candidate') return 'shared'
|
|
119
|
+
if (!validCandidateTagPrefix(candidateTagPrefix) || !tag.startsWith(candidateTagPrefix)) {
|
|
120
|
+
return 'other'
|
|
121
|
+
}
|
|
122
|
+
const suffix = tag.slice(candidateTagPrefix.length)
|
|
123
|
+
if (!/^[0-9a-f]{4,40}$/i.test(suffix)) return 'other'
|
|
124
|
+
// A suffix below MIN_SHORT_SHA is not SHA-keyed at all (isSha rejects it, so shaMatches is
|
|
125
|
+
// false) — and it is not a rival either: `candidate-abc123` collides across a day of builds,
|
|
126
|
+
// so calling it another release's tag would refuse healthy releases on a coincidence.
|
|
127
|
+
if (!isSha(suffix)) return 'other'
|
|
128
|
+
return shaMatches(suffix, sha) ? 'ours' : 'foreign'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---- the cross-artifact rule (used by attest AND serving) ----
|
|
132
|
+
//
|
|
133
|
+
// THE CLASS: **every identity artifact that flows between subcommands is cross-checked against
|
|
134
|
+
// the identity it claims to describe — in every branch, including the DECLARED weakenings,
|
|
135
|
+
// which are exactly the branches where the other comparisons are absent.**
|
|
136
|
+
//
|
|
137
|
+
// A subcommand that reads a `guarantee` field and believes it has verified nothing. The tag and
|
|
138
|
+
// the SHA carried in the same artifact are the evidence for that claim, so both are re-derived
|
|
139
|
+
// at every hand-off rather than taken at their word. Two comparisons, one per leg:
|
|
140
|
+
//
|
|
141
|
+
// - the artifact's own SHA against the SHA this subcommand was given — a stale artifact from
|
|
142
|
+
// an earlier rc describes a different commit, and nothing else notices;
|
|
143
|
+
// - the artifact's tag against the guarantee it claims — `shaKeyedTag: true` is backed only by
|
|
144
|
+
// a tag keyed to THIS commit (`ours`), and `shaKeyedTag: false` only by the shared
|
|
145
|
+
// `candidate` tag. Anything else is an artifact whose claim its own coordinates contradict.
|
|
146
|
+
//
|
|
147
|
+
// What it was missing (the bug this closes): `attest` never compared its `--sha` against the
|
|
148
|
+
// locate artifact's. In the attested branch the tuple comparisons masked it; under
|
|
149
|
+
// `--declare-no-attestation` nothing did. rc.1 dies at SHA_X after `locate` wrote its artifact,
|
|
150
|
+
// the retry mints rc.2 at SHA_Y, a resumed session runs `attest --sha $SHIPPED` (now Y) against
|
|
151
|
+
// X's artifact — and it ACCEPTED, recording `sourceSha: Y` over X's revision, url and tag under
|
|
152
|
+
// `shaKeyedTag: true`. Step 4b then verified X's tag→revision agreement and the digest fallback
|
|
153
|
+
// had no digest to compare, so the release validated one candidate under another's identity.
|
|
154
|
+
export function tagBacksGuarantee(
|
|
155
|
+
tag,
|
|
156
|
+
sha,
|
|
157
|
+
shaKeyedTag,
|
|
158
|
+
candidateTagPrefix = DEFAULT_CANDIDATE_TAG_PREFIX,
|
|
159
|
+
) {
|
|
160
|
+
const kind = classifyTag(str(tag), sha, candidateTagPrefix)
|
|
161
|
+
return shaKeyedTag ? kind === 'ours' : kind === 'shared'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---- the field walk (the general form of the rule above) ----
|
|
165
|
+
//
|
|
166
|
+
// THE RULE the table below exists to serve: **every field an identity record carries is
|
|
167
|
+
// re-derived or revalidated at the hand-off that carries it — never carried across on the
|
|
168
|
+
// strength of the upstream artifact's `decision`.** How far that rule is MECHANIZED is stated
|
|
169
|
+
// under the table, and it is not as far as the rule reaches.
|
|
170
|
+
//
|
|
171
|
+
// `tagBacksGuarantee` closed two fields, the SHA and the tag, and stopping there is what let the
|
|
172
|
+
// rest through. An ACCEPTED locate artifact whose `candidate.url` is `null` — stale, hand-edited,
|
|
173
|
+
// or written by an older build of this script — walked straight past `attest`, which re-derived
|
|
174
|
+
// the SHA and the tag, found both fine, and minted a candidateIdentity carrying `url: null` at
|
|
175
|
+
// exit 0. Every downstream reader treats that record as complete, and the one field the operator
|
|
176
|
+
// is sent to dereference is the hole. "Check the SHA and the tag" was never the rule; it was two
|
|
177
|
+
// instances of it.
|
|
178
|
+
//
|
|
179
|
+
// So the walk is expressed as a table rather than a run of `if`s: a field added to either record
|
|
180
|
+
// has to appear here, or it is being carried on trust again. Each row is (field, predicate,
|
|
181
|
+
// failure id) — the predicate takes the value and the context needed to re-derive it, because
|
|
182
|
+
// "revalidate" for `tag` means re-deriving it against the promoted SHA, not checking that a
|
|
183
|
+
// string is present.
|
|
184
|
+
//
|
|
185
|
+
// HOW FAR THIS GOES. The table is HAND-KEPT and nothing detects an omission from it: a field a
|
|
186
|
+
// record carries with no row here crosses UNWALKED, and an ACCEPT says nothing about it. "Every
|
|
187
|
+
// field is revalidated" is therefore the obligation on whoever adds a field, not a property this
|
|
188
|
+
// file enforces — which is why `--help` states the rule at the table's width and sends the reader
|
|
189
|
+
// to regression-checklist row 4a rather than to this exit code. #452 replaces the table with a
|
|
190
|
+
// structurally-validated schema, in which an unrevalidated field cannot exist to be omitted.
|
|
191
|
+
|
|
192
|
+
// Fields of a locate artifact's candidate entry that flow into candidateIdentity.
|
|
193
|
+
const LOCATE_HANDOFF_FIELDS = [
|
|
194
|
+
['tag', (v, { sha, shaKeyedTag, candidateTagPrefix }) =>
|
|
195
|
+
tagBacksGuarantee(v, sha, shaKeyedTag, candidateTagPrefix), 'e:locate-guarantee-unbacked'],
|
|
196
|
+
['url', (v) => isUrl(v), 'e:locate-entry-incomplete'],
|
|
197
|
+
['revision', (v) => str(v).length > 0, 'e:locate-entry-incomplete'],
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
// Fields of a candidateIdentity record, revalidated where `serving` consumes it.
|
|
201
|
+
const IDENTITY_RECORD_FIELDS = [
|
|
202
|
+
// Two different questions, deliberately two ids: `sourceSha` asks whether this record is THIS
|
|
203
|
+
// release's, and everything below asks whether the record is self-consistent — so the tag is
|
|
204
|
+
// checked against the record's OWN sourceSha, not the promoted one, or a stale artifact would
|
|
205
|
+
// report its every field as broken instead of reporting that it is the wrong artifact.
|
|
206
|
+
['sourceSha', (v, { sha }) => shaMatches(v, sha), 'e:identity-sha-mismatch'],
|
|
207
|
+
['tag', (v, { recordSha, shaKeyedTag, candidateTagPrefix }) =>
|
|
208
|
+
tagBacksGuarantee(v, recordSha, shaKeyedTag, candidateTagPrefix), 'e:identity-guarantee-unbacked'],
|
|
209
|
+
['imageDigest', (v) => isDigest(v), 'e:identity-guarantee-unbacked'],
|
|
210
|
+
// WHERE the digest came from is the difference the weakening makes, so it is backed by the
|
|
211
|
+
// guarantee like everything else: an attested record whose digest is labelled a revision spec
|
|
212
|
+
// (or the reverse) is telling a reader which comparison they got, wrongly.
|
|
213
|
+
['imageDigestSource', (v, { attested }) => v === (attested ? 'attestation' : 'candidate-revision-spec'),
|
|
214
|
+
'e:identity-guarantee-unbacked'],
|
|
215
|
+
['url', (v) => isUrl(v), 'e:identity-record-incomplete'],
|
|
216
|
+
['revision', (v) => str(v).length > 0, 'e:identity-record-incomplete'],
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
// The legs a weakening can be declared on, and the `guarantee` flag each one weakens.
|
|
220
|
+
const WEAKENING_LEGS = [
|
|
221
|
+
['sha-keyed-tag', 'shaKeyedTag'],
|
|
222
|
+
['health-attestation', 'attested'],
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
// `guarantee` and `weakenings` are two halves of one statement — the flag and the reason — so
|
|
226
|
+
// each is the evidence for the other and neither is carried alone. A record claiming
|
|
227
|
+
// `shaKeyedTag: false` with an empty `weakenings` is a weakening with no reason recorded, which
|
|
228
|
+
// is the assumption the declaration flags exist to replace; a `weakenings` entry with no matching
|
|
229
|
+
// flag claims a weaker guarantee than the record admits to. Both are stated-vs-actual gaps of the
|
|
230
|
+
// same kind as an unbacked tag.
|
|
231
|
+
export function weakeningsBackGuarantee(weakenings, guarantee, legs = WEAKENING_LEGS) {
|
|
232
|
+
if (!Array.isArray(weakenings)) return false
|
|
233
|
+
const g = guarantee && typeof guarantee === 'object' ? guarantee : {}
|
|
234
|
+
for (const w of weakenings) {
|
|
235
|
+
if (!w || typeof w !== 'object') return false
|
|
236
|
+
if (!legs.some(([leg]) => leg === w.leg)) return false
|
|
237
|
+
// A declaration's REASON is the whole value of declaring — the same rule `declaration()`
|
|
238
|
+
// enforces at the CLI, re-enforced here because a hand-edited artifact never went through it.
|
|
239
|
+
if (str(w.reason).length === 0) return false
|
|
240
|
+
}
|
|
241
|
+
for (const [leg, flag] of legs) {
|
|
242
|
+
if (!(flag in g)) continue
|
|
243
|
+
if (weakenings.some((w) => w.leg === leg) !== (g[flag] === false)) return false
|
|
244
|
+
}
|
|
245
|
+
return true
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Run a field table, appending each distinct failure id once and recording what was walked, so
|
|
249
|
+
// the artifact shows which fields were revalidated rather than only which ones failed.
|
|
250
|
+
function walkFields(table, source, ctx) {
|
|
251
|
+
const failed = []
|
|
252
|
+
const walked = {}
|
|
253
|
+
for (const [field, ok, id] of table) {
|
|
254
|
+
const passed = !!ok(source ? source[field] : undefined, ctx)
|
|
255
|
+
walked[field] = passed
|
|
256
|
+
if (!passed && !failed.includes(id)) failed.push(id)
|
|
257
|
+
}
|
|
258
|
+
return { failed, walked }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// The locate -> attest hand-off, walked field by field. `locate.decision` is trusted (this side
|
|
262
|
+
// has no traffic read to re-derive it from) but every FACT that decision gated is re-established
|
|
263
|
+
// here, so a stale, truncated or edited artifact refuses instead of being believed.
|
|
264
|
+
export function walkLocateArtifact({ locate, sha }) {
|
|
265
|
+
const located = locate.evidence.candidate
|
|
266
|
+
const shaKeyedTag = !!(locate.guarantee && locate.guarantee.shaKeyedTag)
|
|
267
|
+
const candidateTagPrefix =
|
|
268
|
+
str(locate.guarantee && locate.guarantee.candidateTagPrefix) || DEFAULT_CANDIDATE_TAG_PREFIX
|
|
269
|
+
const failed = []
|
|
270
|
+
|
|
271
|
+
if (!validCandidateTagPrefix(candidateTagPrefix)) failed.push('e:locate-guarantee-unbacked')
|
|
272
|
+
|
|
273
|
+
// The artifact's own subject, first: everything below is only meaningful once this entry is
|
|
274
|
+
// known to describe the commit being released.
|
|
275
|
+
const locatedSha = str(locate.evidence && locate.evidence.sha)
|
|
276
|
+
if (!shaMatches(locatedSha, sha)) failed.push('e:locate-sha-mismatch')
|
|
277
|
+
|
|
278
|
+
const fields = walkFields(LOCATE_HANDOFF_FIELDS, located, {
|
|
279
|
+
sha,
|
|
280
|
+
shaKeyedTag,
|
|
281
|
+
candidateTagPrefix,
|
|
282
|
+
})
|
|
283
|
+
for (const id of fields.failed) if (!failed.includes(id)) failed.push(id)
|
|
284
|
+
|
|
285
|
+
// `percent` does not reach the identity record, but the 0% contract is a fact `locate.decision`
|
|
286
|
+
// gated (principle 12's out clause), and an artifact whose entry now reads otherwise is not the
|
|
287
|
+
// one that was accepted. Re-derived like the rest rather than inherited from the decision.
|
|
288
|
+
if (located.percent !== 0) failed.push('e:locate-traffic-moved')
|
|
289
|
+
|
|
290
|
+
if (!weakeningsBackGuarantee(locate.weakenings || [], locate.guarantee, [WEAKENING_LEGS[0]])) {
|
|
291
|
+
failed.push('e:locate-weakenings-unbacked')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { failed, shaKeyedTag, walked: { sha: shaMatches(locatedSha, sha), ...fields.walked }, locatedSha }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// The attest -> serving hand-off, the same walk over the other record.
|
|
298
|
+
export function walkIdentityRecord({ id, sha }) {
|
|
299
|
+
const shaKeyedTag = !!(id.guarantee && id.guarantee.shaKeyedTag)
|
|
300
|
+
const attested = !!(id.guarantee && id.guarantee.attested)
|
|
301
|
+
const candidateTagPrefix =
|
|
302
|
+
str(id.guarantee && id.guarantee.candidateTagPrefix) || DEFAULT_CANDIDATE_TAG_PREFIX
|
|
303
|
+
const fields = walkFields(IDENTITY_RECORD_FIELDS, id, {
|
|
304
|
+
sha,
|
|
305
|
+
recordSha: id.sourceSha,
|
|
306
|
+
shaKeyedTag,
|
|
307
|
+
attested,
|
|
308
|
+
candidateTagPrefix,
|
|
309
|
+
})
|
|
310
|
+
const failed = [...fields.failed]
|
|
311
|
+
if (!validCandidateTagPrefix(candidateTagPrefix)) failed.push('e:identity-guarantee-unbacked')
|
|
312
|
+
if (!weakeningsBackGuarantee(id.weakenings || [], id.guarantee)) failed.push('e:identity-weakenings-unbacked')
|
|
313
|
+
return { failed, walked: fields.walked }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function evaluateLocate({
|
|
317
|
+
sha,
|
|
318
|
+
traffic,
|
|
319
|
+
declareSharedTag = null,
|
|
320
|
+
candidateTagPrefix = DEFAULT_CANDIDATE_TAG_PREFIX,
|
|
321
|
+
}) {
|
|
322
|
+
const failed = []
|
|
323
|
+
const weakenings = []
|
|
324
|
+
if (!isSha(sha)) {
|
|
325
|
+
return { decision: 'refuse', phase: 'locate', failed: ['e:unusable-shipped-sha'], evidence: { sha } }
|
|
326
|
+
}
|
|
327
|
+
if (!validCandidateTagPrefix(candidateTagPrefix)) {
|
|
328
|
+
return {
|
|
329
|
+
decision: 'refuse',
|
|
330
|
+
phase: 'locate',
|
|
331
|
+
failed: ['e:invalid-candidate-tag-prefix'],
|
|
332
|
+
evidence: { sha, candidateTagPrefix },
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Normalize here too so a caller that built `traffic.rows` by hand (or re-read a capture
|
|
337
|
+
// that skipped parseTraffic) still gets the Cloud Run 0%-blank reconstruction.
|
|
338
|
+
const rows = normalizeTrafficPercents(traffic.rows)
|
|
339
|
+
const classified = rows.map((r) => ({
|
|
340
|
+
...r,
|
|
341
|
+
kind: classifyTag(r.tag, sha, candidateTagPrefix),
|
|
342
|
+
}))
|
|
343
|
+
const ours = classified.filter((r) => r.kind === 'ours')
|
|
344
|
+
const shared = classified.filter((r) => r.kind === 'shared')
|
|
345
|
+
const foreign = classified.filter((r) => r.kind === 'foreign')
|
|
346
|
+
|
|
347
|
+
let candidate = null
|
|
348
|
+
let shaKeyedTag = true
|
|
349
|
+
|
|
350
|
+
if (ours.length > 1) {
|
|
351
|
+
// Two traffic entries claiming the same SHA-keyed tag is not a thing a healthy platform
|
|
352
|
+
// produces; whatever produced it, the read no longer names ONE artifact.
|
|
353
|
+
failed.push('e:ambiguous-candidate')
|
|
354
|
+
} else if (ours.length === 1) {
|
|
355
|
+
candidate = ours[0]
|
|
356
|
+
} else if (shared.length > 0) {
|
|
357
|
+
// The pipeline publishes a shared `candidate` tag rather than a SHA-keyed one. That is a
|
|
358
|
+
// real topology, but it is the WEAKER one — the tag means "the newest candidate" — so it
|
|
359
|
+
// is usable only when pre-flight item 3 declared it.
|
|
360
|
+
if (declareSharedTag) {
|
|
361
|
+
if (shared.length > 1) failed.push('e:ambiguous-candidate')
|
|
362
|
+
else {
|
|
363
|
+
candidate = shared[0]
|
|
364
|
+
shaKeyedTag = false
|
|
365
|
+
weakenings.push({ leg: 'sha-keyed-tag', reason: declareSharedTag })
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
failed.push('e:no-sha-keyed-tag')
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
failed.push('e:candidate-absent')
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// A candidate tag keyed to a DIFFERENT commit is a second release in flight (principle 7).
|
|
375
|
+
// Under the shared-tag weakening this can never fire, which is precisely the guarantee the
|
|
376
|
+
// declaration gives up: there, two releases in flight are indistinguishable from one.
|
|
377
|
+
const rivals = foreign.map((r) => r.tag)
|
|
378
|
+
if (shaKeyedTag && candidate) {
|
|
379
|
+
for (const r of shared) if (r.revision && r.revision !== candidate.revision) rivals.push(r.tag)
|
|
380
|
+
}
|
|
381
|
+
if (rivals.length > 0) failed.push('e:second-candidate-in-flight')
|
|
382
|
+
|
|
383
|
+
// Revision AND url must come from the SAME entry — that is what makes them describe one
|
|
384
|
+
// deployment. An entry missing either cannot do that job, however present the tag is. The
|
|
385
|
+
// percent is read from that same entry and held to principle 12's out clause: the pipeline's
|
|
386
|
+
// contracted output is a candidate at **0% traffic**, and "the build succeeded but traffic
|
|
387
|
+
// already moved" is a pipeline defect the release stops on. That stop was prose while the
|
|
388
|
+
// number sat in this row as evidence for a reader to notice — the one contract fact discovery
|
|
389
|
+
// read and did not enforce. It is a comparison now, like every other fact here.
|
|
390
|
+
if (candidate) {
|
|
391
|
+
if (
|
|
392
|
+
!isUrl(candidate.url) || !candidate.revision || candidate.percent === null ||
|
|
393
|
+
!Number.isFinite(candidate.percent) || candidate.percent < 0 || candidate.percent > 100
|
|
394
|
+
) {
|
|
395
|
+
failed.push('e:incomplete-traffic-entry')
|
|
396
|
+
} else if (candidate.percent !== 0) {
|
|
397
|
+
failed.push('e:candidate-traffic-moved')
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const evidence = {
|
|
402
|
+
sha,
|
|
403
|
+
trafficEntries: classified.length,
|
|
404
|
+
rivalTags: rivals,
|
|
405
|
+
candidate: candidate
|
|
406
|
+
? { tag: candidate.tag, url: candidate.url, revision: candidate.revision, percent: candidate.percent }
|
|
407
|
+
: null,
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
decision: failed.length === 0 ? 'accept' : 'refuse',
|
|
411
|
+
phase: 'locate',
|
|
412
|
+
failed,
|
|
413
|
+
guarantee: { shaKeyedTag, candidateTagPrefix },
|
|
414
|
+
weakenings,
|
|
415
|
+
evidence,
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ---- attestation reading (shared by attest + serving) ----
|
|
420
|
+
|
|
421
|
+
// The tuple a health endpoint attests. Field aliases are deliberately narrow: the point is to
|
|
422
|
+
// read a real endpoint's JSON, not to accept anything tuple-shaped.
|
|
423
|
+
export function readAttestation(health) {
|
|
424
|
+
if (!health || typeof health !== 'object') return { error: 'e:unattested' }
|
|
425
|
+
const sourceSha = str(health.sourceSha ?? health.sha)
|
|
426
|
+
const revision = str(health.revision ?? health.revisionName)
|
|
427
|
+
const imageDigest = str(health.imageDigest ?? health.digest)
|
|
428
|
+
const missing = []
|
|
429
|
+
if (!isSha(sourceSha)) missing.push('sourceSha')
|
|
430
|
+
if (!revision) missing.push('revision')
|
|
431
|
+
if (!isDigest(imageDigest)) missing.push('imageDigest')
|
|
432
|
+
if (missing.length > 0) return { error: 'e:incomplete-attestation', missing, read: { sourceSha, revision, imageDigest } }
|
|
433
|
+
return { tuple: { sourceSha, revision, imageDigest } }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ---- attest ----
|
|
437
|
+
|
|
438
|
+
export function evaluateAttest({ sha, locate, health, declareNoAttestation = null, candidateImageDigest = null }) {
|
|
439
|
+
const failed = []
|
|
440
|
+
const evidence = {}
|
|
441
|
+
|
|
442
|
+
const located = locate && locate.evidence && locate.evidence.candidate
|
|
443
|
+
const locateAccepted = !!locate && locate.decision === 'accept' && !!located
|
|
444
|
+
if (!locateAccepted) {
|
|
445
|
+
return {
|
|
446
|
+
decision: 'refuse',
|
|
447
|
+
phase: 'attest',
|
|
448
|
+
failed: ['e:no-accepted-locate'],
|
|
449
|
+
evidence: { locateDecision: locate && locate.decision },
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (!isSha(sha)) {
|
|
453
|
+
return { decision: 'refuse', phase: 'attest', failed: ['e:unusable-shipped-sha'], evidence: { sha } }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const shaKeyedTag = !!(locate.guarantee && locate.guarantee.shaKeyedTag)
|
|
457
|
+
const weakenings = [...(locate.weakenings || [])]
|
|
458
|
+
let attested = true
|
|
459
|
+
let tuple = null
|
|
460
|
+
|
|
461
|
+
// Walk the locate artifact field by field BEFORE anything downstream reads its coordinates —
|
|
462
|
+
// see the field walk above for the rule and the failures it closes. Not "check the SHA and the
|
|
463
|
+
// tag": every field that flows into the record below, plus the 0% contract the artifact's
|
|
464
|
+
// `decision` gated, is re-established here.
|
|
465
|
+
const walk = walkLocateArtifact({ locate, sha })
|
|
466
|
+
for (const id of walk.failed) failed.push(id)
|
|
467
|
+
evidence.locatedSha = walk.locatedSha || null
|
|
468
|
+
evidence.locateFieldWalk = walk.walked
|
|
469
|
+
|
|
470
|
+
const read = readAttestation(health)
|
|
471
|
+
if (read.error) {
|
|
472
|
+
if (!declareNoAttestation) {
|
|
473
|
+
failed.push(read.error)
|
|
474
|
+
if (read.missing) evidence.missingAttestationFields = read.missing
|
|
475
|
+
} else if (!shaKeyedTag) {
|
|
476
|
+
// The floor. A declaration weakens a guarantee; it never removes the last leg. With no
|
|
477
|
+
// SHA-keyed tag AND no attestation there is nothing left that ties this URL to this
|
|
478
|
+
// commit, so "discovery" would be the agent's belief with a JSON key on it.
|
|
479
|
+
failed.push('e:unverifiable-identity')
|
|
480
|
+
} else if (!isDigest(candidateImageDigest)) {
|
|
481
|
+
// A declaration owes a SUBSTITUTE. Step 4b's fallback for this branch compares the serving
|
|
482
|
+
// revision's image digest against `candidateIdentity.imageDigest` — and this branch used to
|
|
483
|
+
// record that field as `null`, so the check the weakest-pipeline repos are sent to could
|
|
484
|
+
// never match, and an agent reading `null` reads "not applicable" and skips it. The digest
|
|
485
|
+
// is available without an attesting endpoint: it is the CANDIDATE REVISION's configured
|
|
486
|
+
// image, a platform read the release lane is already taking beside this one. So the
|
|
487
|
+
// weakening is declarable only with that substitute in hand.
|
|
488
|
+
failed.push('e:no-fallback-digest')
|
|
489
|
+
} else {
|
|
490
|
+
attested = false
|
|
491
|
+
weakenings.push({
|
|
492
|
+
leg: 'health-attestation',
|
|
493
|
+
reason: declareNoAttestation,
|
|
494
|
+
// What is compared INSTEAD, recorded next to why — so the weaker guarantee is legible in
|
|
495
|
+
// the artifact rather than reconstructed from which fields happen to be set.
|
|
496
|
+
substitute: { imageDigest: candidateImageDigest, source: 'candidate-revision-spec' },
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
tuple = read.tuple
|
|
501
|
+
evidence.attested = tuple
|
|
502
|
+
if (!shaMatches(tuple.sourceSha, sha)) failed.push('e:sha-mismatch')
|
|
503
|
+
if (tuple.revision !== located.revision) failed.push('e:revision-mismatch')
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const identity = {
|
|
507
|
+
sourceSha: tuple ? tuple.sourceSha : sha,
|
|
508
|
+
revision: located.revision,
|
|
509
|
+
imageDigest: tuple ? tuple.imageDigest : isDigest(candidateImageDigest) ? candidateImageDigest : null,
|
|
510
|
+
// WHERE the digest came from, because that is the whole difference the weakening makes: the
|
|
511
|
+
// attested one is what the running process reports, the declared one is what the revision was
|
|
512
|
+
// configured with. Step 4b compares the same kind on both sides either way; a reader of this
|
|
513
|
+
// record must be able to tell which comparison they are getting without inferring it.
|
|
514
|
+
imageDigestSource: tuple ? 'attestation' : 'candidate-revision-spec',
|
|
515
|
+
url: located.url,
|
|
516
|
+
tag: located.tag,
|
|
517
|
+
guarantee: {
|
|
518
|
+
shaKeyedTag,
|
|
519
|
+
candidateTagPrefix:
|
|
520
|
+
str(locate.guarantee && locate.guarantee.candidateTagPrefix) || DEFAULT_CANDIDATE_TAG_PREFIX,
|
|
521
|
+
attested,
|
|
522
|
+
},
|
|
523
|
+
weakenings,
|
|
524
|
+
}
|
|
525
|
+
evidence.identity = identity
|
|
526
|
+
evidence.locatedRevision = located.revision
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
decision: failed.length === 0 ? 'accept' : 'refuse',
|
|
530
|
+
phase: 'attest',
|
|
531
|
+
failed,
|
|
532
|
+
guarantee: identity.guarantee,
|
|
533
|
+
weakenings,
|
|
534
|
+
identity: failed.length === 0 ? identity : null,
|
|
535
|
+
evidence,
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ---- serving (step 4b, after the shift) ----
|
|
540
|
+
|
|
541
|
+
export function evaluateServing({ identity, health, sha = null, servingImageDigest = null }) {
|
|
542
|
+
const failed = []
|
|
543
|
+
const evidence = {}
|
|
544
|
+
const id = identity && identity.identity ? identity.identity : identity
|
|
545
|
+
if (!id || typeof id !== 'object' || !id.revision) {
|
|
546
|
+
return { decision: 'refuse', phase: 'serving', failed: ['e:no-recorded-identity'], evidence: {} }
|
|
547
|
+
}
|
|
548
|
+
evidence.recorded = id
|
|
549
|
+
|
|
550
|
+
// The same cross-artifact rule `attest` owes its locate input, and this is the mirror gap it
|
|
551
|
+
// had: comparing the live tuple against the recorded one proves the record and the process
|
|
552
|
+
// agree, never that the record is THIS release's. Both branches below depend on it —
|
|
553
|
+
// including the declared fallback, which refuses to `e:no-attested-tuple` and sends the
|
|
554
|
+
// operator off to compare digests against *this* record's `imageDigest`. So it runs first: a
|
|
555
|
+
// record describing another commit is a mismatch to stop on, not a routing decision.
|
|
556
|
+
// The recorded `guarantee` has two legs and each is backed by a coordinate in the same record:
|
|
557
|
+
// `shaKeyedTag` by the tag, and BOTH values of `attested` by a usable digest. The digest
|
|
558
|
+
// requirement is unconditional on purpose: it used to apply only when `attested` was true, which
|
|
559
|
+
// left the un-attested branch — the one whose whole remaining check IS a digest comparison —
|
|
560
|
+
// as the single branch permitted to carry `imageDigest: null`. A record with no digest cannot
|
|
561
|
+
// be confirmed either way, so it refuses here rather than routing to a comparison with nothing
|
|
562
|
+
// on one side. The rest of the record is walked with it, same rule, same table.
|
|
563
|
+
const walk = walkIdentityRecord({ id, sha })
|
|
564
|
+
for (const f of walk.failed) failed.push(f)
|
|
565
|
+
evidence.identityFieldWalk = walk.walked
|
|
566
|
+
if (failed.length > 0) return { decision: 'refuse', phase: 'serving', failed, evidence }
|
|
567
|
+
|
|
568
|
+
const weakened = { guarantee: id.guarantee, weakenings: id.weakenings || [] }
|
|
569
|
+
|
|
570
|
+
if (!(id.guarantee && id.guarantee.attested)) {
|
|
571
|
+
// This release declared it has no attesting endpoint (pre-flight item 3), so there is no
|
|
572
|
+
// tuple to re-read — the declared fallback runs instead: the SERVING revision's configured
|
|
573
|
+
// image digest against the CANDIDATE revision's, both recorded, both real. It is weaker in a
|
|
574
|
+
// stated way (a revision's spec is not the running process) and it is a comparison, which is
|
|
575
|
+
// what the branch was missing: the fallback used to be a `# must equal` comment beside a
|
|
576
|
+
// field this branch left null.
|
|
577
|
+
if (!isDigest(servingImageDigest)) {
|
|
578
|
+
return { decision: 'refuse', phase: 'serving', failed: ['e:no-attested-tuple'], ...weakened, evidence }
|
|
579
|
+
}
|
|
580
|
+
evidence.fallback = {
|
|
581
|
+
recordedImageDigest: id.imageDigest,
|
|
582
|
+
recordedImageDigestSource: id.imageDigestSource ?? null,
|
|
583
|
+
servingImageDigest,
|
|
584
|
+
compares: 'revision-spec image digests — not the running process (declared weakening)',
|
|
585
|
+
}
|
|
586
|
+
if (!digestMatches(servingImageDigest, id.imageDigest)) failed.push('e:digest-mismatch')
|
|
587
|
+
return { decision: failed.length === 0 ? 'accept' : 'refuse', phase: 'serving', failed, ...weakened, evidence }
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const read = readAttestation(health)
|
|
591
|
+
if (read.error) {
|
|
592
|
+
failed.push(read.error)
|
|
593
|
+
if (read.missing) evidence.missingAttestationFields = read.missing
|
|
594
|
+
return { decision: 'refuse', phase: 'serving', failed, evidence }
|
|
595
|
+
}
|
|
596
|
+
evidence.serving = read.tuple
|
|
597
|
+
|
|
598
|
+
if (!shaMatches(read.tuple.sourceSha, id.sourceSha)) failed.push('e:sha-mismatch')
|
|
599
|
+
if (read.tuple.revision !== id.revision) failed.push('e:revision-mismatch')
|
|
600
|
+
if (!digestMatches(read.tuple.imageDigest, id.imageDigest)) failed.push('e:digest-mismatch')
|
|
601
|
+
|
|
602
|
+
return { decision: failed.length === 0 ? 'accept' : 'refuse', phase: 'serving', failed, evidence }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ---- CLI ----
|
|
606
|
+
|
|
607
|
+
const USAGE = `candidate-identity — deterministic, fail-closed candidate discovery for the release lane.
|
|
608
|
+
|
|
609
|
+
Principle 12's candidate handle is keyed to the promoted SHA and attested by the running
|
|
610
|
+
process, so "the newest candidate" and "the one I built" cannot be different things. Every
|
|
611
|
+
missing, ambiguous, or mismatched identity REFUSES; it is never a warning to notice.
|
|
612
|
+
|
|
613
|
+
Usage:
|
|
614
|
+
candidate-identity locate --sha <shipped> --traffic <flattened-traffic-read>
|
|
615
|
+
[--candidate-tag-prefix <prefix-ending-in-hyphen>]
|
|
616
|
+
[--declare-shared-candidate-tag <reason>]
|
|
617
|
+
Select THE candidate traffic entry — the row tagged \`candidate-<short-sha>\` for the
|
|
618
|
+
promoted SHA — and emit its tag/url/revision/percent from that ONE entry.
|
|
619
|
+
--candidate-tag-prefix use a repository-declared SHA-keyed namespace such as \`a-\`;
|
|
620
|
+
defaults to \`candidate-\` and is recorded for downstream
|
|
621
|
+
attest/serving cross-artifact validation
|
|
622
|
+
e:invalid-candidate-tag-prefix prefix is not a safe lowercase Cloud Run tag prefix
|
|
623
|
+
e:candidate-absent no traffic entry carries this SHA's candidate tag
|
|
624
|
+
e:no-sha-keyed-tag only a shared \`candidate\` tag exists and pre-flight item 3
|
|
625
|
+
declared no weakening — declare it or fix the pipeline
|
|
626
|
+
e:ambiguous-candidate more than one entry claims the candidate tag
|
|
627
|
+
e:second-candidate-in-flight a candidate tag keyed to another commit is present, or a
|
|
628
|
+
shared \`candidate\` tag resolves elsewhere (principle 7)
|
|
629
|
+
e:incomplete-traffic-entry the entry lacks a url, a revision or a percent, so they
|
|
630
|
+
cannot be read from the same deployment
|
|
631
|
+
e:candidate-traffic-moved the candidate is not at 0% traffic — principle 12's out
|
|
632
|
+
clause is a pipeline contract, so a build that shifted
|
|
633
|
+
traffic is a pipeline defect, not a release to continue
|
|
634
|
+
Input (preferred): gcloud run services describe … --format=json (full service object or
|
|
635
|
+
a JSON array of {tag,url,revisionName,percent}; every omitted percent is
|
|
636
|
+
reconstructed as 0 only when all explicit targets are valid and sum to 100%.
|
|
637
|
+
Legacy: --flatten='status.traffic[]' --format='value(…tag,url,revisionName,percent)'
|
|
638
|
+
is still accepted; prefer JSON so Cloud Run's blank 0% column is not the default.
|
|
639
|
+
|
|
640
|
+
candidate-identity attest --sha <shipped> --locate <locate.json> --health <health.json>
|
|
641
|
+
[--declare-no-attestation <reason> --candidate-image-digest <digest>]
|
|
642
|
+
Bind the located coordinates to the tuple the RUNNING PROCESS attests — source SHA,
|
|
643
|
+
revision name, image digest — and emit \`release.candidateIdentity\` for the manifest.
|
|
644
|
+
e:unattested the health read is missing or not an object
|
|
645
|
+
e:incomplete-attestation a tuple field is absent or unusable (imageDigest must be a
|
|
646
|
+
real \`algorithm:hex\` DIGEST — an image TAG is not one)
|
|
647
|
+
e:sha-mismatch the attested source SHA is not the promoted SHA
|
|
648
|
+
e:revision-mismatch the attested revision is not the located revision
|
|
649
|
+
e:locate-sha-mismatch the locate artifact describes a DIFFERENT commit than --sha
|
|
650
|
+
(a stale artifact from an earlier rc; see the cross-artifact
|
|
651
|
+
rule below)
|
|
652
|
+
e:locate-guarantee-unbacked the locate artifact's \`guarantee\` is contradicted by its own
|
|
653
|
+
tag — a claim, not a weaker guarantee
|
|
654
|
+
e:locate-entry-incomplete the artifact's candidate entry is missing a url or a revision,
|
|
655
|
+
so a field of the identity record would be recorded unset
|
|
656
|
+
e:locate-traffic-moved the artifact's candidate entry no longer reads 0% — the fact
|
|
657
|
+
its own \`decision\` was granted on
|
|
658
|
+
e:locate-weakenings-unbacked the artifact's \`weakenings\` and \`guarantee\` disagree: a
|
|
659
|
+
weakening with no flag, or a weakened flag with no reason
|
|
660
|
+
e:unverifiable-identity both legs declared away: nothing keys identity at all
|
|
661
|
+
e:no-fallback-digest --declare-no-attestation with no usable
|
|
662
|
+
--candidate-image-digest: the weakening would record
|
|
663
|
+
\`imageDigest: null\` and step 4b's fallback compares against
|
|
664
|
+
that field. Read it from the CANDIDATE REVISION's spec —
|
|
665
|
+
\`gcloud run revisions describe <candidate-revision> --region <r>
|
|
666
|
+
--format='value(status.imageDigest)'\` — which needs no
|
|
667
|
+
attesting endpoint.
|
|
668
|
+
e:no-accepted-locate the locate step did not accept
|
|
669
|
+
|
|
670
|
+
candidate-identity serving --sha <shipped> --identity <candidateIdentity.json>
|
|
671
|
+
--health <live-health.json> | --serving-image-digest <digest>
|
|
672
|
+
Step 4b's post-shift confirmation: the LIVE service attests the same tuple (SHA,
|
|
673
|
+
revision, digest). Replaces comparing image-tag strings.
|
|
674
|
+
Under a record carrying the no-attestation weakening there is no tuple to re-read, so pass
|
|
675
|
+
\`--serving-image-digest\` instead — the SERVING revision's configured image digest, compared
|
|
676
|
+
against the candidate revision's digest recorded by \`attest\`. Weaker in a stated way (a
|
|
677
|
+
revision's spec is not the running process), and still a comparison rather than a note.
|
|
678
|
+
e:no-recorded-identity no usable candidateIdentity was handed in
|
|
679
|
+
e:identity-sha-mismatch the recorded identity describes a different commit than
|
|
680
|
+
--sha, so agreeing with the live process proves nothing
|
|
681
|
+
e:identity-guarantee-unbacked the record's \`guarantee\` is contradicted by its own tag,
|
|
682
|
+
by a missing/unusable digest, or by an \`imageDigestSource\`
|
|
683
|
+
naming the other branch's comparison — in EITHER branch: a
|
|
684
|
+
record with no digest cannot be confirmed by the tuple or by
|
|
685
|
+
the fallback, so it stops here
|
|
686
|
+
e:identity-record-incomplete the record is missing a url or a revision
|
|
687
|
+
e:identity-weakenings-unbacked the record's \`weakenings\` and \`guarantee\` disagree
|
|
688
|
+
e:no-attested-tuple this release declared no attesting endpoint and no
|
|
689
|
+
--serving-image-digest was supplied: nothing was compared.
|
|
690
|
+
Take that read and re-run.
|
|
691
|
+
e:sha-mismatch / e:revision-mismatch / e:digest-mismatch
|
|
692
|
+
|
|
693
|
+
The cross-artifact rule, and where it stops: an identity artifact handed between subcommands is
|
|
694
|
+
walked against a NAMED TABLE of fields — its own SHA against the --sha it is used with, its
|
|
695
|
+
\`guarantee\` against the tag, digest and \`imageDigestSource\` backing it, its \`weakenings\`
|
|
696
|
+
against that same \`guarantee\`, and the recorded \`url\` and \`revision\` against the predicate
|
|
697
|
+
that makes each usable. Those fields are re-derived here rather than carried across on the
|
|
698
|
+
strength of the upstream artifact's \`decision\`, and the table is applied in EVERY branch,
|
|
699
|
+
including the declared weakenings, which are precisely the branches where the tuple comparisons
|
|
700
|
+
are absent. The table is also the whole of the rule: a field an artifact carries that is not
|
|
701
|
+
listed in it crosses UNWALKED. So an ACCEPT is not a verification of the whole record — read the
|
|
702
|
+
fields you are about to act on, and run regression-checklist row 4a's identity question yourself
|
|
703
|
+
rather than treating this exit code as having answered it.
|
|
704
|
+
|
|
705
|
+
Declared weakenings (pre-flight item 3): a repo whose pipeline cannot publish a SHA-keyed tag,
|
|
706
|
+
or has no attesting health endpoint, declares it with the matching --declare-* flag and its
|
|
707
|
+
reason. The weakening is then recorded in \`guarantee\`/\`weakenings\` — stated, not assumed. An
|
|
708
|
+
UNdeclared missing leg refuses, and both legs missing refuses even when both are declared.
|
|
709
|
+
|
|
710
|
+
A declaration also owes a SUBSTITUTE, not only a reason: it must leave the operator a check they
|
|
711
|
+
can run. --declare-no-attestation therefore requires --candidate-image-digest, so the fallback
|
|
712
|
+
step 4b routes to has a real digest on both sides. Every field of an emitted identity is
|
|
713
|
+
populated in every branch that accepts — a branch that cannot populate one refuses instead, since
|
|
714
|
+
a null field the doctrine sends someone to compare against reads as "not applicable" and the
|
|
715
|
+
check silently does not happen.
|
|
716
|
+
|
|
717
|
+
Exit codes:
|
|
718
|
+
0 ACCEPT — identity established (possibly under a declared weakening)
|
|
719
|
+
1 REFUSE — the release stops here; the failing ids are named in \`failed\`
|
|
720
|
+
2 usage / unreadable-input error
|
|
721
|
+
|
|
722
|
+
Output: JSON { decision: "accept"|"refuse", phase, failed: [...], guarantee, weakenings, … }
|
|
723
|
+
|
|
724
|
+
Doctrine (why the tag is SHA-keyed, why one traffic entry, what the tuple proves): principle 12
|
|
725
|
+
and step 3 of SKILL.md.`
|
|
726
|
+
|
|
727
|
+
function parseArgs(argv) {
|
|
728
|
+
const opts = {}
|
|
729
|
+
const positional = []
|
|
730
|
+
for (let i = 0; i < argv.length; i++) {
|
|
731
|
+
const a = argv[i]
|
|
732
|
+
if (a === '--help' || a === '-h') opts.help = true
|
|
733
|
+
else if (a.startsWith('--')) opts[a.slice(2)] = argv[++i]
|
|
734
|
+
else positional.push(a)
|
|
735
|
+
}
|
|
736
|
+
return { opts, positional }
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// A declaration's REASON is the whole value of declaring: "we have no SHA-keyed tag" recorded
|
|
740
|
+
// with no reason is the assumption again. A flag whose value is missing (it swallowed the next
|
|
741
|
+
// flag) or empty is a usage error, never a silently-accepted weakening.
|
|
742
|
+
function declaration(opts, flag) {
|
|
743
|
+
if (!(flag in opts)) return { value: null }
|
|
744
|
+
const v = opts[flag]
|
|
745
|
+
if (typeof v !== 'string' || v.trim().length === 0 || v.startsWith('--')) {
|
|
746
|
+
return { error: `--${flag} requires a reason (the weakening is recorded, not assumed)` }
|
|
747
|
+
}
|
|
748
|
+
return { value: v.trim() }
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function readJson(file, what) {
|
|
752
|
+
if (!file) return { error: `usage: --${what} <file> is required` }
|
|
753
|
+
let raw
|
|
754
|
+
try {
|
|
755
|
+
raw = fs.readFileSync(file, 'utf8')
|
|
756
|
+
} catch {
|
|
757
|
+
return { error: `${what} not readable: ${file}` }
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
return { value: JSON.parse(raw) }
|
|
761
|
+
} catch {
|
|
762
|
+
return { error: `${what} is not valid JSON: ${file}` }
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function main(argv) {
|
|
767
|
+
const { opts, positional } = parseArgs(argv)
|
|
768
|
+
const cmd = positional[0]
|
|
769
|
+
|
|
770
|
+
if (opts.help || cmd === 'help') {
|
|
771
|
+
console.log(USAGE)
|
|
772
|
+
return 0
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// A missing subcommand fails to the same exit-2 usage error as an unknown one. Exit 0 here
|
|
776
|
+
// would read as ACCEPT to `candidate-identity … && <continue the release>`.
|
|
777
|
+
if (cmd !== 'locate' && cmd !== 'attest' && cmd !== 'serving') {
|
|
778
|
+
const what = cmd ? `unknown command "${cmd}"` : 'no subcommand given'
|
|
779
|
+
console.error(`candidate-identity: ${what}. Use locate|attest|serving (see --help).`)
|
|
780
|
+
return 2
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let result
|
|
784
|
+
if (cmd === 'locate') {
|
|
785
|
+
// The declaration is validated FIRST so that a flag which swallowed the next flag as its
|
|
786
|
+
// "reason" (`--declare-shared-candidate-tag --traffic f`) is diagnosed as the missing
|
|
787
|
+
// reason it is, rather than as whichever required flag it ate.
|
|
788
|
+
const declared = declaration(opts, 'declare-shared-candidate-tag')
|
|
789
|
+
if (declared.error) {
|
|
790
|
+
console.error(`candidate-identity: ${declared.error}`)
|
|
791
|
+
return 2
|
|
792
|
+
}
|
|
793
|
+
if (!opts.sha) {
|
|
794
|
+
console.error('candidate-identity: usage: locate requires --sha <shipped>')
|
|
795
|
+
return 2
|
|
796
|
+
}
|
|
797
|
+
if (
|
|
798
|
+
'candidate-tag-prefix' in opts &&
|
|
799
|
+
(typeof opts['candidate-tag-prefix'] !== 'string' ||
|
|
800
|
+
!opts['candidate-tag-prefix'] ||
|
|
801
|
+
opts['candidate-tag-prefix'].startsWith('--'))
|
|
802
|
+
) {
|
|
803
|
+
console.error('candidate-identity: --candidate-tag-prefix requires a value')
|
|
804
|
+
return 2
|
|
805
|
+
}
|
|
806
|
+
if (!opts.traffic) {
|
|
807
|
+
console.error('candidate-identity: usage: locate requires --traffic <flattened-traffic-read>')
|
|
808
|
+
return 2
|
|
809
|
+
}
|
|
810
|
+
let text
|
|
811
|
+
try {
|
|
812
|
+
text = fs.readFileSync(opts.traffic, 'utf8')
|
|
813
|
+
} catch {
|
|
814
|
+
console.error(`candidate-identity: traffic read not readable: ${opts.traffic}`)
|
|
815
|
+
return 2
|
|
816
|
+
}
|
|
817
|
+
const traffic = parseTraffic(text)
|
|
818
|
+
if (traffic.error) {
|
|
819
|
+
console.error(`candidate-identity: ${traffic.error}`)
|
|
820
|
+
return 2
|
|
821
|
+
}
|
|
822
|
+
result = evaluateLocate({
|
|
823
|
+
sha: opts.sha,
|
|
824
|
+
traffic,
|
|
825
|
+
declareSharedTag: declared.value,
|
|
826
|
+
candidateTagPrefix: opts['candidate-tag-prefix'] || DEFAULT_CANDIDATE_TAG_PREFIX,
|
|
827
|
+
})
|
|
828
|
+
} else if (cmd === 'attest') {
|
|
829
|
+
const declared = declaration(opts, 'declare-no-attestation')
|
|
830
|
+
if (declared.error) {
|
|
831
|
+
console.error(`candidate-identity: ${declared.error}`)
|
|
832
|
+
return 2
|
|
833
|
+
}
|
|
834
|
+
if (!opts.sha) {
|
|
835
|
+
console.error('candidate-identity: usage: attest requires --sha <shipped>')
|
|
836
|
+
return 2
|
|
837
|
+
}
|
|
838
|
+
const locate = readJson(opts.locate, 'locate')
|
|
839
|
+
if (locate.error) {
|
|
840
|
+
console.error(`candidate-identity: ${locate.error}`)
|
|
841
|
+
return 2
|
|
842
|
+
}
|
|
843
|
+
// An unreadable health file is a REFUSE, not an error: "the endpoint did not answer" is
|
|
844
|
+
// exactly the case that must never fall through to a shift, and it is decided by the
|
|
845
|
+
// e:unattested condition, not by the CLI.
|
|
846
|
+
// The substitute belongs to the declaration, so offering it without one is a mistake worth
|
|
847
|
+
// naming: with an attesting endpoint the digest comes from the attestation, and an input this
|
|
848
|
+
// command silently ignored would be exactly the "looks complete" shape the flag exists to fix.
|
|
849
|
+
if ('candidate-image-digest' in opts && !declared.value) {
|
|
850
|
+
console.error(
|
|
851
|
+
'candidate-identity: --candidate-image-digest is the substitute for --declare-no-attestation; ' +
|
|
852
|
+
'with an attesting endpoint the digest comes from the attestation.',
|
|
853
|
+
)
|
|
854
|
+
return 2
|
|
855
|
+
}
|
|
856
|
+
const health = opts.health ? readJson(opts.health, 'health') : { error: 'absent' }
|
|
857
|
+
result = evaluateAttest({
|
|
858
|
+
sha: opts.sha,
|
|
859
|
+
locate: locate.value,
|
|
860
|
+
health: health.error ? null : health.value,
|
|
861
|
+
declareNoAttestation: declared.value,
|
|
862
|
+
candidateImageDigest: opts['candidate-image-digest'] ?? null,
|
|
863
|
+
})
|
|
864
|
+
} else {
|
|
865
|
+
// `serving` takes the promoted SHA for the same reason `attest` does: without it this
|
|
866
|
+
// subcommand can only prove the recorded identity and the live process agree, which is
|
|
867
|
+
// worth nothing when the record describes another release's candidate.
|
|
868
|
+
if (!opts.sha) {
|
|
869
|
+
console.error('candidate-identity: usage: serving requires --sha <shipped>')
|
|
870
|
+
return 2
|
|
871
|
+
}
|
|
872
|
+
const identity = readJson(opts.identity, 'identity')
|
|
873
|
+
if (identity.error) {
|
|
874
|
+
console.error(`candidate-identity: ${identity.error}`)
|
|
875
|
+
return 2
|
|
876
|
+
}
|
|
877
|
+
// Same rule on this side: the fallback read is for a record that declared the weakening. On an
|
|
878
|
+
// attested record the live tuple is available and stronger, so a supplied digest would be an
|
|
879
|
+
// ignored input standing in for a check that did not run.
|
|
880
|
+
const record = identity.value && identity.value.identity ? identity.value.identity : identity.value
|
|
881
|
+
const recordAttested = !!(record && record.guarantee && record.guarantee.attested)
|
|
882
|
+
if ('serving-image-digest' in opts && recordAttested) {
|
|
883
|
+
console.error(
|
|
884
|
+
'candidate-identity: --serving-image-digest is the declared fallback for a record with ' +
|
|
885
|
+
'`guarantee.attested: false`; this record is attested, so pass --health and compare the tuple.',
|
|
886
|
+
)
|
|
887
|
+
return 2
|
|
888
|
+
}
|
|
889
|
+
const health = opts.health ? readJson(opts.health, 'health') : { error: 'absent' }
|
|
890
|
+
result = evaluateServing({
|
|
891
|
+
identity: identity.value,
|
|
892
|
+
health: health.error ? null : health.value,
|
|
893
|
+
sha: opts.sha,
|
|
894
|
+
servingImageDigest: opts['serving-image-digest'] ?? null,
|
|
895
|
+
})
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
console.log(JSON.stringify(result, null, 2))
|
|
899
|
+
return result.decision === 'accept' ? 0 : 1
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (isMainModule(import.meta.url)) {
|
|
903
|
+
process.exit(main(process.argv.slice(2)))
|
|
904
|
+
}
|