@xemahq/xema 0.26.0 → 0.27.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 (229) hide show
  1. package/README.md +70 -37
  2. package/dist/assets/distribution-assets.manifest.json +1 -2
  3. package/dist/cli.js +2 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/dev.js +1 -0
  6. package/dist/commands/dev.js.map +1 -1
  7. package/dist/commands/down.d.ts.map +1 -1
  8. package/dist/commands/down.js +7 -1
  9. package/dist/commands/down.js.map +1 -1
  10. package/dist/commands/edition.d.ts +3 -0
  11. package/dist/commands/edition.d.ts.map +1 -0
  12. package/dist/commands/edition.js +46 -0
  13. package/dist/commands/edition.js.map +1 -0
  14. package/dist/commands/init.js +2 -2
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/serve.js +1 -0
  17. package/dist/commands/serve.js.map +1 -1
  18. package/dist/commands/up.js +1 -0
  19. package/dist/commands/up.js.map +1 -1
  20. package/dist/dev/env-defaults.d.ts.map +1 -1
  21. package/dist/dev/env-defaults.js +5 -1
  22. package/dist/dev/env-defaults.js.map +1 -1
  23. package/dist/dev/infra-preflight.d.ts +3 -2
  24. package/dist/dev/infra-preflight.d.ts.map +1 -1
  25. package/dist/dev/infra-preflight.js +18 -8
  26. package/dist/dev/infra-preflight.js.map +1 -1
  27. package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
  28. package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
  29. package/dist/runtime/config/schema.d.ts +1 -0
  30. package/dist/runtime/config/schema.d.ts.map +1 -1
  31. package/dist/runtime/custody/local-custody.d.ts +12 -0
  32. package/dist/runtime/custody/local-custody.d.ts.map +1 -0
  33. package/dist/runtime/custody/local-custody.js +22 -0
  34. package/dist/runtime/custody/local-custody.js.map +1 -0
  35. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
  36. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
  37. package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
  38. package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
  39. package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
  40. package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
  41. package/dist/runtime/custody/openbao-custody-store.js +55 -0
  42. package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
  43. package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
  44. package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
  45. package/dist/runtime/custody/openbao-http-client.js +154 -0
  46. package/dist/runtime/custody/openbao-http-client.js.map +1 -0
  47. package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
  48. package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
  49. package/dist/runtime/custody/openbao-local-constants.js +48 -0
  50. package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
  51. package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
  52. package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
  53. package/dist/runtime/custody/openbao-local-runner.js +86 -0
  54. package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
  55. package/dist/runtime/edition/run-entry.d.ts +9 -0
  56. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  57. package/dist/runtime/edition/run-entry.js +36 -0
  58. package/dist/runtime/edition/run-entry.js.map +1 -0
  59. package/dist/runtime/edition/verb-map.d.ts +60 -0
  60. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  61. package/dist/runtime/edition/verb-map.js +146 -0
  62. package/dist/runtime/edition/verb-map.js.map +1 -0
  63. package/dist/runtime/env-seeder.d.ts +2 -0
  64. package/dist/runtime/env-seeder.d.ts.map +1 -1
  65. package/dist/runtime/env-seeder.js +18 -3
  66. package/dist/runtime/env-seeder.js.map +1 -1
  67. package/dist/runtime/launch.d.ts +2 -1
  68. package/dist/runtime/launch.d.ts.map +1 -1
  69. package/dist/runtime/launch.js +31 -7
  70. package/dist/runtime/launch.js.map +1 -1
  71. package/dist/runtime/paths.d.ts +1 -2
  72. package/dist/runtime/paths.d.ts.map +1 -1
  73. package/dist/runtime/paths.js +0 -10
  74. package/dist/runtime/paths.js.map +1 -1
  75. package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
  76. package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
  77. package/dist/runtime/runner/chart-local-provenance.js +114 -0
  78. package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
  79. package/dist/runtime/runner/chart-preflight.d.ts +8 -8
  80. package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
  81. package/dist/runtime/runner/chart-preflight.js +30 -69
  82. package/dist/runtime/runner/chart-preflight.js.map +1 -1
  83. package/dist/runtime/runner/chart-pull.d.ts +14 -0
  84. package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
  85. package/dist/runtime/runner/chart-pull.js +96 -0
  86. package/dist/runtime/runner/chart-pull.js.map +1 -0
  87. package/dist/runtime/runner/chart-resolver.d.ts +4 -12
  88. package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
  89. package/dist/runtime/runner/chart-resolver.js +16 -62
  90. package/dist/runtime/runner/chart-resolver.js.map +1 -1
  91. package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
  92. package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
  93. package/dist/runtime/runner/chart-set-lock.js +55 -99
  94. package/dist/runtime/runner/chart-set-lock.js.map +1 -1
  95. package/dist/runtime/runner/helm-cli.d.ts +3 -1
  96. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  97. package/dist/runtime/runner/helm-cli.js +9 -7
  98. package/dist/runtime/runner/helm-cli.js.map +1 -1
  99. package/dist/runtime/runner/helm-runner.d.ts +4 -4
  100. package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
  101. package/dist/runtime/runner/helm-runner.js +18 -16
  102. package/dist/runtime/runner/helm-runner.js.map +1 -1
  103. package/dist/runtime/sources/kinds/git.d.ts +1 -2
  104. package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
  105. package/dist/runtime/sources/kinds/git.js +8 -5
  106. package/dist/runtime/sources/kinds/git.js.map +1 -1
  107. package/dist/runtime/sources/manifest-walk.d.ts +3 -1
  108. package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
  109. package/dist/runtime/sources/manifest-walk.js +8 -3
  110. package/dist/runtime/sources/manifest-walk.js.map +1 -1
  111. package/dist/runtime/sources/schema.d.ts +3 -0
  112. package/dist/runtime/sources/schema.d.ts.map +1 -1
  113. package/dist/runtime/sources/schema.js +6 -1
  114. package/dist/runtime/sources/schema.js.map +1 -1
  115. package/dist/runtime/supervisor.d.ts +2 -0
  116. package/dist/runtime/supervisor.d.ts.map +1 -1
  117. package/dist/runtime/supervisor.js +1 -0
  118. package/dist/runtime/supervisor.js.map +1 -1
  119. package/edition/README.md +315 -0
  120. package/edition/assemble-producer-input.mjs +1251 -0
  121. package/edition/authorization-base.mjs +347 -0
  122. package/edition/biome-content.mjs +268 -0
  123. package/edition/build-biome-index.mjs +175 -0
  124. package/edition/build-platform-service-index.mjs +288 -0
  125. package/edition/build-runtime-image-source-index.mjs +821 -0
  126. package/edition/built-verifier.mjs +55 -0
  127. package/edition/catalogue-edition.mjs +49 -0
  128. package/edition/catalogue-fetch.mjs +64 -0
  129. package/edition/catalogue.mjs +135 -0
  130. package/edition/chart-set-source-commit.mjs +92 -0
  131. package/edition/check-committed-image-evidence.mjs +383 -0
  132. package/edition/checkout-indexed-source-repositories.mjs +105 -0
  133. package/edition/comment-blanker.mjs +75 -0
  134. package/edition/connector-source-authority.mjs +60 -0
  135. package/edition/customer-definition.mjs +183 -0
  136. package/edition/customer-flags.mjs +7 -0
  137. package/edition/develop-customer-edition.mjs +99 -0
  138. package/edition/develop-edition-resolve.mjs +65 -0
  139. package/edition/develop-edition.mjs +274 -0
  140. package/edition/develop-freeze.mjs +173 -0
  141. package/edition/develop-image-coordinate.mjs +54 -0
  142. package/edition/develop-seal.mjs +77 -0
  143. package/edition/develop-snapshot-inputs.mjs +104 -0
  144. package/edition/dist-check.mjs +986 -0
  145. package/edition/distribution-evidence-set.mjs +987 -0
  146. package/edition/distribution-paths.mjs +191 -0
  147. package/edition/edition-artifact.mjs +270 -0
  148. package/edition/edition-biome-manifests.mjs +85 -0
  149. package/edition/edition-channel.mjs +6 -0
  150. package/edition/edition-chart-set.mjs +93 -0
  151. package/edition/edition-config.mjs +207 -0
  152. package/edition/edition-constants.mjs +57 -0
  153. package/edition/edition-contract.json +23 -0
  154. package/edition/edition-list.mjs +35 -0
  155. package/edition/edition-locks.mjs +69 -0
  156. package/edition/edition-protocol.mjs +228 -0
  157. package/edition/edition-workspace.mjs +176 -0
  158. package/edition/evidence-checkout.mjs +418 -0
  159. package/edition/evidence-grammars.mjs +16 -0
  160. package/edition/evidence-trust-pin.mjs +304 -0
  161. package/edition/evidence-trust-policy.mjs +256 -0
  162. package/edition/external-source-walk.mjs +463 -0
  163. package/edition/first-party-source-ids.mjs +59 -0
  164. package/edition/fleet-coordinate-finality.mjs +350 -0
  165. package/edition/git-source-auth.mjs +52 -0
  166. package/edition/image-evidence-replay-state.mjs +242 -0
  167. package/edition/image-evidence.mjs +1225 -0
  168. package/edition/kernel-contract-authority.mjs +443 -0
  169. package/edition/lib.mjs +3018 -0
  170. package/edition/no-follow-file.mjs +94 -0
  171. package/edition/oci-evidence-verbs.mjs +392 -0
  172. package/edition/oci-image-source-evidence.mjs +239 -0
  173. package/edition/oras.mjs +648 -0
  174. package/edition/pin-source.mjs +227 -0
  175. package/edition/plan-image-evidence-refresh.mjs +408 -0
  176. package/edition/platform-lock-readability.mjs +892 -0
  177. package/edition/platform-service-class.mjs +18 -0
  178. package/edition/platform-service-source-authority.mjs +105 -0
  179. package/edition/positive-closure.mjs +162 -0
  180. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  181. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  182. package/edition/produce-image-source-evidence.mjs +765 -0
  183. package/edition/producer-evidence-bundle.mjs +157 -0
  184. package/edition/registry-credentials.mjs +193 -0
  185. package/edition/release-catalogue.mjs +39 -0
  186. package/edition/release-channel-guard.mjs +99 -0
  187. package/edition/release-edition-artifact.mjs +158 -0
  188. package/edition/remedy-command.mjs +89 -0
  189. package/edition/repin-evidence-trust.mjs +101 -0
  190. package/edition/required-resource-provider.mjs +58 -0
  191. package/edition/resolve-chart-set.mjs +415 -0
  192. package/edition/resolve-customer-edition.mjs +288 -0
  193. package/edition/resolve-distribution.mjs +2318 -0
  194. package/edition/resolve-edition.mjs +186 -0
  195. package/edition/resolve-images.mjs +1606 -0
  196. package/edition/seal-image-digests.mjs +855 -0
  197. package/edition/sealed-evidence-accept.mjs +82 -0
  198. package/edition/source-authority.mjs +725 -0
  199. package/edition/source-closure.mjs +116 -0
  200. package/edition/strict-json.mjs +121 -0
  201. package/edition/string-order.mjs +5 -0
  202. package/edition/trust-policy-arming.mjs +120 -0
  203. package/edition/trusted-executable.mjs +166 -0
  204. package/edition/verifier-binding.mjs +45 -0
  205. package/edition/verifier-files.mjs +8 -0
  206. package/edition/verifier-kernel-schemas.mjs +22 -0
  207. package/edition/verifier-provenance.mjs +64 -0
  208. package/edition/verify-image-evidence.mjs +430 -0
  209. package/edition/verify-image-platforms.mjs +495 -0
  210. package/edition/why.mjs +56 -0
  211. package/edition/workspace-sections.mjs +195 -0
  212. package/examples/06-biome-sources.yaml +1 -0
  213. package/package.json +14 -6
  214. package/dist/commands/dist.d.ts +0 -16
  215. package/dist/commands/dist.d.ts.map +0 -1
  216. package/dist/commands/dist.js +0 -182
  217. package/dist/commands/dist.js.map +0 -1
  218. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  219. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  220. package/dist/runtime/distribution/available-biome.js +0 -156
  221. package/dist/runtime/distribution/available-biome.js.map +0 -1
  222. package/dist/runtime/distribution/compose.d.ts +0 -23
  223. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  224. package/dist/runtime/distribution/compose.js +0 -48
  225. package/dist/runtime/distribution/compose.js.map +0 -1
  226. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  227. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  228. package/dist/runtime/runner/chart-trust.js +0 -270
  229. package/dist/runtime/runner/chart-trust.js.map +0 -1
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+ // ═══════════════════════════════════════════════════════════════════════════
3
+ // IS THE COMMITTED EVIDENCE BUNDLE FOR THIS EXACT PRODUCER INPUT STILL USABLE?
4
+ //
5
+ // node check-committed-image-evidence.mjs \
6
+ // --input <producer-input.json> \
7
+ // --repository-root <absolute-directory> \
8
+ // [--github-output <path>] [--now <epoch-millis>]
9
+ //
10
+ // WHY THIS EXISTS
11
+ //
12
+ // Every producing workflow guarded its expensive signing step with
13
+ //
14
+ // if [ -d "${EVIDENCE_DIR}" ]; then exists=true; fi
15
+ //
16
+ // and then reported `::notice::… already produced; reusing it`. That step
17
+ // verified NOTHING about what is on disk — this repository's most-repeated
18
+ // defect, a check that reports green while executing nothing. Concretely it
19
+ // let three separate broken states read as "covered":
20
+ //
21
+ // * a bundle from a PREVIOUS, half-committed run (statement present,
22
+ // bundle truncated, descriptor index absent);
23
+ // * a bundle whose statement describes a DIFFERENT subject or a different
24
+ // source coordinate than the input about to be signed;
25
+ // * an EXPIRED bundle. Producer evidence carries a hard `expiresAt` that
26
+ // `image-evidence.mjs assertTimeWindow` enforces at verification, and the
27
+ // directory-exists guard made expiry UNRECOVERABLE: every re-run of the
28
+ // producing workflow saw the directory, skipped production, and the
29
+ // distribution simply stayed undeliverable with a green CI.
30
+ //
31
+ // So the reuse decision is made HERE, by actually reading the bundle:
32
+ // reusable ⇔ well-formed AND bound to this exact input AND unexpired with
33
+ // enough remaining life to be worth keeping. Anything else re-produces.
34
+ //
35
+ // WHAT "BOUND TO THIS INPUT" MEANS (each is a real substitution this catches)
36
+ //
37
+ // * the descriptor index names exactly the input's subject set;
38
+ // * every statement parses under the kernel's own
39
+ // `parseImageProducerStatement` at the input's contract authority;
40
+ // * statement digests match the descriptor, and the DSSE payload inside the
41
+ // bundle is byte-identical to the statement (`validateProducedBundle`);
42
+ // * the statement's artifact digest is the digest this run is evidencing —
43
+ // the directory is NAMED after that digest, so a mismatch means the tree
44
+ // was hand-edited or the naming convention moved;
45
+ // * source identity (repo/path/commit/tree hash) equals the input's.
46
+ //
47
+ // EXIT CODES — the distinction is deliberate
48
+ //
49
+ // 0 a verdict was reached: `reusable=true|false` on stdout / --github-output.
50
+ // An unusable or absent bundle is a NORMAL outcome; it means "produce".
51
+ // 1 no verdict could be reached: bad arguments, an unreadable repository
52
+ // root, kernel schemas that will not load. A tool that cannot decide must
53
+ // never answer "reuse".
54
+ // ═══════════════════════════════════════════════════════════════════════════
55
+
56
+ import { appendFileSync, readFileSync, statSync } from 'node:fs';
57
+ import { isAbsolute, join, resolve } from 'node:path';
58
+
59
+ import {
60
+ WorkspaceRoot,
61
+ configureEditionWorkspaceFromArgv,
62
+ dataRoot,
63
+ isEntryModule,
64
+ } from './edition-workspace.mjs';
65
+ import {
66
+ evidenceDirectoryForDigest,
67
+ sha256Digest,
68
+ } from './image-evidence.mjs';
69
+ import { loadKernelDistributionSchemas } from './lib.mjs';
70
+ import {
71
+ assertProducerInput,
72
+ buildImageSourceStatements,
73
+ readPinnedContractAuthority,
74
+ validateProducedBundle,
75
+ } from './produce-image-source-evidence.mjs';
76
+ import { producerStatementMatchesInput } from './producer-evidence-bundle.mjs';
77
+
78
+ /**
79
+ * Remaining life below which a technically-valid bundle is re-produced anyway.
80
+ *
81
+ * A bundle that expires in ten minutes is not reusable in any meaningful sense:
82
+ * the release, delivery or appliance install that will read it happens after
83
+ * this workflow, not during it. Refusing to hand one forward is what makes the
84
+ * refresh path actually reachable instead of theoretically reachable.
85
+ *
86
+ * Six hours is the smallest window that survives a full re-run of the slowest
87
+ * producing lane (emulated arm64 build + merge + attest) plus the release step
88
+ * that consumes the result, with room to spare.
89
+ */
90
+ export const MIN_REMAINING_TTL_SECONDS = 6 * 60 * 60;
91
+
92
+ const MAX_EVIDENCE_FILE_BYTES = 4 * 1024 * 1024;
93
+
94
+ function readBoundedFile(path) {
95
+ const stat = statSync(path);
96
+ if (!stat.isFile()) {
97
+ throw new Error(`${path} is not a regular file`);
98
+ }
99
+ if (stat.size > MAX_EVIDENCE_FILE_BYTES) {
100
+ throw new Error(
101
+ `${path} is ${stat.size} bytes, over the ${MAX_EVIDENCE_FILE_BYTES}-byte evidence file bound`,
102
+ );
103
+ }
104
+ return readFileSync(path);
105
+ }
106
+
107
+ /** A descriptor path is repository-relative, without traversal or absolutes. */
108
+ function assertContainedRelativePath(value, label) {
109
+ if (
110
+ typeof value !== 'string' ||
111
+ value.length === 0 ||
112
+ value.startsWith('/') ||
113
+ value
114
+ .split('/')
115
+ .some((segment) => segment === '' || segment === '.' || segment === '..')
116
+ ) {
117
+ throw new Error(`${label} is not a contained repository-relative path`);
118
+ }
119
+ return value;
120
+ }
121
+
122
+ /**
123
+ * The reuse verdict for one producer input.
124
+ *
125
+ * @returns {{ evidenceDirectory: string, reusable: boolean, reason: string }}
126
+ */
127
+ export async function assessCommittedEvidence({
128
+ input,
129
+ repositoryRoot,
130
+ schemas,
131
+ expectedContractAuthority,
132
+ now = Date.now(),
133
+ }) {
134
+ // The input is validated FIRST and unconditionally: a verdict about the
135
+ // committed tree is meaningless if the thing it is compared against is not
136
+ // itself a well-formed producer input.
137
+ assertProducerInput(input, expectedContractAuthority);
138
+ const subjects = input.subjects;
139
+ const digests = new Set(subjects.map((subject) => subject.manifestDigest));
140
+ if (digests.size !== 1) {
141
+ throw new Error(
142
+ 'a producer input must evidence exactly one manifest digest; the ' +
143
+ `committed evidence tree is keyed by it (got ${[...digests].join(', ')})`,
144
+ );
145
+ }
146
+ const evidenceDirectory = evidenceDirectoryForDigest([...digests][0]);
147
+ const absoluteDirectory = join(repositoryRoot, evidenceDirectory);
148
+
149
+ const unusable = (reason) => ({ evidenceDirectory, reusable: false, reason });
150
+
151
+ let stat;
152
+ try {
153
+ stat = statSync(absoluteDirectory);
154
+ } catch {
155
+ return unusable('no evidence has been committed for this digest yet');
156
+ }
157
+ if (!stat.isDirectory()) {
158
+ return unusable(`${evidenceDirectory} exists but is not a directory`);
159
+ }
160
+
161
+ let indexDocument;
162
+ try {
163
+ indexDocument = JSON.parse(
164
+ readBoundedFile(join(absoluteDirectory, 'producer-evidence.json')).toString(
165
+ 'utf8',
166
+ ),
167
+ );
168
+ } catch (error) {
169
+ return unusable(
170
+ `producer-evidence.json is missing or unreadable: ${error.message}`,
171
+ );
172
+ }
173
+ if (
174
+ indexDocument?.schemaVersion !== 1 ||
175
+ !Array.isArray(indexDocument.producerEvidence)
176
+ ) {
177
+ return unusable('producer-evidence.json is not a schemaVersion 1 index');
178
+ }
179
+
180
+ // The expected statements are REBUILT from the input, so the comparison is
181
+ // against exactly what a fresh production would sign — not against a
182
+ // hand-written expectation that could drift from the producer.
183
+ let expectedStatements;
184
+ try {
185
+ expectedStatements = buildImageSourceStatements(input, schemas, expectedContractAuthority);
186
+ } catch (error) {
187
+ throw new Error(
188
+ `the producer input does not build a valid statement set: ${error.message}`,
189
+ );
190
+ }
191
+ const expectedByKey = new Map(
192
+ expectedStatements.map((statement) => [
193
+ `${statement.predicate.source.kind}:${statement.predicate.source.id}`,
194
+ statement,
195
+ ]),
196
+ );
197
+
198
+ const seen = new Set();
199
+ for (const entry of indexDocument.producerEvidence) {
200
+ if (entry === null || typeof entry !== 'object') {
201
+ return unusable('producer-evidence.json carries a non-object entry');
202
+ }
203
+ const key = `${entry.subjectKind}:${entry.subjectId}`;
204
+ if (seen.has(key)) {
205
+ return unusable(`producer-evidence.json duplicates subject ${key}`);
206
+ }
207
+ seen.add(key);
208
+ const expected = expectedByKey.get(key);
209
+ if (expected === undefined) {
210
+ return unusable(
211
+ `committed evidence carries subject ${key}, which this input does not evidence`,
212
+ );
213
+ }
214
+ const descriptor = entry.evidence;
215
+ if (descriptor === null || typeof descriptor !== 'object') {
216
+ return unusable(`subject ${key} has no evidence descriptor`);
217
+ }
218
+ let statementBytes;
219
+ let bundleBytes;
220
+ try {
221
+ statementBytes = readBoundedFile(
222
+ join(
223
+ absoluteDirectory,
224
+ assertContainedRelativePath(
225
+ descriptor.statementPath,
226
+ `subject ${key} statementPath`,
227
+ ),
228
+ ),
229
+ );
230
+ bundleBytes = readBoundedFile(
231
+ join(
232
+ absoluteDirectory,
233
+ assertContainedRelativePath(
234
+ descriptor.bundlePath,
235
+ `subject ${key} bundlePath`,
236
+ ),
237
+ ),
238
+ );
239
+ } catch (error) {
240
+ return unusable(`subject ${key} artifacts are unreadable: ${error.message}`);
241
+ }
242
+ if (
243
+ sha256Digest(statementBytes) !== descriptor.statementDigest ||
244
+ sha256Digest(bundleBytes) !== descriptor.bundleDigest
245
+ ) {
246
+ return unusable(
247
+ `subject ${key} artifacts do not match the digests producer-evidence.json records`,
248
+ );
249
+ }
250
+ try {
251
+ validateProducedBundle(statementBytes, bundleBytes);
252
+ } catch (error) {
253
+ return unusable(`subject ${key} bundle is malformed: ${error.message}`);
254
+ }
255
+ let committed;
256
+ try {
257
+ committed = schemas.parseImageProducerStatement(
258
+ JSON.parse(statementBytes.toString('utf8')),
259
+ );
260
+ } catch (error) {
261
+ return unusable(
262
+ `subject ${key} statement does not parse under the kernel schema: ${error.message}`,
263
+ );
264
+ }
265
+ // producerRun is deliberately fresh on every execution (run id, attempt,
266
+ // nonce and validity window). Requiring byte equality with a freshly built
267
+ // statement made every retry look stale and caused the evidence commit
268
+ // storm. Compare the immutable authority/source/build/artifact binding;
269
+ // validate the existing run's own signature and lifetime independently.
270
+ if (!producerStatementMatchesInput(committed, expected)) {
271
+ return unusable(
272
+ `subject ${key} committed statement is not bound to this producer input ` +
273
+ '(authority, source, build, artifact or platform set changed)',
274
+ );
275
+ }
276
+ const expiresAt = Date.parse(committed.predicate.producerRun.expiresAt);
277
+ if (!Number.isFinite(expiresAt)) {
278
+ return unusable(`subject ${key} statement has an unreadable expiresAt`);
279
+ }
280
+ const remainingSeconds = Math.floor((expiresAt - now) / 1000);
281
+ if (remainingSeconds <= 0) {
282
+ return unusable(
283
+ `subject ${key} evidence EXPIRED at ${committed.predicate.producerRun.expiresAt} — ` +
284
+ 'it can never verify again and must be re-produced',
285
+ );
286
+ }
287
+ if (remainingSeconds < MIN_REMAINING_TTL_SECONDS) {
288
+ return unusable(
289
+ `subject ${key} evidence expires in ${remainingSeconds}s, under the ` +
290
+ `${MIN_REMAINING_TTL_SECONDS}s reuse floor — refreshing it now`,
291
+ );
292
+ }
293
+ }
294
+
295
+ const missing = [...expectedByKey.keys()].filter((key) => !seen.has(key));
296
+ if (missing.length > 0) {
297
+ return unusable(
298
+ `committed evidence omits subject(s) ${missing.join(', ')} that this input evidences`,
299
+ );
300
+ }
301
+
302
+ return {
303
+ evidenceDirectory,
304
+ reusable: true,
305
+ reason:
306
+ `verified ${seen.size} committed subject bundle(s): well-formed, bound to ` +
307
+ 'this exact producer input, and unexpired',
308
+ };
309
+ }
310
+
311
+ function requireValue(flags, name) {
312
+ const value = flags.get(name);
313
+ if (value === undefined) {
314
+ throw new Error(`--${name} is required`);
315
+ }
316
+ return value;
317
+ }
318
+
319
+ async function main() {
320
+ const args = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
321
+ WorkspaceRoot.Data,
322
+ WorkspaceRoot.Kernel,
323
+ ]);
324
+ const flags = new Map();
325
+ for (let index = 0; index < args.length; index += 2) {
326
+ const flag = args[index];
327
+ if (!flag.startsWith('--')) {
328
+ throw new Error(`unexpected argument "${flag}"`);
329
+ }
330
+ const name = flag.slice(2);
331
+ const value = args[index + 1];
332
+ if (value === undefined || value.startsWith('--')) {
333
+ throw new Error(`--${name} requires a value`);
334
+ }
335
+ if (flags.has(name)) {
336
+ throw new Error(`--${name} was given twice`);
337
+ }
338
+ flags.set(name, value);
339
+ }
340
+ for (const name of flags.keys()) {
341
+ if (!['input', 'repository-root', 'github-output', 'now'].includes(name)) {
342
+ throw new Error(`unknown option "--${name}"`);
343
+ }
344
+ }
345
+ const repositoryRoot = resolve(requireValue(flags, 'repository-root'));
346
+ if (!isAbsolute(repositoryRoot) || !statSync(repositoryRoot).isDirectory()) {
347
+ throw new Error('--repository-root must be an absolute directory');
348
+ }
349
+ const input = JSON.parse(readFileSync(resolve(requireValue(flags, 'input')), 'utf8'));
350
+ const now = flags.has('now') ? Number(flags.get('now')) : Date.now();
351
+ if (!Number.isSafeInteger(now) || now <= 0) {
352
+ throw new Error('--now must be a positive epoch-millisecond integer');
353
+ }
354
+ const schemas = await loadKernelDistributionSchemas();
355
+ const verdict = await assessCommittedEvidence({
356
+ input,
357
+ repositoryRoot,
358
+ schemas,
359
+ expectedContractAuthority: readPinnedContractAuthority({ dataRoot: dataRoot(), schemas }),
360
+ now,
361
+ });
362
+ const line = verdict.reusable
363
+ ? `[check-committed-image-evidence] REUSE ${verdict.evidenceDirectory}: ${verdict.reason}`
364
+ : `[check-committed-image-evidence] PRODUCE ${verdict.evidenceDirectory}: ${verdict.reason}`;
365
+ console.log(line);
366
+ const githubOutput = flags.get('github-output');
367
+ if (githubOutput !== undefined) {
368
+ appendFileSync(
369
+ githubOutput,
370
+ `reusable=${verdict.reusable}\n` +
371
+ `evidence-directory=${verdict.evidenceDirectory}\n` +
372
+ `reason=${verdict.reason.replaceAll('\n', ' ')}\n`,
373
+ 'utf8',
374
+ );
375
+ }
376
+ }
377
+
378
+ if (isEntryModule(import.meta.url)) {
379
+ main().catch((error) => {
380
+ console.error(`[check-committed-image-evidence] ${error.message}`);
381
+ process.exitCode = 1;
382
+ });
383
+ }
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
5
+ import { dirname, resolve } from 'node:path';
6
+
7
+ import { editionWorkspaceConfig } from './edition-config.mjs';
8
+ import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, runtimeImageSourceIndexPath, sourcesRoot } from './edition-workspace.mjs';
9
+ import { gitAuthEnv } from './git-source-auth.mjs';
10
+ import { applyPinSource, snapshotPinSource } from './pin-source.mjs';
11
+ import { readStrictJsonFile } from './strict-json.mjs';
12
+
13
+ const REPO_PATH = /^(repos|submodules)\/([a-z0-9][a-z0-9-]*)$/;
14
+ const SHA = /^[a-f0-9]{40}$/;
15
+
16
+ export function sourceCheckoutPlan(index, { sourceRepositoryUrlPrefix }) {
17
+ if (!Array.isArray(index?.repositoryPins) || index.repositoryPins.length === 0) {
18
+ throw new Error('runtime image source index declares no repositoryPins');
19
+ }
20
+ const seen = new Set();
21
+ return index.repositoryPins.map((pin) => {
22
+ const match = REPO_PATH.exec(String(pin?.repo ?? ''));
23
+ if (!match) {throw new Error(`invalid repository checkout path ${JSON.stringify(pin?.repo)}`);}
24
+ if (seen.has(pin.repo)) {throw new Error(`duplicate repository pin ${pin.repo}`);}
25
+ seen.add(pin.repo);
26
+ const expectedUrl = `${sourceRepositoryUrlPrefix}${match[2]}.git`;
27
+ const suffixlessExpectedUrl = expectedUrl.slice(0, -4);
28
+ if (pin.repositoryUrl !== expectedUrl && pin.repositoryUrl !== suffixlessExpectedUrl) {
29
+ throw new Error(
30
+ `${pin.repo} repositoryUrl must be ${expectedUrl} (the equivalent suffixless GitHub URL is also accepted); got ${pin.repositoryUrl}`,
31
+ );
32
+ }
33
+ if (!SHA.test(String(pin.sourceCommit ?? ''))) {
34
+ throw new Error(`${pin.repo} sourceCommit is not exact lowercase 40-hex`);
35
+ }
36
+ return Object.freeze({
37
+ repo: pin.repo,
38
+ repositoryUrl: pin.repositoryUrl,
39
+ sourceCommit: pin.sourceCommit,
40
+ });
41
+ });
42
+ }
43
+
44
+ function git(args, env) {
45
+ execFileSync('git', args, { env, stdio: 'inherit' });
46
+ }
47
+
48
+ export function checkoutIndexedSources({ workspaceRoot, token, index }) {
49
+ if (!workspaceRoot) {throw new Error('workspaceRoot is required');}
50
+ if (!token) {throw new Error('EVIDENCE_REPOSITORIES_TOKEN is required');}
51
+ const workspace = editionWorkspaceConfig();
52
+ const env = gitAuthEnv(workspace.sourceRepositoryUrlPrefix, workspace.sourceAuthUsername, token);
53
+ const plan = sourceCheckoutPlan(index, workspace);
54
+ for (const entry of plan) {
55
+ const target = resolve(workspaceRoot, entry.repo);
56
+ if (existsSync(target)) {
57
+ throw new Error(`${entry.repo} target already exists; refusing to reuse an unverified checkout`);
58
+ }
59
+ mkdirSync(dirname(target), { recursive: true });
60
+ git(['init', '--quiet', target], env);
61
+ git(['-C', target, 'remote', 'add', 'origin', entry.repositoryUrl], env);
62
+ git(['-C', target, 'fetch', '--quiet', '--depth=1', 'origin', entry.sourceCommit], env);
63
+ git(['-C', target, 'checkout', '--quiet', '--detach', entry.sourceCommit], env);
64
+ const actual = execFileSync('git', ['-C', target, 'rev-parse', 'HEAD'], {
65
+ env,
66
+ encoding: 'utf8',
67
+ }).trim();
68
+ if (actual !== entry.sourceCommit) {
69
+ throw new Error(`${entry.repo} resolved ${actual}, expected ${entry.sourceCommit}`);
70
+ }
71
+ console.log(`[source-checkout] ${entry.repo} @ ${entry.sourceCommit}`);
72
+ }
73
+ return plan.length;
74
+ }
75
+
76
+ const SNAPSHOT_PINS_FLAG = '--snapshot-pins';
77
+
78
+ /**
79
+ * `--snapshot-pins <file>` is the only argument: with it, the plan is the
80
+ * committed index applied to that snapshot (the develop edition's frozen pins);
81
+ * without it, the committed index untouched.
82
+ */
83
+ export function indexForCheckout(committedIndex, argv) {
84
+ if (argv.length === 0) {return committedIndex;}
85
+ if (argv[0] !== SNAPSHOT_PINS_FLAG || argv.length !== 2 || argv[1].startsWith('--')) {
86
+ throw new Error(`usage: checkout-indexed-source-repositories.mjs [${SNAPSHOT_PINS_FLAG} <pins.json>]`);
87
+ }
88
+ return applyPinSource(committedIndex, snapshotPinSource(readStrictJsonFile(argv[1])));
89
+ }
90
+
91
+ if (isEntryModule(import.meta.url)) {
92
+ try {
93
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Sources]);
94
+ const index = indexForCheckout(JSON.parse(readFileSync(runtimeImageSourceIndexPath(), 'utf8')), process.argv.slice(2));
95
+ const count = checkoutIndexedSources({
96
+ workspaceRoot: sourcesRoot(),
97
+ token: process.env.EVIDENCE_REPOSITORIES_TOKEN,
98
+ index,
99
+ });
100
+ console.log(`[source-checkout] materialized ${count} indexed repositories at exact commits`);
101
+ } catch (error) {
102
+ console.error(`[source-checkout] ${error.message}`);
103
+ process.exit(1);
104
+ }
105
+ }
@@ -0,0 +1,75 @@
1
+ // Blank comments before asking a source file "what does this file DO?".
2
+ //
3
+ // A structural assertion that greps RAW source text cannot tell a CALL from a
4
+ // MENTION, so it can be satisfied by — or tripped by — the scanned file's own
5
+ // prose. That is not a hypothetical here: the call sites this repository
6
+ // asserts on carry twenty-line docblocks explaining the very invariant being
7
+ // asserted, so a raw `includes()` passes on a file whose call has been deleted
8
+ // and whose comment survives. It is the mutation an ordinary mutation test
9
+ // misses, because the usual mutation removes the code and its comment
10
+ // together.
11
+ //
12
+ // SHARED BECAUSE THERE ARE NOW TWO CONSUMERS, and a second private copy is
13
+ // what the fleet's own rule forbids: `seal-image-digests.test.mjs` (the
14
+ // fleet-finality call) and `editions-root-platform-verification.test.mjs` (the
15
+ // two `verifyDistributionPlatforms` call sites). Both previously would have
16
+ // needed the same fourteen lines.
17
+ //
18
+ // DELIBERATELY NARROW: `/* */` blocks, and `//` only where it STARTS a line. A
19
+ // trailing `//` is left alone because `'file://'` occurs in executable code in
20
+ // these very files, and blanking it would delete a real line. A line that
21
+ // BEGINS with `//` is unambiguously a comment.
22
+ //
23
+ // LINE COUNT IS PRESERVED — every blanked character becomes a space rather
24
+ // than being dropped — so a caller may still report a line number derived from
25
+ // a match without it silently shifting.
26
+ //
27
+ // It is NOT a parser and must not grow into one: a `//` inside a string
28
+ // literal at the start of a line would be blanked wrongly. No such line exists
29
+ // in either consumer, and each consumer's two-sided control is what proves the
30
+ // blanker discriminated on ITS file rather than trusting this note.
31
+ export function blankComments(source) {
32
+ return source
33
+ .replace(/\/\*[\s\S]*?\*\//gu, (match) => match.replace(/[^\n]/gu, ' '))
34
+ .replace(/^[ \t]*\/\/.*$/gmu, (match) => match.replace(/[^\n]/gu, ' '));
35
+ }
36
+
37
+ // ── AND THE TRAILING `//`, FOR THE ONE CALLER THAT CANNOT LEAVE IT ──────────
38
+ //
39
+ // `blankComments` above deliberately leaves a trailing `//` alone, and the
40
+ // reason it gives is exact: `'file://'` occurs in executable code in its two
41
+ // consumers, and blanking from there would delete a real line.
42
+ //
43
+ // `platform-lock-readability.mjs` asks a question those two do not — "does
44
+ // this repository CALL the lock parser?" — over source it did not write, in
45
+ // seven other repositories. There, prose naming the parser is the NORM rather
46
+ // than the exception, and it is not hypothetical: measured 2026-09-27 at the
47
+ // committed pins, `submodules/xema-host-web`'s `src/lib/nav-registry.ts:1135`
48
+ // and `repos/xema-cultivars`'s `decision-api.client.ts:53` each name
49
+ // `distribution.lock.json` in a comment and neither parses one.
50
+ //
51
+ // So this variant blanks a `//` to end of line UNLESS it is immediately
52
+ // preceded by `:`. That single guard is what keeps `'file://'`,
53
+ // `'https://…'` and every other scheme intact, which is the whole objection
54
+ // the narrow version was written around.
55
+ //
56
+ // THE FAILURE DIRECTION IS THE POINT, and it decides why the guard is this
57
+ // shape rather than a smarter one. Under-blanking makes a MENTION read as a
58
+ // call — the caller then grades a repository it need not, which is merely
59
+ // over-strict. OVER-blanking makes a CALL disappear — the caller then skips a
60
+ // real reader, which is the 2026-09-17 incident itself. A real call cannot
61
+ // follow a `//` on a line that is not a comment except inside a string,
62
+ // template or regex, and none of those is a call; so the only way this hides
63
+ // one is a call interpolated after a literal `//` inside a template on the
64
+ // same line. Its caller reports RAW-vs-EXECUTABLE file counts for exactly that
65
+ // reason: an over-blank shows up as a difference a human can see, rather than
66
+ // as a silently smaller corpus.
67
+ //
68
+ // It is NOT a parser, for the same reason its sibling is not, and the guard
69
+ // above is the whole of its cleverness.
70
+ export function blankAllComments(source) {
71
+ return blankComments(source).replace(
72
+ /(^|[^:])\/\/.*$/gmu,
73
+ (match, before) => before + ' '.repeat(match.length - before.length),
74
+ );
75
+ }
@@ -0,0 +1,60 @@
1
+ import { editionWorkspaceConfig } from './edition-config.mjs';
2
+
3
+ // The connector repository, its production connector biomes and the ids that are
4
+ // deliberately not production are the data root's (`connectorSources` in
5
+ // edition-workspace.json); this module is the rule over them.
6
+
7
+ // A data root with no connector repository declares no `connectorSources`:
8
+ // there is then nothing to exclude and nothing to vendor, which is a stated
9
+ // absence of the thing, not a skipped check. `undefined` below means exactly that.
10
+ const connectorSources = () => editionWorkspaceConfig().connectorSources;
11
+
12
+ /** The production connector biomes: id, repo, repo-relative path and aggregate path. */
13
+ export const productionConnectorSourceBindings = () => {
14
+ const sources = connectorSources();
15
+ if (sources === undefined) {return [];}
16
+ const { repository, bindings } = sources;
17
+ return bindings.map(({ id, path }) =>
18
+ Object.freeze({ id, repo: repository, repoPath: path, path: `${repository}/${path}` }),
19
+ );
20
+ };
21
+
22
+ /** The cross-repo build inputs that vendor each production connector into the consuming image. */
23
+ export const connectorCrossRepoBuildInputs = () => {
24
+ const sources = connectorSources();
25
+ if (sources === undefined) {return [];}
26
+ const { buildInput } = sources;
27
+ return productionConnectorSourceBindings().map(({ id, repo, repoPath }) => ({
28
+ consumerRepo: buildInput.consumerRepo,
29
+ consumerService: buildInput.consumerService,
30
+ sourceRepo: repo,
31
+ sourcePath: repoPath,
32
+ destinationPath: `${buildInput.destinationRoot}/${id}`,
33
+ }));
34
+ };
35
+
36
+ /**
37
+ * Decide whether a discovered manifest is an authoritative production source.
38
+ *
39
+ * The old community connector trees are intentionally ignored, not used as a
40
+ * fallback. The connector repository's `examples/` tree is also never a
41
+ * production source, even when an example carries a valid biome manifest.
42
+ */
43
+ export function isAuthoritativeProductionBiomeSource({ id, repo, path }) {
44
+ const sources = connectorSources();
45
+ if (sources === undefined) {return true;}
46
+ const { nonProductionIds, repository } = sources;
47
+ if (nonProductionIds.includes(id)) {return false;}
48
+
49
+ const binding = productionConnectorSourceBindings().find((candidate) => candidate.id === id);
50
+ if (binding) {return repo === binding.repo && path === binding.path;}
51
+
52
+ if (repo !== repository) {return true;}
53
+ if (path.startsWith(`${repository}/examples/`)) {return false;}
54
+ if (path.startsWith(`${repository}/connectors/`)) {
55
+ throw new Error(
56
+ `${path}/xema-biome.json declares unrecognized production connector id "${String(id)}"`,
57
+ );
58
+ }
59
+ return false;
60
+ }