@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.
Files changed (397) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +90 -0
  3. package/bin/1aboveio-skills.mjs +18 -0
  4. package/package.json +28 -0
  5. package/runtime/skills/distribution/generated/recipes.json +1189 -0
  6. package/runtime/skills/distribution/scripts/bundles.mjs +280 -0
  7. package/runtime/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
  8. package/skills/backend/airflow-dag-develop/LICENSE +3 -0
  9. package/skills/backend/airflow-dag-develop/SKILL.md +111 -0
  10. package/skills/backend/app-debug/LICENSE +3 -0
  11. package/skills/backend/app-debug/SKILL.md +109 -0
  12. package/skills/backend/app-debug/references/common-errors.md +128 -0
  13. package/skills/backend/python-backend/LICENSE +3 -0
  14. package/skills/backend/python-backend/SKILL.md +326 -0
  15. package/skills/cicd-pipeline/cloud-build/LICENSE +3 -0
  16. package/skills/cicd-pipeline/cloud-build/SKILL.md +707 -0
  17. package/skills/cicd-pipeline/cloud-debug/LICENSE +3 -0
  18. package/skills/cicd-pipeline/cloud-debug/SKILL.md +316 -0
  19. package/skills/cicd-pipeline/cloud-debug/references/build-failures.md +238 -0
  20. package/skills/cicd-pipeline/cloud-debug/references/deploy-failures.md +376 -0
  21. package/skills/cicd-pipeline/cloud-debug/references/pipeline-failures.md +378 -0
  22. package/skills/cicd-pipeline/cloud-deploy/LICENSE +3 -0
  23. package/skills/cicd-pipeline/cloud-deploy/SKILL.md +229 -0
  24. package/skills/cicd-pipeline/cloud-deploy/references/config-templates.md +257 -0
  25. package/skills/cicd-pipeline/docker/LICENSE +3 -0
  26. package/skills/cicd-pipeline/docker/SKILL.md +126 -0
  27. package/skills/cicd-pipeline/google-cloud/LICENSE +3 -0
  28. package/skills/cicd-pipeline/google-cloud/SKILL.md +118 -0
  29. package/skills/cicd-pipeline/google-cloud/references/gcs.md +469 -0
  30. package/skills/cicd-pipeline/google-cloud/references/iam.md +451 -0
  31. package/skills/cicd-pipeline/google-cloud/references/project.md +349 -0
  32. package/skills/cicd-pipeline/google-cloud/references/secrets.md +336 -0
  33. package/skills/cicd-pipeline/google-cloud/references/vpc.md +312 -0
  34. package/skills/cicd-pipeline/google-cloud/scripts/create-sa.sh +36 -0
  35. package/skills/cicd-pipeline/google-cloud/scripts/gcp-config.sh +31 -0
  36. package/skills/cicd-pipeline/google-cloud/scripts/grant-iap.sh +41 -0
  37. package/skills/cicd-pipeline/google-cloud/scripts/setup-secrets.sh +48 -0
  38. package/skills/cicd-pipeline/mergify/LICENSE +3 -0
  39. package/skills/cicd-pipeline/mergify/SKILL.md +138 -0
  40. package/skills/cicd-pipeline/mergify/assets/templates/mergify.yml +237 -0
  41. package/skills/cicd-pipeline/mergify/assets/templates/ruleset.json +46 -0
  42. package/skills/cicd-pipeline/mergify/references/branch-protection.md +277 -0
  43. package/skills/cicd-pipeline/mergify/references/configuration.md +183 -0
  44. package/skills/cicd-pipeline/mergify/references/diagnosis.md +73 -0
  45. package/skills/cicd-pipeline/mergify/references/traps.md +78 -0
  46. package/skills/cicd-pipeline/mergify/references/watch-contract.md +218 -0
  47. package/skills/cicd-pipeline/mergify/scripts/audit-core.mjs +131 -0
  48. package/skills/cicd-pipeline/mergify/scripts/audit.mjs +4 -0
  49. package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs +663 -0
  50. package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery.mjs +4 -0
  51. package/skills/cicd-pipeline/podman/LICENSE +3 -0
  52. package/skills/cicd-pipeline/podman/SKILL.md +70 -0
  53. package/skills/cicd-pipeline/podman/agents/openai.yaml +4 -0
  54. package/skills/cicd-pipeline/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
  55. package/skills/cicd-pipeline/podman/assets/templates/podman-service-override.conf +3 -0
  56. package/skills/cicd-pipeline/podman/references/compose-compatibility.md +70 -0
  57. package/skills/cicd-pipeline/podman/references/networking-and-ports.md +74 -0
  58. package/skills/cicd-pipeline/podman/references/rootless-services-and-sockets.md +156 -0
  59. package/skills/cicd-pipeline/podman/references/troubleshooting.md +98 -0
  60. package/skills/engineering/e2e-test/LICENSE +3 -0
  61. package/skills/engineering/e2e-test/SKILL.md +156 -0
  62. package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +272 -0
  63. package/skills/engineering/e2e-test/assets/ci-gates.github.yml +451 -0
  64. package/skills/engineering/e2e-test/assets/e2e-workflow.yml +282 -0
  65. package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
  66. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +71 -0
  67. package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +219 -0
  68. package/skills/engineering/e2e-test/references/authoring/test-skipping.md +44 -0
  69. package/skills/engineering/e2e-test/references/ci-integration.md +121 -0
  70. package/skills/engineering/e2e-test/references/ci-playwright-container.md +280 -0
  71. package/skills/engineering/e2e-test/references/debugging.md +36 -0
  72. package/skills/engineering/e2e-test/references/presentation-sweep.md +131 -0
  73. package/skills/engineering/e2e-test/references/reviewing.md +39 -0
  74. package/skills/engineering/e2e-test/references/route-discovery.md +50 -0
  75. package/skills/engineering/e2e-test/references/route-manifest.md +44 -0
  76. package/skills/engineering/e2e-test/scripts/detect-routes-fastapi.py +290 -0
  77. package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
  78. package/skills/engineering/e2e-test/scripts/post-visual-evidence.mjs +158 -0
  79. package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
  80. package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
  81. package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
  82. package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
  83. package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +148 -0
  84. package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
  85. package/skills/engineering/e2e-test/scripts/presentation-visual.mjs +84 -0
  86. package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +75 -0
  87. package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
  88. package/skills/engineering/engineering-runtime/LICENSE +3 -0
  89. package/skills/engineering/engineering-runtime/SKILL.md +10 -0
  90. package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
  91. package/skills/engineering/engineering-runtime/coherence/workflow.json +553 -0
  92. package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
  93. package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
  94. package/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
  95. package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
  96. package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
  97. package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
  98. package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
  99. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +576 -0
  100. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +166 -0
  101. package/skills/engineering/ensure-coverage/LICENSE +3 -0
  102. package/skills/engineering/ensure-coverage/SKILL.md +136 -0
  103. package/skills/engineering/ensure-coverage/evals/evals.json +125 -0
  104. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +91 -0
  105. package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +83 -0
  106. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
  107. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
  108. package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
  109. package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +25 -0
  110. package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
  111. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +87 -0
  112. package/skills/engineering/ensure-coverage/references/depth/test-smells.md +23 -0
  113. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +164 -0
  114. package/skills/engineering/ensure-coverage/references/enforcement/hooks.md +85 -0
  115. package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +109 -0
  116. package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +33 -0
  117. package/skills/engineering/ensure-coverage/references/presentation/axis.md +78 -0
  118. package/skills/engineering/ensure-coverage/references/presentation/runner-contract.md +74 -0
  119. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +33 -0
  120. package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
  121. package/skills/engineering/ensure-coverage/references/process/review-contract-template.md +119 -0
  122. package/skills/engineering/ensure-coverage/references/process/scope-class.md +178 -0
  123. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +55 -0
  124. package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +45 -0
  125. package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +93 -0
  126. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +178 -0
  127. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +101 -0
  128. package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +358 -0
  129. package/skills/engineering/ensure-coverage/scripts/coverage-checklist.mjs +494 -0
  130. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +663 -0
  131. package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
  132. package/skills/engineering/ensure-coverage/scripts/evidence-block.mjs +367 -0
  133. package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +269 -0
  134. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
  135. package/skills/engineering/ensure-coverage/scripts/package-lock.json +76 -0
  136. package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
  137. package/skills/engineering/ensure-coverage/scripts/scope-class.mjs +554 -0
  138. package/skills/engineering/harness-runtime/LICENSE +3 -0
  139. package/skills/engineering/harness-runtime/SKILL.md +18 -0
  140. package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
  141. package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
  142. package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
  143. package/skills/engineering/harness-runtime/contracts.md +15 -0
  144. package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
  145. package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
  146. package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
  147. package/skills/engineering/harness-runtime/index.mjs +767 -0
  148. package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
  149. package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
  150. package/skills/engineering/harness-runtime/references/model-catalog-seed.json +159 -0
  151. package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
  152. package/skills/engineering/implement-and-pr/LICENSE +3 -0
  153. package/skills/engineering/implement-and-pr/SKILL.md +176 -0
  154. package/skills/engineering/implement-and-pr/references/ci-iteration.md +10 -0
  155. package/skills/engineering/implement-and-pr/references/closeout.md +27 -0
  156. package/skills/engineering/implement-and-pr/references/contract-complete-fix-rounds.md +34 -0
  157. package/skills/engineering/implement-and-pr/references/evidence-rules.md +39 -0
  158. package/skills/engineering/implement-and-pr/references/incremental-plan.md +16 -0
  159. package/skills/engineering/implement-and-pr/references/self-review.md +23 -0
  160. package/skills/engineering/implement-and-pr/references/tdd-mode.md +18 -0
  161. package/skills/engineering/resolve-issues/LICENSE +3 -0
  162. package/skills/engineering/resolve-issues/SKILL.md +167 -0
  163. package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +448 -0
  164. package/skills/engineering/resolve-issues/references/breaker.md +82 -0
  165. package/skills/engineering/resolve-issues/references/deliverables.md +27 -0
  166. package/skills/engineering/resolve-issues/references/delivery.md +108 -0
  167. package/skills/engineering/resolve-issues/references/evidence-lane.md +21 -0
  168. package/skills/engineering/resolve-issues/references/exact-head-ci.md +287 -0
  169. package/skills/engineering/resolve-issues/references/fan-out.md +33 -0
  170. package/skills/engineering/resolve-issues/references/finalization.md +68 -0
  171. package/skills/engineering/resolve-issues/references/guarantees.md +10 -0
  172. package/skills/engineering/resolve-issues/references/high-risk.md +29 -0
  173. package/skills/engineering/resolve-issues/references/incidents/848/README.md +156 -0
  174. package/skills/engineering/resolve-issues/references/intake.md +86 -0
  175. package/skills/engineering/resolve-issues/references/integration-gate.md +53 -0
  176. package/skills/engineering/resolve-issues/references/interference.md +87 -0
  177. package/skills/engineering/resolve-issues/references/loop.md +134 -0
  178. package/skills/engineering/resolve-issues/references/model-catalog.md +9 -0
  179. package/skills/engineering/resolve-issues/references/postmortem.md +27 -0
  180. package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +41 -0
  181. package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +48 -0
  182. package/skills/engineering/resolve-issues/references/pre-flight.md +41 -0
  183. package/skills/engineering/resolve-issues/references/regression-checklist.md +26 -0
  184. package/skills/engineering/resolve-issues/references/run-state.md +288 -0
  185. package/skills/engineering/resolve-issues/references/sandboxed-testing.md +48 -0
  186. package/skills/engineering/resolve-issues/references/spawn-contract.md +96 -0
  187. package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +40 -0
  188. package/skills/engineering/resolve-issues/references/why.md +653 -0
  189. package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +49 -0
  190. package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +721 -0
  191. package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +962 -0
  192. package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +220 -0
  193. package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +420 -0
  194. package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +256 -0
  195. package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +168 -0
  196. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
  197. package/skills/engineering/resolve-issues/scripts/doctrine.mjs +62 -0
  198. package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +191 -0
  199. package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +413 -0
  200. package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +332 -0
  201. package/skills/engineering/resolve-issues/scripts/finalize.mjs +488 -0
  202. package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +3307 -0
  203. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +16 -0
  204. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +51 -0
  205. package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +17 -0
  206. package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +166 -0
  207. package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +92 -0
  208. package/skills/engineering/resolve-issues/scripts/guide-index.mjs +73 -0
  209. package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +159 -0
  210. package/skills/engineering/resolve-issues/scripts/interference.mjs +427 -0
  211. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
  212. package/skills/engineering/resolve-issues/scripts/next-operations.mjs +419 -0
  213. package/skills/engineering/resolve-issues/scripts/postmortem.mjs +909 -0
  214. package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +322 -0
  215. package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +415 -0
  216. package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +697 -0
  217. package/skills/engineering/resolve-issues/scripts/report.mjs +494 -0
  218. package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +172 -0
  219. package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +79 -0
  220. package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +334 -0
  221. package/skills/engineering/resolve-issues/scripts/run-state.mjs +4784 -0
  222. package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +395 -0
  223. package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +290 -0
  224. package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +170 -0
  225. package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +293 -0
  226. package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +197 -0
  227. package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +127 -0
  228. package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +893 -0
  229. package/skills/engineering/resolve-issues/scripts/workspaces.mjs +829 -0
  230. package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +290 -0
  231. package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +80 -0
  232. package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +102 -0
  233. package/skills/engineering/resolve-release/LICENSE +3 -0
  234. package/skills/engineering/resolve-release/SKILL.md +112 -0
  235. package/skills/engineering/resolve-release/references/assembly.md +137 -0
  236. package/skills/engineering/resolve-release/references/auto-when-green.md +56 -0
  237. package/skills/engineering/resolve-release/references/candidate.md +167 -0
  238. package/skills/engineering/resolve-release/references/exposure.md +178 -0
  239. package/skills/engineering/resolve-release/references/handoff.md +24 -0
  240. package/skills/engineering/resolve-release/references/postmortem.md +230 -0
  241. package/skills/engineering/resolve-release/references/preflight.md +207 -0
  242. package/skills/engineering/resolve-release/references/principles.md +94 -0
  243. package/skills/engineering/resolve-release/references/regression-checklist.md +36 -0
  244. package/skills/engineering/resolve-release/references/related-skills.md +12 -0
  245. package/skills/engineering/resolve-release/references/routing.md +149 -0
  246. package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +285 -0
  247. package/skills/engineering/resolve-release/references/versioning.md +202 -0
  248. package/skills/engineering/resolve-release/references/why.md +53 -0
  249. package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +389 -0
  250. package/skills/engineering/resolve-release/scripts/build-changes.mjs +209 -0
  251. package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +407 -0
  252. package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +904 -0
  253. package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +81 -0
  254. package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +612 -0
  255. package/skills/engineering/resolve-release/scripts/close-attempt.mjs +135 -0
  256. package/skills/engineering/resolve-release/scripts/closeout-release.mjs +161 -0
  257. package/skills/engineering/resolve-release/scripts/doctrine.mjs +106 -0
  258. package/skills/engineering/resolve-release/scripts/durable-processing.mjs +522 -0
  259. package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +659 -0
  260. package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +1014 -0
  261. package/skills/engineering/resolve-release/scripts/finalize-release.mjs +526 -0
  262. package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +169 -0
  263. package/skills/engineering/resolve-release/scripts/green-gate.mjs +599 -0
  264. package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +752 -0
  265. package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +56 -0
  266. package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +1538 -0
  267. package/skills/engineering/resolve-release/scripts/postmortem.mjs +381 -0
  268. package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +498 -0
  269. package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +326 -0
  270. package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +272 -0
  271. package/skills/engineering/resolve-release/scripts/report.mjs +417 -0
  272. package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +114 -0
  273. package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +238 -0
  274. package/skills/engineering/resolve-release/scripts/review-packet.mjs +503 -0
  275. package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +263 -0
  276. package/skills/engineering/resolve-release/scripts/version-assert.mjs +339 -0
  277. package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +485 -0
  278. package/skills/engineering/resolve-release/scripts/version.mjs +1199 -0
  279. package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +449 -0
  280. package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +21 -0
  281. package/skills/engineering/resolve-release/vendor/README.md +60 -0
  282. package/skills/engineering/resolve-release/vendor/acorn.mjs +6233 -0
  283. package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +21 -0
  284. package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +341 -0
  285. package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +70 -0
  286. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +2 -0
  287. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +1 -0
  288. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +7 -0
  289. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
  290. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +2033 -0
  291. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +2033 -0
  292. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +7 -0
  293. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +1 -0
  294. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +2 -0
  295. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +1 -0
  296. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +2 -0
  297. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +1 -0
  298. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +2 -0
  299. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
  300. package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +49 -0
  301. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +21 -0
  302. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +5 -0
  303. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +549 -0
  304. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +549 -0
  305. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +2 -0
  306. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +1 -0
  307. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +2 -0
  308. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +1 -0
  309. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +36 -0
  310. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +47 -0
  311. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +82 -0
  312. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +11 -0
  313. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +22 -0
  314. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +21 -0
  315. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
  316. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
  317. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +11 -0
  318. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +25 -0
  319. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
  320. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +85 -0
  321. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +85 -0
  322. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +2 -0
  323. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +1 -0
  324. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +2 -0
  325. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +1 -0
  326. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +20 -0
  327. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +20 -0
  328. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +2 -0
  329. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +1 -0
  330. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +2 -0
  331. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +1 -0
  332. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +61 -0
  333. package/skills/engineering/review-pr/LICENSE +3 -0
  334. package/skills/engineering/review-pr/SKILL.md +123 -0
  335. package/skills/engineering/review-pr/references/adversarial-reviewer-prompt.md +42 -0
  336. package/skills/engineering/review-pr/references/code-correctness.md +5 -0
  337. package/skills/engineering/review-pr/references/contract-freshness.md +9 -0
  338. package/skills/engineering/review-pr/references/coordination.md +18 -0
  339. package/skills/engineering/review-pr/references/domain-hazards.md +123 -0
  340. package/skills/engineering/review-pr/references/finding-themes.md +7 -0
  341. package/skills/engineering/review-pr/references/github-posting.md +98 -0
  342. package/skills/engineering/review-pr/references/golden-path-smoke.md +5 -0
  343. package/skills/engineering/review-pr/references/incremental-output.md +16 -0
  344. package/skills/engineering/review-pr/references/inputs-and-discovery.md +31 -0
  345. package/skills/engineering/review-pr/references/output-format.md +99 -0
  346. package/skills/engineering/review-pr/references/over-mock-screen.md +7 -0
  347. package/skills/engineering/review-pr/references/promotion-prs.md +16 -0
  348. package/skills/engineering/review-pr/references/re-review.md +18 -0
  349. package/skills/engineering/review-pr/references/review-method.md +199 -0
  350. package/skills/engineering/review-pr/references/review-mode.md +30 -0
  351. package/skills/engineering/review-pr/references/review-posture.md +53 -0
  352. package/skills/engineering/review-pr/references/round1-depth.md +62 -0
  353. package/skills/engineering/review-pr/references/scripts.md +17 -0
  354. package/skills/engineering/review-pr/references/workflow.md +16 -0
  355. package/skills/engineering/review-pr/schemas/findings.schema.json +282 -0
  356. package/skills/engineering/review-pr/scripts/finding-contract.mjs +285 -0
  357. package/skills/engineering/review-pr/scripts/post-review.mjs +405 -0
  358. package/skills/engineering/review-pr/scripts/pr-context.mjs +207 -0
  359. package/skills/engineering/review-pr/scripts/scan-diff.mjs +365 -0
  360. package/skills/engineering/review-pr/scripts/theme-contract.mjs +57 -0
  361. package/skills/engineering/smoke/LICENSE +3 -0
  362. package/skills/engineering/smoke/SKILL.md +131 -0
  363. package/skills/engineering/smoke/assets/smoke.manifest.example.json +53 -0
  364. package/skills/engineering/smoke/references/manifest.md +192 -0
  365. package/skills/engineering/smoke/scripts/smoke.mjs +713 -0
  366. package/skills/fullstack/better-auth/LICENSE +3 -0
  367. package/skills/fullstack/better-auth/SKILL.md +601 -0
  368. package/skills/fullstack/better-auth/references/feishu-api.md +270 -0
  369. package/skills/fullstack/monorepo/LICENSE +3 -0
  370. package/skills/fullstack/monorepo/SKILL.md +465 -0
  371. package/skills/fullstack/nextjs-fullstack/LICENSE +3 -0
  372. package/skills/fullstack/nextjs-fullstack/SKILL.md +210 -0
  373. package/skills/fullstack/nextjs-fullstack/conventions.md +318 -0
  374. package/skills/fullstack/nextjs-fullstack/frontend-conventions.md +61 -0
  375. package/skills/fullstack/nextjs-fullstack/nextjs16.md +287 -0
  376. package/skills/fullstack/nextjs-fullstack/server-actions.md +409 -0
  377. package/skills/fullstack/prisma-setup/LICENSE +3 -0
  378. package/skills/fullstack/prisma-setup/SKILL.md +180 -0
  379. package/skills/fullstack/prisma-setup/nextjs.md +258 -0
  380. package/skills/fullstack/prisma-setup/turborepo.md +301 -0
  381. package/skills/fullstack/shadcn/LICENSE +3 -0
  382. package/skills/fullstack/shadcn/SKILL.md +119 -0
  383. package/skills/fullstack/shadcn/assets/shadcn-small.png +0 -0
  384. package/skills/fullstack/shadcn/assets/shadcn.png +0 -0
  385. package/skills/fullstack/shadcn/cli.md +411 -0
  386. package/skills/fullstack/shadcn/customization.md +224 -0
  387. package/skills/fullstack/shadcn/evals/evals.json +90 -0
  388. package/skills/fullstack/shadcn/mcp.md +101 -0
  389. package/skills/fullstack/shadcn/rules/base-vs-radix.md +323 -0
  390. package/skills/fullstack/shadcn/rules/component-selection.md +67 -0
  391. package/skills/fullstack/shadcn/rules/composition.md +195 -0
  392. package/skills/fullstack/shadcn/rules/data-table.md +201 -0
  393. package/skills/fullstack/shadcn/rules/forms.md +255 -0
  394. package/skills/fullstack/shadcn/rules/icons.md +103 -0
  395. package/skills/fullstack/shadcn/rules/styling.md +167 -0
  396. package/skills/fullstack/zod-v4/LICENSE +3 -0
  397. 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)))