@1aboveio/skills 0.19.3 → 0.20.2

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 (206) hide show
  1. package/README.md +9 -4
  2. package/package.json +1 -1
  3. package/runtime/skills/distribution/generated/recipes.json +54 -48
  4. package/runtime/skills/distribution/scripts/bundles.mjs +113 -7
  5. package/runtime/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +4 -6
  6. package/runtime/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +3 -36
  7. package/skills/cicd-pipeline/cloud-build/SKILL.md +6 -6
  8. package/skills/cicd-pipeline/mergify/SKILL.md +1 -1
  9. package/skills/engineering/engineering-runtime/coherence/workflow.json +15 -84
  10. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +4 -6
  11. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +3 -36
  12. package/skills/engineering/rush-release/SKILL.md +46 -25
  13. package/skills/engineering/rush-release/evals/evals.json +22 -8
  14. package/skills/engineering/rush-release/references/cut.md +21 -4
  15. package/skills/engineering/rush-release/references/preflight.md +25 -6
  16. package/skills/engineering/rush-release/references/promotion.md +70 -0
  17. package/skills/engineering/rush-release/references/publish.md +49 -16
  18. package/skills/engineering/rush-release/scripts/plan.mjs +33 -7
  19. package/skills/engineering/smoke/SKILL.md +4 -4
  20. package/skills/engineering/smoke/references/manifest.md +3 -3
  21. package/skills/payment/payment-analysis/SKILL.md +5 -3
  22. package/skills/payment/tld-detection/SKILL.md +163 -0
  23. package/skills/payment/tld-detection/agents/openai.yaml +9 -0
  24. package/skills/payment/tld-detection/evals/evals.json +107 -0
  25. package/skills/payment/tld-detection/references/clean.md +51 -0
  26. package/skills/payment/tld-detection/references/clustering.md +219 -0
  27. package/skills/payment/tld-detection/references/decision.md +85 -0
  28. package/skills/payment/tld-detection/references/report-template.md +169 -0
  29. package/skills/payment/tld-detection/references/terminology.md +69 -0
  30. package/skills/payment/tld-detection/references/workflow.md +78 -0
  31. package/skills/payment/tld-detection/scripts/detect_ladders.py +390 -0
  32. package/skills/payment/tld-detection/tests/test_detect_ladders.py +132 -0
  33. package/skills/engineering/resolve-issues/SKILL.md +0 -168
  34. package/skills/engineering/resolve-issues/agents/openai.yaml +0 -9
  35. package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +0 -499
  36. package/skills/engineering/resolve-issues/references/breaker.md +0 -82
  37. package/skills/engineering/resolve-issues/references/deliverables.md +0 -27
  38. package/skills/engineering/resolve-issues/references/delivery.md +0 -108
  39. package/skills/engineering/resolve-issues/references/evidence-lane.md +0 -21
  40. package/skills/engineering/resolve-issues/references/exact-head-ci.md +0 -287
  41. package/skills/engineering/resolve-issues/references/fan-out.md +0 -33
  42. package/skills/engineering/resolve-issues/references/finalization.md +0 -68
  43. package/skills/engineering/resolve-issues/references/guarantees.md +0 -10
  44. package/skills/engineering/resolve-issues/references/high-risk.md +0 -29
  45. package/skills/engineering/resolve-issues/references/incidents/848/README.md +0 -156
  46. package/skills/engineering/resolve-issues/references/intake.md +0 -86
  47. package/skills/engineering/resolve-issues/references/integration-gate.md +0 -53
  48. package/skills/engineering/resolve-issues/references/interference.md +0 -87
  49. package/skills/engineering/resolve-issues/references/loop.md +0 -134
  50. package/skills/engineering/resolve-issues/references/model-catalog.md +0 -9
  51. package/skills/engineering/resolve-issues/references/postmortem.md +0 -27
  52. package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +0 -41
  53. package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +0 -48
  54. package/skills/engineering/resolve-issues/references/pre-flight.md +0 -41
  55. package/skills/engineering/resolve-issues/references/regression-checklist.md +0 -26
  56. package/skills/engineering/resolve-issues/references/run-state.md +0 -288
  57. package/skills/engineering/resolve-issues/references/sandboxed-testing.md +0 -48
  58. package/skills/engineering/resolve-issues/references/spawn-contract.md +0 -96
  59. package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +0 -40
  60. package/skills/engineering/resolve-issues/references/why.md +0 -653
  61. package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +0 -49
  62. package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +0 -721
  63. package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +0 -962
  64. package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +0 -220
  65. package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +0 -420
  66. package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +0 -256
  67. package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +0 -168
  68. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +0 -9
  69. package/skills/engineering/resolve-issues/scripts/doctrine.mjs +0 -62
  70. package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +0 -191
  71. package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +0 -413
  72. package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +0 -332
  73. package/skills/engineering/resolve-issues/scripts/finalize.mjs +0 -488
  74. package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +0 -3307
  75. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +0 -16
  76. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +0 -51
  77. package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +0 -17
  78. package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +0 -166
  79. package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +0 -92
  80. package/skills/engineering/resolve-issues/scripts/guide-index.mjs +0 -73
  81. package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +0 -159
  82. package/skills/engineering/resolve-issues/scripts/interference.mjs +0 -427
  83. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +0 -9
  84. package/skills/engineering/resolve-issues/scripts/next-operations.mjs +0 -419
  85. package/skills/engineering/resolve-issues/scripts/postmortem.mjs +0 -909
  86. package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +0 -345
  87. package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +0 -415
  88. package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +0 -697
  89. package/skills/engineering/resolve-issues/scripts/report.mjs +0 -494
  90. package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +0 -172
  91. package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +0 -79
  92. package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +0 -334
  93. package/skills/engineering/resolve-issues/scripts/run-state.mjs +0 -4792
  94. package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +0 -395
  95. package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +0 -290
  96. package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +0 -170
  97. package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +0 -293
  98. package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +0 -197
  99. package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +0 -127
  100. package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +0 -893
  101. package/skills/engineering/resolve-issues/scripts/workspaces.mjs +0 -829
  102. package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +0 -290
  103. package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +0 -80
  104. package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +0 -102
  105. package/skills/engineering/resolve-release/LICENSE +0 -3
  106. package/skills/engineering/resolve-release/SKILL.md +0 -113
  107. package/skills/engineering/resolve-release/agents/openai.yaml +0 -9
  108. package/skills/engineering/resolve-release/references/assembly.md +0 -137
  109. package/skills/engineering/resolve-release/references/auto-when-green.md +0 -56
  110. package/skills/engineering/resolve-release/references/candidate.md +0 -167
  111. package/skills/engineering/resolve-release/references/exposure.md +0 -178
  112. package/skills/engineering/resolve-release/references/handoff.md +0 -24
  113. package/skills/engineering/resolve-release/references/postmortem.md +0 -230
  114. package/skills/engineering/resolve-release/references/preflight.md +0 -208
  115. package/skills/engineering/resolve-release/references/principles.md +0 -94
  116. package/skills/engineering/resolve-release/references/regression-checklist.md +0 -36
  117. package/skills/engineering/resolve-release/references/related-skills.md +0 -13
  118. package/skills/engineering/resolve-release/references/routing.md +0 -149
  119. package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +0 -285
  120. package/skills/engineering/resolve-release/references/versioning.md +0 -202
  121. package/skills/engineering/resolve-release/references/why.md +0 -53
  122. package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +0 -389
  123. package/skills/engineering/resolve-release/scripts/build-changes.mjs +0 -209
  124. package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +0 -407
  125. package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +0 -904
  126. package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +0 -81
  127. package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +0 -612
  128. package/skills/engineering/resolve-release/scripts/close-attempt.mjs +0 -135
  129. package/skills/engineering/resolve-release/scripts/closeout-release.mjs +0 -161
  130. package/skills/engineering/resolve-release/scripts/doctrine.mjs +0 -106
  131. package/skills/engineering/resolve-release/scripts/durable-processing.mjs +0 -522
  132. package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +0 -659
  133. package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +0 -1014
  134. package/skills/engineering/resolve-release/scripts/finalize-release.mjs +0 -526
  135. package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +0 -169
  136. package/skills/engineering/resolve-release/scripts/green-gate.mjs +0 -599
  137. package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +0 -752
  138. package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +0 -56
  139. package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +0 -1538
  140. package/skills/engineering/resolve-release/scripts/postmortem.mjs +0 -381
  141. package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +0 -510
  142. package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +0 -326
  143. package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +0 -272
  144. package/skills/engineering/resolve-release/scripts/report.mjs +0 -417
  145. package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +0 -114
  146. package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +0 -238
  147. package/skills/engineering/resolve-release/scripts/review-packet.mjs +0 -503
  148. package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +0 -263
  149. package/skills/engineering/resolve-release/scripts/version-assert.mjs +0 -339
  150. package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +0 -485
  151. package/skills/engineering/resolve-release/scripts/version.mjs +0 -1199
  152. package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +0 -449
  153. package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +0 -21
  154. package/skills/engineering/resolve-release/vendor/README.md +0 -60
  155. package/skills/engineering/resolve-release/vendor/acorn.mjs +0 -6233
  156. package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +0 -21
  157. package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +0 -341
  158. package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +0 -70
  159. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +0 -2
  160. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +0 -1
  161. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +0 -7
  162. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +0 -1
  163. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +0 -2033
  164. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +0 -2033
  165. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +0 -7
  166. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +0 -1
  167. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +0 -2
  168. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +0 -1
  169. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +0 -2
  170. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +0 -1
  171. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +0 -2
  172. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +0 -1
  173. package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +0 -49
  174. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +0 -21
  175. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +0 -5
  176. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +0 -549
  177. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +0 -549
  178. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +0 -2
  179. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +0 -1
  180. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +0 -2
  181. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +0 -1
  182. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +0 -36
  183. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +0 -47
  184. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +0 -82
  185. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +0 -11
  186. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +0 -22
  187. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +0 -21
  188. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +0 -21
  189. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +0 -11
  190. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +0 -11
  191. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +0 -25
  192. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
  193. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +0 -85
  194. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +0 -85
  195. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +0 -2
  196. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +0 -1
  197. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +0 -2
  198. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +0 -1
  199. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +0 -20
  200. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +0 -20
  201. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +0 -2
  202. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +0 -1
  203. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +0 -2
  204. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +0 -1
  205. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +0 -61
  206. /package/skills/{engineering/resolve-issues → payment/tld-detection}/LICENSE +0 -0
@@ -1,407 +0,0 @@
1
- // candidate-hygiene — pre-locate cleanup of stale candidate-* traffic tags.
2
- //
3
- // Previous successful releases can leave `candidate-<sha>` tags on the now-100% revision
4
- // (the incumbent) or on an orphan 0% revision. Those tags are NOT a second release in flight,
5
- // but they make `candidate-identity.mjs locate` report `e:second-candidate-in-flight`. This
6
- // script reads the same traffic capture `locate` uses, classifies the foreign tags, and emits the
7
- // `gcloud run services update-traffic --remove-tags` command that removes only the stale ones.
8
- //
9
- // A foreign tag is stale only when the shared traffic parser proves it has 0%, matching
10
- // terminal release evidence identifies the same tag and revision, and the product-scoped lease
11
- // report is valid and empty. Traffic, ambiguous traffic, missing/nonterminal evidence, or a
12
- // held/unknown lease remains a competing release and refuses. A missing lease report after the
13
- // other proofs reports `decision: "needs-lease-report"` and exits 3.
14
- //
15
- // The script is PURE: it does not shell out to gcloud or change production. The release lane runs
16
- // the emitted command, then re-runs `candidate-identity.mjs locate` on the cleaned traffic.
17
- //
18
- // Usage:
19
- // candidate-hygiene locate --sha <shipped> --traffic <file>
20
- // [--candidate-tag-prefix <prefix-ending-in-hyphen>]
21
- // [--lease-report <file>] [--terminal-evidence <file>] [--service <svc>] [--region <r>]
22
- //
23
- // Input `--traffic` is the same flattened / JSON form accepted by `candidate-identity.mjs`.
24
- // `--lease-report` is the JSON output of `run-state.mjs check-release-lease-scoped` for the
25
- // same product. `--terminal-evidence` is a JSON array of canonical resolve-release manifests
26
- // or immutable release evidence bundles. Each stale candidate must match one verified terminal
27
- // attempt by exact tag and revision. Without either proof, cleanup is not authorized.
28
- //
29
- // Exit codes:
30
- // 0 clean / no-action — only stale tags (or none), and no true rival
31
- // 1 refuse — a true second release is present; the failing ids are in `refuse`
32
- // 2 usage / unreadable input
33
- // 3 needs-lease-report — a 0% foreign candidate requires a lease report to classify; supply one and re-run
34
-
35
- import fs from 'node:fs'
36
- import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
37
- import { parseTraffic, normalizeTrafficPercents } from './candidate-traffic.mjs'
38
- import { MIN_SHORT_SHA } from './candidate-identity.mjs'
39
- import { RELEASE_STATE_CONTRACT } from '../../resolve-issues/scripts/release-state-contract.mjs'
40
- import { validateBundle } from './evidence-bundle.mjs'
41
-
42
- const SHA_RE = new RegExp(`^[0-9a-f]{${MIN_SHORT_SHA},40}$`, 'i')
43
- const isSha = (s) => typeof s === 'string' && SHA_RE.test(s)
44
-
45
- const shaMatches = (a, b) => {
46
- if (!isSha(a) || !isSha(b)) return false
47
- const [x, y] = [a.toLowerCase(), b.toLowerCase()]
48
- const short = x.length <= y.length ? x : y
49
- const long = short === x ? y : x
50
- return long.startsWith(short)
51
- }
52
-
53
- const DEFAULT_CANDIDATE_TAG_PREFIX = 'candidate-'
54
-
55
- function validCandidateTagPrefix(prefix) {
56
- return typeof prefix === 'string' && /^[a-z][a-z0-9-]{0,30}-$/.test(prefix)
57
- }
58
-
59
- function classifyTag(tag, sha, candidateTagPrefix = DEFAULT_CANDIDATE_TAG_PREFIX) {
60
- if (tag === 'candidate') return 'shared'
61
- if (!validCandidateTagPrefix(candidateTagPrefix) || !tag.startsWith(candidateTagPrefix)) {
62
- return 'other'
63
- }
64
- const suffix = tag.slice(candidateTagPrefix.length)
65
- if (!/^[0-9a-f]{4,40}$/i.test(suffix) || !isSha(suffix)) return 'other'
66
- return shaMatches(suffix, sha) ? 'ours' : 'foreign'
67
- }
68
-
69
- function readJson(file, what) {
70
- if (!file) return { error: `usage: --${what} <file> is required` }
71
- let raw
72
- try {
73
- raw = fs.readFileSync(file, 'utf8')
74
- } catch {
75
- return { error: `${what} not readable: ${file}` }
76
- }
77
- try {
78
- return { value: JSON.parse(raw) }
79
- } catch {
80
- return { error: `${what} is not valid JSON: ${file}` }
81
- }
82
- }
83
-
84
- const isObject = (value) => value != null && typeof value === 'object' && !Array.isArray(value)
85
- const hasText = (value) => typeof value === 'string' && value.trim().length > 0
86
-
87
- function matchingTerminalEvidence(terminalEvidence, candidate) {
88
- if (!Array.isArray(terminalEvidence)) return null
89
- const { leaseClosingStates, closeCauses } = RELEASE_STATE_CONTRACT.attempt
90
- for (const evidence of terminalEvidence) {
91
- if (!isObject(evidence)) continue
92
-
93
- if (evidence.skill === 'resolve-release') {
94
- const release = isObject(evidence.release) ? evidence.release : null
95
- const attempt = release && isObject(release.attempt) ? release.attempt : null
96
- const identity = release && isObject(release.candidateIdentity) ? release.candidateIdentity : null
97
- if (
98
- attempt && identity && hasText(attempt.id) && leaseClosingStates.includes(attempt.state) &&
99
- attempt.closedBy === closeCauses[attempt.state] && hasText(attempt.evidence) &&
100
- identity.tag === candidate.tag && identity.revision === candidate.revision
101
- ) {
102
- return { attemptId: attempt.id, source: 'manifest' }
103
- }
104
- continue
105
- }
106
-
107
- const bundle = validateBundle(evidence)
108
- if (!bundle.valid) continue
109
- const receipt = isObject(evidence.candidateReceipt) ? evidence.candidateReceipt : null
110
- if (
111
- hasText(evidence.attemptId) && receipt && leaseClosingStates.includes(evidence.terminalState) &&
112
- evidence.closedBy === closeCauses[evidence.terminalState] &&
113
- receipt.tag === candidate.tag && receipt.revision === candidate.revision
114
- ) {
115
- return { attemptId: evidence.attemptId, source: 'evidence-bundle' }
116
- }
117
- }
118
- return null
119
- }
120
-
121
- function classifyLeaseReport(leaseReport) {
122
- if (leaseReport === null || leaseReport === undefined) return { state: 'missing' }
123
- if (!Array.isArray(leaseReport)) return { state: 'unknown', reason: 'product lease report is not an array' }
124
- if (leaseReport.length === 0) return { state: 'clear' }
125
-
126
- let held = false
127
- for (const entry of leaseReport) {
128
- if (!isObject(entry) || !isObject(entry.lease) || !isObject(entry.productMatch)) {
129
- return { state: 'unknown', reason: 'product lease report contains an unreadable matching entry' }
130
- }
131
- if (entry.productMatch.repo === 'unknown' || entry.productMatch.service === 'unknown') {
132
- return { state: 'unknown', reason: 'product lease identity is unknown' }
133
- }
134
- if (entry.productMatch.repo === false || entry.productMatch.service === false) {
135
- return { state: 'unknown', reason: 'product lease report contains an entry outside its declared scope' }
136
- }
137
- if (entry.lease.code === 2 || entry.lease.state === 'unknown' || entry.lease.state === 'ambiguous') {
138
- return { state: 'unknown', reason: entry.lease.reason || 'matching product lease is ambiguous' }
139
- }
140
- if (entry.lease.code === 1 || entry.lease.state === 'held') held = true
141
- else return { state: 'unknown', reason: 'product lease report contains an unrecognized lease state' }
142
- }
143
- return held
144
- ? { state: 'held', reason: 'a release attempt holds the matching product lease' }
145
- : { state: 'unknown', reason: 'product lease report did not prove the matching product free' }
146
- }
147
-
148
- export function evaluateHygiene({
149
- sha,
150
- traffic,
151
- leaseReport = null,
152
- terminalEvidence = null,
153
- incumbentRevision = null,
154
- candidateTagPrefix = DEFAULT_CANDIDATE_TAG_PREFIX,
155
- }) {
156
- if (!isSha(sha)) {
157
- return { decision: 'usage', refuse: [], sha, evidence: { error: 'e:unusable-shipped-sha' } }
158
- }
159
- if (!validCandidateTagPrefix(candidateTagPrefix)) {
160
- return {
161
- decision: 'usage',
162
- refuse: [],
163
- sha,
164
- evidence: { error: 'e:invalid-candidate-tag-prefix', candidateTagPrefix },
165
- }
166
- }
167
- const rows = normalizeTrafficPercents(traffic.rows)
168
- const classified = rows.map((row) => ({
169
- ...row,
170
- kind: classifyTag(row.tag, sha, candidateTagPrefix),
171
- }))
172
- const ours = classified.filter((row) => row.kind === 'ours')
173
- const foreign = classified.filter((row) => row.kind === 'foreign')
174
-
175
- let incumbent = incumbentRevision
176
- if (!incumbent) {
177
- const hundredRows = classified.filter((row) => row.percent === 100 && row.revision)
178
- if (hundredRows.length === 1) incumbent = hundredRows[0].revision
179
- }
180
-
181
- const lease = classifyLeaseReport(leaseReport)
182
- const staleTags = []
183
- const rivals = []
184
- let needsLeaseReport = false
185
-
186
- for (const row of foreign) {
187
- if (
188
- row.percent === null || !Number.isFinite(row.percent) ||
189
- row.percent < 0 || row.percent > 100
190
- ) {
191
- rivals.push({ ...candidateConflict(row, 'foreign candidate has ambiguous traffic') })
192
- continue
193
- }
194
- if (row.percent !== 0) {
195
- rivals.push({ ...candidateConflict(row, 'foreign candidate has non-zero traffic') })
196
- continue
197
- }
198
-
199
- const terminal = matchingTerminalEvidence(terminalEvidence, row)
200
- if (!terminal) {
201
- rivals.push({ ...candidateConflict(row, 'foreign zero-traffic candidate has no matching terminal release evidence') })
202
- continue
203
- }
204
- if (lease.state === 'missing') {
205
- needsLeaseReport = true
206
- continue
207
- }
208
- if (lease.state === 'held' || lease.state === 'unknown') {
209
- rivals.push({ ...candidateConflict(row, `foreign zero-traffic candidate is competing because the matching product lease is ${lease.state}: ${lease.reason}`) })
210
- continue
211
- }
212
-
213
- staleTags.push({
214
- tag: row.tag,
215
- revision: row.revision,
216
- percent: row.percent,
217
- kind: row.revision && row.revision === incumbent ? 'stale-on-incumbent' : 'stale-foreign-zero',
218
- terminalAttempt: terminal.attemptId,
219
- })
220
- }
221
-
222
- const shared = classified.filter((row) => row.kind === 'shared')
223
- if (ours.length > 0) {
224
- for (const row of shared) {
225
- if (row.revision && row.revision !== ours[0].revision) {
226
- rivals.push(candidateConflict(row, 'shared candidate tag resolves to a different revision'))
227
- }
228
- }
229
- }
230
- if (ours.length > 1) {
231
- rivals.push({
232
- tag: ours.map((row) => row.tag).join(', '),
233
- revision: null,
234
- percent: null,
235
- reason: 'multiple entries claim this SHA-keyed candidate tag',
236
- })
237
- }
238
-
239
- const cleanupCommand = staleTags.length > 0
240
- ? `gcloud run services update-traffic <service> --region <region> --remove-tags ${staleTags.map((tag) => tag.tag).join(',')}`
241
- : null
242
- const evidence = {
243
- trafficEntries: classified.length,
244
- ourTagCount: ours.length,
245
- staleTagCount: staleTags.length,
246
- ...(rivals.length > 0 ? { rivalCount: rivals.length } : {}),
247
- }
248
-
249
- if (rivals.length > 0) {
250
- return {
251
- decision: 'refuse', sha, incumbentRevision: incumbent || null, staleTags, rivals,
252
- cleanupCommand: null, refuse: ['e:second-candidate-in-flight'], evidence,
253
- }
254
- }
255
- if (needsLeaseReport) {
256
- return {
257
- decision: 'needs-lease-report', sha, incumbentRevision: incumbent || null, staleTags, rivals,
258
- cleanupCommand: null, refuse: [], evidence: { ...evidence, foreignZeroWithoutLeaseReport: true },
259
- }
260
- }
261
- return {
262
- decision: staleTags.length > 0 ? 'clean' : 'no-action',
263
- sha,
264
- incumbentRevision: incumbent || null,
265
- staleTags,
266
- rivals,
267
- cleanupCommand,
268
- refuse: [],
269
- evidence,
270
- }
271
- }
272
-
273
- function candidateConflict(row, reason) {
274
- return { tag: row.tag, revision: row.revision, percent: row.percent, reason }
275
- }
276
-
277
- const USAGE = `candidate-hygiene — pre-locate cleanup of stale candidate-* traffic tags.
278
-
279
- Reads the same normalized traffic capture as candidate identity. A foreign \`candidate-*\`
280
- tag is stale only with proven zero traffic, matching terminal release evidence, and a valid
281
- empty product-scoped lease report; every ambiguous or competing shape refuses.
282
-
283
- Usage:
284
- candidate-hygiene locate --sha <shipped> --traffic <file>
285
- [--candidate-tag-prefix <prefix-ending-in-hyphen>]
286
- [--lease-report <file>] [--terminal-evidence <file>]
287
- [--service <svc>] [--region <r>]
288
-
289
- Options:
290
- --sha <shipped> the promoted SHA
291
- --traffic <file> the flattened or JSON traffic capture (same form as locate)
292
- --candidate-tag-prefix repository SHA-keyed namespace; defaults to candidate-
293
- --lease-report <file> JSON output of run-state.mjs check-release-lease-scoped for the
294
- same product; without it, a 0% foreign candidate cannot be
295
- classified as stale and the script reports "needs-lease-report"
296
- --terminal-evidence <file> JSON array of canonical resolve-release manifests or immutable
297
- evidence bundles; exact candidate tag/revision must match a verified
298
- terminal close
299
- --service <svc> Cloud Run service name (used to render a concrete cleanup command)
300
- --region <r> Cloud Run region (used to render a concrete cleanup command)
301
-
302
- Exit codes:
303
- 0 clean or no-action — stale tags were found (or none) and no true rival
304
- 1 refuse — a true second release is present; \`refuse\` contains e:second-candidate-in-flight
305
- 2 usage / unreadable input
306
- 3 needs-lease-report — a 0% foreign candidate requires a lease report to classify; supply one and re-run
307
- `
308
-
309
- function parseArgs(argv) {
310
- const opts = {}
311
- const positional = []
312
- for (let i = 0; i < argv.length; i++) {
313
- const a = argv[i]
314
- if (a === '--help' || a === '-h') opts.help = true
315
- else if (a.startsWith('--')) opts[a.slice(2)] = argv[++i]
316
- else positional.push(a)
317
- }
318
- return { opts, positional }
319
- }
320
-
321
- export function main(argv) {
322
- const { opts, positional } = parseArgs(argv)
323
- const cmd = positional[0]
324
- if (opts.help || cmd === 'help') {
325
- console.log(USAGE)
326
- return 0
327
- }
328
- if (cmd !== 'locate') {
329
- const what = cmd ? `unknown command "${cmd}"` : 'no subcommand given'
330
- console.error(`candidate-hygiene: ${what}. Use locate (see --help).`)
331
- return 2
332
- }
333
- if (!opts.sha) {
334
- console.error('candidate-hygiene: usage: locate requires --sha <shipped>')
335
- return 2
336
- }
337
- if (
338
- 'candidate-tag-prefix' in opts &&
339
- (typeof opts['candidate-tag-prefix'] !== 'string' ||
340
- !opts['candidate-tag-prefix'] ||
341
- opts['candidate-tag-prefix'].startsWith('--'))
342
- ) {
343
- console.error('candidate-hygiene: --candidate-tag-prefix requires a value')
344
- return 2
345
- }
346
- if (!opts.traffic) {
347
- console.error('candidate-hygiene: usage: locate requires --traffic <file>')
348
- return 2
349
- }
350
-
351
- let text
352
- try {
353
- text = fs.readFileSync(opts.traffic, 'utf8')
354
- } catch {
355
- console.error(`candidate-hygiene: traffic read not readable: ${opts.traffic}`)
356
- return 2
357
- }
358
- const traffic = parseTraffic(text)
359
- if (traffic.error) {
360
- console.error(`candidate-hygiene: ${traffic.error}`)
361
- return 2
362
- }
363
-
364
- let leaseReport = null
365
- let terminalEvidence = null
366
- if (opts['lease-report']) {
367
- const read = readJson(opts['lease-report'], 'lease-report')
368
- if (read.error) {
369
- console.error(`candidate-hygiene: ${read.error}`)
370
- return 2
371
- }
372
- leaseReport = read.value
373
- }
374
-
375
- if (opts['terminal-evidence']) {
376
- const read = readJson(opts['terminal-evidence'], 'terminal-evidence')
377
- if (read.error) {
378
- console.error(`candidate-hygiene: ${read.error}`)
379
- return 2
380
- }
381
- terminalEvidence = read.value
382
- }
383
-
384
- const result = evaluateHygiene({
385
- sha: opts.sha,
386
- traffic,
387
- leaseReport,
388
- terminalEvidence,
389
- incumbentRevision: opts['incumbent-revision'] || null,
390
- candidateTagPrefix: opts['candidate-tag-prefix'] || DEFAULT_CANDIDATE_TAG_PREFIX,
391
- })
392
-
393
- // Replace the <service>/<region> placeholders with concrete values if the caller supplied them.
394
- if (result.cleanupCommand && opts.service && opts.region) {
395
- result.cleanupCommand = result.cleanupCommand.replace('<service>', opts.service).replace('<region>', opts.region)
396
- }
397
-
398
- console.log(JSON.stringify(result, null, 2))
399
- if (result.decision === 'refuse') return 1
400
- if (result.decision === 'usage') return 2
401
- if (result.decision === 'needs-lease-report') return 3
402
- return 0
403
- }
404
-
405
- if (isMainModule(import.meta.url)) {
406
- process.exit(main(process.argv.slice(2)))
407
- }