@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,713 @@
|
|
|
1
|
+
// smoke — the curated E2E happy-path GATE runner. Loads the smoke manifest (the
|
|
2
|
+
// small "if these flows work, the build is alive" set), selects the paths safe
|
|
3
|
+
// for the target, and reduces drive results into a single ALIVE / DEAD verdict.
|
|
4
|
+
//
|
|
5
|
+
// This is the mechanized version of the manual QA pass that catches integrated
|
|
6
|
+
// runtime bugs a green unit/functional suite sails past (the #237 invisible
|
|
7
|
+
// button / Feishu-QR crash class). It does NOT author or run the full suite — the driving
|
|
8
|
+
// is runtime observation of the REAL running app (browser paths via e2e-test's
|
|
9
|
+
// Playwright runner; api/cli paths via /verify-style capture). This script owns
|
|
10
|
+
// the decidable shell: validate the manifest, pick the target-safe subset, and
|
|
11
|
+
// compute the gate verdict from drive results.
|
|
12
|
+
//
|
|
13
|
+
// node smoke.mjs validate --manifest smoke.manifest.json [--journeys e2e/journeys]
|
|
14
|
+
// (--journeys reconciles browser paths against the @smoke tags in the specs:
|
|
15
|
+
// a referenced journey that isn't @smoke-tagged, or an @smoke journey missing
|
|
16
|
+
// from the manifest (a silent false-ALIVE), fails validation.)
|
|
17
|
+
// node smoke.mjs plan --manifest smoke.manifest.json --profile pr-local
|
|
18
|
+
// node smoke.mjs plan --manifest smoke.manifest.json --profile prod-preview --target https://rev-xyz.run.app
|
|
19
|
+
// (--profile names a Validation Profile from the manifest's `profiles` registry. The
|
|
20
|
+
// profile — not the caller — decides WHICH paths run and WHETHER writes are allowed:
|
|
21
|
+
// each profile binds a gate/environment/endpoint plus a mutationPolicy. Production
|
|
22
|
+
// profiles (prod-preview/prod-active) are mechanically read-only, so a synthetic
|
|
23
|
+
// tenant grants nothing there; local profiles allow isolated writes; dev-active
|
|
24
|
+
// allows a registered synthetic tenant. --target is just the ADDRESS to drive against
|
|
25
|
+
// (the candidate URL), and it no longer participates in selection — the profile does.)
|
|
26
|
+
// node smoke.mjs suggest --inventory docs/tests/_generated/surface-inventory.json [--out smoke.candidates.json]
|
|
27
|
+
// node smoke.mjs verdict --manifest smoke.manifest.json --profile <name> --results smoke-results.json [--target <url>] [--out summary.json]
|
|
28
|
+
// (pass the SAME --profile the run used. Selection is a pure function of the profile
|
|
29
|
+
// name + the manifest, so the run and the verdict cannot disagree about the selected
|
|
30
|
+
// path set — the old --target/--no-mutations mismatch is unrepresentable. The verdict
|
|
31
|
+
// stamps the `profile` it selected under and the `noMutations` flag DERIVED from that
|
|
32
|
+
// profile's mutationPolicy (read-only ⇒ true), which resolve-release's green-gate reads.)
|
|
33
|
+
// (`generatedAt` dates the VERDICT, not the RUN. This subcommand reduces a results file
|
|
34
|
+
// of any age and stamps the current instant over it, so re-running `verdict` alone
|
|
35
|
+
// re-mints freshness without re-validating anything. A consumer bounding the evidence's
|
|
36
|
+
// age (resolve-release's shift gate) is bounding the mint; re-drive the paths into a new
|
|
37
|
+
// smoke-results.json whenever what you need is fresh OBSERVATIONS, not a fresh stamp.)
|
|
38
|
+
//
|
|
39
|
+
// The manifest is JSON (a repo may author in YAML and project to JSON, the same
|
|
40
|
+
// way e2e-test's route-manifest is generated). Results is a JSON array of
|
|
41
|
+
// { id, status: pass|fail|blocked, evidence?, note? } produced by the drive step.
|
|
42
|
+
|
|
43
|
+
import fs from 'node:fs'
|
|
44
|
+
import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
|
|
45
|
+
|
|
46
|
+
// ---- pure, unit-testable core (no browser, no network) ----
|
|
47
|
+
|
|
48
|
+
export const SURFACES = ['browser', 'api', 'cli']
|
|
49
|
+
const GOLDEN_KEYWORDS = [
|
|
50
|
+
'checkout', 'pay', 'payment', 'billing', 'invoice', 'subscribe', 'subscription',
|
|
51
|
+
'order', 'purchase', 'refund', 'login', 'log-in', 'signin', 'sign-in', 'signup',
|
|
52
|
+
'sign-up', 'register', 'sso', 'oauth', 'auth', 'onboard', 'invite', 'admin',
|
|
53
|
+
'settings', 'connect', 'connector', 'create', 'new',
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
// ---- Validation Profiles (issue #440) ----
|
|
57
|
+
//
|
|
58
|
+
// A Validation Profile is a NAMED binding of three axes plus policy, so a caller
|
|
59
|
+
// selects one name instead of assembling free-form targeting flags. The three axes
|
|
60
|
+
// the old single `--target` string conflated:
|
|
61
|
+
// - gate WHY evidence is required (the lifecycle moment)
|
|
62
|
+
// - environment WHERE the app runs
|
|
63
|
+
// - endpoint WHICH revision (a distinction that only exists once deployed)
|
|
64
|
+
// plus `prerequisites` (what the profile needs in place), `evidencePolicy` (what
|
|
65
|
+
// evidence it demands), and `mutationPolicy` (attached to the PROFILE, not the run).
|
|
66
|
+
//
|
|
67
|
+
// The core fix this delivers: selection becomes a pure function of the profile name
|
|
68
|
+
// + the manifest, so the run and the verdict cannot disagree about the selected path
|
|
69
|
+
// set (the old --target/--no-mutations mismatch is unrepresentable), and production
|
|
70
|
+
// read-only-ness is enforced by the profile rather than by a flag the caller must
|
|
71
|
+
// remember (resolve-release principle 4).
|
|
72
|
+
export const GATES = ['pull-request', 'merge-queue', 'trunk-integration', 'pre-promotion', 'post-promotion']
|
|
73
|
+
export const ENVIRONMENTS = ['local', 'dev', 'prod']
|
|
74
|
+
export const ENDPOINTS = ['local', 'preview', 'active']
|
|
75
|
+
export const MUTATION_POLICIES = ['isolated-writes', 'synthetic-tenant', 'read-only']
|
|
76
|
+
|
|
77
|
+
// Each gate pins exactly one valid (environment, endpoint) pair — the coherence table
|
|
78
|
+
// the "invalid gate/environment/endpoint combination fails closed" rule checks against.
|
|
79
|
+
// The reasoning, not an arbitrary enumeration: the PR and merge-queue gates run the app
|
|
80
|
+
// locally (endpoint `local`); trunk-integration runs against the dev environment's live
|
|
81
|
+
// (100%-traffic → `active`) revision; pre-promotion validates the production candidate at
|
|
82
|
+
// 0% traffic (a `preview` revision); post-promotion validates the live production service
|
|
83
|
+
// (`active`). A local environment never has a preview/active split, and a deployed
|
|
84
|
+
// environment never uses the `local` endpoint — so a bad pairing (e.g. pull-request in
|
|
85
|
+
// prod, or pre-promotion against `active`) is rejected rather than silently accepted.
|
|
86
|
+
const GATE_AXES = {
|
|
87
|
+
'pull-request': { environment: 'local', endpoint: 'local' },
|
|
88
|
+
'merge-queue': { environment: 'local', endpoint: 'local' },
|
|
89
|
+
'trunk-integration': { environment: 'dev', endpoint: 'active' },
|
|
90
|
+
'pre-promotion': { environment: 'prod', endpoint: 'preview' },
|
|
91
|
+
'post-promotion': { environment: 'prod', endpoint: 'active' },
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Reserved profile NAMES → the gate they MUST bind. These names are consumed by
|
|
95
|
+
// `resolve-release` (and the other lanes) by HARDCODED string, not by their binding
|
|
96
|
+
// — step 4a drives `--profile prod-preview`, 4b drives `prod-active` — so the name is
|
|
97
|
+
// itself a safety-bearing key. Without this pin, a registry entry NAMED `prod-preview`
|
|
98
|
+
// but bound to `{gate:"trunk-integration", environment:"dev", mutationPolicy:"synthetic-tenant"}`
|
|
99
|
+
// validates clean (its binding is internally coherent) and `plan --profile prod-preview`
|
|
100
|
+
// then selects a tenant-ful WRITE path against the production candidate. Pinning the gate
|
|
101
|
+
// cascades through GATE_AXES (environment + endpoint) and the prod-read-only rule
|
|
102
|
+
// (mutationPolicy), so a rename-and-rebind of a reserved name fails closed. See the
|
|
103
|
+
// `prod-profile-name-drift` hazard (references/manifest.md).
|
|
104
|
+
export const RESERVED_PROFILE_GATES = {
|
|
105
|
+
'pr-local': 'pull-request',
|
|
106
|
+
'queue-local': 'merge-queue',
|
|
107
|
+
'dev-active': 'trunk-integration',
|
|
108
|
+
'prod-preview': 'pre-promotion',
|
|
109
|
+
'prod-active': 'post-promotion',
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate ONE profile's shape and axis/policy coherence. Decidable, fail-closed:
|
|
113
|
+
// returns an array of error strings (empty = valid). The load-bearing rule is that a
|
|
114
|
+
// production profile is mechanically read-only regardless of any declared tenant.
|
|
115
|
+
export function validateProfile(name, profile) {
|
|
116
|
+
const at = `profiles.${name}`
|
|
117
|
+
if (!profile || typeof profile !== 'object' || Array.isArray(profile)) return [`${at}: must be an object`]
|
|
118
|
+
const errors = []
|
|
119
|
+
if (!GATES.includes(profile.gate)) errors.push(`${at}: gate must be one of ${GATES.join('/')}`)
|
|
120
|
+
if (!ENVIRONMENTS.includes(profile.environment)) errors.push(`${at}: environment must be one of ${ENVIRONMENTS.join('/')}`)
|
|
121
|
+
if (!ENDPOINTS.includes(profile.endpoint)) errors.push(`${at}: endpoint must be one of ${ENDPOINTS.join('/')}`)
|
|
122
|
+
if (!MUTATION_POLICIES.includes(profile.mutationPolicy)) errors.push(`${at}: mutationPolicy must be one of ${MUTATION_POLICIES.join('/')}`)
|
|
123
|
+
// Reserved-name binding. A name another lane consumes by hardcoded string MUST bind its
|
|
124
|
+
// reserved gate; from that gate, GATE_AXES + the prod-read-only rule below force the
|
|
125
|
+
// environment/endpoint/read-only-ness. So a rename-and-rebind attack (name `prod-preview`,
|
|
126
|
+
// bind it to a writable dev gate) fails HERE, before selection can inherit the wrong policy.
|
|
127
|
+
const reservedGate = RESERVED_PROFILE_GATES[name]
|
|
128
|
+
if (reservedGate && profile.gate !== reservedGate) {
|
|
129
|
+
errors.push(`${at}: "${name}" is a reserved profile name consumed by another lane by name — it MUST bind gate "${reservedGate}", not "${profile.gate}". A safety property (production read-only-ness) is keyed to this name, so rebinding it would let the consumer silently inherit the wrong policy.`)
|
|
130
|
+
}
|
|
131
|
+
// Axis coherence — only when the gate is a known one (an unknown gate already errored).
|
|
132
|
+
const axes = GATE_AXES[profile.gate]
|
|
133
|
+
if (axes) {
|
|
134
|
+
if (ENVIRONMENTS.includes(profile.environment) && profile.environment !== axes.environment) {
|
|
135
|
+
errors.push(`${at}: gate "${profile.gate}" runs in environment "${axes.environment}", not "${profile.environment}"`)
|
|
136
|
+
}
|
|
137
|
+
if (ENDPOINTS.includes(profile.endpoint) && profile.endpoint !== axes.endpoint) {
|
|
138
|
+
errors.push(`${at}: gate "${profile.gate}" targets the "${axes.endpoint}" endpoint, not "${profile.endpoint}"`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Mutation-policy coherence. Production is read-only, full stop (principle 4): the
|
|
142
|
+
// candidate/service is wired to the real production database, so a synthetic tenant
|
|
143
|
+
// grants nothing. Isolated writes only make sense on a disposable local store; a
|
|
144
|
+
// synthetic tenant only on a shared deployed dev store.
|
|
145
|
+
if (profile.environment === 'prod' && MUTATION_POLICIES.includes(profile.mutationPolicy) && profile.mutationPolicy !== 'read-only') {
|
|
146
|
+
errors.push(`${at}: a prod profile must be read-only (mutationPolicy "read-only"), not "${profile.mutationPolicy}" — production is read-only regardless of any declared tenant`)
|
|
147
|
+
}
|
|
148
|
+
if (profile.mutationPolicy === 'isolated-writes' && ENVIRONMENTS.includes(profile.environment) && profile.environment !== 'local') {
|
|
149
|
+
errors.push(`${at}: mutationPolicy "isolated-writes" is only valid in the local environment, not "${profile.environment}"`)
|
|
150
|
+
}
|
|
151
|
+
if (profile.mutationPolicy === 'synthetic-tenant' && ENVIRONMENTS.includes(profile.environment) && profile.environment !== 'dev') {
|
|
152
|
+
errors.push(`${at}: mutationPolicy "synthetic-tenant" is only valid in the dev environment, not "${profile.environment}"`)
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(profile.prerequisites) || profile.prerequisites.some((x) => typeof x !== 'string')) {
|
|
155
|
+
errors.push(`${at}: prerequisites must be an array of strings (the set the profile requires)`)
|
|
156
|
+
}
|
|
157
|
+
if (!profile.evidencePolicy || typeof profile.evidencePolicy !== 'string') {
|
|
158
|
+
errors.push(`${at}: evidencePolicy must be a string (what evidence this profile demands)`)
|
|
159
|
+
}
|
|
160
|
+
return errors
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate the whole `profiles` registry and each path's profile membership. A missing
|
|
164
|
+
// registry is a WARNING (legacy manifests still parse) — but the moment a registry is
|
|
165
|
+
// present, every path must declare a non-empty `profiles` array of DECLARED profile
|
|
166
|
+
// names, or the path is a silent non-runner (a false ALIVE). Fail closed on both an
|
|
167
|
+
// invalid profile and a dangling path→profile reference.
|
|
168
|
+
export function validateProfiles(manifest, paths, errors, warnings) {
|
|
169
|
+
const registry = manifest && manifest.profiles
|
|
170
|
+
if (registry === undefined) {
|
|
171
|
+
warnings.push('no `profiles` registry — selection is by named Validation Profile (references/manifest.md). Without one, `plan`/`verdict --profile` cannot run. Add a `profiles` block binding gate + environment + endpoint + prerequisites + evidencePolicy + mutationPolicy.')
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if (!registry || typeof registry !== 'object' || Array.isArray(registry)) {
|
|
175
|
+
errors.push('profiles: must be an object mapping a profile name to its binding — see references/manifest.md')
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
const names = new Set(Object.keys(registry))
|
|
179
|
+
if (names.size === 0) errors.push('profiles: the registry is empty — declare at least one Validation Profile, or drop the `profiles` key to run in legacy mode')
|
|
180
|
+
for (const name of names) errors.push(...validateProfile(name, registry[name]))
|
|
181
|
+
// With a registry present, path membership is mandatory and must resolve.
|
|
182
|
+
for (const p of paths) {
|
|
183
|
+
if (!p || !p.id) continue
|
|
184
|
+
if (!Array.isArray(p.profiles) || p.profiles.length === 0) {
|
|
185
|
+
errors.push(`paths (${p.id}): with a profiles registry present, a path must declare a non-empty "profiles" array — the profiles it is required in — or it silently never runs (a false ALIVE)`)
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
for (const ref of p.profiles) {
|
|
189
|
+
if (!names.has(ref)) errors.push(`paths (${p.id}): profile "${ref}" is not in the profiles registry (${[...names].join(', ') || 'empty'})`)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Validate the manifest shape. Decidable, ~zero-judgment: structural rules only,
|
|
195
|
+
// so a malformed gate fails loudly instead of passing vacuously. Returns
|
|
196
|
+
// `errors` (block the gate) and `warnings` (surfaced gaps that don't fail it —
|
|
197
|
+
// e.g. a missing `setup` block, which leaves the prerequisites undeclared).
|
|
198
|
+
export function validateManifest(manifest) {
|
|
199
|
+
const errors = []
|
|
200
|
+
const warnings = []
|
|
201
|
+
const paths = manifest && manifest.paths
|
|
202
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
203
|
+
return { ok: false, errors: ['manifest.paths must be a non-empty array — a smoke gate with no golden paths proves nothing'], warnings }
|
|
204
|
+
}
|
|
205
|
+
const seen = new Set()
|
|
206
|
+
for (const [i, p] of paths.entries()) {
|
|
207
|
+
const at = `paths[${i}]${p && p.id ? ` (${p.id})` : ''}`
|
|
208
|
+
if (!p || typeof p !== 'object') { errors.push(`${at}: not an object`); continue }
|
|
209
|
+
if (!p.id) errors.push(`${at}: missing id`)
|
|
210
|
+
else if (seen.has(p.id)) errors.push(`${at}: duplicate id "${p.id}"`)
|
|
211
|
+
else seen.add(p.id)
|
|
212
|
+
if (!SURFACES.includes(p.surface)) errors.push(`${at}: surface must be one of ${SURFACES.join('/')}`)
|
|
213
|
+
// A browser path does NOT re-specify the flow — it references the @smoke-tagged
|
|
214
|
+
// e2e Tier-3 journey that already drives it; that spec owns the steps and the
|
|
215
|
+
// asserted observable. api/cli surfaces (which e2e-test doesn't cover) carry
|
|
216
|
+
// their own start/command + observable.
|
|
217
|
+
if (p.surface === 'browser') {
|
|
218
|
+
if (!p.journey || typeof p.journey !== 'string') errors.push(`${at}: a browser path needs a "journey" — the @smoke-tagged e2e Tier-3 journey (its @covers id) that drives it; don't re-specify start/observable here`)
|
|
219
|
+
} else {
|
|
220
|
+
if (!p.start && !p.command) errors.push(`${at}: needs a "start" (route/request) or "command" to drive`)
|
|
221
|
+
if (!p.observable || typeof p.observable !== 'string') errors.push(`${at}: needs an "observable" — the concrete thing that proves the path worked`)
|
|
222
|
+
}
|
|
223
|
+
// Prod-safety: a path that mutates durable state must declare a synthetic
|
|
224
|
+
// test tenant, or it can never run against a deployed/prod target.
|
|
225
|
+
if (p.mutates === true && !p.tenant) errors.push(`${at}: mutates:true requires "tenant" (a synthetic/test tenant) so it is safe off-local`)
|
|
226
|
+
}
|
|
227
|
+
validateSetup(manifest.setup, paths, errors, warnings)
|
|
228
|
+
validateProfiles(manifest, paths, errors, warnings)
|
|
229
|
+
// The e2e-* naming convention: synthetic tenants (and the test accounts they hold)
|
|
230
|
+
// carry an `e2e-` prefix so test data is identifiable at a glance, bulk-cleanable
|
|
231
|
+
// (`… WHERE tenant LIKE 'e2e-%'`), and mechanically distinguishable from live data
|
|
232
|
+
// (e2e-test references/authoring/auth-flows.md). A warning, not an error — legacy
|
|
233
|
+
// manifests still gate — but an unprefixed tenant is the one that gets mistaken for real.
|
|
234
|
+
const tenantIds = new Set([
|
|
235
|
+
...paths.filter((p) => p && typeof p.tenant === 'string').map((p) => p.tenant),
|
|
236
|
+
...(manifest.setup && Array.isArray(manifest.setup.tenants) ? manifest.setup.tenants.filter((t) => typeof t === 'string') : []),
|
|
237
|
+
])
|
|
238
|
+
for (const t of tenantIds) {
|
|
239
|
+
if (!t.startsWith('e2e-')) {
|
|
240
|
+
warnings.push(`tenant "${t}": name synthetic tenants with the e2e- prefix (e.g. "e2e-${t}") so test data is identifiable, bulk-cleanable, and never mistaken for live data`)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Validate the (recommended) `setup` block — the co-located prerequisites a run
|
|
247
|
+
// needs *before* it can drive a path: how to boot the app, env/secrets, the
|
|
248
|
+
// auth-bypass, seed commands, and the synthetic-tenant registry. Keeping these
|
|
249
|
+
// in the manifest (not in prose elsewhere) makes them checkable and keeps them
|
|
250
|
+
// next to the gate that consumes them. A missing block is a *warning*, not an
|
|
251
|
+
// error — legacy manifests still validate — but a present block is shape-checked,
|
|
252
|
+
// and any path tenant the registry doesn't list is flagged so it can't drift.
|
|
253
|
+
export function validateSetup(setup, paths, errors, warnings) {
|
|
254
|
+
if (setup === undefined) {
|
|
255
|
+
warnings.push('no `setup` block — the gate does not record how to boot/seed the app, the auth-bypass, or which tenants are synthetic. Add one (references/manifest.md) so the prerequisites live next to the gate and are checkable, instead of in prose elsewhere.')
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
// Reject falsy (incl. `setup: null`, which is valid JSON and `typeof === 'object'`)
|
|
259
|
+
// as a malformed block, not a missing one — before reading any property off it.
|
|
260
|
+
if (!setup || typeof setup !== 'object' || Array.isArray(setup)) { errors.push('setup: must be an object — see references/manifest.md'); return }
|
|
261
|
+
if (!setup.boot || typeof setup.boot !== 'string') {
|
|
262
|
+
errors.push('setup.boot: needs a string — how to start the app for a `local` run (a command, or "use the run skill")')
|
|
263
|
+
}
|
|
264
|
+
for (const f of ['authBypass', 'seed', 'fixtures']) {
|
|
265
|
+
if (f in setup && typeof setup[f] !== 'string') errors.push(`setup.${f}: must be a string`)
|
|
266
|
+
}
|
|
267
|
+
for (const f of ['env', 'tenants']) {
|
|
268
|
+
if (f in setup && (!Array.isArray(setup[f]) || setup[f].some((x) => typeof x !== 'string'))) {
|
|
269
|
+
errors.push(`setup.${f}: must be an array of strings`)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// The synthetic-tenant registry must cover every tenant a path declares, or a
|
|
273
|
+
// mutating path could run off-local against a tenant nobody vetted as synthetic.
|
|
274
|
+
const declared = new Set(Array.isArray(setup.tenants) ? setup.tenants : [])
|
|
275
|
+
for (const p of paths) {
|
|
276
|
+
if (p && p.tenant && !declared.has(p.tenant)) {
|
|
277
|
+
warnings.push(`paths (${p.id}): tenant "${p.tenant}" is not listed in setup.tenants — add it so the synthetic-tenant registry is complete`)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Select the paths to run FOR A NAMED PROFILE, and the ones excluded (with why).
|
|
283
|
+
// Selection is a pure function of the profile name + the manifest — no target/flag
|
|
284
|
+
// assembly — so the run and the verdict, both driven by the same name, cannot
|
|
285
|
+
// disagree about the selected set (the old mismatch is unrepresentable).
|
|
286
|
+
//
|
|
287
|
+
// A path runs iff it declares this profile in its `profiles` array AND the profile's
|
|
288
|
+
// mutationPolicy admits it:
|
|
289
|
+
// - read-only → every mutating path is excluded, tenant or not (principle 4:
|
|
290
|
+
// the production profiles, wired to the real prod DB, never write)
|
|
291
|
+
// - synthetic-tenant → a mutating path runs only if it declares a synthetic `tenant`
|
|
292
|
+
// - isolated-writes → mutating paths run (a disposable local store)
|
|
293
|
+
// Read-only paths always run when they declare the profile.
|
|
294
|
+
//
|
|
295
|
+
// Throws on an unknown profile name — fail closed: an unnamed profile selects nothing
|
|
296
|
+
// safe to reason about, so it is a hard stop, not a silent empty set.
|
|
297
|
+
export function selectPathsForProfile(manifest, profileName) {
|
|
298
|
+
const registry = (manifest && manifest.profiles) || {}
|
|
299
|
+
const profile = registry[profileName]
|
|
300
|
+
if (!profile) {
|
|
301
|
+
const known = Object.keys(registry)
|
|
302
|
+
throw new Error(`unknown profile "${profileName}" — not in the manifest's profiles registry (${known.length ? known.join(', ') : 'no registry'})`)
|
|
303
|
+
}
|
|
304
|
+
const run = []
|
|
305
|
+
const excluded = []
|
|
306
|
+
for (const p of (manifest.paths || [])) {
|
|
307
|
+
if (!Array.isArray(p.profiles) || !p.profiles.includes(profileName)) {
|
|
308
|
+
excluded.push({ id: p.id, reason: `not required in profile "${profileName}"` })
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
if (p.mutates === true) {
|
|
312
|
+
if (profile.mutationPolicy === 'read-only') {
|
|
313
|
+
excluded.push({ id: p.id, reason: `mutating path excluded — profile "${profileName}" is read-only (${profile.environment} is never written by this lane, tenant or not)` })
|
|
314
|
+
} else if (profile.mutationPolicy === 'synthetic-tenant') {
|
|
315
|
+
if (p.tenant) run.push(p)
|
|
316
|
+
else excluded.push({ id: p.id, reason: `mutating path excluded — profile "${profileName}" requires a synthetic tenant and this path declares none` })
|
|
317
|
+
} else {
|
|
318
|
+
run.push(p) // isolated-writes
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
run.push(p)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { profile: { name: profileName, ...profile }, run, excluded }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Reduce drive results into the gate verdict.
|
|
328
|
+
//
|
|
329
|
+
// Issue #823: only a `fail` is evidence that the CANDIDATE is broken (DEAD). Every other
|
|
330
|
+
// non-pass status means the GATE could not produce an observation: `blocked` (surface
|
|
331
|
+
// unreachable), `cannot-run` (runner/secret/auth/fixture failure), or `missing` (no result
|
|
332
|
+
// reported). Those must be CANNOT-RUN, not DEAD, so a misconfigured secret does not discard
|
|
333
|
+
// a healthy candidate.
|
|
334
|
+
export function computeVerdict(selected, results) {
|
|
335
|
+
const byId = new Map((results || []).map((r) => [r.id, r]))
|
|
336
|
+
const rows = selected.map((p) => {
|
|
337
|
+
const r = byId.get(p.id)
|
|
338
|
+
const status = r ? r.status : 'missing'
|
|
339
|
+
return {
|
|
340
|
+
id: p.id, surface: p.surface, status,
|
|
341
|
+
evidence: r && r.evidence,
|
|
342
|
+
note: r ? r.note : 'no result reported for this path — it was not driven',
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
// An EMPTY selection is not a pass. "Every selected path passed" is vacuously true when
|
|
347
|
+
// nothing was selected, and that vacuous truth is reachable in the one place it does the
|
|
348
|
+
// most damage: a production run under a read-only profile over a golden-path set that is
|
|
349
|
+
// mostly mutating — which is the normal shape, since golden paths are signup / checkout / purchase.
|
|
350
|
+
// The gate would then subtract itself to nothing, emit a well-formed ALIVE verdict having
|
|
351
|
+
// driven zero paths, and hand a green light to whatever shifts traffic next. A gate that
|
|
352
|
+
// proves nothing must not be indistinguishable from a gate that proved everything.
|
|
353
|
+
if (rows.length === 0) {
|
|
354
|
+
return {
|
|
355
|
+
verdict: 'EMPTY',
|
|
356
|
+
ok: false,
|
|
357
|
+
total: 0,
|
|
358
|
+
broken: [],
|
|
359
|
+
rows: [],
|
|
360
|
+
note: 'no paths were selected, so nothing was proven — check the manifest and the selected profile (its `profiles` membership on the paths, and its mutationPolicy: a read-only profile excludes every mutating path)',
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const failedPaths = rows.filter((r) => r.status === 'fail')
|
|
365
|
+
const cannotRunPaths = rows.filter((r) => r.status !== 'pass' && r.status !== 'fail')
|
|
366
|
+
|
|
367
|
+
// CANNOT-RUN takes precedence: if the gate itself could not run, the verdict is about the
|
|
368
|
+
// gate, not the candidate. DEAD means the candidate was reached and failed.
|
|
369
|
+
if (cannotRunPaths.length > 0) {
|
|
370
|
+
return {
|
|
371
|
+
verdict: 'CANNOT-RUN',
|
|
372
|
+
ok: false,
|
|
373
|
+
total: rows.length,
|
|
374
|
+
broken: cannotRunPaths.map((r) => ({ id: r.id, status: r.status, note: r.note })),
|
|
375
|
+
rows,
|
|
376
|
+
note: 'the gate could not run for at least one path — the failure is in the gate environment (credential, secret, auth, fixture, driver, or a missing result) and must be repaired before the candidate can be judged. This is not a DEAD candidate.',
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
verdict: failedPaths.length === 0 ? 'ALIVE' : 'DEAD',
|
|
382
|
+
ok: failedPaths.length === 0,
|
|
383
|
+
total: rows.length,
|
|
384
|
+
broken: failedPaths.map((r) => ({ id: r.id, status: r.status, note: r.note })),
|
|
385
|
+
rows,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Stamp the verdict with WHAT it was run against, and WHEN.
|
|
390
|
+
//
|
|
391
|
+
// A bare {verdict, ok, rows} says a gate passed but not what it passed on, so any consumer
|
|
392
|
+
// asking "is this ALIVE for the revision I am about to expose?" can only take the runner's
|
|
393
|
+
// word for it. That is fine for a human reading output and wrong for an automated gate:
|
|
394
|
+
// `resolve-release`'s auto-when-green shifts production traffic on condition (e), "an ALIVE
|
|
395
|
+
// verdict on THAT EXACT candidate", which is unfalsifiable unless the verdict names its
|
|
396
|
+
// target. Recording it turns an assertion into a check.
|
|
397
|
+
//
|
|
398
|
+
// `generatedAt` is the same argument in the time dimension (issue #431). "On that exact
|
|
399
|
+
// candidate" bounds WHICH revision the evidence covers; it does not bound HOW OLD the
|
|
400
|
+
// evidence is — and the release lane validates at 0% traffic, then waits for a human
|
|
401
|
+
// authorization that may arrive hours later, by which time the coordinates may have moved.
|
|
402
|
+
// A consumer can only bound that age if the artifact carries its generation time, so the
|
|
403
|
+
// field is stamped unconditionally (after `extra`, so no caller can suppress it): an
|
|
404
|
+
// unstamped verdict must be impossible to produce, not merely discouraged. Consumers treat
|
|
405
|
+
// its absence as NOT-fresh — absent evidence is not fresh evidence.
|
|
406
|
+
//
|
|
407
|
+
// WHAT IT DATES: the VERDICT, not the RUN. This function stamps when the reduction happened,
|
|
408
|
+
// and the reduction reads a results file of any age — so `verdict --results <old file>` mints
|
|
409
|
+
// a gate-fresh verdict over stale observations, and no consumer can tell. That is deliberate
|
|
410
|
+
// scope, not an oversight: this script owns the decidable shell and never drives the app, so
|
|
411
|
+
// it has no run time to carry. The freshness contract it therefore supports is "this verdict
|
|
412
|
+
// was computed just now," which is only evidence of live coordinates when the results were
|
|
413
|
+
// driven just now. Re-minting a verdict is not re-validation; re-drive the paths.
|
|
414
|
+
export function stampTarget(verdict, target, extra = {}) {
|
|
415
|
+
return { ...verdict, target: target || 'local', ...extra, generatedAt: new Date().toISOString() }
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// A browser path's `journey` field points at the Tier-3 spec that drives it,
|
|
419
|
+
// written as "@smoke <covers-id>" (or a bare id). Pull the @covers id out — that's
|
|
420
|
+
// the key both sides reconcile on. Mirrors the ledger's `route:` stripping.
|
|
421
|
+
export function journeyCoversId(journey) {
|
|
422
|
+
if (!journey || typeof journey !== 'string') return null
|
|
423
|
+
const id = journey.trim().replace(/^@smoke\b[:\s]*/i, '').replace(/^route:/, '').trim()
|
|
424
|
+
return id || null
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Reconcile the manifest's browser paths against the authoritative set of @smoke
|
|
428
|
+
// @covers ids (scanned from the specs). BOTH directions matter and BOTH are errors:
|
|
429
|
+
// - a manifest path pointing at a journey that isn't @smoke-tagged → the gate
|
|
430
|
+
// can't drive it (fail-closed, but a broken gate).
|
|
431
|
+
// - a @smoke-tagged journey missing from the manifest → it silently never runs,
|
|
432
|
+
// so the gate reports ALIVE while a do-not-ship flow went unchecked (a FALSE
|
|
433
|
+
// ALIVE — the exact hole the browser=journey reference otherwise leaves open).
|
|
434
|
+
// This is the mechanical backstop for the "make sure the @smoke journey exists"
|
|
435
|
+
// prose: a described precondition degrades to a lookalike, a checked one doesn't.
|
|
436
|
+
export function reconcileJourneys(manifest, smokeCoversIds) {
|
|
437
|
+
const errors = []
|
|
438
|
+
const warnings = []
|
|
439
|
+
const smoke = new Set(smokeCoversIds || [])
|
|
440
|
+
const referenced = new Set()
|
|
441
|
+
for (const p of (manifest && manifest.paths) || []) {
|
|
442
|
+
if (!p || p.surface !== 'browser') continue
|
|
443
|
+
const id = journeyCoversId(p.journey)
|
|
444
|
+
if (!id) { warnings.push(`paths (${p.id}): could not read a @covers id from journey "${p.journey}" — can't reconcile it against the @smoke tags`); continue }
|
|
445
|
+
referenced.add(id)
|
|
446
|
+
if (!smoke.has(id)) errors.push(`paths (${p.id}): journey "${id}" is not @smoke-tagged in the scanned specs — the gate can't drive it (tag that Tier-3 journey @smoke, or fix the reference)`)
|
|
447
|
+
}
|
|
448
|
+
for (const id of smoke) {
|
|
449
|
+
if (!referenced.has(id)) errors.push(`@smoke journey "${id}" is tagged do-not-ship but absent from the smoke manifest — it would silently never gate (a false ALIVE). Add a browser path referencing it, or drop the @smoke tag.`)
|
|
450
|
+
}
|
|
451
|
+
return { errors, warnings }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function normalizeInventory(inventory) {
|
|
455
|
+
if (!inventory || typeof inventory !== 'object') return []
|
|
456
|
+
if (Array.isArray(inventory.surfaces)) return inventory.surfaces
|
|
457
|
+
const grouped = []
|
|
458
|
+
const groups = [
|
|
459
|
+
['routes', 'route'],
|
|
460
|
+
['apis', 'api'],
|
|
461
|
+
['tables', 'table'],
|
|
462
|
+
['events', 'event'],
|
|
463
|
+
['jobs', 'job'],
|
|
464
|
+
['permissions', 'permission'],
|
|
465
|
+
['integrations', 'integration'],
|
|
466
|
+
]
|
|
467
|
+
for (const [key, kind] of groups) {
|
|
468
|
+
for (const id of inventory[key] || []) grouped.push({ id, kind })
|
|
469
|
+
}
|
|
470
|
+
return grouped
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function scoreSurface(surface) {
|
|
474
|
+
const id = String(surface.id || '')
|
|
475
|
+
const haystack = id.toLowerCase()
|
|
476
|
+
let score = 0
|
|
477
|
+
const reasons = []
|
|
478
|
+
|
|
479
|
+
if (surface.kind === 'route') {
|
|
480
|
+
score += 20
|
|
481
|
+
reasons.push('browser route')
|
|
482
|
+
if (surface.reachability === 'reachable' || surface.reachability === 'guarded') {
|
|
483
|
+
score += 20
|
|
484
|
+
reasons.push(`${surface.reachability} route`)
|
|
485
|
+
}
|
|
486
|
+
if (surface.class === 'mutation') {
|
|
487
|
+
score += 55
|
|
488
|
+
reasons.push('mutation route')
|
|
489
|
+
} else if (surface.class === 'detail') {
|
|
490
|
+
score += 20
|
|
491
|
+
reasons.push('detail route')
|
|
492
|
+
}
|
|
493
|
+
} else if (surface.kind === 'api') {
|
|
494
|
+
if (/health|ready|live/.test(haystack)) {
|
|
495
|
+
score += 45
|
|
496
|
+
reasons.push('health/liveness endpoint')
|
|
497
|
+
}
|
|
498
|
+
if (surface.class === 'mutation') {
|
|
499
|
+
score += 25
|
|
500
|
+
reasons.push('mutation API')
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
for (const keyword of GOLDEN_KEYWORDS) {
|
|
505
|
+
if (haystack.includes(keyword)) {
|
|
506
|
+
score += 30
|
|
507
|
+
reasons.push(`keyword:${keyword}`)
|
|
508
|
+
break
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (/about|help|docs|terms|privacy|status/.test(haystack)) score -= 60
|
|
513
|
+
return { score, reasons }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function suggestCandidates(inventory, options = {}) {
|
|
517
|
+
const limit = Number.isFinite(options.limit) ? options.limit : 8
|
|
518
|
+
const surfaces = normalizeInventory(inventory)
|
|
519
|
+
const scored = surfaces
|
|
520
|
+
.map((surface) => ({ surface, ...scoreSurface(surface) }))
|
|
521
|
+
.filter(({ score }) => score > 0)
|
|
522
|
+
.sort((a, b) => b.score - a.score || String(a.surface.id).localeCompare(String(b.surface.id)))
|
|
523
|
+
.slice(0, limit)
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
_comment: 'CANDIDATES ONLY. Code can suggest likely golden paths from routes/APIs, but only product/release judgment can decide the smoke manifest. Promote curated entries into smoke.manifest.json paths by writing concrete observables and synthetic tenant data.',
|
|
527
|
+
candidates: scored.map(({ surface, score, reasons }) => {
|
|
528
|
+
const isRoute = surface.kind === 'route'
|
|
529
|
+
const mutates = surface.class === 'mutation'
|
|
530
|
+
return {
|
|
531
|
+
id: String(surface.id || '').replace(/^\/+/, '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '') || 'root',
|
|
532
|
+
surface: isRoute ? 'browser' : 'api',
|
|
533
|
+
start: isRoute ? surface.id : `${surface.method || (mutates ? 'POST' : 'GET')} ${surface.id}`,
|
|
534
|
+
mutates,
|
|
535
|
+
score,
|
|
536
|
+
reasons,
|
|
537
|
+
observable: 'TODO: replace with the concrete user-visible/API evidence that proves this critical path worked',
|
|
538
|
+
tenant: mutates ? 'TODO: synthetic test tenant if this candidate is promoted' : undefined,
|
|
539
|
+
}
|
|
540
|
+
}),
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ---- CLI ----
|
|
545
|
+
|
|
546
|
+
function parseArgs(argv) {
|
|
547
|
+
const f = {}
|
|
548
|
+
for (let i = 0; i < argv.length; i++) {
|
|
549
|
+
const a = argv[i]
|
|
550
|
+
if (a === '--manifest') f.manifest = argv[++i]
|
|
551
|
+
else if (a === '--results') f.results = argv[++i]
|
|
552
|
+
else if (a === '--target') f.target = argv[++i]
|
|
553
|
+
else if (a === '--inventory') f.inventory = argv[++i]
|
|
554
|
+
else if (a === '--journeys') f.journeys = argv[++i]
|
|
555
|
+
else if (a === '--profile') f.profile = argv[++i]
|
|
556
|
+
else if (a === '--limit') f.limit = Number(argv[++i])
|
|
557
|
+
else if (a === '--out') f.out = argv[++i]
|
|
558
|
+
else if (a === '--json') f.json = true
|
|
559
|
+
// `--no-mutations` was removed (issue #440): mutation policy is now a property of the
|
|
560
|
+
// selected profile, not a run flag. Refuse it LOUDLY rather than swallow it — a caller
|
|
561
|
+
// following the old doctrine would otherwise get silently weaker exclusion (the flag does
|
|
562
|
+
// nothing) with no warning. Name the replacement so the fix is obvious.
|
|
563
|
+
else if (a === '--no-mutations') {
|
|
564
|
+
if (!f._error) f._error = { code: 2, message: 'smoke: --no-mutations was removed — mutation policy is now a profile property, not a run flag. Select a read-only production profile instead (--profile prod-preview / prod-active); production profiles exclude every mutating path regardless of tenant. (see references/manifest.md)' }
|
|
565
|
+
}
|
|
566
|
+
// Reject any other unknown flag by name instead of silently ignoring it — a swallowed
|
|
567
|
+
// flag is a footgun (the caller believes it took effect). This closes the whole class,
|
|
568
|
+
// not just the one removed flag.
|
|
569
|
+
else if (a.startsWith('--')) {
|
|
570
|
+
if (!f._error) f._error = { code: 2, message: `smoke: unknown flag "${a}" — valid flags are --manifest --profile --target --results --inventory --journeys --out --limit --json (see references/manifest.md)` }
|
|
571
|
+
}
|
|
572
|
+
else if (!f.cmd) f.cmd = a
|
|
573
|
+
}
|
|
574
|
+
return f
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function readJson(p) {
|
|
578
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Scan a journeys dir for the authoritative @smoke set: any spec file carrying an
|
|
582
|
+
// @smoke marker contributes its @covers ids (same `@covers <id>` regex the ledger
|
|
583
|
+
// uses). File-level association — journey specs are one-journey-per-file by
|
|
584
|
+
// e2e-test convention; a multi-journey file contributes all its ids.
|
|
585
|
+
function scanSmokeTags(dir) {
|
|
586
|
+
const ids = new Set()
|
|
587
|
+
const noCovers = []
|
|
588
|
+
const walk = (d) => {
|
|
589
|
+
for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
|
|
590
|
+
const full = `${d}/${ent.name}`
|
|
591
|
+
if (ent.isDirectory()) { if (ent.name !== 'node_modules') walk(full); continue }
|
|
592
|
+
if (!/\.(spec|test)\.[cm]?[jt]sx?$/.test(ent.name)) continue
|
|
593
|
+
const text = fs.readFileSync(full, 'utf8')
|
|
594
|
+
if (!/@smoke\b/.test(text)) continue
|
|
595
|
+
const covers = [...text.matchAll(/@covers\s+(\S+)/g)].map((m) => m[1].replace(/^route:/, ''))
|
|
596
|
+
if (covers.length === 0) noCovers.push(full)
|
|
597
|
+
for (const c of covers) ids.add(c)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (fs.existsSync(dir)) walk(dir)
|
|
601
|
+
return { ids: [...ids], noCovers }
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Reconcile browser paths against the @smoke tags when a --journeys dir is given.
|
|
605
|
+
// Without it, only warn (and only if there ARE browser paths to reconcile) — an
|
|
606
|
+
// api/cli-only repo has nothing to check, and forcing the flag would break every
|
|
607
|
+
// existing call. With it, drift is an error (see reconcileJourneys).
|
|
608
|
+
function reconcile(manifest, journeysDir) {
|
|
609
|
+
const hasBrowser = ((manifest && manifest.paths) || []).some((p) => p && p.surface === 'browser')
|
|
610
|
+
if (!journeysDir) {
|
|
611
|
+
return hasBrowser
|
|
612
|
+
? { errors: [], warnings: ['browser paths not reconciled against @smoke tags — pass --journeys <dir> so a manifest/tag drift is caught (a @smoke journey missing from the manifest is a silent false-ALIVE)'] }
|
|
613
|
+
: { errors: [], warnings: [] }
|
|
614
|
+
}
|
|
615
|
+
const { ids, noCovers } = scanSmokeTags(journeysDir)
|
|
616
|
+
const r = reconcileJourneys(manifest, ids)
|
|
617
|
+
for (const f of noCovers) r.warnings.push(`spec tagged @smoke has no @covers id to key on: ${f}`)
|
|
618
|
+
return r
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function runCli(argv) {
|
|
622
|
+
const f = parseArgs(argv)
|
|
623
|
+
if (f._error) { console.error(f._error.message); return f._error.code }
|
|
624
|
+
if (!f.cmd) { console.error('usage: smoke <validate|plan|suggest|verdict> --manifest <f> [--profile <name>] [--target <url>] [--results <f>]'); return 2 }
|
|
625
|
+
|
|
626
|
+
if (f.cmd === 'suggest') {
|
|
627
|
+
if (!f.inventory) { console.error('--inventory is required for suggest'); return 2 }
|
|
628
|
+
const candidates = suggestCandidates(readJson(f.inventory), { limit: f.limit })
|
|
629
|
+
if (f.out) fs.writeFileSync(f.out, JSON.stringify(candidates, null, 2))
|
|
630
|
+
console.log(JSON.stringify(candidates, null, 2))
|
|
631
|
+
return 0
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (!f.manifest) { console.error('--manifest is required'); return 2 }
|
|
635
|
+
// A *missing* manifest is not an *invalid* one — it means no golden-path set
|
|
636
|
+
// has been curated yet. Route to bootstrap instead of crashing with ENOENT, so
|
|
637
|
+
// "ensure a manifest exists" is a clear next step rather than a stack trace.
|
|
638
|
+
if (!fs.existsSync(f.manifest)) {
|
|
639
|
+
console.error(`smoke: no manifest at ${f.manifest} — there is no curated golden-path set yet.`)
|
|
640
|
+
console.error(' ensure one before gating: seed candidates from the surface inventory, then curate per product judgment:')
|
|
641
|
+
console.error(' node smoke.mjs suggest --inventory docs/tests/_generated/surface-inventory.json --out smoke.candidates.json')
|
|
642
|
+
console.error(' promote only the release-blocking candidates into smoke.manifest.json (concrete observables, synthetic tenants), then `validate`.')
|
|
643
|
+
console.error(' (see references/manifest.md — the skill ships no default set; you author it per app.)')
|
|
644
|
+
return 2
|
|
645
|
+
}
|
|
646
|
+
const manifest = readJson(f.manifest)
|
|
647
|
+
|
|
648
|
+
if (f.cmd === 'validate') {
|
|
649
|
+
const shape = validateManifest(manifest)
|
|
650
|
+
const rec = reconcile(manifest, f.journeys)
|
|
651
|
+
const errors = [...shape.errors, ...rec.errors]
|
|
652
|
+
const warnings = [...shape.warnings, ...rec.warnings]
|
|
653
|
+
const ok = errors.length === 0
|
|
654
|
+
if (f.json) console.log(JSON.stringify({ ok, errors, warnings }, null, 2))
|
|
655
|
+
else {
|
|
656
|
+
if (ok) console.log(`smoke: ✓ manifest valid (${manifest.paths.length} golden path(s))`)
|
|
657
|
+
else { console.log('smoke: ✗ manifest invalid:'); errors.forEach((e) => console.log(` - ${e}`)) }
|
|
658
|
+
warnings.forEach((w) => console.log(` ⚠ ${w}`))
|
|
659
|
+
}
|
|
660
|
+
return ok ? 0 : 1
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const { ok, errors, warnings } = validateManifest(manifest)
|
|
664
|
+
if (!ok) { console.error('smoke: manifest invalid — fix it before plan/verdict:'); errors.forEach((e) => console.error(` - ${e}`)); return 1 }
|
|
665
|
+
warnings.forEach((w) => console.error(`smoke: ⚠ ${w}`))
|
|
666
|
+
const rec = reconcile(manifest, f.journeys)
|
|
667
|
+
if (rec.errors.length) { console.error('smoke: manifest/@smoke reconciliation failed — fix before plan/verdict:'); rec.errors.forEach((e) => console.error(` - ${e}`)); return 1 }
|
|
668
|
+
rec.warnings.forEach((w) => console.error(`smoke: ⚠ ${w}`))
|
|
669
|
+
|
|
670
|
+
if (f.cmd === 'plan') {
|
|
671
|
+
if (!f.profile) { console.error('--profile is required for plan (selection is by named Validation Profile — see references/manifest.md)'); return 2 }
|
|
672
|
+
let sel
|
|
673
|
+
try { sel = selectPathsForProfile(manifest, f.profile) } catch (e) { console.error(`smoke: ${e.message}`); return 1 }
|
|
674
|
+
const { profile, run, excluded } = sel
|
|
675
|
+
const target = f.target || 'local'
|
|
676
|
+
const plan = { profile, target, run: run.map((p) => ({ id: p.id, surface: p.surface, drive: p.journey || p.start || p.command, observable: p.observable })), excluded }
|
|
677
|
+
if (f.json) console.log(JSON.stringify(plan, null, 2))
|
|
678
|
+
else {
|
|
679
|
+
console.log(`smoke plan — profile: ${profile.name} (${profile.gate} · ${profile.environment}/${profile.endpoint} · ${profile.mutationPolicy}) — target: ${target}`)
|
|
680
|
+
run.forEach((p) => console.log(` ▶ ${p.id} [${p.surface}] — ${p.observable || p.journey}`))
|
|
681
|
+
excluded.forEach((e) => console.log(` ⤫ ${e.id} — ${e.reason}`))
|
|
682
|
+
}
|
|
683
|
+
return 0
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (f.cmd === 'verdict') {
|
|
687
|
+
if (!f.profile) { console.error('--profile is required for verdict (the SAME profile the run used, so run and verdict select one set)'); return 2 }
|
|
688
|
+
if (!f.results) { console.error('--results is required for verdict'); return 2 }
|
|
689
|
+
let sel
|
|
690
|
+
try { sel = selectPathsForProfile(manifest, f.profile) } catch (e) { console.error(`smoke: ${e.message}`); return 1 }
|
|
691
|
+
const { profile, run } = sel
|
|
692
|
+
// The read-only flag green-gate.mjs reads is DERIVED from the profile, not passed by the
|
|
693
|
+
// caller: a production (read-only) profile ⇒ noMutations:true, mechanically enforcing
|
|
694
|
+
// principle 4 without a flag anyone has to remember.
|
|
695
|
+
const noMutations = profile.mutationPolicy === 'read-only'
|
|
696
|
+
const v = stampTarget(computeVerdict(run, readJson(f.results)), f.target, { profile: f.profile, noMutations })
|
|
697
|
+
if (f.out) fs.writeFileSync(f.out, JSON.stringify(v, null, 2))
|
|
698
|
+
if (f.json) console.log(JSON.stringify(v, null, 2))
|
|
699
|
+
else if (v.verdict === 'EMPTY') {
|
|
700
|
+
console.log('smoke: ⚠ EMPTY — 0 golden paths selected, so this run proves nothing')
|
|
701
|
+
console.log(` ${v.note}`)
|
|
702
|
+
} else {
|
|
703
|
+
console.log(`smoke: ${v.ok ? '✅ ALIVE' : '❌ DEAD'} — ${v.total - v.broken.length}/${v.total} golden path(s) passed`)
|
|
704
|
+
v.broken.forEach((b) => console.log(` ✗ ${b.id} [${b.status}] — ${b.note}`))
|
|
705
|
+
}
|
|
706
|
+
return v.ok ? 0 : 1
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
console.error(`unknown command: ${f.cmd}`)
|
|
710
|
+
return 2
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (isMainModule(import.meta.url)) process.exit(runCli(process.argv.slice(2)))
|