@1aboveio/skills 0.19.2 → 0.20.1

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 (203) 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/mergify/SKILL.md +1 -1
  8. package/skills/engineering/engineering-runtime/coherence/workflow.json +14 -83
  9. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +4 -6
  10. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +3 -36
  11. package/skills/engineering/smoke/SKILL.md +1 -1
  12. package/skills/payment/payment-analysis/SKILL.md +13 -7
  13. package/skills/payment/payment-analysis/references/chargebacks.md +43 -18
  14. package/skills/payment/payment-analysis/references/event-layers.md +4 -0
  15. package/skills/payment/payment-analysis/references/metrics.md +9 -7
  16. package/skills/payment/payment-analysis/references/report-template.md +3 -3
  17. package/skills/payment/payment-analysis/references/terminology.md +6 -1
  18. package/skills/payment/payment-analysis/references/visualization.md +1 -1
  19. package/skills/payment/tld-detection/SKILL.md +163 -0
  20. package/skills/payment/tld-detection/agents/openai.yaml +9 -0
  21. package/skills/payment/tld-detection/evals/evals.json +107 -0
  22. package/skills/payment/tld-detection/references/clean.md +51 -0
  23. package/skills/payment/tld-detection/references/clustering.md +219 -0
  24. package/skills/payment/tld-detection/references/decision.md +85 -0
  25. package/skills/payment/tld-detection/references/report-template.md +169 -0
  26. package/skills/payment/tld-detection/references/terminology.md +69 -0
  27. package/skills/payment/tld-detection/references/workflow.md +78 -0
  28. package/skills/payment/tld-detection/scripts/detect_ladders.py +390 -0
  29. package/skills/payment/tld-detection/tests/test_detect_ladders.py +132 -0
  30. package/skills/engineering/resolve-issues/SKILL.md +0 -168
  31. package/skills/engineering/resolve-issues/agents/openai.yaml +0 -9
  32. package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +0 -499
  33. package/skills/engineering/resolve-issues/references/breaker.md +0 -82
  34. package/skills/engineering/resolve-issues/references/deliverables.md +0 -27
  35. package/skills/engineering/resolve-issues/references/delivery.md +0 -108
  36. package/skills/engineering/resolve-issues/references/evidence-lane.md +0 -21
  37. package/skills/engineering/resolve-issues/references/exact-head-ci.md +0 -287
  38. package/skills/engineering/resolve-issues/references/fan-out.md +0 -33
  39. package/skills/engineering/resolve-issues/references/finalization.md +0 -68
  40. package/skills/engineering/resolve-issues/references/guarantees.md +0 -10
  41. package/skills/engineering/resolve-issues/references/high-risk.md +0 -29
  42. package/skills/engineering/resolve-issues/references/incidents/848/README.md +0 -156
  43. package/skills/engineering/resolve-issues/references/intake.md +0 -86
  44. package/skills/engineering/resolve-issues/references/integration-gate.md +0 -53
  45. package/skills/engineering/resolve-issues/references/interference.md +0 -87
  46. package/skills/engineering/resolve-issues/references/loop.md +0 -134
  47. package/skills/engineering/resolve-issues/references/model-catalog.md +0 -9
  48. package/skills/engineering/resolve-issues/references/postmortem.md +0 -27
  49. package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +0 -41
  50. package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +0 -48
  51. package/skills/engineering/resolve-issues/references/pre-flight.md +0 -41
  52. package/skills/engineering/resolve-issues/references/regression-checklist.md +0 -26
  53. package/skills/engineering/resolve-issues/references/run-state.md +0 -288
  54. package/skills/engineering/resolve-issues/references/sandboxed-testing.md +0 -48
  55. package/skills/engineering/resolve-issues/references/spawn-contract.md +0 -96
  56. package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +0 -40
  57. package/skills/engineering/resolve-issues/references/why.md +0 -653
  58. package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +0 -49
  59. package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +0 -721
  60. package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +0 -962
  61. package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +0 -220
  62. package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +0 -420
  63. package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +0 -256
  64. package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +0 -168
  65. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +0 -9
  66. package/skills/engineering/resolve-issues/scripts/doctrine.mjs +0 -62
  67. package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +0 -191
  68. package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +0 -413
  69. package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +0 -332
  70. package/skills/engineering/resolve-issues/scripts/finalize.mjs +0 -488
  71. package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +0 -3307
  72. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +0 -16
  73. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +0 -51
  74. package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +0 -17
  75. package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +0 -166
  76. package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +0 -92
  77. package/skills/engineering/resolve-issues/scripts/guide-index.mjs +0 -73
  78. package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +0 -159
  79. package/skills/engineering/resolve-issues/scripts/interference.mjs +0 -427
  80. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +0 -9
  81. package/skills/engineering/resolve-issues/scripts/next-operations.mjs +0 -419
  82. package/skills/engineering/resolve-issues/scripts/postmortem.mjs +0 -909
  83. package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +0 -345
  84. package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +0 -415
  85. package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +0 -697
  86. package/skills/engineering/resolve-issues/scripts/report.mjs +0 -494
  87. package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +0 -172
  88. package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +0 -79
  89. package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +0 -334
  90. package/skills/engineering/resolve-issues/scripts/run-state.mjs +0 -4792
  91. package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +0 -395
  92. package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +0 -290
  93. package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +0 -170
  94. package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +0 -293
  95. package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +0 -197
  96. package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +0 -127
  97. package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +0 -893
  98. package/skills/engineering/resolve-issues/scripts/workspaces.mjs +0 -829
  99. package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +0 -290
  100. package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +0 -80
  101. package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +0 -102
  102. package/skills/engineering/resolve-release/LICENSE +0 -3
  103. package/skills/engineering/resolve-release/SKILL.md +0 -113
  104. package/skills/engineering/resolve-release/agents/openai.yaml +0 -9
  105. package/skills/engineering/resolve-release/references/assembly.md +0 -137
  106. package/skills/engineering/resolve-release/references/auto-when-green.md +0 -56
  107. package/skills/engineering/resolve-release/references/candidate.md +0 -167
  108. package/skills/engineering/resolve-release/references/exposure.md +0 -178
  109. package/skills/engineering/resolve-release/references/handoff.md +0 -24
  110. package/skills/engineering/resolve-release/references/postmortem.md +0 -230
  111. package/skills/engineering/resolve-release/references/preflight.md +0 -208
  112. package/skills/engineering/resolve-release/references/principles.md +0 -94
  113. package/skills/engineering/resolve-release/references/regression-checklist.md +0 -36
  114. package/skills/engineering/resolve-release/references/related-skills.md +0 -13
  115. package/skills/engineering/resolve-release/references/routing.md +0 -149
  116. package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +0 -285
  117. package/skills/engineering/resolve-release/references/versioning.md +0 -202
  118. package/skills/engineering/resolve-release/references/why.md +0 -53
  119. package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +0 -389
  120. package/skills/engineering/resolve-release/scripts/build-changes.mjs +0 -209
  121. package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +0 -407
  122. package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +0 -904
  123. package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +0 -81
  124. package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +0 -612
  125. package/skills/engineering/resolve-release/scripts/close-attempt.mjs +0 -135
  126. package/skills/engineering/resolve-release/scripts/closeout-release.mjs +0 -161
  127. package/skills/engineering/resolve-release/scripts/doctrine.mjs +0 -106
  128. package/skills/engineering/resolve-release/scripts/durable-processing.mjs +0 -522
  129. package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +0 -659
  130. package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +0 -1014
  131. package/skills/engineering/resolve-release/scripts/finalize-release.mjs +0 -526
  132. package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +0 -169
  133. package/skills/engineering/resolve-release/scripts/green-gate.mjs +0 -599
  134. package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +0 -752
  135. package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +0 -56
  136. package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +0 -1538
  137. package/skills/engineering/resolve-release/scripts/postmortem.mjs +0 -381
  138. package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +0 -510
  139. package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +0 -326
  140. package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +0 -272
  141. package/skills/engineering/resolve-release/scripts/report.mjs +0 -417
  142. package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +0 -114
  143. package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +0 -238
  144. package/skills/engineering/resolve-release/scripts/review-packet.mjs +0 -503
  145. package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +0 -263
  146. package/skills/engineering/resolve-release/scripts/version-assert.mjs +0 -339
  147. package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +0 -485
  148. package/skills/engineering/resolve-release/scripts/version.mjs +0 -1199
  149. package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +0 -449
  150. package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +0 -21
  151. package/skills/engineering/resolve-release/vendor/README.md +0 -60
  152. package/skills/engineering/resolve-release/vendor/acorn.mjs +0 -6233
  153. package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +0 -21
  154. package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +0 -341
  155. package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +0 -70
  156. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +0 -2
  157. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +0 -1
  158. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +0 -7
  159. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +0 -1
  160. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +0 -2033
  161. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +0 -2033
  162. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +0 -7
  163. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +0 -1
  164. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +0 -2
  165. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +0 -1
  166. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +0 -2
  167. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +0 -1
  168. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +0 -2
  169. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +0 -1
  170. package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +0 -49
  171. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +0 -21
  172. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +0 -5
  173. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +0 -549
  174. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +0 -549
  175. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +0 -2
  176. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +0 -1
  177. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +0 -2
  178. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +0 -1
  179. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +0 -36
  180. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +0 -47
  181. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +0 -82
  182. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +0 -11
  183. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +0 -22
  184. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +0 -21
  185. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +0 -21
  186. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +0 -11
  187. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +0 -11
  188. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +0 -25
  189. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
  190. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +0 -85
  191. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +0 -85
  192. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +0 -2
  193. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +0 -1
  194. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +0 -2
  195. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +0 -1
  196. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +0 -20
  197. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +0 -20
  198. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +0 -2
  199. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +0 -1
  200. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +0 -2
  201. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +0 -1
  202. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +0 -61
  203. /package/skills/{engineering/resolve-issues → payment/tld-detection}/LICENSE +0 -0
@@ -1,293 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { createHash } from 'node:crypto'
4
- import fs from 'node:fs'
5
- import os from 'node:os'
6
- import path from 'node:path'
7
- import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
8
- import { deliveryForUnit } from './terminal-dispositions.mjs'
9
-
10
- export const TERMINAL_EVIDENCE_SCHEMA_VERSION = 1
11
- export const TERMINAL_EVIDENCE_KIND = 'resolver-terminal-provider-evidence'
12
- export const TERMINAL_EVIDENCE_DIGEST_ALGORITHM = 'sha256-canonical-json-v1'
13
- const SHA = /^[0-9a-f]{40}$/i
14
- const CHECK_CONCLUSIONS = new Set([
15
- 'success', 'failure', 'cancelled', 'skipped', 'neutral', 'timed_out',
16
- 'action_required', 'stale', 'startup_failure',
17
- ])
18
-
19
- const object = (value) => value != null && typeof value === 'object' && !Array.isArray(value)
20
- const text = (value) => typeof value === 'string' && value.trim().length > 0
21
- const same = (left, right) => JSON.stringify(left) === JSON.stringify(right)
22
-
23
- function exact(value, fields, label, errors) {
24
- if (!object(value)) {
25
- errors.push(`${label} must be an object`)
26
- return false
27
- }
28
- const unknown = Object.keys(value).filter((field) => !fields.includes(field))
29
- if (unknown.length) errors.push(`${label} has unknown field ${unknown[0]}`)
30
- return true
31
- }
32
-
33
- function iso(value, label, errors) {
34
- if (!text(value) || !Number.isFinite(Date.parse(value))) errors.push(`${label} must be an ISO timestamp`)
35
- }
36
-
37
- function fullSha(value, label, errors) {
38
- if (!text(value) || !SHA.test(value)) errors.push(`${label} must be a full 40-character commit SHA`)
39
- }
40
-
41
- function positivePr(value) {
42
- if (Number.isInteger(value) && value > 0) return value
43
- const match = String(value ?? '').match(/^#?(\d+)$/)
44
- return match && Number(match[1]) > 0 ? Number(match[1]) : null
45
- }
46
-
47
- function canonicalEntryBytes(entry) {
48
- return `${JSON.stringify(entry)}\n`
49
- }
50
-
51
- export function terminalEvidenceDigest(entry) {
52
- return createHash('sha256').update(canonicalEntryBytes(entry)).digest('hex')
53
- }
54
-
55
- export function validateTerminalEvidenceEntry(entry) {
56
- const errors = []
57
- if (!exact(entry, ['schemaVersion', 'kind', 'run', 'subject', 'provider', 'observations'], 'entry', errors)) return { ok: false, errors }
58
- if (entry.schemaVersion !== TERMINAL_EVIDENCE_SCHEMA_VERSION) errors.push(`entry.schemaVersion must be ${TERMINAL_EVIDENCE_SCHEMA_VERSION}`)
59
- if (entry.kind !== TERMINAL_EVIDENCE_KIND) errors.push(`entry.kind must be ${TERMINAL_EVIDENCE_KIND}`)
60
-
61
- if (exact(entry.run, ['slug', 'repository', 'targetBranch'], 'entry.run', errors)) {
62
- if (!text(entry.run.slug)) errors.push('entry.run.slug is required')
63
- if (!text(entry.run.repository) || !/^[^/\s]+\/[^/\s]+$/.test(entry.run.repository)) errors.push('entry.run.repository must be owner/name')
64
- if (!text(entry.run.targetBranch)) errors.push('entry.run.targetBranch is required')
65
- }
66
- if (exact(entry.subject, ['unitId', 'componentId', 'pr', 'headSha'], 'entry.subject', errors)) {
67
- if (!text(entry.subject.unitId)) errors.push('entry.subject.unitId is required')
68
- if (!text(entry.subject.componentId)) errors.push('entry.subject.componentId is required')
69
- if (positivePr(entry.subject.pr) !== entry.subject.pr) errors.push('entry.subject.pr must be a positive integer')
70
- fullSha(entry.subject.headSha, 'entry.subject.headSha', errors)
71
- }
72
- if (exact(entry.provider, ['id', 'observedAt'], 'entry.provider', errors)) {
73
- if (!text(entry.provider.id)) errors.push('entry.provider.id is required')
74
- iso(entry.provider.observedAt, 'entry.provider.observedAt', errors)
75
- }
76
-
77
- const observations = entry.observations
78
- if (exact(observations, ['pullRequest', 'checks', 'targetAncestry', 'queueExecutionArtifact'], 'entry.observations', errors)) {
79
- const pr = observations.pullRequest
80
- if (exact(pr, ['state', 'headSha', 'mergeCommitSha', 'mergedAt'], 'entry.observations.pullRequest', errors)) {
81
- if (pr.state !== 'MERGED') errors.push('entry.observations.pullRequest.state must be MERGED')
82
- fullSha(pr.headSha, 'entry.observations.pullRequest.headSha', errors)
83
- fullSha(pr.mergeCommitSha, 'entry.observations.pullRequest.mergeCommitSha', errors)
84
- iso(pr.mergedAt, 'entry.observations.pullRequest.mergedAt', errors)
85
- if (entry.subject?.headSha && pr.headSha !== entry.subject.headSha) errors.push('pull request head contradicts the bound exact head')
86
- }
87
-
88
- if (!Array.isArray(observations.checks) || observations.checks.length === 0) {
89
- errors.push('entry.observations.checks must be a non-empty array')
90
- } else {
91
- const names = new Set()
92
- for (const [index, check] of observations.checks.entries()) {
93
- const label = `entry.observations.checks[${index}]`
94
- if (!exact(check, ['name', 'headSha', 'conclusion', 'completedAt'], label, errors)) continue
95
- if (!text(check.name)) errors.push(`${label}.name is required`)
96
- else if (names.has(check.name)) errors.push(`entry.observations.checks has duplicate check ${check.name}`)
97
- else names.add(check.name)
98
- fullSha(check.headSha, `${label}.headSha`, errors)
99
- if (entry.subject?.headSha && check.headSha !== entry.subject.headSha) errors.push(`${label}.headSha contradicts the bound exact head`)
100
- if (!CHECK_CONCLUSIONS.has(check.conclusion)) errors.push(`${label}.conclusion is unsupported`)
101
- if (check.completedAt !== undefined && check.completedAt !== null) iso(check.completedAt, `${label}.completedAt`, errors)
102
- }
103
- }
104
-
105
- const ancestry = observations.targetAncestry
106
- if (exact(ancestry, ['targetRef', 'targetHeadSha', 'mergeCommitSha', 'isAncestor', 'observedAt'], 'entry.observations.targetAncestry', errors)) {
107
- if (!text(ancestry.targetRef)) errors.push('entry.observations.targetAncestry.targetRef is required')
108
- fullSha(ancestry.targetHeadSha, 'entry.observations.targetAncestry.targetHeadSha', errors)
109
- fullSha(ancestry.mergeCommitSha, 'entry.observations.targetAncestry.mergeCommitSha', errors)
110
- if (ancestry.isAncestor !== true) errors.push('entry.observations.targetAncestry.isAncestor must be true')
111
- iso(ancestry.observedAt, 'entry.observations.targetAncestry.observedAt', errors)
112
- if (pr?.mergeCommitSha && ancestry.mergeCommitSha !== pr.mergeCommitSha) errors.push('target ancestry merge commit contradicts the pull request merge commit')
113
- }
114
- if (observations.queueExecutionArtifact !== undefined && !object(observations.queueExecutionArtifact)) {
115
- errors.push('entry.observations.queueExecutionArtifact must be an object when present')
116
- }
117
- }
118
- return { ok: errors.length === 0, errors }
119
- }
120
-
121
- export function validateTerminalEvidenceEnvelope(envelope) {
122
- const errors = []
123
- if (!exact(envelope, ['schemaVersion', 'kind', 'digestAlgorithm', 'digest', 'entry'], 'envelope', errors)) return { ok: false, errors }
124
- if (envelope.schemaVersion !== TERMINAL_EVIDENCE_SCHEMA_VERSION) errors.push(`envelope.schemaVersion must be ${TERMINAL_EVIDENCE_SCHEMA_VERSION}`)
125
- if (envelope.kind !== 'resolver-terminal-evidence-journal-entry') errors.push('envelope.kind must be resolver-terminal-evidence-journal-entry')
126
- if (envelope.digestAlgorithm !== TERMINAL_EVIDENCE_DIGEST_ALGORITHM) errors.push(`envelope.digestAlgorithm must be ${TERMINAL_EVIDENCE_DIGEST_ALGORITHM}`)
127
- const entryVerdict = validateTerminalEvidenceEntry(envelope.entry)
128
- errors.push(...entryVerdict.errors)
129
- const expected = object(envelope.entry) ? terminalEvidenceDigest(envelope.entry) : null
130
- if (!/^[0-9a-f]{64}$/.test(envelope.digest ?? '') || envelope.digest !== expected) {
131
- errors.push('envelope digest does not match the canonical entry content address')
132
- }
133
- return { ok: errors.length === 0, errors }
134
- }
135
-
136
- export function resolveTerminalEvidenceBinding(manifest, subject) {
137
- if (!object(manifest) || manifest.skill !== 'resolve-issues') throw new Error('terminal evidence applies only to a resolve-issues run manifest')
138
- if (!object(subject)) throw new Error('terminal evidence subject must be an object')
139
- const unit = (Array.isArray(manifest.units) ? manifest.units : [])
140
- .find((candidate) => candidate?.id === subject.unitId)
141
- if (!unit) throw new Error(`terminal evidence unit ${subject.unitId ?? '(missing)'} is not in this run`)
142
- const component = (Array.isArray(manifest.components) ? manifest.components : [])
143
- .find((candidate) => candidate?.id === subject.componentId)
144
- if (!component || !Array.isArray(component.units) || !component.units.includes(unit.id)) {
145
- throw new Error(`terminal evidence component ${subject.componentId ?? '(missing)'} does not own unit ${unit.id}`)
146
- }
147
-
148
- let delivery = deliveryForUnit(unit, manifest)
149
- let pr = positivePr(unit.pr)
150
- let headSha = unit.pushedSha
151
- let canonicalComponentDelivery = object(component.delivery) && delivery === component.delivery
152
- if (unit.deliveryRef !== undefined) {
153
- const ref = unit.deliveryRef
154
- if (!object(ref) || ref.componentId !== component.id) {
155
- throw new Error(`terminal evidence unit ${unit.id} is not bound to component delivery ${component.id}`)
156
- }
157
- delivery = object(component.delivery) && component.delivery.generation === ref.generation
158
- ? component.delivery
159
- : null
160
- if (!delivery) throw new Error(`terminal evidence unit ${unit.id} has a stale canonical component delivery binding`)
161
- pr = positivePr(delivery.pr)
162
- headSha = delivery.authorizedHeadSha
163
- canonicalComponentDelivery = true
164
- }
165
- if (pr == null) throw new Error(`terminal evidence delivery for unit ${unit.id} has no positive PR identity`)
166
- if (!text(headSha) || !SHA.test(headSha)) throw new Error(`terminal evidence delivery for unit ${unit.id} has no full exact head SHA`)
167
-
168
- return {
169
- unit,
170
- component,
171
- delivery,
172
- canonicalComponentDelivery,
173
- subject: { unitId: unit.id, componentId: component.id, pr, headSha },
174
- }
175
- }
176
-
177
- function boundEntry(manifest, observation) {
178
- if (!object(manifest) || manifest.skill !== 'resolve-issues') throw new Error('terminal evidence applies only to a resolve-issues run manifest')
179
- if (!text(manifest.runSlug) || !text(manifest.repository) || !text(manifest.targetBranch)) {
180
- throw new Error('terminal evidence requires run slug, repository, and target branch bindings')
181
- }
182
- if (!object(observation)) throw new Error('terminal evidence observation must be an object')
183
- const binding = resolveTerminalEvidenceBinding(manifest, observation)
184
- const expectedTargetRef = `origin/${manifest.targetBranch}`
185
- if (observation.pullRequest?.headSha !== binding.subject.headSha) {
186
- throw new Error(`terminal evidence pull request head does not match delivery for unit ${binding.unit.id} exact head`)
187
- }
188
- if (observation.targetAncestry?.targetRef !== expectedTargetRef) throw new Error(`terminal evidence target ancestry must bind ${expectedTargetRef}`)
189
-
190
- return {
191
- schemaVersion: TERMINAL_EVIDENCE_SCHEMA_VERSION,
192
- kind: TERMINAL_EVIDENCE_KIND,
193
- run: { slug: manifest.runSlug, repository: manifest.repository, targetBranch: manifest.targetBranch },
194
- subject: binding.subject,
195
- provider: structuredClone(observation.provider),
196
- observations: {
197
- pullRequest: structuredClone(observation.pullRequest),
198
- checks: structuredClone(observation.checks),
199
- targetAncestry: structuredClone(observation.targetAncestry),
200
- ...(observation.queueExecutionArtifact !== undefined
201
- ? { queueExecutionArtifact: structuredClone(observation.queueExecutionArtifact) }
202
- : {}),
203
- },
204
- }
205
- }
206
-
207
- export function defaultTerminalEvidenceDirectory(env = process.env) {
208
- if (text(env.RESOLVER_TERMINAL_EVIDENCE_DIR)) return path.resolve(env.RESOLVER_TERMINAL_EVIDENCE_DIR)
209
- const stateHome = text(env.XDG_STATE_HOME) ? env.XDG_STATE_HOME : path.join(env.HOME || os.homedir(), '.local', 'state')
210
- return path.resolve(stateHome, 'resolver-terminal-evidence')
211
- }
212
-
213
- export function appendTerminalEvidence(manifest, observation, { directory = defaultTerminalEvidenceDirectory() } = {}) {
214
- const entry = boundEntry(manifest, observation)
215
- const verdict = validateTerminalEvidenceEntry(entry)
216
- if (!verdict.ok) throw new Error(`terminal evidence is invalid: ${verdict.errors[0]}`)
217
- const digest = terminalEvidenceDigest(entry)
218
- const envelope = {
219
- schemaVersion: TERMINAL_EVIDENCE_SCHEMA_VERSION,
220
- kind: 'resolver-terminal-evidence-journal-entry',
221
- digestAlgorithm: TERMINAL_EVIDENCE_DIGEST_ALGORITHM,
222
- digest,
223
- entry,
224
- }
225
- const bytes = `${JSON.stringify(envelope, null, 2)}\n`
226
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 })
227
- const destination = path.join(directory, `${digest}.json`)
228
- let descriptor = null
229
- try {
230
- descriptor = fs.openSync(destination, 'wx', 0o444)
231
- fs.writeFileSync(descriptor, bytes, 'utf8')
232
- fs.fsyncSync(descriptor)
233
- } catch (error) {
234
- if (error.code !== 'EEXIST') throw error
235
- const existing = fs.readFileSync(destination, 'utf8')
236
- if (existing !== bytes) throw new Error(`terminal evidence content-address collision or tampering at ${destination}`)
237
- } finally {
238
- if (descriptor != null) fs.closeSync(descriptor)
239
- }
240
- fs.chmodSync(destination, 0o444)
241
- return { digest, path: destination, entry }
242
- }
243
-
244
- export function readTerminalEvidence(file) {
245
- const envelope = JSON.parse(fs.readFileSync(file, 'utf8'))
246
- const verdict = validateTerminalEvidenceEnvelope(envelope)
247
- if (!verdict.ok) throw new Error(`terminal evidence journal entry is invalid: ${verdict.errors[0]}`)
248
- const basename = path.basename(file)
249
- if (basename !== `${envelope.digest}.json`) throw new Error('terminal evidence filename does not match its content address')
250
- return envelope
251
- }
252
-
253
- function parseArgs(argv) {
254
- const command = argv[0]
255
- const options = {}
256
- const positional = []
257
- for (let index = 1; index < argv.length; index += 1) {
258
- const argument = argv[index]
259
- if (['--manifest', '--input', '--dir'].includes(argument)) options[argument.slice(2)] = argv[++index]
260
- else positional.push(argument)
261
- }
262
- return { command, options, positional }
263
- }
264
-
265
- export function runTerminalEvidenceJournalCli(argv) {
266
- const { command, options, positional } = parseArgs(argv)
267
- try {
268
- if (['--help', '-h', 'help'].includes(command)) {
269
- process.stdout.write('usage: terminal-evidence-journal.mjs append --manifest <run.json> --input <observation.json> [--dir <journal-dir>] | validate <entry.json>\n')
270
- return 0
271
- }
272
- if (command === 'append') {
273
- if (!options.manifest || !options.input) throw new Error('append requires --manifest <run.json> --input <observation.json> [--dir <journal-dir>]')
274
- const manifest = JSON.parse(fs.readFileSync(options.manifest, 'utf8'))
275
- const observation = JSON.parse(fs.readFileSync(options.input, 'utf8'))
276
- const result = appendTerminalEvidence(manifest, observation, options.dir ? { directory: path.resolve(options.dir) } : {})
277
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
278
- return 0
279
- }
280
- if (command === 'validate') {
281
- if (positional.length !== 1) throw new Error('validate requires <content-addressed-entry.json>')
282
- const envelope = readTerminalEvidence(positional[0])
283
- process.stdout.write(`terminal-evidence-journal: valid ${envelope.digest}\n`)
284
- return 0
285
- }
286
- throw new Error('usage: terminal-evidence-journal.mjs append --manifest <run.json> --input <observation.json> [--dir <journal-dir>] | validate <entry.json>')
287
- } catch (error) {
288
- process.stderr.write(`terminal-evidence-journal: ${error.message}\n`)
289
- return 2
290
- }
291
- }
292
-
293
- if (isMainModule(import.meta.url)) process.exitCode = runTerminalEvidenceJournalCli(process.argv.slice(2))
@@ -1,197 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs, { realpathSync } from 'node:fs'
4
- import { fileURLToPath } from 'node:url'
5
-
6
- export const UNIT_KINDS = ['implementation', 'external-verification', 'human-decision', 'prerequisite']
7
-
8
- const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0
9
-
10
- export const EVIDENCE_LIFECYCLE_CLASSES = ['ordinary', 'evidence-authorization']
11
-
12
- const EVIDENCE_SEMANTICS = /\b(authorization|authorisation|required[- ]checks?|admission|delivery evidence|enqueue|queue[- ]head|synthetic queue|mergeable|target[- ]branch ancestry)\b/i
13
- const EVIDENCE_CONTROL_SURFACES = /(?:^|\/)resolve-issues\/scripts\/(?:unit-kind|spawn-contract|evidence-lifecycle-contract|required-check-plan|combine-and-verify|component-candidate|exact-head(?:-ci|-github-provider)?|watch-delivery|finalize)(?:\.|\/|$)/i
14
- const EVIDENCE_DOCTRINE_SURFACES = /(?:^|\/)resolve-issues\/(?:SKILL\.md|references\/(?:breaker|deliverables|delivery|evidence-lane|exact-head-ci|fan-out|finalization|high-risk|intake|integration-gate|interference|loop|pre-flight(?:-[^/]+)?|regression-checklist|run-state|spawn-contract|why)\.md)$/i
15
-
16
- const isEvidenceLifecycleSurface = (path) => (
17
- EVIDENCE_CONTROL_SURFACES.test(path) || EVIDENCE_DOCTRINE_SURFACES.test(path)
18
- )
19
-
20
- function classifyEvidenceLifecycle(input, text) {
21
- const signals = []
22
- const declaration = input.declaredScope
23
- if (declaration !== undefined) {
24
- if (declaration == null || typeof declaration !== 'object' || Array.isArray(declaration)
25
- || !EVIDENCE_LIFECYCLE_CLASSES.includes(declaration.classification)) {
26
- signals.push({ type: 'unknown-declared-scope', source: 'declaredScope' })
27
- } else if (declaration.classification === 'evidence-authorization') {
28
- signals.push({ type: 'declared-lifecycle-scope', source: 'declaredScope' })
29
- }
30
- if (nonEmpty(declaration?.summary) && EVIDENCE_SEMANTICS.test(declaration.summary)) {
31
- signals.push({ type: 'declared-lifecycle-semantics', source: 'declaredScope.summary' })
32
- }
33
- }
34
-
35
- if (input.changedSurfaces !== undefined && !Array.isArray(input.changedSurfaces)) {
36
- signals.push({ type: 'unknown-changed-surface', source: 'changedSurfaces' })
37
- } else {
38
- for (const [index, surface] of (input.changedSurfaces || []).entries()) {
39
- if (surface == null || typeof surface !== 'object' || Array.isArray(surface)
40
- || !nonEmpty(surface.path) || !EVIDENCE_LIFECYCLE_CLASSES.includes(surface.classification)) {
41
- signals.push({ type: 'unknown-changed-surface', source: `changedSurfaces[${index}]` })
42
- continue
43
- }
44
- if (surface.classification === 'evidence-authorization') {
45
- signals.push({ type: 'declared-lifecycle-surface', source: surface.path })
46
- }
47
- if (isEvidenceLifecycleSurface(surface.path)) {
48
- signals.push({ type: 'sensitive-changed-surface', source: surface.path })
49
- }
50
- }
51
- }
52
-
53
- if (EVIDENCE_SEMANTICS.test(text)) signals.push({ type: 'lifecycle-semantics', source: 'title/body' })
54
- const required = signals.length > 0
55
- return {
56
- classification: required ? 'evidence-authorization' : 'ordinary',
57
- required,
58
- denyByDefault: true,
59
- signals,
60
- }
61
- }
62
-
63
- const SIGNALS = [
64
- {
65
- type: 'physical-presence',
66
- pattern: /\b(physically present|physical presence|on[- ]site|in person|live (?:application|terminal|device|lane) operator)\b/i,
67
- recommendation: 'external-verification',
68
- },
69
- {
70
- type: 'external-dispatch',
71
- pattern: /\b(dispatch|send)\b[^.\n]{0,80}\b(external|operator|technician|auditor)\b|\bexternal dispatch\b/i,
72
- recommendation: 'external-verification',
73
- },
74
- {
75
- type: 'provider-observation',
76
- pattern: /\b(provider|acquirer|issuer|third[- ]party)\b[^.\n]{0,100}\b(observe|observations?|attest|certif(?:y|ication))\b|\b(observe|observations?)\b[^.\n]{0,100}\b(provider|acquirer|issuer|third[- ]party)\b/i,
77
- recommendation: 'external-verification',
78
- },
79
- {
80
- type: 'third-party-approval',
81
- pattern: /\b(provider|acquirer|issuer|third[- ]party|legal|regulator)\b[^.\n]{0,100}\b(approve|approval|sign[- ]off|consent)\b/i,
82
- recommendation: 'external-verification',
83
- },
84
- {
85
- type: 'human-decision',
86
- pattern: /\b(product owner|human|legal|compliance|stakeholder)\b[^.\n]{0,100}\b(decide|decision|choose|select|approve)\b|\b(decide|decision|choose)\b[^.\n]{0,100}\b(policy|option|whether)\b/i,
87
- recommendation: 'human-decision',
88
- },
89
- {
90
- type: 'credentials',
91
- pattern: /\b(credential|credentials|api key|secret|certificate|production access)\b/i,
92
- recommendation: 'prerequisite',
93
- },
94
- {
95
- type: 'hardware',
96
- pattern: /\b(hardware|security key|physical terminal|device)\b/i,
97
- recommendation: 'prerequisite',
98
- },
99
- {
100
- type: 'prerequisite-input',
101
- pattern: /\b(obtain|provision|procure|provide|land)\b[^.\n]{0,100}\b(before|prerequisite|input|required)\b|\b(before .* can begin|before implementation)\b/i,
102
- recommendation: 'prerequisite',
103
- },
104
- ]
105
-
106
- function recommend(signals) {
107
- const kinds = new Set(signals.map((signal) => signal.recommendation))
108
- if (kinds.has('human-decision')) return 'human-decision'
109
- if (kinds.has('external-verification')) return 'external-verification'
110
- if (kinds.has('prerequisite')) return 'prerequisite'
111
- return 'implementation'
112
- }
113
-
114
- export function classifyUnitIntake(input = {}) {
115
- if (input == null || typeof input !== 'object' || Array.isArray(input)) {
116
- throw new TypeError('intake must be a JSON object')
117
- }
118
- if (input.selectedKind !== undefined && !UNIT_KINDS.includes(input.selectedKind)) {
119
- throw new TypeError(`selectedKind must be one of ${UNIT_KINDS.join('|')}`)
120
- }
121
-
122
- const text = `${nonEmpty(input.title) ? input.title : ''}\n${nonEmpty(input.body) ? input.body : ''}`
123
- const signals = SIGNALS
124
- .filter((signal) => signal.pattern.test(text))
125
- .map(({ type, recommendation }) => ({ type, recommendation }))
126
- const recommendedKind = recommend(signals)
127
- const evidenceLifecycle = classifyEvidenceLifecycle(input, text)
128
- const unitKind = input.selectedKind ?? recommendedKind
129
- const required = recommendedKind !== 'implementation' || unitKind !== 'implementation'
130
- const confirmed = required && nonEmpty(input.confirmedBy) && nonEmpty(input.confirmedAt)
131
- const confirmation = {
132
- required,
133
- status: required ? (confirmed ? 'confirmed' : 'pending') : 'not-required',
134
- kind: unitKind,
135
- ...(confirmed ? { confirmedBy: input.confirmedBy.trim(), confirmedAt: input.confirmedAt.trim() } : {}),
136
- }
137
-
138
- return { unitKind, recommendedKind, signals, confirmation, evidenceLifecycle }
139
- }
140
-
141
- export function intakePatch(input = {}) {
142
- const result = classifyUnitIntake(input)
143
- return {
144
- unitKind: result.unitKind,
145
- kindRecommendation: {
146
- kind: result.recommendedKind,
147
- signals: result.signals,
148
- },
149
- kindConfirmation: result.confirmation,
150
- evidenceLifecycle: result.evidenceLifecycle,
151
- }
152
- }
153
-
154
- function usage() {
155
- return 'usage: unit-kind.mjs classify --stdin\n'
156
- }
157
-
158
- function main(argv = process.argv.slice(2)) {
159
- if (argv.length === 1 && ['--help', '-h', 'help'].includes(argv[0])) {
160
- process.stdout.write(usage())
161
- return 0
162
- }
163
- if (argv.length !== 2 || argv[0] !== 'classify' || argv[1] !== '--stdin') {
164
- process.stderr.write(usage())
165
- return 2
166
- }
167
- try {
168
- const input = JSON.parse(fs.readFileSync(0, 'utf8'))
169
- process.stdout.write(`${JSON.stringify(intakePatch(input), null, 2)}\n`)
170
- return 0
171
- } catch (error) {
172
- process.stderr.write(`unit-kind: ${error.message}\n`)
173
- return 2
174
- }
175
- }
176
-
177
- let launchedDirectly = false
178
- let mainModuleError = null
179
- try {
180
- const { isMainModule } = await import('../../engineering-runtime/scripts/main-module.mjs')
181
- if (typeof isMainModule !== 'function') throw new Error('main-module.mjs does not export isMainModule')
182
- launchedDirectly = isMainModule(import.meta.url)
183
- } catch (error) {
184
- mainModuleError = error
185
- try {
186
- launchedDirectly = realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
187
- } catch {
188
- launchedDirectly = false
189
- }
190
- }
191
-
192
- if (launchedDirectly && mainModuleError) {
193
- console.error(`unit-kind: cannot load engineering-runtime main-module helper: ${mainModuleError.message}`)
194
- process.exitCode = 1
195
- } else if (launchedDirectly) {
196
- process.exitCode = main()
197
- }
@@ -1,127 +0,0 @@
1
- import { UNIT_KINDS } from './unit-kind.mjs'
2
-
3
- export const EXTERNAL_COMPLETION_SOURCES = ['authorized-actor-observation', 'third-party-decision']
4
-
5
- const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0
6
- const isObject = (value) => value != null && typeof value === 'object' && !Array.isArray(value)
7
- const stringList = (value) => Array.isArray(value) && value.length > 0 && value.every(nonEmpty)
8
-
9
- export function effectiveUnitKind(unit) {
10
- return UNIT_KINDS.includes(unit?.unitKind) ? unit.unitKind : 'implementation'
11
- }
12
-
13
- function validateConfirmation(unit, label, errors) {
14
- const recommendation = isObject(unit.kindRecommendation) ? unit.kindRecommendation.kind : 'implementation'
15
- const kind = effectiveUnitKind(unit)
16
- const required = kind !== 'implementation' || recommendation !== 'implementation'
17
- if (!required) return
18
- const confirmation = unit.kindConfirmation
19
- if (!isObject(confirmation) || confirmation.status !== 'confirmed') {
20
- errors.push(`${label}.kindConfirmation must be explicitly confirmed before ${kind} enters the roster`)
21
- return
22
- }
23
- if (confirmation.kind !== kind) errors.push(`${label}.kindConfirmation.kind must match unitKind ${kind}`)
24
- if (!nonEmpty(confirmation.confirmedBy)) errors.push(`${label}.kindConfirmation.confirmedBy is required`)
25
- if (!nonEmpty(confirmation.confirmedAt)) errors.push(`${label}.kindConfirmation.confirmedAt is required`)
26
- }
27
-
28
- function validateExternalProtocol(protocol, label, errors) {
29
- if (!isObject(protocol)) {
30
- errors.push(`${label}.externalProtocol is required for external-verification`)
31
- return
32
- }
33
- for (const field of ['prerequisites', 'authorizedActors', 'allowedOperations']) {
34
- if (!stringList(protocol[field])) errors.push(`${label}.externalProtocol.${field} must be a non-empty string array`)
35
- }
36
- for (const field of ['environmentIdentity', 'stopPolicy', 'retryPolicy', 'completionSource']) {
37
- if (!nonEmpty(protocol[field])) errors.push(`${label}.externalProtocol.${field} is required`)
38
- }
39
- if (!EXTERNAL_COMPLETION_SOURCES.includes(protocol.completionSource)) {
40
- errors.push(`${label}.externalProtocol.completionSource must be one of ${EXTERNAL_COMPLETION_SOURCES.join('|')}`)
41
- }
42
- if (!isObject(protocol.evidenceSchema) || !stringList(protocol.evidenceSchema.required)) {
43
- errors.push(`${label}.externalProtocol.evidenceSchema.required must be a non-empty string array`)
44
- }
45
- }
46
-
47
- function externalCompletionErrors(unit, label) {
48
- const errors = []
49
- const completion = unit.externalCompletion
50
- if (completion === undefined || completion === null) return errors
51
- const protocol = unit.externalProtocol
52
- if (!isObject(completion)) return [`${label}.externalCompletion must be an object`]
53
- if (!isObject(protocol)) return [`${label}.externalCompletion cannot be checked without externalProtocol`]
54
- if (completion.source !== protocol.completionSource) errors.push(`${label}.externalCompletion.source must match externalProtocol.completionSource`)
55
- if (!Array.isArray(protocol.authorizedActors) || !protocol.authorizedActors.includes(completion.actor)) {
56
- errors.push(`${label}.externalCompletion.actor must be one of externalProtocol.authorizedActors`)
57
- }
58
- if (completion.environmentIdentity !== protocol.environmentIdentity) {
59
- errors.push(`${label}.externalCompletion.environmentIdentity must match externalProtocol.environmentIdentity`)
60
- }
61
- if (!Array.isArray(protocol.allowedOperations) || !protocol.allowedOperations.includes(completion.operation)) {
62
- errors.push(`${label}.externalCompletion.operation must be one of externalProtocol.allowedOperations`)
63
- }
64
- if (!nonEmpty(completion.observedAt)) errors.push(`${label}.externalCompletion.observedAt is required`)
65
- if (!isObject(completion.evidence)) {
66
- errors.push(`${label}.externalCompletion.evidence must satisfy externalProtocol.evidenceSchema`)
67
- } else {
68
- const missing = (protocol.evidenceSchema?.required ?? []).filter((key) => !nonEmpty(completion.evidence[key]))
69
- if (missing.length) errors.push(`${label}.externalCompletion.evidenceSchema missing required evidence: ${missing.join(', ')}`)
70
- }
71
- return errors
72
- }
73
-
74
- export function validateUnitKind(unit, label = 'unit') {
75
- const errors = []
76
- if (!isObject(unit)) return [`${label} must be an object`]
77
- if (unit.unitKind !== undefined && !UNIT_KINDS.includes(unit.unitKind)) {
78
- errors.push(`${label}.unitKind must be one of ${UNIT_KINDS.join('|')}`)
79
- return errors
80
- }
81
- if (unit.kindRecommendation !== undefined) {
82
- if (!isObject(unit.kindRecommendation) || !UNIT_KINDS.includes(unit.kindRecommendation.kind)) {
83
- errors.push(`${label}.kindRecommendation.kind must be one of ${UNIT_KINDS.join('|')}`)
84
- } else if (!Array.isArray(unit.kindRecommendation.signals)) {
85
- errors.push(`${label}.kindRecommendation.signals must be an array`)
86
- }
87
- }
88
- validateConfirmation(unit, label, errors)
89
- const kind = effectiveUnitKind(unit)
90
- if (kind === 'external-verification') {
91
- validateExternalProtocol(unit.externalProtocol, label, errors)
92
- errors.push(...externalCompletionErrors(unit, label))
93
- }
94
- if (kind !== 'implementation' && nonEmpty(unit.pr)) {
95
- errors.push(`${label}: ${kind} may reference artifacts but may not own an implementation PR`)
96
- }
97
- if (kind !== 'implementation' && (unit.status === 'merge-ready' || unit.delivery !== undefined || unit.lastVerdict !== undefined)) {
98
- errors.push(`${label}: ${kind} never uses code delivery or review PASS; an artifact PASS is not ${kind} completion`)
99
- }
100
- if (unit.completionDependsOn !== undefined && (!Array.isArray(unit.completionDependsOn) || !unit.completionDependsOn.every(nonEmpty))) {
101
- errors.push(`${label}.completionDependsOn must be an array of non-empty unit ids`)
102
- }
103
- return errors
104
- }
105
-
106
- export function unitKindComplete(unit) {
107
- const kind = effectiveUnitKind(unit)
108
- if (kind === 'implementation') return false
109
- if (kind === 'external-verification') {
110
- const protocolErrors = []
111
- validateExternalProtocol(unit.externalProtocol, 'unit', protocolErrors)
112
- return protocolErrors.length === 0 && externalCompletionErrors(unit, 'unit').length === 0 && isObject(unit.externalCompletion)
113
- }
114
- if (kind === 'human-decision') {
115
- return isObject(unit.humanDecision) && nonEmpty(unit.humanDecision.decision)
116
- && nonEmpty(unit.humanDecision.decidedBy) && nonEmpty(unit.humanDecision.decidedAt)
117
- }
118
- return isObject(unit.prerequisiteCompletion) && nonEmpty(unit.prerequisiteCompletion.source)
119
- && nonEmpty(unit.prerequisiteCompletion.completedAt) && nonEmpty(unit.prerequisiteCompletion.evidence)
120
- }
121
-
122
- export function completionBlockers(manifest, unit) {
123
- const units = Array.isArray(manifest?.units) ? manifest.units : []
124
- const byId = new Map(units.filter(isObject).map((entry) => [entry.id, entry]))
125
- return (Array.isArray(unit?.completionDependsOn) ? unit.completionDependsOn : [])
126
- .filter((id) => !unitKindComplete(byId.get(id)))
127
- }