@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,962 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AUTHORIZATION_BOUNDARY_KINDS,
|
|
3
|
+
REOPEN_REASONS,
|
|
4
|
+
classifyRoundMetadata,
|
|
5
|
+
completedReviewRounds,
|
|
6
|
+
deliveryFlow,
|
|
7
|
+
deliveryMode,
|
|
8
|
+
deliveryProvider,
|
|
9
|
+
DELIVERY_MAX_NUDGES,
|
|
10
|
+
validateDeliveryAttempt,
|
|
11
|
+
} from './fix-rounds.mjs'
|
|
12
|
+
import {
|
|
13
|
+
checksForPhase,
|
|
14
|
+
validatePublishOnlyEvidence,
|
|
15
|
+
validateQueueExecutionArtifact,
|
|
16
|
+
validateRequiredCheckExecution,
|
|
17
|
+
validateRequiredCheckPlan,
|
|
18
|
+
} from '../../engineering-runtime/scripts/required-check-plan.mjs'
|
|
19
|
+
import { normalizeChangedSurfaces } from './round-metadata.mjs'
|
|
20
|
+
import { completionBlockers, effectiveUnitKind } from './unit-lifecycle.mjs'
|
|
21
|
+
import { reconciliationErrors } from './reconcile-contained-unit-prs.mjs'
|
|
22
|
+
|
|
23
|
+
export const COMPONENT_CANDIDATE_STATES = ['assembling', 'frozen', 'authorized']
|
|
24
|
+
export const COMPONENT_ROUND_CLASSES = ['unit-discovery', 'integration-fix', 'terminal-authorization']
|
|
25
|
+
export const COMPONENT_ASSEMBLY_RESULTS = ['clean', 'conflict']
|
|
26
|
+
export const COMPONENT_TEST_RESULTS = ['green', 'red', 'not-run']
|
|
27
|
+
|
|
28
|
+
const REOPEN_BOUNDARY_KIND = {
|
|
29
|
+
'ci-red': 'ci',
|
|
30
|
+
'head-rewritten': 'head',
|
|
31
|
+
'contract-revised': 'contract',
|
|
32
|
+
'integration-finding': 'integration',
|
|
33
|
+
// #941 / epic-876: external bot/human CHANGES_REQUESTED after independent PASS
|
|
34
|
+
'external-review': 'head',
|
|
35
|
+
}
|
|
36
|
+
const SHA_RE = /^[0-9a-f]{7,64}$/i
|
|
37
|
+
const clone = (value) => structuredClone(value)
|
|
38
|
+
const isObject = (value) => value != null && typeof value === 'object' && !Array.isArray(value)
|
|
39
|
+
const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0
|
|
40
|
+
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right)
|
|
41
|
+
const unique = (values) => Array.isArray(values) && new Set(values).size === values.length
|
|
42
|
+
|
|
43
|
+
function assertExactObject(value, allowed, label) {
|
|
44
|
+
if (!isObject(value)) throw new Error(`${label} must be an object`)
|
|
45
|
+
const unknown = Object.keys(value).filter((key) => !allowed.includes(key))
|
|
46
|
+
if (unknown.length) throw new Error(`${label} has unknown property ${unknown[0]}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertSha(value, label) {
|
|
50
|
+
if (!nonEmpty(value) || !SHA_RE.test(value)) throw new Error(`${label} must be a 7-64 character hexadecimal SHA`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function componentAndUnits(manifest, componentId) {
|
|
54
|
+
const components = Array.isArray(manifest?.components) ? manifest.components : []
|
|
55
|
+
const component = components.find((entry) => entry?.id === componentId)
|
|
56
|
+
if (!component) throw new Error(`component candidate: no component ${componentId} in this run`)
|
|
57
|
+
if (!Array.isArray(component.units) || !component.units.length || !unique(component.units) || component.units.some((id) => !nonEmpty(id))) {
|
|
58
|
+
throw new Error(`component candidate ${componentId}: units must be a non-empty unique array of unit ids`)
|
|
59
|
+
}
|
|
60
|
+
const byId = new Map((Array.isArray(manifest?.units) ? manifest.units : []).map((unit) => [unit?.id, unit]))
|
|
61
|
+
for (const id of component.units) if (!byId.has(id)) throw new Error(`component candidate ${componentId}: unit ${id} is not in the run roster`)
|
|
62
|
+
return { component, byId }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function boundary(value, label) {
|
|
66
|
+
assertExactObject(value, ['kind', 'value'], label)
|
|
67
|
+
if (!AUTHORIZATION_BOUNDARY_KINDS.includes(value.kind)) throw new Error(`${label}.kind must be one of ${AUTHORIZATION_BOUNDARY_KINDS.join('|')}`)
|
|
68
|
+
if (!nonEmpty(value.value)) throw new Error(`${label}.value must be a non-empty string`)
|
|
69
|
+
return clone(value)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function typedPreliminaryReview(entry, headSha, label) {
|
|
73
|
+
if (!isObject(entry) || entry.verdict !== 'PASS' || entry.headSha !== headSha) return null
|
|
74
|
+
if (classifyRoundMetadata(entry).legacy) return null
|
|
75
|
+
if (!Number.isInteger(entry.round) || entry.round < 1) throw new Error(`${label}.round must be a positive integer`)
|
|
76
|
+
if (!['code', 'evidence'].includes(entry.reviewSubject)) throw new Error(`${label} needs typed reviewSubject`)
|
|
77
|
+
return {
|
|
78
|
+
round: entry.round,
|
|
79
|
+
verdict: entry.verdict,
|
|
80
|
+
headSha: entry.headSha,
|
|
81
|
+
reviewSubject: entry.reviewSubject,
|
|
82
|
+
authorizationBoundary: boundary(entry.authorizationBoundary, `${label}.authorizationBoundary`),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function legacyRoundAuthority(entry, headSha) {
|
|
87
|
+
if (!isObject(entry) || entry.verdict !== 'PASS' || entry.headSha !== headSha) return null
|
|
88
|
+
if (!classifyRoundMetadata(entry).legacy) return null
|
|
89
|
+
const allowed = ['round', 'verdict', 'headSha', 'themeIds', 'scope', 'at', 'evidenceClasses', 'productionMoved']
|
|
90
|
+
if (Object.keys(entry).some((key) => !allowed.includes(key))) return null
|
|
91
|
+
if (!Number.isInteger(entry.round) || entry.round < 1 || typeof entry.productionMoved !== 'boolean') return null
|
|
92
|
+
return entry
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function deepReviewAuthority(entry, headSha) {
|
|
96
|
+
if (!isObject(entry) || entry.verdict !== 'PASS' || entry.headSha !== headSha) return null
|
|
97
|
+
const hasThemes = Object.prototype.hasOwnProperty.call(entry, 'themes')
|
|
98
|
+
const hasFindings = Object.prototype.hasOwnProperty.call(entry, 'findings')
|
|
99
|
+
const allowed = hasThemes
|
|
100
|
+
? ['round', 'verdict', 'themes', 'oracleEscalation', 'round1Depth', 'headSha']
|
|
101
|
+
: ['round', 'verdict', 'findings', 'oracleEscalation', 'round1Depth', 'headSha']
|
|
102
|
+
if (Object.keys(entry).some((key) => !allowed.includes(key))) return null
|
|
103
|
+
if (!Number.isInteger(entry.round) || entry.round < 1 || hasThemes === hasFindings) return null
|
|
104
|
+
if (hasThemes && !Array.isArray(entry.themes)) return null
|
|
105
|
+
if (hasFindings && !Array.isArray(entry.findings)) return null
|
|
106
|
+
return entry
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function legacyPreliminaryReview(unit, headSha) {
|
|
110
|
+
const candidates = []
|
|
111
|
+
for (const [index, entry] of unit.rounds.entries()) {
|
|
112
|
+
const valid = legacyRoundAuthority(entry, headSha)
|
|
113
|
+
if (valid) candidates.push({ entry: valid, source: 'rounds', sourceRank: 0, index })
|
|
114
|
+
}
|
|
115
|
+
for (const [index, entry] of unit.reviewHistory.entries()) {
|
|
116
|
+
const valid = deepReviewAuthority(entry, headSha)
|
|
117
|
+
if (valid) candidates.push({ entry: valid, source: 'reviewHistory', sourceRank: 1, index })
|
|
118
|
+
}
|
|
119
|
+
candidates.sort((left, right) => left.entry.round - right.entry.round
|
|
120
|
+
|| left.sourceRank - right.sourceRank
|
|
121
|
+
|| left.index - right.index)
|
|
122
|
+
const selected = candidates.at(-1)
|
|
123
|
+
if (!selected) return null
|
|
124
|
+
return {
|
|
125
|
+
round: selected.entry.round,
|
|
126
|
+
verdict: selected.entry.verdict,
|
|
127
|
+
headSha: selected.entry.headSha,
|
|
128
|
+
reviewSubject: 'unknown',
|
|
129
|
+
authorizationBoundary: 'unknown',
|
|
130
|
+
legacy: true,
|
|
131
|
+
source: selected.source,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function preliminaryEvidence(unit, componentId) {
|
|
136
|
+
if (unit?.lastVerdict !== 'PASS') throw new Error(`component candidate ${componentId}: unit ${unit?.id} is not preliminarily ready (lastVerdict must be PASS)`)
|
|
137
|
+
assertSha(unit?.pushedSha, `component candidate ${componentId}: unit ${unit?.id}.pushedSha`)
|
|
138
|
+
if (unit.rounds !== undefined && !Array.isArray(unit.rounds)) throw new Error(`component candidate ${componentId}: unit ${unit.id}.rounds must be an array`)
|
|
139
|
+
if (unit.reviewHistory !== undefined && !Array.isArray(unit.reviewHistory)) throw new Error(`component candidate ${componentId}: unit ${unit.id}.reviewHistory must be an array`)
|
|
140
|
+
const rounds = Array.isArray(unit.rounds) ? unit.rounds : []
|
|
141
|
+
const reviewHistory = Array.isArray(unit.reviewHistory) ? unit.reviewHistory : []
|
|
142
|
+
const typed = rounds
|
|
143
|
+
.map((entry, index) => typedPreliminaryReview(entry, unit.pushedSha, `component candidate ${componentId}: unit ${unit.id}.rounds[${index}]`))
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.at(-1)
|
|
146
|
+
const preliminaryReview = typed ?? legacyPreliminaryReview({ rounds, reviewHistory }, unit.pushedSha)
|
|
147
|
+
if (!preliminaryReview) {
|
|
148
|
+
throw new Error(`component candidate ${componentId}: unit ${unit?.id} needs a preliminary PASS on its exact pushed head`)
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
unitId: unit.id,
|
|
152
|
+
headSha: unit.pushedSha,
|
|
153
|
+
pr: nonEmpty(unit.pr) ? unit.pr : null,
|
|
154
|
+
preliminaryReview,
|
|
155
|
+
reviewRoundCount: completedReviewRounds(unit),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function assertIncludedUnits(component, includedUnits) {
|
|
160
|
+
if (!Array.isArray(includedUnits) || !includedUnits.length || !unique(includedUnits) || includedUnits.some((id) => !nonEmpty(id))) {
|
|
161
|
+
throw new Error(`component candidate ${component.id}: includedUnits must be a non-empty unique array`)
|
|
162
|
+
}
|
|
163
|
+
const members = new Set(component.units)
|
|
164
|
+
for (const id of includedUnits) if (!members.has(id)) throw new Error(`component candidate ${component.id}: included unit ${id} is not owned by this component`)
|
|
165
|
+
const ordered = component.units.filter((id) => includedUnits.includes(id))
|
|
166
|
+
if (!same(ordered, includedUnits)) throw new Error(`component candidate ${component.id}: includedUnits must follow component unit order`)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function integratedResult(value, componentId) {
|
|
170
|
+
assertExactObject(value, ['assembly', 'tests', 'conflicts', 'failedTests'], `component candidate ${componentId}.integrated`)
|
|
171
|
+
if (!COMPONENT_ASSEMBLY_RESULTS.includes(value.assembly)) throw new Error(`component candidate ${componentId}: integrated.assembly must be ${COMPONENT_ASSEMBLY_RESULTS.join('|')}`)
|
|
172
|
+
if (!COMPONENT_TEST_RESULTS.includes(value.tests)) throw new Error(`component candidate ${componentId}: integrated.tests must be ${COMPONENT_TEST_RESULTS.join('|')}`)
|
|
173
|
+
if (!Array.isArray(value.conflicts) || value.conflicts.some((conflict) => !isObject(conflict) || !nonEmpty(conflict.unit) || !Array.isArray(conflict.files) || conflict.files.some((file) => !nonEmpty(file)))) {
|
|
174
|
+
throw new Error(`component candidate ${componentId}: integrated.conflicts must be [{ unit, files[] }]`)
|
|
175
|
+
}
|
|
176
|
+
if (!Array.isArray(value.failedTests) || value.failedTests.some((test) => !nonEmpty(test))) throw new Error(`component candidate ${componentId}: integrated.failedTests must be an array of test ids`)
|
|
177
|
+
if (value.assembly === 'clean' && value.conflicts.length) throw new Error(`component candidate ${componentId}: clean assembly cannot carry conflicts`)
|
|
178
|
+
if (value.assembly === 'conflict' && !value.conflicts.length) throw new Error(`component candidate ${componentId}: conflict assembly must name conflicts`)
|
|
179
|
+
if (value.tests === 'green' && value.failedTests.length) throw new Error(`component candidate ${componentId}: green integrated tests cannot carry failures`)
|
|
180
|
+
if (value.tests === 'red' && !value.failedTests.length) throw new Error(`component candidate ${componentId}: red integrated tests must name failed tests`)
|
|
181
|
+
return clone(value)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function integrationFix(value, componentId) {
|
|
185
|
+
assertExactObject(value, ['owner', 'reason', 'changedSurfaces'], `component candidate ${componentId}.integrationFix`)
|
|
186
|
+
if (value.owner !== 'integration') throw new Error(`component candidate ${componentId}: integration-fix owner must be "integration"; unit branches retain their passed heads`)
|
|
187
|
+
if (!nonEmpty(value.reason)) throw new Error(`component candidate ${componentId}: integration-fix requires a reason`)
|
|
188
|
+
return { owner: 'integration', reason: value.reason.trim(), changedSurfaces: normalizeChangedSurfaces(value.changedSurfaces) }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function candidateBase(existing) {
|
|
192
|
+
if (isObject(existing)) return clone(existing)
|
|
193
|
+
return { state: 'assembling', generation: 1, updates: [], frozen: null, authorizations: [], reopens: [] }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function transitionResult(out, componentId) {
|
|
197
|
+
const errors = validateComponentCandidates(out)
|
|
198
|
+
if (errors.length) throw new Error(`component candidate ${componentId}: invalid transition result: ${errors[0]}`)
|
|
199
|
+
return out
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function positivePr(value) {
|
|
203
|
+
if (Number.isInteger(value) && value > 0) return value
|
|
204
|
+
const match = String(value ?? '').match(/(?:^|#)(\d+)$/)
|
|
205
|
+
return match ? Number(match[1]) : null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function initializedDelivery(manifest, component, candidate, authorization) {
|
|
209
|
+
const mode = deliveryMode(manifest)
|
|
210
|
+
const provider = deliveryProvider(manifest)
|
|
211
|
+
if (!['direct', 'queue'].includes(mode)) throw new Error(`component candidate ${component.id}: deliveryMode must be direct|queue`)
|
|
212
|
+
if (mode === 'queue' && !['mergify', 'github'].includes(provider)) {
|
|
213
|
+
throw new Error(`component candidate ${component.id}: queue delivery requires queueProvider mergify|github before terminal authorization`)
|
|
214
|
+
}
|
|
215
|
+
const pr = authorization.ci.pr
|
|
216
|
+
const publishedPr = positivePr(component.pr)
|
|
217
|
+
if (publishedPr != null && publishedPr !== pr) {
|
|
218
|
+
throw new Error(`component candidate ${component.id}: terminal CI PR ${pr} does not match published component PR ${publishedPr}`)
|
|
219
|
+
}
|
|
220
|
+
const prior = isObject(component.delivery) ? component.delivery : null
|
|
221
|
+
if (prior?.outcome === 'merged' || prior?.outcome === 'handed-back') {
|
|
222
|
+
throw new Error(`component candidate ${component.id}: terminal delivery outcome ${prior.outcome} cannot be re-authorized`)
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
pr,
|
|
226
|
+
generation: candidate.generation,
|
|
227
|
+
authorizedHeadSha: authorization.headSha,
|
|
228
|
+
mode,
|
|
229
|
+
provider: mode === 'queue' ? provider : null,
|
|
230
|
+
attempts: Array.isArray(prior?.attempts) ? clone(prior.attempts) : [],
|
|
231
|
+
observations: Array.isArray(prior?.observations) ? clone(prior.observations) : [],
|
|
232
|
+
watch: null,
|
|
233
|
+
queueExecution: null,
|
|
234
|
+
outcome: null,
|
|
235
|
+
outcomeAt: null,
|
|
236
|
+
mergedSha: null,
|
|
237
|
+
mergedAt: null,
|
|
238
|
+
blockedOn: null,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function componentDeliveryContext(manifest, componentId, generation, headSha) {
|
|
243
|
+
const { component, byId } = componentAndUnits(manifest, componentId)
|
|
244
|
+
const candidate = candidateBase(component.candidate)
|
|
245
|
+
const authorization = Array.isArray(candidate.authorizations)
|
|
246
|
+
? candidate.authorizations.findLast((entry) => entry?.generation === generation)
|
|
247
|
+
: null
|
|
248
|
+
if (candidate.state !== 'authorized' || generation !== candidate.generation || authorization?.generation !== generation) {
|
|
249
|
+
throw new Error(`component delivery ${componentId}: generation ${generation} is not the currently authorized component generation ${candidate.generation}`)
|
|
250
|
+
}
|
|
251
|
+
if (headSha !== candidate.frozen?.headSha || authorization.headSha !== headSha) {
|
|
252
|
+
throw new Error(`component delivery ${componentId}: ${headSha} is not the authorized frozen component HEAD ${candidate.frozen?.headSha ?? '(missing)'}`)
|
|
253
|
+
}
|
|
254
|
+
const delivery = component.delivery
|
|
255
|
+
if (!isObject(delivery) || delivery.generation !== generation || delivery.authorizedHeadSha !== headSha) {
|
|
256
|
+
throw new Error(`component delivery ${componentId}: canonical delivery record is stale or missing for authorized generation ${generation}`)
|
|
257
|
+
}
|
|
258
|
+
for (const id of component.units) {
|
|
259
|
+
const unit = byId.get(id)
|
|
260
|
+
if (effectiveUnitKind(unit) !== 'implementation') throw new Error(`component delivery ${componentId}: member ${id} is not an implementation unit`)
|
|
261
|
+
if (unit.status !== 'merge-ready') throw new Error(`component delivery ${componentId}: member ${id} is ${unit.status}, not merge-ready`)
|
|
262
|
+
const blocked = completionBlockers(manifest, unit)
|
|
263
|
+
if (blocked.length) throw new Error(`component delivery ${componentId}: member ${id} has incomplete completion dependencies: ${blocked.join(', ')}`)
|
|
264
|
+
if (unit.delivery !== undefined) throw new Error(`component delivery ${componentId}: member ${id} carries a unit delivery record; shared delivery must be canonical on the component`)
|
|
265
|
+
}
|
|
266
|
+
return { component, candidate, authorization, byId, delivery }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function deliveryObservation(value, label) {
|
|
270
|
+
assertExactObject(value, ['at', 'queueState', 'position', 'unmetConditions', 'ack'], label)
|
|
271
|
+
if (!nonEmpty(value.at)) throw new Error(`${label}.at is required`)
|
|
272
|
+
if (!nonEmpty(value.queueState)) throw new Error(`${label}.queueState is required`)
|
|
273
|
+
if (value.unmetConditions !== undefined && (!Array.isArray(value.unmetConditions) || value.unmetConditions.some((condition) => !nonEmpty(condition)))) {
|
|
274
|
+
throw new Error(`${label}.unmetConditions must be an array of non-empty strings`)
|
|
275
|
+
}
|
|
276
|
+
if (value.ack !== undefined && (!isObject(value.ack) || !['reaction', 'payload'].includes(value.ack.kind) || !nonEmpty(value.ack.ref))) {
|
|
277
|
+
throw new Error(`${label}.ack requires {kind: reaction|payload, ref}`)
|
|
278
|
+
}
|
|
279
|
+
return clone(value)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function recordComponentDeliveryAttempt(manifest, componentId, payload) {
|
|
283
|
+
assertExactObject(payload, ['generation', 'headSha', 'attempt'], `record-component-delivery-attempt ${componentId}`)
|
|
284
|
+
if (!Number.isSafeInteger(payload.generation) || payload.generation < 1) throw new Error(`record-component-delivery-attempt ${componentId}.generation must be a positive integer`)
|
|
285
|
+
assertSha(payload.headSha, `record-component-delivery-attempt ${componentId}.headSha`)
|
|
286
|
+
const out = clone(manifest)
|
|
287
|
+
const { component, delivery } = componentDeliveryContext(out, componentId, payload.generation, payload.headSha)
|
|
288
|
+
if (delivery.outcome != null) throw new Error(`component delivery ${componentId} already reached terminal outcome ${delivery.outcome}`)
|
|
289
|
+
const attemptErrors = validateDeliveryAttempt(payload.attempt, delivery.mode, `component delivery ${componentId} attempt`, delivery.provider)
|
|
290
|
+
if (attemptErrors.length) throw new Error(attemptErrors[0])
|
|
291
|
+
if (payload.attempt.headSha !== payload.headSha) throw new Error(`component delivery ${componentId}: attempt must target the authorized component HEAD ${payload.headSha}`)
|
|
292
|
+
if (payload.attempt.pr !== undefined && positivePr(payload.attempt.pr) !== delivery.pr) throw new Error(`component delivery ${componentId}: attempt PR must equal component PR ${delivery.pr}`)
|
|
293
|
+
const sameGeneration = delivery.attempts.filter((attempt) => attempt?.generation === payload.generation)
|
|
294
|
+
if (sameGeneration.some((attempt) => attempt.action === payload.attempt.action)) {
|
|
295
|
+
throw new Error(`component delivery ${componentId}: exactly one ${payload.attempt.action} attempt is allowed for authorized generation ${payload.generation}; duplicate enqueue/merge refused`)
|
|
296
|
+
}
|
|
297
|
+
component.delivery = {
|
|
298
|
+
...delivery,
|
|
299
|
+
attempts: [...delivery.attempts, {
|
|
300
|
+
generation: payload.generation,
|
|
301
|
+
...clone(payload.attempt),
|
|
302
|
+
...(delivery.mode === 'queue' ? { pr: delivery.pr } : {}),
|
|
303
|
+
}],
|
|
304
|
+
}
|
|
305
|
+
return transitionResult(out, componentId)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function recordComponentDeliveryObservation(manifest, componentId, payload) {
|
|
309
|
+
assertExactObject(payload, ['generation', 'headSha', 'observation'], `record-component-delivery-observation ${componentId}`)
|
|
310
|
+
if (!Number.isSafeInteger(payload.generation) || payload.generation < 1) throw new Error(`record-component-delivery-observation ${componentId}.generation must be a positive integer`)
|
|
311
|
+
assertSha(payload.headSha, `record-component-delivery-observation ${componentId}.headSha`)
|
|
312
|
+
const out = clone(manifest)
|
|
313
|
+
const { component, delivery } = componentDeliveryContext(out, componentId, payload.generation, payload.headSha)
|
|
314
|
+
if (delivery.mode !== 'queue') throw new Error(`component delivery ${componentId}: direct delivery has no queue observations`)
|
|
315
|
+
if (delivery.outcome != null) throw new Error(`component delivery ${componentId} already reached terminal outcome ${delivery.outcome}`)
|
|
316
|
+
const observation = deliveryObservation(payload.observation, `component delivery ${componentId} observation`)
|
|
317
|
+
const previous = isObject(delivery.watch) ? delivery.watch : null
|
|
318
|
+
const position = observation.position ?? null
|
|
319
|
+
const unmetConditions = observation.unmetConditions ?? previous?.unmetConditions ?? null
|
|
320
|
+
const conditionKey = (value) => Array.isArray(value) ? value.join('\u0000') : ''
|
|
321
|
+
const moved = !previous || previous.queueState !== observation.queueState || (previous.position ?? null) !== position
|
|
322
|
+
|| conditionKey(previous.unmetConditions) !== conditionKey(unmetConditions)
|
|
323
|
+
component.delivery = {
|
|
324
|
+
...delivery,
|
|
325
|
+
observations: [...delivery.observations, { generation: payload.generation, headSha: payload.headSha, ...observation }],
|
|
326
|
+
watch: {
|
|
327
|
+
firstObservedAt: previous?.firstObservedAt ?? observation.at,
|
|
328
|
+
lastObservedAt: observation.at,
|
|
329
|
+
lastChangeAt: moved ? observation.at : previous.lastChangeAt,
|
|
330
|
+
queueState: observation.queueState,
|
|
331
|
+
position,
|
|
332
|
+
polls: (previous?.polls ?? 0) + 1,
|
|
333
|
+
...(unmetConditions == null ? {} : { unmetConditions }),
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
return transitionResult(out, componentId)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function recordComponentDeliveryNudge(manifest, componentId, payload) {
|
|
340
|
+
assertExactObject(payload, ['generation', 'headSha', 'nudge'], `record-component-delivery-nudge ${componentId}`)
|
|
341
|
+
if (!Number.isSafeInteger(payload.generation) || payload.generation < 1) throw new Error(`record-component-delivery-nudge ${componentId}.generation must be a positive integer`)
|
|
342
|
+
assertSha(payload.headSha, `record-component-delivery-nudge ${componentId}.headSha`)
|
|
343
|
+
assertExactObject(payload.nudge, ['at', 'command', 'ref'], `record-component-delivery-nudge ${componentId}.nudge`)
|
|
344
|
+
const out = clone(manifest)
|
|
345
|
+
const { component, delivery } = componentDeliveryContext(out, componentId, payload.generation, payload.headSha)
|
|
346
|
+
const flow = deliveryFlow(out)
|
|
347
|
+
if (!flow?.supportsNudge) throw new Error(`component delivery ${componentId}: the ${flow?.key ?? 'unresolved'} flow has no queue re-evaluation nudge`)
|
|
348
|
+
if (!isObject(delivery.watch)) throw new Error(`component delivery ${componentId}: record a queue observation before a nudge`)
|
|
349
|
+
const nudges = Array.isArray(delivery.watch.nudges) ? [...delivery.watch.nudges] : []
|
|
350
|
+
if (nudges.length >= DELIVERY_MAX_NUDGES) throw new Error(`component delivery ${componentId}: already spent its ${DELIVERY_MAX_NUDGES} nudge(s)`)
|
|
351
|
+
const at = nonEmpty(payload.nudge.at) ? payload.nudge.at : new Date().toISOString()
|
|
352
|
+
nudges.push({ at, command: nonEmpty(payload.nudge.command) ? payload.nudge.command : flow.ops.nudge, ...(nonEmpty(payload.nudge.ref) ? { ref: payload.nudge.ref } : {}) })
|
|
353
|
+
component.delivery = { ...delivery, watch: { ...delivery.watch, nudges } }
|
|
354
|
+
return transitionResult(out, componentId)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function validatedQueueExecution(componentId, delivery, authorization, value) {
|
|
358
|
+
if (value === undefined) return null
|
|
359
|
+
assertExactObject(value, ['artifact', 'evidence'], `component delivery ${componentId}.queueExecution`)
|
|
360
|
+
if (!nonEmpty(value.artifact)) throw new Error(`component delivery ${componentId}.queueExecution.artifact is required`)
|
|
361
|
+
const plan = authorization.ci?.requiredCheckPlan
|
|
362
|
+
const validation = validateQueueExecutionArtifact(plan, value.evidence, {
|
|
363
|
+
expectedRepository: plan?.repository,
|
|
364
|
+
expectedPr: delivery.pr,
|
|
365
|
+
expectedPrHeadSha: delivery.authorizedHeadSha,
|
|
366
|
+
label: `component delivery ${componentId}.queueExecution.evidence`,
|
|
367
|
+
})
|
|
368
|
+
if (!validation.ok) throw new Error(validation.errors[0])
|
|
369
|
+
return clone(value)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function recordComponentDeliveryOutcome(manifest, componentId, payload) {
|
|
373
|
+
assertExactObject(payload, ['generation', 'headSha', 'outcome', 'at', 'mergedSha', 'mergedAt', 'blockedOn', 'queueExecution'], `record-component-delivery-outcome ${componentId}`)
|
|
374
|
+
if (!Number.isSafeInteger(payload.generation) || payload.generation < 1) throw new Error(`record-component-delivery-outcome ${componentId}.generation must be a positive integer`)
|
|
375
|
+
assertSha(payload.headSha, `record-component-delivery-outcome ${componentId}.headSha`)
|
|
376
|
+
if (!['merged', 'handed-back'].includes(payload.outcome)) throw new Error(`record-component-delivery-outcome ${componentId}.outcome must be merged|handed-back`)
|
|
377
|
+
const out = clone(manifest)
|
|
378
|
+
const { component, authorization, delivery, byId } = componentDeliveryContext(out, componentId, payload.generation, payload.headSha)
|
|
379
|
+
if (delivery.outcome != null) throw new Error(`component delivery ${componentId} already reached terminal outcome ${delivery.outcome}`)
|
|
380
|
+
let queueExecution = null
|
|
381
|
+
if (payload.outcome === 'merged') {
|
|
382
|
+
assertSha(payload.mergedSha, `record-component-delivery-outcome ${componentId}.mergedSha`)
|
|
383
|
+
const requiredAction = delivery.mode === 'queue' ? 'enqueue' : 'merge'
|
|
384
|
+
if (!delivery.attempts.some((attempt) => attempt.generation === payload.generation && attempt.action === requiredAction)) {
|
|
385
|
+
throw new Error(`component delivery ${componentId}: merged outcome requires a recorded ${requiredAction} attempt in the authorized generation`)
|
|
386
|
+
}
|
|
387
|
+
const queueProofRequired = checksForPhase(authorization.ci?.requiredCheckPlan, 'queue-head').length > 0
|
|
388
|
+
if (delivery.mode === 'direct' && payload.queueExecution !== undefined) throw new Error(`component delivery ${componentId}: direct delivery cannot carry queue-execution evidence`)
|
|
389
|
+
if (delivery.mode === 'queue' && queueProofRequired && payload.queueExecution === undefined) throw new Error(`component delivery ${componentId}: successful queue delivery requires validated queue-execution evidence`)
|
|
390
|
+
queueExecution = validatedQueueExecution(componentId, delivery, authorization, payload.queueExecution)
|
|
391
|
+
} else {
|
|
392
|
+
if (!nonEmpty(payload.blockedOn)) throw new Error(`record-component-delivery-outcome ${componentId}.blockedOn is required for handed-back`)
|
|
393
|
+
if (payload.queueExecution !== undefined) throw new Error(`component delivery ${componentId}: handed-back cannot claim queue-execution success`)
|
|
394
|
+
}
|
|
395
|
+
component.delivery = {
|
|
396
|
+
...delivery,
|
|
397
|
+
queueExecution,
|
|
398
|
+
outcome: payload.outcome,
|
|
399
|
+
outcomeAt: nonEmpty(payload.at) ? payload.at : new Date().toISOString(),
|
|
400
|
+
mergedSha: payload.outcome === 'merged' ? payload.mergedSha : null,
|
|
401
|
+
mergedAt: payload.outcome === 'merged' && nonEmpty(payload.mergedAt) ? payload.mergedAt : null,
|
|
402
|
+
blockedOn: payload.outcome === 'handed-back' ? payload.blockedOn : null,
|
|
403
|
+
}
|
|
404
|
+
for (const id of component.units) {
|
|
405
|
+
const unit = byId.get(id)
|
|
406
|
+
unit.disposition = payload.outcome === 'merged' ? 'delivered' : 'handed-back'
|
|
407
|
+
unit.deliveryRef = { componentId, generation: payload.generation }
|
|
408
|
+
}
|
|
409
|
+
return transitionResult(out, componentId)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function recordComponentCandidate(manifest, componentId, payload, { at = new Date().toISOString() } = {}) {
|
|
413
|
+
// targetBaseSha optional (#944 / epic-867): the fetched origin/<targetBranch> tip at
|
|
414
|
+
// this assemble/re-ALIVE. Stored as components[].baseShaAtAssemble so mergeable can
|
|
415
|
+
// refuse when the trunk moved without a re-verify.
|
|
416
|
+
assertExactObject(payload, ['roundClass', 'branch', 'headSha', 'includedUnits', 'integrated', 'integrationFix', 'targetBaseSha'], `record-component-candidate ${componentId}`)
|
|
417
|
+
if (!['unit-discovery', 'integration-fix'].includes(payload.roundClass)) throw new Error(`record-component-candidate ${componentId}: roundClass must be unit-discovery|integration-fix`)
|
|
418
|
+
if (!nonEmpty(payload.branch)) throw new Error(`record-component-candidate ${componentId}: branch is required`)
|
|
419
|
+
assertSha(payload.headSha, `record-component-candidate ${componentId}.headSha`)
|
|
420
|
+
if (payload.targetBaseSha !== undefined) assertSha(payload.targetBaseSha, `record-component-candidate ${componentId}.targetBaseSha`)
|
|
421
|
+
|
|
422
|
+
const out = clone(manifest)
|
|
423
|
+
const { component, byId } = componentAndUnits(out, componentId)
|
|
424
|
+
assertIncludedUnits(component, payload.includedUnits)
|
|
425
|
+
const candidate = candidateBase(component.candidate)
|
|
426
|
+
if (candidate.state !== 'assembling') throw new Error(`component candidate ${componentId} is ${candidate.state}; reopen it before moving the candidate head`)
|
|
427
|
+
const prior = candidate.updates.at(-1)
|
|
428
|
+
if (prior?.headSha === payload.headSha) throw new Error(`component candidate ${componentId}: candidate head did not move`)
|
|
429
|
+
if (prior?.branch && prior.branch !== payload.branch) throw new Error(`component candidate ${componentId}: integration branch cannot move from ${prior.branch} to ${payload.branch}`)
|
|
430
|
+
if (prior) {
|
|
431
|
+
const current = new Set(payload.includedUnits)
|
|
432
|
+
if (prior.includedUnits.some((id) => !current.has(id))) throw new Error(`component candidate ${componentId}: included units cannot shrink; unit ownership/evidence must be preserved`)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const unitEvidence = payload.includedUnits.map((id) => preliminaryEvidence(byId.get(id), componentId))
|
|
436
|
+
let fix = null
|
|
437
|
+
if (payload.roundClass === 'integration-fix') {
|
|
438
|
+
fix = integrationFix(payload.integrationFix, componentId)
|
|
439
|
+
if (!prior || !same(prior.includedUnits, payload.includedUnits) || !same(prior.unitEvidence, unitEvidence)) {
|
|
440
|
+
throw new Error(`component candidate ${componentId}: integration-fix must preserve included units and unit heads byte-for-byte; record changed unit evidence as unit-discovery`)
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
if (payload.integrationFix !== undefined) throw new Error(`component candidate ${componentId}: unit-discovery cannot carry integrationFix`)
|
|
444
|
+
if (prior && same(prior.unitEvidence, unitEvidence)) throw new Error(`component candidate ${componentId}: unit-discovery must add or update unit candidate evidence`)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
candidate.updates.push({
|
|
448
|
+
roundClass: payload.roundClass,
|
|
449
|
+
generation: candidate.generation,
|
|
450
|
+
branch: payload.branch,
|
|
451
|
+
headSha: payload.headSha,
|
|
452
|
+
includedUnits: clone(payload.includedUnits),
|
|
453
|
+
unitEvidence,
|
|
454
|
+
integrated: integratedResult(payload.integrated, componentId),
|
|
455
|
+
...(fix ? { integrationFix: fix } : {}),
|
|
456
|
+
at,
|
|
457
|
+
})
|
|
458
|
+
component.candidate = candidate
|
|
459
|
+
if (payload.targetBaseSha !== undefined) {
|
|
460
|
+
component.baseShaAtAssemble = payload.targetBaseSha
|
|
461
|
+
// A re-ALIVE against a new trunk tip invalidates any prior enqueue-base attestation.
|
|
462
|
+
delete component.baseShaAtEnqueue
|
|
463
|
+
}
|
|
464
|
+
return transitionResult(out, componentId)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// #944 / epic-867: record the fetched target tip immediately before delivery. Direct
|
|
468
|
+
// delivery owns target integration, so its enqueue/merge attestation must still equal the
|
|
469
|
+
// assembled base. A queue provider owns speculative target integration: it may observe a
|
|
470
|
+
// newer descendant tip, while the live delivery gate proves baseShaAtAssemble remains in
|
|
471
|
+
// that target history and tests the immutable PR head on the synthetic queue branch.
|
|
472
|
+
export function recordComponentTargetBase(manifest, componentId, payload, { at = new Date().toISOString() } = {}) {
|
|
473
|
+
assertExactObject(payload, ['phase', 'baseSha'], `record-component-target-base ${componentId}`)
|
|
474
|
+
if (!['assemble', 'enqueue'].includes(payload.phase)) {
|
|
475
|
+
throw new Error(`record-component-target-base ${componentId}: phase must be assemble|enqueue`)
|
|
476
|
+
}
|
|
477
|
+
assertSha(payload.baseSha, `record-component-target-base ${componentId}.baseSha`)
|
|
478
|
+
const out = clone(manifest)
|
|
479
|
+
const { component } = componentAndUnits(out, componentId)
|
|
480
|
+
if (payload.phase === 'assemble') {
|
|
481
|
+
component.baseShaAtAssemble = payload.baseSha
|
|
482
|
+
delete component.baseShaAtEnqueue
|
|
483
|
+
} else {
|
|
484
|
+
if (!nonEmpty(component.baseShaAtAssemble)) {
|
|
485
|
+
throw new Error(`record-component-target-base ${componentId}: baseShaAtAssemble is required before enqueue attestation — re-assemble/re-ALIVE with targetBaseSha first`)
|
|
486
|
+
}
|
|
487
|
+
const queueOwned = out.deliveryMode === 'queue'
|
|
488
|
+
// Direct delivery must integrate the exact live tip before merge. Queue delivery only
|
|
489
|
+
// records the observed tip here; ancestry is deliberately not trusted from manifest
|
|
490
|
+
// data and is proved live by combine-and-verify before enqueue.
|
|
491
|
+
if (!queueOwned && component.baseShaAtAssemble !== payload.baseSha) {
|
|
492
|
+
throw new Error(`record-component-target-base ${componentId}: target tip ${payload.baseSha} advanced past baseShaAtAssemble ${component.baseShaAtAssemble} — target-drift path: back-merge/reassemble, re-verify, re-ALIVE, then re-attest`)
|
|
493
|
+
}
|
|
494
|
+
component.baseShaAtEnqueue = payload.baseSha
|
|
495
|
+
component.targetIntegrationOwner = queueOwned ? 'queue' : 'source'
|
|
496
|
+
}
|
|
497
|
+
component.targetBaseRecordedAt = at
|
|
498
|
+
return out
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Pure policy over live ancestry facts. Direct delivery requires exact-tip integration.
|
|
502
|
+
// Queue delivery permits forward movement only when the recorded assembly base is still an
|
|
503
|
+
// ancestor of the live target; the queue then proves compatibility on its synthetic head.
|
|
504
|
+
// Absent assemble base remains a legacy no-signal shape.
|
|
505
|
+
export function targetBaseDrift({ baseShaAtAssemble, baseShaAtEnqueue, liveTargetSha, deliveryMode = 'direct', assembleBaseIsAncestor = false } = {}) {
|
|
506
|
+
if (!nonEmpty(baseShaAtAssemble)) {
|
|
507
|
+
return { drifted: false, reasons: [] }
|
|
508
|
+
}
|
|
509
|
+
if (!nonEmpty(liveTargetSha)) {
|
|
510
|
+
return { drifted: true, reasons: ['live target SHA is unavailable — target integration ancestry cannot be proved'] }
|
|
511
|
+
}
|
|
512
|
+
if (baseShaAtAssemble === liveTargetSha) {
|
|
513
|
+
// Tip still at assemble base — enqueue attestation optional when tip is unchanged.
|
|
514
|
+
return { drifted: false, reasons: [] }
|
|
515
|
+
}
|
|
516
|
+
if (deliveryMode === 'queue' && assembleBaseIsAncestor) {
|
|
517
|
+
return { drifted: false, reasons: [] }
|
|
518
|
+
}
|
|
519
|
+
// Direct delivery may re-attest only after integrating and re-verifying the new tip.
|
|
520
|
+
if (deliveryMode !== 'queue' && nonEmpty(baseShaAtEnqueue) && baseShaAtEnqueue === liveTargetSha) {
|
|
521
|
+
return { drifted: false, reasons: [] }
|
|
522
|
+
}
|
|
523
|
+
const reason = deliveryMode === 'queue'
|
|
524
|
+
? `recorded assembly base ${baseShaAtAssemble} is not an ancestor of live target ${liveTargetSha} — queue-owned speculative integration accepts forward target movement, never rewritten or divergent target history`
|
|
525
|
+
: `target trunk moved since assemble (baseShaAtAssemble ${baseShaAtAssemble} → live ${liveTargetSha}) without re-ALIVE / baseShaAtEnqueue matching the new tip — back-merge, re-verify CI/smoke, delta integrated review, then record-component-target-base enqueue (epic-867/#944)`
|
|
526
|
+
return { drifted: true, reasons: [reason] }
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function freezeComponentCandidate(manifest, componentId, payload, { at = new Date().toISOString() } = {}) {
|
|
530
|
+
assertExactObject(payload, ['headSha'], `freeze-component-candidate ${componentId}`)
|
|
531
|
+
assertSha(payload.headSha, `freeze-component-candidate ${componentId}.headSha`)
|
|
532
|
+
const out = clone(manifest)
|
|
533
|
+
const { component, byId } = componentAndUnits(out, componentId)
|
|
534
|
+
const candidate = candidateBase(component.candidate)
|
|
535
|
+
if (candidate.state !== 'assembling') throw new Error(`component candidate ${componentId} must be assembling before freeze (got ${candidate.state})`)
|
|
536
|
+
const latest = candidate.updates.at(-1)
|
|
537
|
+
const complete = latest && same(latest.includedUnits, component.units)
|
|
538
|
+
const clean = latest?.integrated?.assembly === 'clean' && latest?.integrated?.tests === 'green'
|
|
539
|
+
&& latest.integrated.conflicts.length === 0 && latest.integrated.failedTests.length === 0
|
|
540
|
+
const currentUnitEvidence = complete ? component.units.map((id) => preliminaryEvidence(byId.get(id), componentId)) : []
|
|
541
|
+
const current = complete && same(latest.unitEvidence, currentUnitEvidence)
|
|
542
|
+
if (!latest || latest.headSha !== payload.headSha || !complete || !clean || !current) {
|
|
543
|
+
throw new Error(`component candidate ${componentId}: freeze requires the latest exact head with the complete component, clean assembly, and green integrated tests`)
|
|
544
|
+
}
|
|
545
|
+
candidate.state = 'frozen'
|
|
546
|
+
candidate.frozen = {
|
|
547
|
+
generation: candidate.generation,
|
|
548
|
+
headSha: latest.headSha,
|
|
549
|
+
branch: latest.branch,
|
|
550
|
+
includedUnits: clone(latest.includedUnits),
|
|
551
|
+
unitEvidence: clone(latest.unitEvidence),
|
|
552
|
+
authorizationBoundary: { kind: 'integration', value: latest.headSha },
|
|
553
|
+
at,
|
|
554
|
+
}
|
|
555
|
+
component.candidate = candidate
|
|
556
|
+
return transitionResult(out, componentId)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function terminalEvidence(payload, component, candidate, unitById = component.__unitById) {
|
|
560
|
+
assertExactObject(payload, ['headSha', 'ci', 'review', 'smoke'], `authorize-component-candidate ${component.id}`)
|
|
561
|
+
const head = candidate.frozen?.headSha
|
|
562
|
+
assertSha(payload.headSha, `authorize-component-candidate ${component.id}.headSha`)
|
|
563
|
+
if (candidate.state !== 'frozen' || payload.headSha !== head) throw new Error(`component candidate ${component.id}: terminal authorization must target the exact frozen HEAD`)
|
|
564
|
+
|
|
565
|
+
assertExactObject(payload.ci, ['headSha', 'status', 'artifact', 'pr', 'jobs', 'requiredCheckPlan', 'publishOnlyEvidence'], `component candidate ${component.id}.ci`)
|
|
566
|
+
assertExactObject(payload.review, ['headSha', 'verdict', 'reviewSubject', 'highRisk', 'authorizationBoundary', 'checklist', 'scanFindings', 'artifact'], `component candidate ${component.id}.review`)
|
|
567
|
+
assertExactObject(payload.smoke, ['headSha', 'status', 'artifact', 'reason'], `component candidate ${component.id}.smoke`)
|
|
568
|
+
for (const [name, evidence] of [['CI', payload.ci], ['review', payload.review], ['smoke', payload.smoke]]) {
|
|
569
|
+
if (evidence.headSha !== head) throw new Error(`component candidate ${component.id}: terminal ${name} headSha must equal the exact frozen HEAD ${head}`)
|
|
570
|
+
}
|
|
571
|
+
if (payload.ci.status !== 'green') throw new Error(`component candidate ${component.id}: exact-head integrated CI must be green`)
|
|
572
|
+
if (!Number.isInteger(payload.ci.pr) || payload.ci.pr < 1) throw new Error(`component candidate ${component.id}: exact-head integrated CI requires a positive PR number`)
|
|
573
|
+
const plan = validateRequiredCheckPlan(payload.ci.requiredCheckPlan, { expectedHeadSha: head, requireEvidence: true, label: `component candidate ${component.id}.ci.requiredCheckPlan` })
|
|
574
|
+
if (!plan.ok) throw new Error(plan.errors[0])
|
|
575
|
+
const publishOnly = validatePublishOnlyEvidence(payload.ci.requiredCheckPlan, payload.ci.publishOnlyEvidence, { expectedHeadSha: head, expectedRepository: payload.ci.requiredCheckPlan.repository, expectedPr: payload.ci.pr, label: `component candidate ${component.id}.ci.publishOnlyEvidence` })
|
|
576
|
+
if (!publishOnly.ok) throw new Error(publishOnly.errors[0])
|
|
577
|
+
const execution = validateRequiredCheckExecution(payload.ci.requiredCheckPlan, payload.ci.jobs, { expectedHeadSha: head })
|
|
578
|
+
if (!execution.ok) throw new Error(`component candidate ${component.id}: required-check-plan execution invalid: ${execution.errors[0]}`)
|
|
579
|
+
for (const [name, evidence] of [['CI', payload.ci], ['review', payload.review], ['smoke', payload.smoke]]) {
|
|
580
|
+
if (!nonEmpty(evidence.artifact)) throw new Error(`component candidate ${component.id}: terminal ${name} requires an artifact locator`)
|
|
581
|
+
}
|
|
582
|
+
const reviewBoundary = boundary(payload.review.authorizationBoundary, `component candidate ${component.id}.review.authorizationBoundary`)
|
|
583
|
+
if (payload.review.verdict !== 'PASS' || payload.review.reviewSubject !== 'code' || payload.review.checklist !== 'PASS' || payload.review.scanFindings !== 0) {
|
|
584
|
+
throw new Error(`component candidate ${component.id}: terminal integrated review must PASS code on the exact head with checklist PASS and zero scan findings`)
|
|
585
|
+
}
|
|
586
|
+
if (reviewBoundary.kind !== 'integration' || reviewBoundary.value !== head) throw new Error(`component candidate ${component.id}: terminal review authorization boundary must be the component HEAD`)
|
|
587
|
+
const highRisk = candidate.frozen.unitEvidence.some((evidence) => {
|
|
588
|
+
const unit = unitById?.get(evidence.unitId)
|
|
589
|
+
return unit?.highRisk === true
|
|
590
|
+
})
|
|
591
|
+
if (highRisk && payload.review.highRisk !== true) throw new Error(`component candidate ${component.id}: high-risk integrated review is mandatory`)
|
|
592
|
+
const smokeAlive = payload.smoke.status === 'ALIVE'
|
|
593
|
+
const smokeNA = payload.smoke.status === 'not-applicable' && nonEmpty(payload.smoke.reason)
|
|
594
|
+
if (!smokeAlive && !smokeNA) throw new Error(`component candidate ${component.id}: smoke must be ALIVE, or not-applicable with a reason`)
|
|
595
|
+
return { ci: clone(payload.ci), review: { ...clone(payload.review), authorizationBoundary: reviewBoundary }, smoke: clone(payload.smoke) }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function legacyTerminalEvidence(payload, component, candidate, unitById = component.__unitById) {
|
|
599
|
+
assertExactObject(payload, ['headSha', 'ci', 'review', 'smoke'], `legacy component authorization ${component.id}`)
|
|
600
|
+
const head = candidate.frozen?.headSha
|
|
601
|
+
assertSha(payload.headSha, `legacy component authorization ${component.id}.headSha`)
|
|
602
|
+
if (candidate.state !== 'frozen' || payload.headSha !== head) throw new Error(`legacy component authorization ${component.id} must target the exact frozen HEAD`)
|
|
603
|
+
|
|
604
|
+
assertExactObject(payload.ci, ['headSha', 'status', 'artifact', 'jobs'], `legacy component authorization ${component.id}.ci`)
|
|
605
|
+
assertExactObject(payload.review, ['headSha', 'verdict', 'reviewSubject', 'highRisk', 'authorizationBoundary', 'checklist', 'scanFindings', 'artifact'], `legacy component authorization ${component.id}.review`)
|
|
606
|
+
assertExactObject(payload.smoke, ['headSha', 'status', 'artifact', 'reason'], `legacy component authorization ${component.id}.smoke`)
|
|
607
|
+
for (const [name, evidence] of [['CI', payload.ci], ['review', payload.review], ['smoke', payload.smoke]]) {
|
|
608
|
+
if (evidence.headSha !== head) throw new Error(`legacy component authorization ${component.id}: ${name} headSha must equal ${head}`)
|
|
609
|
+
if (!nonEmpty(evidence.artifact)) throw new Error(`legacy component authorization ${component.id}: ${name} requires an artifact locator`)
|
|
610
|
+
}
|
|
611
|
+
if (payload.ci.status !== 'green') throw new Error(`legacy component authorization ${component.id}: exact-head CI must be green`)
|
|
612
|
+
const reviewBoundary = boundary(payload.review.authorizationBoundary, `legacy component authorization ${component.id}.review.authorizationBoundary`)
|
|
613
|
+
if (payload.review.verdict !== 'PASS' || payload.review.reviewSubject !== 'code' || payload.review.checklist !== 'PASS' || payload.review.scanFindings !== 0) {
|
|
614
|
+
throw new Error(`legacy component authorization ${component.id}: integrated review must PASS code with checklist PASS and zero scan findings`)
|
|
615
|
+
}
|
|
616
|
+
if (reviewBoundary.kind !== 'integration' || reviewBoundary.value !== head) throw new Error(`legacy component authorization ${component.id}: review boundary must bind the component HEAD`)
|
|
617
|
+
const highRisk = candidate.frozen.unitEvidence.some((evidence) => unitById?.get(evidence.unitId)?.highRisk === true)
|
|
618
|
+
if (highRisk && payload.review.highRisk !== true) throw new Error(`legacy component authorization ${component.id}: high-risk integrated review is mandatory`)
|
|
619
|
+
const smokeAlive = payload.smoke.status === 'ALIVE'
|
|
620
|
+
const smokeNA = payload.smoke.status === 'not-applicable' && nonEmpty(payload.smoke.reason)
|
|
621
|
+
if (!smokeAlive && !smokeNA) throw new Error(`legacy component authorization ${component.id}: smoke must be ALIVE, or not-applicable with a reason`)
|
|
622
|
+
return { ci: clone(payload.ci), review: { ...clone(payload.review), authorizationBoundary: reviewBoundary }, smoke: clone(payload.smoke) }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function authorizeComponentCandidate(manifest, componentId, payload, { at = new Date().toISOString() } = {}) {
|
|
626
|
+
const out = clone(manifest)
|
|
627
|
+
const { component, byId } = componentAndUnits(out, componentId)
|
|
628
|
+
const candidate = candidateBase(component.candidate)
|
|
629
|
+
const evidence = terminalEvidence(payload, component, candidate, byId)
|
|
630
|
+
candidate.state = 'authorized'
|
|
631
|
+
const authorization = {
|
|
632
|
+
roundClass: 'terminal-authorization',
|
|
633
|
+
generation: candidate.generation,
|
|
634
|
+
headSha: payload.headSha,
|
|
635
|
+
authorizationBoundary: clone(evidence.review.authorizationBoundary),
|
|
636
|
+
...evidence,
|
|
637
|
+
at,
|
|
638
|
+
}
|
|
639
|
+
candidate.authorizations.push(authorization)
|
|
640
|
+
component.candidate = candidate
|
|
641
|
+
component.authorizationBoundary = clone(evidence.review.authorizationBoundary)
|
|
642
|
+
component.delivery = initializedDelivery(out, component, candidate, authorization)
|
|
643
|
+
return transitionResult(out, componentId)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function reopenComponentCandidate(manifest, componentId, payload, { at = new Date().toISOString() } = {}) {
|
|
647
|
+
assertExactObject(payload, ['reopenReason', 'authorizationBoundary'], `reopen-component-candidate ${componentId}`)
|
|
648
|
+
if (!REOPEN_REASONS.includes(payload.reopenReason)) throw new Error(`reopen-component-candidate ${componentId}: reopen reason must be one of ${REOPEN_REASONS.join('|')}`)
|
|
649
|
+
const nextBoundary = boundary(payload.authorizationBoundary, `reopen-component-candidate ${componentId}.authorizationBoundary`)
|
|
650
|
+
if (nextBoundary.kind !== REOPEN_BOUNDARY_KIND[payload.reopenReason]) throw new Error(`reopen-component-candidate ${componentId}: reopen reason ${payload.reopenReason} requires authorizationBoundary.kind ${REOPEN_BOUNDARY_KIND[payload.reopenReason]}`)
|
|
651
|
+
|
|
652
|
+
const out = clone(manifest)
|
|
653
|
+
const { component } = componentAndUnits(out, componentId)
|
|
654
|
+
const candidate = candidateBase(component.candidate)
|
|
655
|
+
if (!['frozen', 'authorized'].includes(candidate.state)) throw new Error(`component candidate ${componentId}: only a frozen or authorized candidate can reopen`)
|
|
656
|
+
if (component.delivery?.outcome === 'merged' || component.delivery?.outcome === 'handed-back') {
|
|
657
|
+
throw new Error(`component candidate ${componentId}: terminal component delivery cannot reopen`)
|
|
658
|
+
}
|
|
659
|
+
const priorBoundary = candidate.state === 'authorized' ? component.authorizationBoundary : candidate.frozen?.authorizationBoundary
|
|
660
|
+
if (!isObject(priorBoundary)) throw new Error(`component candidate ${componentId}: prior authorization boundary is missing`)
|
|
661
|
+
if (same(priorBoundary, nextBoundary)) throw new Error(`component candidate ${componentId}: authorizationBoundary must change when the candidate reopens`)
|
|
662
|
+
candidate.reopens.push({
|
|
663
|
+
fromGeneration: candidate.generation,
|
|
664
|
+
fromHeadSha: candidate.frozen?.headSha ?? null,
|
|
665
|
+
reopenReason: payload.reopenReason,
|
|
666
|
+
priorAuthorizationBoundary: clone(priorBoundary),
|
|
667
|
+
authorizationBoundary: nextBoundary,
|
|
668
|
+
at,
|
|
669
|
+
})
|
|
670
|
+
candidate.generation += 1
|
|
671
|
+
candidate.state = 'assembling'
|
|
672
|
+
candidate.frozen = null
|
|
673
|
+
component.candidate = candidate
|
|
674
|
+
component.authorizationBoundary = nextBoundary
|
|
675
|
+
return transitionResult(out, componentId)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function componentRoundProfile(component) {
|
|
679
|
+
const candidate = isObject(component?.candidate) ? component.candidate : {}
|
|
680
|
+
const updates = Array.isArray(candidate.updates) ? candidate.updates : []
|
|
681
|
+
const authorizations = Array.isArray(candidate.authorizations) ? candidate.authorizations : []
|
|
682
|
+
const unitDiscovery = updates.filter((entry) => entry?.roundClass === 'unit-discovery').length
|
|
683
|
+
const integrationFix = updates.filter((entry) => entry?.roundClass === 'integration-fix').length
|
|
684
|
+
const terminalAuthorization = authorizations.filter((entry) => entry?.roundClass === 'terminal-authorization').length
|
|
685
|
+
return { unitDiscovery, integrationFix, terminalAuthorization, total: unitDiscovery + integrationFix + terminalAuthorization }
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function captureError(errors, fn) {
|
|
689
|
+
try { fn() } catch (error) { errors.push(error.message) }
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function validateStoredUnitEvidence(evidence, label, errors) {
|
|
693
|
+
captureError(errors, () => assertExactObject(evidence, ['unitId', 'headSha', 'pr', 'preliminaryReview', 'reviewRoundCount'], label))
|
|
694
|
+
if (!isObject(evidence)) return
|
|
695
|
+
if (!nonEmpty(evidence.unitId)) errors.push(`${label}.unitId must be a non-empty string`)
|
|
696
|
+
captureError(errors, () => assertSha(evidence.headSha, `${label}.headSha`))
|
|
697
|
+
if (evidence.pr !== null && !nonEmpty(evidence.pr)) errors.push(`${label}.pr must be null or a non-empty string`)
|
|
698
|
+
if (!Number.isInteger(evidence.reviewRoundCount) || evidence.reviewRoundCount < 1) errors.push(`${label}.reviewRoundCount must be a positive integer`)
|
|
699
|
+
const review = evidence.preliminaryReview
|
|
700
|
+
const legacy = review?.legacy === true
|
|
701
|
+
const allowed = legacy
|
|
702
|
+
? ['round', 'verdict', 'headSha', 'reviewSubject', 'authorizationBoundary', 'legacy', 'source']
|
|
703
|
+
: ['round', 'verdict', 'headSha', 'reviewSubject', 'authorizationBoundary']
|
|
704
|
+
captureError(errors, () => assertExactObject(review, allowed, `${label}.preliminaryReview`))
|
|
705
|
+
if (!isObject(review)) return
|
|
706
|
+
if (!Number.isInteger(review.round) || review.round < 1) errors.push(`${label}.preliminaryReview.round must be a positive integer`)
|
|
707
|
+
if (review.verdict !== 'PASS') errors.push(`${label}.preliminaryReview.verdict must be PASS`)
|
|
708
|
+
if (review.headSha !== evidence.headSha) errors.push(`${label}.preliminaryReview must target the evidence head`)
|
|
709
|
+
if (legacy) {
|
|
710
|
+
if (review.reviewSubject !== 'unknown') errors.push(`${label}.preliminaryReview legacy reviewSubject must be unknown`)
|
|
711
|
+
if (review.authorizationBoundary !== 'unknown') errors.push(`${label}.preliminaryReview legacy authorizationBoundary must be unknown`)
|
|
712
|
+
if (!['rounds', 'reviewHistory'].includes(review.source)) errors.push(`${label}.preliminaryReview legacy source must be rounds|reviewHistory`)
|
|
713
|
+
} else {
|
|
714
|
+
if (!['code', 'evidence'].includes(review.reviewSubject)) errors.push(`${label}.preliminaryReview.reviewSubject must be code|evidence`)
|
|
715
|
+
captureError(errors, () => boundary(review.authorizationBoundary, `${label}.preliminaryReview.authorizationBoundary`))
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function validateStoredAuthorization(manifest, component, candidate, authorization, label, errors, { allowLegacyCiEvidence = false } = {}) {
|
|
720
|
+
captureError(errors, () => assertExactObject(authorization, ['roundClass', 'generation', 'headSha', 'authorizationBoundary', 'ci', 'review', 'smoke', 'at'], label))
|
|
721
|
+
if (!isObject(authorization)) return
|
|
722
|
+
if (authorization.roundClass !== 'terminal-authorization') errors.push(`${label}.roundClass must be terminal-authorization`)
|
|
723
|
+
if (!Number.isInteger(authorization.generation) || authorization.generation < 1 || authorization.generation > candidate.generation) errors.push(`${label}.generation is invalid`)
|
|
724
|
+
const updates = Array.isArray(candidate.updates) ? candidate.updates : []
|
|
725
|
+
const update = updates.filter((entry) => entry?.generation === authorization.generation).at(-1)
|
|
726
|
+
if (!update || update.headSha !== authorization.headSha) errors.push(`${label} must authorize the latest update in its generation`)
|
|
727
|
+
const validationCandidate = {
|
|
728
|
+
...candidate,
|
|
729
|
+
state: 'frozen',
|
|
730
|
+
frozen: { headSha: authorization.headSha, unitEvidence: clone(update?.unitEvidence ?? []) },
|
|
731
|
+
}
|
|
732
|
+
const unitById = new Map((Array.isArray(manifest?.units) ? manifest.units : []).map((unit) => [unit?.id, unit]))
|
|
733
|
+
const legacyCi = allowLegacyCiEvidence
|
|
734
|
+
&& isObject(authorization.ci)
|
|
735
|
+
&& Object.keys(authorization.ci).every((key) => ['headSha', 'status', 'artifact', 'jobs'].includes(key))
|
|
736
|
+
&& !Object.hasOwn(authorization.ci, 'requiredCheckPlan')
|
|
737
|
+
const validateEvidence = legacyCi ? legacyTerminalEvidence : terminalEvidence
|
|
738
|
+
captureError(errors, () => validateEvidence({
|
|
739
|
+
headSha: authorization.headSha,
|
|
740
|
+
ci: authorization.ci,
|
|
741
|
+
review: authorization.review,
|
|
742
|
+
smoke: authorization.smoke,
|
|
743
|
+
}, component, validationCandidate, unitById))
|
|
744
|
+
if (!same(authorization.authorizationBoundary, authorization.review?.authorizationBoundary)) errors.push(`${label}.authorizationBoundary must equal the terminal review boundary`)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function validateStoredComponentDelivery(manifest, component, candidate, index, errors) {
|
|
748
|
+
const delivery = component?.delivery
|
|
749
|
+
const label = `components[${index}] (${component?.id ?? 'unknown'}).delivery`
|
|
750
|
+
const unitById = new Map((Array.isArray(manifest?.units) ? manifest.units : []).map((unit) => [unit?.id, unit]))
|
|
751
|
+
const memberIds = Array.isArray(component?.units) ? component.units : []
|
|
752
|
+
|
|
753
|
+
// Compatibility is absence-based, not schemaVersion-based: #1009 can migrate a valid
|
|
754
|
+
// pre-feature manifest to v2 without inventing delivery facts. Such records retain the
|
|
755
|
+
// legacy unit reader until the component is terminally re-authorized, which creates this
|
|
756
|
+
// canonical record. Once present, member unit delivery duplication is refused below.
|
|
757
|
+
if (delivery === undefined || delivery === null) return
|
|
758
|
+
captureError(errors, () => assertExactObject(delivery, ['pr', 'generation', 'authorizedHeadSha', 'mode', 'provider', 'attempts', 'observations', 'watch', 'queueExecution', 'outcome', 'outcomeAt', 'mergedSha', 'mergedAt', 'blockedOn', 'reconciliations'], label))
|
|
759
|
+
if (!isObject(delivery)) return
|
|
760
|
+
if (!Number.isSafeInteger(delivery.pr) || delivery.pr < 1) errors.push(`${label}.pr must be a positive integer`)
|
|
761
|
+
const publishedPr = positivePr(component.pr)
|
|
762
|
+
if (publishedPr != null && publishedPr !== delivery.pr) errors.push(`${label}.pr must equal the published component PR ${publishedPr}`)
|
|
763
|
+
if (!Number.isSafeInteger(delivery.generation) || delivery.generation < 1) errors.push(`${label}.generation must be a positive integer`)
|
|
764
|
+
captureError(errors, () => assertSha(delivery.authorizedHeadSha, `${label}.authorizedHeadSha`))
|
|
765
|
+
if (!['direct', 'queue'].includes(delivery.mode)) errors.push(`${label}.mode must be direct|queue`)
|
|
766
|
+
if (delivery.mode === 'direct' && delivery.provider !== null) errors.push(`${label}.provider must be null for direct delivery`)
|
|
767
|
+
if (delivery.mode === 'queue' && !['mergify', 'github'].includes(delivery.provider)) errors.push(`${label}.provider must be mergify|github for queue delivery`)
|
|
768
|
+
if (delivery.mode !== deliveryMode(manifest)) errors.push(`${label}.mode must equal the run deliveryMode`)
|
|
769
|
+
if (delivery.provider !== (delivery.mode === 'queue' ? deliveryProvider(manifest) : null)) errors.push(`${label}.provider must equal the run queueProvider`)
|
|
770
|
+
|
|
771
|
+
const authorization = Array.isArray(candidate?.authorizations)
|
|
772
|
+
? candidate.authorizations.findLast((entry) => entry?.generation === delivery.generation)
|
|
773
|
+
: null
|
|
774
|
+
if (!authorization || authorization.headSha !== delivery.authorizedHeadSha) errors.push(`${label} must bind an existing terminal authorization generation and head`)
|
|
775
|
+
if (candidate?.state === 'authorized' && (candidate.generation !== delivery.generation || candidate.frozen?.headSha !== delivery.authorizedHeadSha)) {
|
|
776
|
+
errors.push(`${label} must bind the currently authorized frozen component generation`)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (!Array.isArray(delivery.attempts)) errors.push(`${label}.attempts must be an array`)
|
|
780
|
+
else {
|
|
781
|
+
const attemptKeys = new Set()
|
|
782
|
+
for (const [attemptIndex, attempt] of delivery.attempts.entries()) {
|
|
783
|
+
const at = `${label}.attempts[${attemptIndex}]`
|
|
784
|
+
if (!isObject(attempt)) { errors.push(`${at} must be an object`); continue }
|
|
785
|
+
if (!Number.isSafeInteger(attempt.generation) || attempt.generation < 1) errors.push(`${at}.generation must be a positive integer`)
|
|
786
|
+
const { generation, ...storedAttempt } = attempt
|
|
787
|
+
errors.push(...validateDeliveryAttempt(storedAttempt, delivery.mode, at, delivery.provider))
|
|
788
|
+
const key = `${generation}:${attempt.action}`
|
|
789
|
+
if (attemptKeys.has(key)) errors.push(`${at} duplicates the ${attempt.action} attempt for generation ${generation}`)
|
|
790
|
+
attemptKeys.add(key)
|
|
791
|
+
const attemptAuthorization = Array.isArray(candidate?.authorizations)
|
|
792
|
+
? candidate.authorizations.find((entry) => entry?.generation === generation)
|
|
793
|
+
: null
|
|
794
|
+
if (!attemptAuthorization || attempt.headSha !== attemptAuthorization.headSha) errors.push(`${at}.headSha must equal its generation's authorized component HEAD`)
|
|
795
|
+
if (delivery.mode === 'queue' && positivePr(attempt.pr) !== delivery.pr) errors.push(`${at}.pr must equal component delivery PR ${delivery.pr}`)
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (!Array.isArray(delivery.observations)) errors.push(`${label}.observations must be an array`)
|
|
799
|
+
else for (const [observationIndex, observation] of delivery.observations.entries()) {
|
|
800
|
+
const at = `${label}.observations[${observationIndex}]`
|
|
801
|
+
captureError(errors, () => assertExactObject(observation, ['generation', 'headSha', 'at', 'queueState', 'position', 'unmetConditions', 'ack'], at))
|
|
802
|
+
if (!isObject(observation)) continue
|
|
803
|
+
const observationAuthorization = Array.isArray(candidate?.authorizations)
|
|
804
|
+
? candidate.authorizations.find((entry) => entry?.generation === observation.generation)
|
|
805
|
+
: null
|
|
806
|
+
if (!observationAuthorization || observation.headSha !== observationAuthorization.headSha) errors.push(`${at}.headSha must equal its generation's authorized component HEAD`)
|
|
807
|
+
captureError(errors, () => deliveryObservation({ at: observation.at, queueState: observation.queueState, ...(observation.position !== undefined ? { position: observation.position } : {}), ...(observation.unmetConditions !== undefined ? { unmetConditions: observation.unmetConditions } : {}), ...(observation.ack !== undefined ? { ack: observation.ack } : {}) }, at))
|
|
808
|
+
}
|
|
809
|
+
if (delivery.mode === 'direct' && (delivery.observations?.length || delivery.watch != null)) errors.push(`${label}: direct delivery cannot carry queue observations or a watch`)
|
|
810
|
+
if (delivery.watch !== null && !isObject(delivery.watch)) errors.push(`${label}.watch must be null or an object`)
|
|
811
|
+
if (isObject(delivery.watch)) {
|
|
812
|
+
const unknownWatch = Object.keys(delivery.watch).filter((key) => !['firstObservedAt', 'lastObservedAt', 'lastChangeAt', 'queueState', 'position', 'polls', 'nudges', 'unmetConditions'].includes(key))
|
|
813
|
+
if (unknownWatch.length) errors.push(`${label}.watch has unknown property ${unknownWatch[0]}`)
|
|
814
|
+
for (const field of ['firstObservedAt', 'lastObservedAt', 'lastChangeAt', 'queueState']) if (!nonEmpty(delivery.watch[field])) errors.push(`${label}.watch.${field} is required`)
|
|
815
|
+
if (!Number.isSafeInteger(delivery.watch.polls) || delivery.watch.polls < 1) errors.push(`${label}.watch.polls must be a positive integer`)
|
|
816
|
+
if (delivery.watch.nudges !== undefined) {
|
|
817
|
+
if (!Array.isArray(delivery.watch.nudges) || delivery.watch.nudges.length > DELIVERY_MAX_NUDGES) errors.push(`${label}.watch.nudges must contain at most ${DELIVERY_MAX_NUDGES} entries`)
|
|
818
|
+
else for (const [nudgeIndex, nudge] of delivery.watch.nudges.entries()) {
|
|
819
|
+
const at = `${label}.watch.nudges[${nudgeIndex}]`
|
|
820
|
+
captureError(errors, () => assertExactObject(nudge, ['at', 'command', 'ref'], at))
|
|
821
|
+
if (!isObject(nudge) || !nonEmpty(nudge.at) || !nonEmpty(nudge.command)) errors.push(`${at} requires at and command`)
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (delivery.outcome !== null && !['merged', 'handed-back'].includes(delivery.outcome)) errors.push(`${label}.outcome must be null|merged|handed-back`)
|
|
826
|
+
if (delivery.outcome != null && !nonEmpty(delivery.outcomeAt)) errors.push(`${label}.outcomeAt is required for a terminal outcome`)
|
|
827
|
+
if (delivery.outcome === 'merged') {
|
|
828
|
+
captureError(errors, () => assertSha(delivery.mergedSha, `${label}.mergedSha`))
|
|
829
|
+
const requiredAction = delivery.mode === 'queue' ? 'enqueue' : 'merge'
|
|
830
|
+
if (!Array.isArray(delivery.attempts) || !delivery.attempts.some((attempt) => attempt?.generation === delivery.generation && attempt.action === requiredAction)) {
|
|
831
|
+
errors.push(`${label}: merged outcome requires a ${requiredAction} attempt in the authorized generation`)
|
|
832
|
+
}
|
|
833
|
+
const queueRequired = checksForPhase(authorization?.ci?.requiredCheckPlan, 'queue-head').length > 0
|
|
834
|
+
if (delivery.mode === 'direct' && delivery.queueExecution != null) errors.push(`${label}: direct delivery cannot carry queueExecution evidence`)
|
|
835
|
+
if (delivery.mode === 'queue' && queueRequired && !isObject(delivery.queueExecution)) errors.push(`${label}: merged queue delivery requires queueExecution evidence`)
|
|
836
|
+
if (delivery.mode === 'queue' && isObject(delivery.queueExecution)) captureError(errors, () => validatedQueueExecution(component.id, delivery, authorization, delivery.queueExecution))
|
|
837
|
+
}
|
|
838
|
+
if (delivery.outcome === 'handed-back') {
|
|
839
|
+
if (!nonEmpty(delivery.blockedOn)) errors.push(`${label}.blockedOn is required for handed-back`)
|
|
840
|
+
if (delivery.mergedSha != null || delivery.mergedAt != null || delivery.queueExecution != null) errors.push(`${label}: handed-back delivery cannot carry merge or queue-execution success evidence`)
|
|
841
|
+
}
|
|
842
|
+
if (delivery.outcome === null && (delivery.outcomeAt != null || delivery.mergedSha != null || delivery.mergedAt != null || delivery.blockedOn != null || delivery.queueExecution != null)) errors.push(`${label}: nonterminal delivery cannot carry terminal evidence`)
|
|
843
|
+
|
|
844
|
+
for (const id of memberIds) {
|
|
845
|
+
const unit = unitById.get(id)
|
|
846
|
+
if (!unit) continue
|
|
847
|
+
if (unit.delivery !== undefined) errors.push(`${label}: member ${id} must not duplicate canonical attempts or observations in units[].delivery`)
|
|
848
|
+
if (delivery.outcome != null) {
|
|
849
|
+
const expected = delivery.outcome === 'merged' ? 'delivered' : 'handed-back'
|
|
850
|
+
if (unit.disposition !== expected) errors.push(`${label}: terminal outcome must atomically derive disposition ${expected} for member ${id}`)
|
|
851
|
+
if (!same(unit.deliveryRef, { componentId: component.id, generation: delivery.generation })) errors.push(`${label}: member ${id}.deliveryRef must bind this component generation`)
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function candidateErrors(manifest, component, index, options = {}) {
|
|
857
|
+
const c = component?.candidate
|
|
858
|
+
if (c === undefined || c === null) return []
|
|
859
|
+
const at = `components[${index}] (${component?.id ?? 'unknown'}).candidate`
|
|
860
|
+
const errors = []
|
|
861
|
+
if (!isObject(c)) return [`${at} must be an object`]
|
|
862
|
+
const unknown = Object.keys(c).filter((key) => !['state', 'generation', 'updates', 'frozen', 'authorizations', 'reopens'].includes(key))
|
|
863
|
+
if (unknown.length) errors.push(`${at} has unknown property ${unknown[0]}`)
|
|
864
|
+
if (!COMPONENT_CANDIDATE_STATES.includes(c.state)) errors.push(`${at}.state must be one of ${COMPONENT_CANDIDATE_STATES.join('|')}`)
|
|
865
|
+
if (!Number.isInteger(c.generation) || c.generation < 1) errors.push(`${at}.generation must be a positive integer`)
|
|
866
|
+
for (const key of ['updates', 'authorizations', 'reopens']) if (!Array.isArray(c[key])) errors.push(`${at}.${key} must be an array`)
|
|
867
|
+
const updates = Array.isArray(c.updates) ? c.updates : []
|
|
868
|
+
for (const [i, update] of updates.entries()) {
|
|
869
|
+
const label = `${at}.updates[${i}]`
|
|
870
|
+
captureError(errors, () => assertExactObject(update, ['roundClass', 'generation', 'branch', 'headSha', 'includedUnits', 'unitEvidence', 'integrated', 'integrationFix', 'at'], label))
|
|
871
|
+
if (!isObject(update)) continue
|
|
872
|
+
if (!['unit-discovery', 'integration-fix'].includes(update.roundClass)) errors.push(`${label} has invalid roundClass`)
|
|
873
|
+
if (!Number.isInteger(update.generation) || update.generation < 1 || update.generation > c.generation) errors.push(`${label}.generation is invalid`)
|
|
874
|
+
if (i > 0 && update.generation < updates[i - 1]?.generation) errors.push(`${label}.generation cannot move backwards`)
|
|
875
|
+
if (!nonEmpty(update.branch)) errors.push(`${label}.branch is required`)
|
|
876
|
+
captureError(errors, () => assertSha(update.headSha, `${label}.headSha`))
|
|
877
|
+
captureError(errors, () => assertIncludedUnits(component, update.includedUnits))
|
|
878
|
+
if (!Array.isArray(update.unitEvidence) || update.unitEvidence.length !== update.includedUnits?.length) {
|
|
879
|
+
errors.push(`${label}.unitEvidence must match includedUnits`)
|
|
880
|
+
} else {
|
|
881
|
+
update.unitEvidence.forEach((evidence, evidenceIndex) => validateStoredUnitEvidence(evidence, `${label}.unitEvidence[${evidenceIndex}]`, errors))
|
|
882
|
+
if (!same(update.unitEvidence.map((evidence) => evidence?.unitId), update.includedUnits)) errors.push(`${label}.unitEvidence order must match includedUnits`)
|
|
883
|
+
}
|
|
884
|
+
captureError(errors, () => integratedResult(update.integrated, component.id))
|
|
885
|
+
if (update.roundClass === 'integration-fix') captureError(errors, () => integrationFix(update.integrationFix, component.id))
|
|
886
|
+
else if (update.integrationFix !== undefined) errors.push(`${label}: unit-discovery cannot carry integrationFix`)
|
|
887
|
+
const prior = updates[i - 1]
|
|
888
|
+
if (prior) {
|
|
889
|
+
if (prior.branch !== update.branch) errors.push(`${label}: integration branch cannot move`)
|
|
890
|
+
if (prior.headSha === update.headSha) errors.push(`${label}: candidate head must move`)
|
|
891
|
+
if (prior.includedUnits?.some((id) => !update.includedUnits?.includes(id))) errors.push(`${label}: included units cannot shrink`)
|
|
892
|
+
if (update.roundClass === 'integration-fix' && (!same(prior.includedUnits, update.includedUnits) || !same(prior.unitEvidence, update.unitEvidence))) errors.push(`${label}: integration-fix must preserve unit evidence byte-for-byte`)
|
|
893
|
+
if (update.roundClass === 'unit-discovery' && same(prior.unitEvidence, update.unitEvidence)) errors.push(`${label}: unit-discovery must change unit evidence`)
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const reopens = Array.isArray(c.reopens) ? c.reopens : []
|
|
897
|
+
if (reopens.length !== c.generation - 1) errors.push(`${at}.reopens must contain one typed transition for every completed generation`)
|
|
898
|
+
for (const [i, reopen] of reopens.entries()) {
|
|
899
|
+
const label = `${at}.reopens[${i}]`
|
|
900
|
+
captureError(errors, () => assertExactObject(reopen, ['fromGeneration', 'fromHeadSha', 'reopenReason', 'priorAuthorizationBoundary', 'authorizationBoundary', 'at'], label))
|
|
901
|
+
if (!isObject(reopen)) continue
|
|
902
|
+
if (reopen.fromGeneration !== i + 1) errors.push(`${label}.fromGeneration must preserve generation order`)
|
|
903
|
+
const generationHead = updates.filter((entry) => entry?.generation === reopen.fromGeneration).at(-1)?.headSha
|
|
904
|
+
if (reopen.fromHeadSha !== generationHead) errors.push(`${label}.fromHeadSha must equal that generation's latest candidate head`)
|
|
905
|
+
if (!REOPEN_REASONS.includes(reopen.reopenReason)) errors.push(`${label}.reopenReason is invalid`)
|
|
906
|
+
const priorBoundary = (() => { try { return boundary(reopen.priorAuthorizationBoundary, `${label}.priorAuthorizationBoundary`) } catch (error) { errors.push(error.message); return null } })()
|
|
907
|
+
const nextBoundary = (() => { try { return boundary(reopen.authorizationBoundary, `${label}.authorizationBoundary`) } catch (error) { errors.push(error.message); return null } })()
|
|
908
|
+
if (nextBoundary && nextBoundary.kind !== REOPEN_BOUNDARY_KIND[reopen.reopenReason]) errors.push(`${label}.authorizationBoundary.kind does not match reopenReason`)
|
|
909
|
+
if (priorBoundary && (priorBoundary.kind !== 'integration' || priorBoundary.value !== reopen.fromHeadSha)) errors.push(`${label}.priorAuthorizationBoundary must bind the prior frozen component HEAD`)
|
|
910
|
+
if (priorBoundary && nextBoundary && same(priorBoundary, nextBoundary)) errors.push(`${label}.authorizationBoundary must change`)
|
|
911
|
+
}
|
|
912
|
+
const authorizations = Array.isArray(c.authorizations) ? c.authorizations : []
|
|
913
|
+
for (const [i, authorization] of authorizations.entries()) {
|
|
914
|
+
validateStoredAuthorization(manifest, component, c, authorization, `${at}.authorizations[${i}]`, errors, options)
|
|
915
|
+
if (i > 0 && authorization?.generation <= authorizations[i - 1]?.generation) errors.push(`${at}.authorizations[${i}].generation must increase`)
|
|
916
|
+
}
|
|
917
|
+
if (c.state === 'assembling' && c.frozen != null) errors.push(`${at}: assembling candidate cannot carry a current frozen boundary`)
|
|
918
|
+
if (['frozen', 'authorized'].includes(c.state)) {
|
|
919
|
+
captureError(errors, () => assertExactObject(c.frozen, ['generation', 'headSha', 'branch', 'includedUnits', 'unitEvidence', 'authorizationBoundary', 'at'], `${at}.frozen`))
|
|
920
|
+
if (!isObject(c.frozen)) errors.push(`${at}: ${c.state} candidate requires frozen evidence`)
|
|
921
|
+
else {
|
|
922
|
+
const latest = updates.filter((entry) => entry?.generation === c.generation).at(-1)
|
|
923
|
+
if (!latest || latest.headSha !== c.frozen.headSha) errors.push(`${at}: frozen head must equal the latest update in its generation`)
|
|
924
|
+
if (c.frozen.generation !== c.generation) errors.push(`${at}: frozen generation must equal the current generation`)
|
|
925
|
+
if (c.frozen.branch !== latest?.branch) errors.push(`${at}: frozen branch must equal the latest update branch`)
|
|
926
|
+
if (!same(c.frozen.includedUnits, component.units)) errors.push(`${at}: frozen candidate must include the complete component`)
|
|
927
|
+
if (!same(c.frozen.unitEvidence, latest?.unitEvidence)) errors.push(`${at}: frozen unit evidence must equal the latest update`)
|
|
928
|
+
const byId = new Map((Array.isArray(manifest?.units) ? manifest.units : []).map((unit) => [unit?.id, unit]))
|
|
929
|
+
const currentEvidence = Array.isArray(component.units)
|
|
930
|
+
? component.units.map((id) => { try { return preliminaryEvidence(byId.get(id), component.id) } catch (error) { errors.push(error.message); return null } })
|
|
931
|
+
: []
|
|
932
|
+
if (!currentEvidence.includes(null) && !same(c.frozen.unitEvidence, currentEvidence)) errors.push(`${at}: frozen unit evidence is stale against the current unit heads`)
|
|
933
|
+
if (latest?.integrated?.assembly !== 'clean' || latest?.integrated?.tests !== 'green' || latest?.integrated?.conflicts?.length || latest?.integrated?.failedTests?.length) errors.push(`${at}: frozen candidate requires clean assembly and green tests`)
|
|
934
|
+
const frozenBoundary = (() => { try { return boundary(c.frozen.authorizationBoundary, `${at}.frozen.authorizationBoundary`) } catch (error) { errors.push(error.message); return null } })()
|
|
935
|
+
if (frozenBoundary && (frozenBoundary.kind !== 'integration' || frozenBoundary.value !== c.frozen.headSha)) errors.push(`${at}: frozen authorization boundary must bind the integration HEAD`)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (c.state === 'authorized') {
|
|
939
|
+
const latest = authorizations.at(-1)
|
|
940
|
+
if (!latest || latest.generation !== c.generation || latest.headSha !== c.frozen?.headSha) errors.push(`${at}: authorized candidate requires terminal authorization on the frozen head in this generation`)
|
|
941
|
+
if (!same(component.authorizationBoundary, latest?.authorizationBoundary)) errors.push(`${at}: component authorizationBoundary must equal terminal component HEAD boundary`)
|
|
942
|
+
}
|
|
943
|
+
const roster = new Set((Array.isArray(manifest?.units) ? manifest.units : []).map((unit) => unit?.id))
|
|
944
|
+
for (const id of Array.isArray(component.units) ? component.units : []) if (!roster.has(id)) errors.push(`${at}: component unit ${id} is not in the roster`)
|
|
945
|
+
validateStoredComponentDelivery(manifest, component, c, index, errors)
|
|
946
|
+
return errors
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function validateComponentCandidates(manifest, options = {}) {
|
|
950
|
+
if (manifest?.components === undefined || manifest?.components === null) return []
|
|
951
|
+
if (!Array.isArray(manifest.components)) return ['components must be an array']
|
|
952
|
+
const errors = []
|
|
953
|
+
const ids = new Set()
|
|
954
|
+
manifest.components.forEach((component, index) => {
|
|
955
|
+
if (!isObject(component) || !nonEmpty(component.id)) errors.push(`components[${index}] requires a non-empty id`)
|
|
956
|
+
else if (ids.has(component.id)) errors.push(`components[${index}] duplicates component id ${component.id}`)
|
|
957
|
+
else ids.add(component.id)
|
|
958
|
+
errors.push(...candidateErrors(manifest, component, index, options))
|
|
959
|
+
})
|
|
960
|
+
errors.push(...reconciliationErrors(manifest))
|
|
961
|
+
return errors
|
|
962
|
+
}
|