@xemahq/xema 0.25.1 → 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 (239) hide show
  1. package/README.md +70 -37
  2. package/dist/assets/appliance-base/images.lock.json +126 -126
  3. package/dist/assets/appliance-lean/images.lock.json +123 -123
  4. package/dist/assets/cloud/images.lock.json +126 -126
  5. package/dist/assets/core/images.lock.json +120 -120
  6. package/dist/assets/distribution-assets.manifest.json +15 -16
  7. package/dist/assets/minimal/images.lock.json +108 -108
  8. package/dist/assets/oss/images.lock.json +126 -126
  9. package/dist/assets/starter/images.lock.json +123 -123
  10. package/dist/cli.js +2 -2
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/dev.js +1 -0
  13. package/dist/commands/dev.js.map +1 -1
  14. package/dist/commands/down.d.ts.map +1 -1
  15. package/dist/commands/down.js +7 -1
  16. package/dist/commands/down.js.map +1 -1
  17. package/dist/commands/edition.d.ts +3 -0
  18. package/dist/commands/edition.d.ts.map +1 -0
  19. package/dist/commands/edition.js +46 -0
  20. package/dist/commands/edition.js.map +1 -0
  21. package/dist/commands/init.js +2 -2
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/serve.js +1 -0
  24. package/dist/commands/serve.js.map +1 -1
  25. package/dist/commands/up.js +1 -0
  26. package/dist/commands/up.js.map +1 -1
  27. package/dist/dev/env-defaults.d.ts.map +1 -1
  28. package/dist/dev/env-defaults.js +6 -1
  29. package/dist/dev/env-defaults.js.map +1 -1
  30. package/dist/dev/infra-preflight.d.ts +3 -2
  31. package/dist/dev/infra-preflight.d.ts.map +1 -1
  32. package/dist/dev/infra-preflight.js +18 -8
  33. package/dist/dev/infra-preflight.js.map +1 -1
  34. package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
  35. package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
  36. package/dist/runtime/backing-registration.d.ts.map +1 -1
  37. package/dist/runtime/backing-registration.js +7 -2
  38. package/dist/runtime/backing-registration.js.map +1 -1
  39. package/dist/runtime/config/schema.d.ts +1 -0
  40. package/dist/runtime/config/schema.d.ts.map +1 -1
  41. package/dist/runtime/custody/local-custody.d.ts +12 -0
  42. package/dist/runtime/custody/local-custody.d.ts.map +1 -0
  43. package/dist/runtime/custody/local-custody.js +22 -0
  44. package/dist/runtime/custody/local-custody.js.map +1 -0
  45. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
  46. package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
  47. package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
  48. package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
  49. package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
  50. package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
  51. package/dist/runtime/custody/openbao-custody-store.js +55 -0
  52. package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
  53. package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
  54. package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
  55. package/dist/runtime/custody/openbao-http-client.js +154 -0
  56. package/dist/runtime/custody/openbao-http-client.js.map +1 -0
  57. package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
  58. package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
  59. package/dist/runtime/custody/openbao-local-constants.js +48 -0
  60. package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
  61. package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
  62. package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
  63. package/dist/runtime/custody/openbao-local-runner.js +86 -0
  64. package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
  65. package/dist/runtime/edition/run-entry.d.ts +9 -0
  66. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  67. package/dist/runtime/edition/run-entry.js +36 -0
  68. package/dist/runtime/edition/run-entry.js.map +1 -0
  69. package/dist/runtime/edition/verb-map.d.ts +60 -0
  70. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  71. package/dist/runtime/edition/verb-map.js +146 -0
  72. package/dist/runtime/edition/verb-map.js.map +1 -0
  73. package/dist/runtime/env-seeder.d.ts +3 -0
  74. package/dist/runtime/env-seeder.d.ts.map +1 -1
  75. package/dist/runtime/env-seeder.js +20 -3
  76. package/dist/runtime/env-seeder.js.map +1 -1
  77. package/dist/runtime/launch.d.ts +3 -1
  78. package/dist/runtime/launch.d.ts.map +1 -1
  79. package/dist/runtime/launch.js +37 -8
  80. package/dist/runtime/launch.js.map +1 -1
  81. package/dist/runtime/paths.d.ts +1 -2
  82. package/dist/runtime/paths.d.ts.map +1 -1
  83. package/dist/runtime/paths.js +0 -10
  84. package/dist/runtime/paths.js.map +1 -1
  85. package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
  86. package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
  87. package/dist/runtime/runner/chart-local-provenance.js +114 -0
  88. package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
  89. package/dist/runtime/runner/chart-preflight.d.ts +8 -8
  90. package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
  91. package/dist/runtime/runner/chart-preflight.js +30 -69
  92. package/dist/runtime/runner/chart-preflight.js.map +1 -1
  93. package/dist/runtime/runner/chart-pull.d.ts +14 -0
  94. package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
  95. package/dist/runtime/runner/chart-pull.js +96 -0
  96. package/dist/runtime/runner/chart-pull.js.map +1 -0
  97. package/dist/runtime/runner/chart-resolver.d.ts +4 -12
  98. package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
  99. package/dist/runtime/runner/chart-resolver.js +16 -62
  100. package/dist/runtime/runner/chart-resolver.js.map +1 -1
  101. package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
  102. package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
  103. package/dist/runtime/runner/chart-set-lock.js +55 -99
  104. package/dist/runtime/runner/chart-set-lock.js.map +1 -1
  105. package/dist/runtime/runner/helm-cli.d.ts +3 -1
  106. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  107. package/dist/runtime/runner/helm-cli.js +9 -7
  108. package/dist/runtime/runner/helm-cli.js.map +1 -1
  109. package/dist/runtime/runner/helm-runner.d.ts +4 -4
  110. package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
  111. package/dist/runtime/runner/helm-runner.js +18 -16
  112. package/dist/runtime/runner/helm-runner.js.map +1 -1
  113. package/dist/runtime/sources/kinds/git.d.ts +1 -2
  114. package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
  115. package/dist/runtime/sources/kinds/git.js +8 -5
  116. package/dist/runtime/sources/kinds/git.js.map +1 -1
  117. package/dist/runtime/sources/manifest-walk.d.ts +3 -1
  118. package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
  119. package/dist/runtime/sources/manifest-walk.js +8 -3
  120. package/dist/runtime/sources/manifest-walk.js.map +1 -1
  121. package/dist/runtime/sources/schema.d.ts +3 -0
  122. package/dist/runtime/sources/schema.d.ts.map +1 -1
  123. package/dist/runtime/sources/schema.js +6 -1
  124. package/dist/runtime/sources/schema.js.map +1 -1
  125. package/dist/runtime/supervisor.d.ts +3 -0
  126. package/dist/runtime/supervisor.d.ts.map +1 -1
  127. package/dist/runtime/supervisor.js +2 -0
  128. package/dist/runtime/supervisor.js.map +1 -1
  129. package/edition/README.md +315 -0
  130. package/edition/assemble-producer-input.mjs +1251 -0
  131. package/edition/authorization-base.mjs +347 -0
  132. package/edition/biome-content.mjs +268 -0
  133. package/edition/build-biome-index.mjs +175 -0
  134. package/edition/build-platform-service-index.mjs +288 -0
  135. package/edition/build-runtime-image-source-index.mjs +821 -0
  136. package/edition/built-verifier.mjs +55 -0
  137. package/edition/catalogue-edition.mjs +49 -0
  138. package/edition/catalogue-fetch.mjs +64 -0
  139. package/edition/catalogue.mjs +135 -0
  140. package/edition/chart-set-source-commit.mjs +92 -0
  141. package/edition/check-committed-image-evidence.mjs +383 -0
  142. package/edition/checkout-indexed-source-repositories.mjs +105 -0
  143. package/edition/comment-blanker.mjs +75 -0
  144. package/edition/connector-source-authority.mjs +60 -0
  145. package/edition/customer-definition.mjs +183 -0
  146. package/edition/customer-flags.mjs +7 -0
  147. package/edition/develop-customer-edition.mjs +99 -0
  148. package/edition/develop-edition-resolve.mjs +65 -0
  149. package/edition/develop-edition.mjs +274 -0
  150. package/edition/develop-freeze.mjs +173 -0
  151. package/edition/develop-image-coordinate.mjs +54 -0
  152. package/edition/develop-seal.mjs +77 -0
  153. package/edition/develop-snapshot-inputs.mjs +104 -0
  154. package/edition/dist-check.mjs +986 -0
  155. package/edition/distribution-evidence-set.mjs +987 -0
  156. package/edition/distribution-paths.mjs +191 -0
  157. package/edition/edition-artifact.mjs +270 -0
  158. package/edition/edition-biome-manifests.mjs +85 -0
  159. package/edition/edition-channel.mjs +6 -0
  160. package/edition/edition-chart-set.mjs +93 -0
  161. package/edition/edition-config.mjs +207 -0
  162. package/edition/edition-constants.mjs +57 -0
  163. package/edition/edition-contract.json +23 -0
  164. package/edition/edition-list.mjs +35 -0
  165. package/edition/edition-locks.mjs +69 -0
  166. package/edition/edition-protocol.mjs +228 -0
  167. package/edition/edition-workspace.mjs +176 -0
  168. package/edition/evidence-checkout.mjs +418 -0
  169. package/edition/evidence-grammars.mjs +16 -0
  170. package/edition/evidence-trust-pin.mjs +304 -0
  171. package/edition/evidence-trust-policy.mjs +256 -0
  172. package/edition/external-source-walk.mjs +463 -0
  173. package/edition/first-party-source-ids.mjs +59 -0
  174. package/edition/fleet-coordinate-finality.mjs +350 -0
  175. package/edition/git-source-auth.mjs +52 -0
  176. package/edition/image-evidence-replay-state.mjs +242 -0
  177. package/edition/image-evidence.mjs +1225 -0
  178. package/edition/kernel-contract-authority.mjs +443 -0
  179. package/edition/lib.mjs +3018 -0
  180. package/edition/no-follow-file.mjs +94 -0
  181. package/edition/oci-evidence-verbs.mjs +392 -0
  182. package/edition/oci-image-source-evidence.mjs +239 -0
  183. package/edition/oras.mjs +648 -0
  184. package/edition/pin-source.mjs +227 -0
  185. package/edition/plan-image-evidence-refresh.mjs +408 -0
  186. package/edition/platform-lock-readability.mjs +892 -0
  187. package/edition/platform-service-class.mjs +18 -0
  188. package/edition/platform-service-source-authority.mjs +105 -0
  189. package/edition/positive-closure.mjs +162 -0
  190. package/edition/produce-distribution-completeness-evidence.mjs +311 -0
  191. package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
  192. package/edition/produce-image-source-evidence.mjs +765 -0
  193. package/edition/producer-evidence-bundle.mjs +157 -0
  194. package/edition/registry-credentials.mjs +193 -0
  195. package/edition/release-catalogue.mjs +39 -0
  196. package/edition/release-channel-guard.mjs +99 -0
  197. package/edition/release-edition-artifact.mjs +158 -0
  198. package/edition/remedy-command.mjs +89 -0
  199. package/edition/repin-evidence-trust.mjs +101 -0
  200. package/edition/required-resource-provider.mjs +58 -0
  201. package/edition/resolve-chart-set.mjs +415 -0
  202. package/edition/resolve-customer-edition.mjs +288 -0
  203. package/edition/resolve-distribution.mjs +2318 -0
  204. package/edition/resolve-edition.mjs +186 -0
  205. package/edition/resolve-images.mjs +1606 -0
  206. package/edition/seal-image-digests.mjs +855 -0
  207. package/edition/sealed-evidence-accept.mjs +82 -0
  208. package/edition/source-authority.mjs +725 -0
  209. package/edition/source-closure.mjs +116 -0
  210. package/edition/strict-json.mjs +121 -0
  211. package/edition/string-order.mjs +5 -0
  212. package/edition/trust-policy-arming.mjs +120 -0
  213. package/edition/trusted-executable.mjs +166 -0
  214. package/edition/verifier-binding.mjs +45 -0
  215. package/edition/verifier-files.mjs +8 -0
  216. package/edition/verifier-kernel-schemas.mjs +22 -0
  217. package/edition/verifier-provenance.mjs +64 -0
  218. package/edition/verify-image-evidence.mjs +430 -0
  219. package/edition/verify-image-platforms.mjs +495 -0
  220. package/edition/why.mjs +56 -0
  221. package/edition/workspace-sections.mjs +195 -0
  222. package/examples/06-biome-sources.yaml +1 -0
  223. package/package.json +18 -7
  224. package/dist/commands/dist.d.ts +0 -16
  225. package/dist/commands/dist.d.ts.map +0 -1
  226. package/dist/commands/dist.js +0 -182
  227. package/dist/commands/dist.js.map +0 -1
  228. package/dist/runtime/distribution/available-biome.d.ts +0 -10
  229. package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
  230. package/dist/runtime/distribution/available-biome.js +0 -154
  231. package/dist/runtime/distribution/available-biome.js.map +0 -1
  232. package/dist/runtime/distribution/compose.d.ts +0 -23
  233. package/dist/runtime/distribution/compose.d.ts.map +0 -1
  234. package/dist/runtime/distribution/compose.js +0 -48
  235. package/dist/runtime/distribution/compose.js.map +0 -1
  236. package/dist/runtime/runner/chart-trust.d.ts +0 -57
  237. package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
  238. package/dist/runtime/runner/chart-trust.js +0 -270
  239. package/dist/runtime/runner/chart-trust.js.map +0 -1
@@ -0,0 +1,725 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import {
3
+ existsSync,
4
+ mkdtempSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ rmSync,
8
+ } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join, relative, resolve, sep } from 'node:path';
11
+
12
+ import { dataRoot, sourcesRoot } from './edition-workspace.mjs';
13
+
14
+ export const PUBLISHED_SOURCE_REF = 'refs/remotes/origin/main';
15
+
16
+ export const DistributionSourceMode = Object.freeze({
17
+ Published: 'published',
18
+ Workspace: 'workspace',
19
+ });
20
+
21
+ function git(directory, args, options = {}) {
22
+ return execFileSync('git', ['-C', directory, ...args], {
23
+ encoding: 'utf8',
24
+ stdio: ['ignore', 'pipe', 'pipe'],
25
+ ...options,
26
+ }).trim();
27
+ }
28
+
29
+ function exactCommit(directory, ref, label) {
30
+ try {
31
+ return git(directory, ['rev-parse', '--verify', `${ref}^{commit}`]);
32
+ } catch {
33
+ throw new Error(`${label} has no exact Git commit at ${ref}`);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * How far back to look for a built commit before refusing. Generous on purpose:
39
+ * the fleet commits 150-353 times a day and builds 4-12 times, so the newest
40
+ * built commit is routinely dozens of commits behind the tip.
41
+ */
42
+ const BUILT_COMMIT_SEARCH_LIMIT = 500;
43
+
44
+ /**
45
+ * Every commit this repository has PUBLISHED AN IMAGE FROM, read out of the
46
+ * committed evidence tree.
47
+ *
48
+ * `predicate.source.sourceCommit` in each signed in-toto statement records the
49
+ * exact commit its image was built from. That is a fact in git, at the ref being
50
+ * resolved — no network, no registry credential, no clock. Keeping this resolver
51
+ * pure-git is deliberate: it decides what every distribution lock names, so a
52
+ * network dependency here would make the whole release plane fail differently
53
+ * depending on whether ghcr answered.
54
+ *
55
+ * A repository with NO evidence tree publishes no images (it contributes
56
+ * vendored content only, like xema-connectors). For those the tip is the right
57
+ * answer and always was.
58
+ */
59
+ /**
60
+ * The committed projection of "which commits of this repository have a
61
+ * published image", written by `project-build-ledger.mjs` from the registry's
62
+ * own tags.
63
+ *
64
+ * WHY A LEDGER AND NOT THE REGISTRY DIRECTLY. This resolver decides what every
65
+ * distribution lock names, and the README states the property that depends on
66
+ * it: locks contain only immutable inputs, so two runs over the same sources
67
+ * produce byte-identical CONTENT and an identical `inputHash`.
68
+ *
69
+ * That sentence used to read "byte-identical files", and measured over two full
70
+ * producer runs on 2026-09-06 it is false by exactly one field: every
71
+ * `distribution.lock.json` carries a wall-clock `resolvedAt`, so all eight
72
+ * differ on every run while their content and `inputHash` are stable, and
73
+ * `images.lock.json` and the three indexes are byte-identical. The property
74
+ * this paragraph is defending is unaffected — `resolvedAt` is provenance and
75
+ * feeds no hash — but a determinism claim that a single `diff` disproves is
76
+ * one nobody trusts the second time, so it is stated precisely instead.
77
+ *
78
+ * A live registry read would make lock
79
+ * generation depend on registry state at that instant and on the token's
80
+ * package visibility — the same sources would resolve differently on Tuesday
81
+ * and Thursday, and differently again for a different credential. A committed
82
+ * ledger is addressed by a ref, so it is part of "the same sources". It also
83
+ * keeps this path pure-git, which is what lets it run with no network at all.
84
+ *
85
+ * WHY NOT THE EVIDENCE TREE ALONE. It was the only source until 2026-09-02, and
86
+ * four of the five producing repositories had stopped writing it on 2026-08-25
87
+ * when they moved to publishing OCI referrers — a change whose commit message
88
+ * says nothing about evidence. So the reader and the producers spoke different
89
+ * media, silently: measured that day, `xema-cultivars` resolved to an 2026-08-25
90
+ * commit 153 behind its tip, and the tip WAS built and tagged in the registry.
91
+ * A pin frozen eight days in the past is what kept a fix for a production
92
+ * crashloop out of every image built from it.
93
+ */
94
+ export const BUILD_LEDGER_FILE = 'build-ledger.json';
95
+
96
+ /**
97
+ * Read the committed ledger. A caller that resolves many repositories should
98
+ * read it ONCE and pass it in — the parameter exists for that, not for tests.
99
+ *
100
+ * A malformed ledger THROWS rather than degrading to "no builds recorded":
101
+ * degrading would silently re-pin every repository to its bare tip, which is
102
+ * precisely the defect the ledger exists to prevent. An ABSENT ledger is a
103
+ * different fact and is allowed — it means this checkout has not projected one
104
+ * yet, and the evidence tree still answers.
105
+ */
106
+ export function loadBuildLedger(
107
+ distributionsDir = dataRoot(),
108
+ ) {
109
+ const path = join(distributionsDir, BUILD_LEDGER_FILE);
110
+ let raw;
111
+ try {
112
+ raw = JSON.parse(readFileSync(path, 'utf8'));
113
+ } catch (error) {
114
+ if (error?.code === 'ENOENT') {return null;}
115
+ throw error;
116
+ }
117
+ if (
118
+ raw === null ||
119
+ typeof raw !== 'object' ||
120
+ raw.schemaVersion !== 1 ||
121
+ typeof raw.repositories !== 'object' ||
122
+ raw.repositories === null ||
123
+ Array.isArray(raw.repositories)
124
+ ) {
125
+ throw new Error(`${BUILD_LEDGER_FILE} is not strict schema v1`);
126
+ }
127
+ return raw.repositories;
128
+ }
129
+
130
+ /**
131
+ * The committed record of which commit each source repository is pinned at.
132
+ *
133
+ * WHY THIS IS THE AUTHORITY AND NOT A CACHE — the argument is
134
+ * {@link createPinnedSourceAuthority}'s and is not repeated here. What this
135
+ * function adds is that the record is reachable WITHOUT `lib.mjs`, so a builder
136
+ * can read it before anything that needs the kernel dist or the aggregator
137
+ * layout, and so this module keeps its one dependency rule: `node:` builtins
138
+ * only, resolvable from a bare checkout with no install.
139
+ *
140
+ * `runtime-image-source-index.json` OWNS these pins — it is the one artifact
141
+ * whose job is to decide them. Every other derived artifact READS them. That is
142
+ * the whole of "one authority": the decider resolves the world, and the
143
+ * consumers resolve the decider.
144
+ *
145
+ * A MISSING or MALFORMED index THROWS rather than degrading to "no pins
146
+ * recorded". Degrading would silently re-resolve every repository from its tip,
147
+ * which is precisely the defect the pins exist to prevent — and it would do it
148
+ * in the one situation nobody is watching, a checkout where the file failed to
149
+ * read. `loadBuildLedger` takes the same posture for the same reason, and
150
+ * differs on the ABSENT case only because an absent ledger is a real state
151
+ * (this checkout has not projected one) while an absent pin index is not: no
152
+ * edition can resolve without it.
153
+ */
154
+ export const RUNTIME_IMAGE_SOURCE_INDEX_FILE = 'runtime-image-source-index.json';
155
+
156
+ export function committedRepositoryPins(
157
+ distributionsDir = dataRoot(),
158
+ ) {
159
+ const path = join(distributionsDir, RUNTIME_IMAGE_SOURCE_INDEX_FILE);
160
+ let raw;
161
+ try {
162
+ raw = JSON.parse(readFileSync(path, 'utf8'));
163
+ } catch (error) {
164
+ throw new Error(
165
+ `Cannot read the committed source pins from ${path}: ${error.message}. ` +
166
+ `Every derived artifact in this plane resolves each repository's commit ` +
167
+ `through that record; there is no fallback to a live resolution, because ` +
168
+ `falling back is how one repository's refresh silently re-pins every other.`,
169
+ );
170
+ }
171
+ const pins = raw?.repositoryPins;
172
+ if (!Array.isArray(pins) || pins.length === 0) {
173
+ throw new Error(
174
+ `${RUNTIME_IMAGE_SOURCE_INDEX_FILE} declares no repositoryPins`,
175
+ );
176
+ }
177
+ return pins;
178
+ }
179
+
180
+ let defaultLedger;
181
+
182
+ function ledgerCommits(repo, ledger) {
183
+ let repositories = ledger;
184
+ if (repositories === undefined) {
185
+ if (defaultLedger === undefined) {defaultLedger = loadBuildLedger();}
186
+ repositories = defaultLedger;
187
+ }
188
+ if (repositories === null) {return null;}
189
+ const commits = repositories[repo];
190
+ return Array.isArray(commits) && commits.length > 0 ? commits : null;
191
+ }
192
+
193
+ /**
194
+ * Every commit this repository has published an image from, as the UNION of the
195
+ * two records of that one fact: the committed evidence tree and the build
196
+ * ledger. Union rather than precedence, because neither can produce a false
197
+ * positive — each is a record that a build actually happened — while either
198
+ * alone has been measured missing whole repositories.
199
+ */
200
+ function builtCommits(directory, ref, repo, ledger) {
201
+ const fromTree = evidenceTreeCommits(directory, ref);
202
+ const fromLedger = ledgerCommits(repo, ledger);
203
+ // Null from BOTH means neither record has ever heard of this repository, so
204
+ // it publishes no images and its tip is right. An EMPTY union is a different
205
+ // fact — something recorded this repository and named no usable commit — and
206
+ // it must reach the refusal below rather than be read as "publishes nothing".
207
+ // Collapsing the two would silently re-pin to the bare tip, which is the one
208
+ // outcome this whole function exists to prevent.
209
+ if (fromTree === null && fromLedger === null) {return null;}
210
+ const union = new Set(fromTree ?? []);
211
+ for (const commit of fromLedger ?? []) {
212
+ if (/^[0-9a-f]{40}$/u.test(commit)) {union.add(commit);}
213
+ }
214
+ return union;
215
+ }
216
+
217
+ function evidenceTreeCommits(directory, ref) {
218
+ let listing;
219
+ try {
220
+ listing = git(directory, ['ls-tree', '-r', '--name-only', ref, 'evidence/'], {
221
+ maxBuffer: 64 * 1024 * 1024,
222
+ });
223
+ } catch {
224
+ return null;
225
+ }
226
+ const statements = listing
227
+ .split('\n')
228
+ .filter((file) => /statements\/[^/]+\.json$/u.test(file));
229
+ if (statements.length === 0) {return null;}
230
+
231
+ const commits = new Set();
232
+ for (const file of statements) {
233
+ let statement;
234
+ try {
235
+ statement = JSON.parse(
236
+ git(directory, ['show', `${ref}:${file}`], { maxBuffer: 16 * 1024 * 1024 }),
237
+ );
238
+ } catch {
239
+ continue;
240
+ }
241
+ const commit = statement?.predicate?.source?.sourceCommit;
242
+ if (typeof commit === 'string' && /^[0-9a-f]{40}$/u.test(commit)) {
243
+ commits.add(commit);
244
+ }
245
+ }
246
+ return commits.size > 0 ? commits : null;
247
+ }
248
+
249
+ /**
250
+ * Resolve the immutable release source without consulting local HEAD.
251
+ *
252
+ * THIS RETURNS THE NEWEST *BUILT* COMMIT, NOT THE TIP, and the difference is the
253
+ * whole point.
254
+ *
255
+ * It returned `refs/remotes/origin/main` verbatim until 2026-09-01, with nothing
256
+ * checking that anything had ever been built there. Measured that day against
257
+ * the last twelve `xema-base` commits, probing ghcr for a published `:<sha>`
258
+ * tag: ONE had an image, and that one only because it had been hand-dispatched.
259
+ * Builds run 4-12 a day against 150-353 commits, so an arbitrary tip is almost
260
+ * never buildable — most of `main` is `evidence(base)` receipts, which are
261
+ * authored by `github-actions[bot]` and therefore get no CI run under GitHub's
262
+ * anti-recursion rule, hence no `workflow_run`, hence no build.
263
+ *
264
+ * The consequence was not subtle. A re-pin that landed on an unbuilt commit made
265
+ * every distribution lock declare coordinates the registry answers
266
+ * MANIFEST_UNKNOWN for — 41 of 43 after one such re-pin — and `Verify image
267
+ * platforms` went red across every edition. The producer and the resolver held
268
+ * two different definitions of "the release commit": a commit that was BUILT,
269
+ * versus whatever is at the tip.
270
+ *
271
+ * Building the missing coordinate by hand clears the symptom at one commit and
272
+ * leaves this mechanism intact, so the next re-pin reproduces it. This is the
273
+ * mechanism.
274
+ */
275
+ /**
276
+ * The published TIP of a source repository — the SOURCE-provenance axis.
277
+ *
278
+ * ── TWO AXES, ONE PUBLISHED FACT ──
279
+ *
280
+ * This and {@link publishedSourceCommit} read the SAME ref and differ only in
281
+ * what they are allowed to SELECT from it, which is the whole distinction:
282
+ *
283
+ * - **image provenance** asks *"which commit did we actually build an image
284
+ * from?"* and must therefore walk back to published image evidence;
285
+ * - **source provenance** asks *"where do these vendored bytes come from?"*
286
+ * and needs only a commit that is published and immutable.
287
+ *
288
+ * Answering the second question with the first answer is not conservative, it
289
+ * is WRONG, and the way it fails is structural rather than occasional:
290
+ *
291
+ * a CONTENT-ONLY component — one with no `api/` directory, shipping no
292
+ * service — can never produce an image, therefore never earns image
293
+ * evidence, therefore can never be named by the image axis, therefore its
294
+ * source changes CANNOT REACH A CONSUMER'S IMAGE AT ALL.
295
+ *
296
+ * Measured 2026-09-14: `biomes/platform/memory-maintenance` in `xema-cultivars`
297
+ * is exactly that shape. Its agent frontmatter had migrated `modelClass` ->
298
+ * `modelLane`; the fix sat on that repository's `main`, and the image-evidence
299
+ * pin could only name a commit 130 behind it that still carried the retired
300
+ * key. `biome-host-api` baked the stale file, its own strict parser refused it,
301
+ * and the new ReplicaSet crashlooped in production while the previous one kept
302
+ * serving — so the defect was invisible to every health signal.
303
+ *
304
+ * `publishedSourceCommit` already contained half of this distinction and could
305
+ * not express it: its `built === null` arm returns the tip for a repository
306
+ * that publishes no images at all. That arm is right, and it was keyed on the
307
+ * REPOSITORY when the question belongs to the SUBJECT — a repository that
308
+ * publishes some images still vendors content-only source out of it.
309
+ *
310
+ * ── THIS IS NOT A RETURN TO "WHATEVER HEAD HAPPENED TO BE" ──
311
+ *
312
+ * Nothing here reads local `HEAD` or a working tree, and the value is resolved
313
+ * ONCE into a committed pin that the build then reads. The revision stays
314
+ * DECLARED; only the menu it is declared from stops being restricted to
315
+ * commits that happen to have shipped an image.
316
+ */
317
+ export function publishedSourceTip(directory, repo) {
318
+ try {
319
+ return exactCommit(
320
+ directory,
321
+ PUBLISHED_SOURCE_REF,
322
+ `source repository "${repo}"`,
323
+ );
324
+ } catch {
325
+ throw new Error(
326
+ `source repository "${repo}" has no ${PUBLISHED_SOURCE_REF} — a distribution ` +
327
+ `pin must name a published commit. Fetch the repository before regenerating.`,
328
+ );
329
+ }
330
+ }
331
+
332
+ export function publishedSourceCommit(directory, repo, { ledger } = {}) {
333
+ const tip = publishedSourceTip(directory, repo);
334
+
335
+ const built = builtCommits(directory, PUBLISHED_SOURCE_REF, repo, ledger);
336
+ // Neither record knows this repository, so it publishes no images at all
337
+ // (xema-connectors contributes vendored content only). Nothing about it can
338
+ // be unbuildable and the tip is exactly right — as it always was.
339
+ if (built === null) {return tip;}
340
+
341
+ let walked;
342
+ try {
343
+ walked = git(directory, [
344
+ 'rev-list',
345
+ `--max-count=${BUILT_COMMIT_SEARCH_LIMIT}`,
346
+ tip,
347
+ ]).split('\n');
348
+ } catch {
349
+ throw new Error(
350
+ `source repository "${repo}" could not be walked back from ${PUBLISHED_SOURCE_REF}`,
351
+ );
352
+ }
353
+
354
+ for (const commit of walked) {
355
+ if (built.has(commit)) {return commit;}
356
+ }
357
+
358
+ // Fail fast and loudly. Silently falling back to the tip is the defect this
359
+ // function exists to remove, and a pin nobody can build is worse than a
360
+ // refusal somebody can read.
361
+ throw new Error(
362
+ `source repository "${repo}" has no commit with published image evidence within ` +
363
+ `${BUILT_COMMIT_SEARCH_LIMIT} commits of ${PUBLISHED_SOURCE_REF}. A distribution ` +
364
+ `pin must name a commit an image was actually built from — pinning the bare tip ` +
365
+ `is what made every lock declare coordinates the registry answers ` +
366
+ `MANIFEST_UNKNOWN for. Build that repository at a recent commit, or widen ` +
367
+ `BUILT_COMMIT_SEARCH_LIMIT if its release cadence is genuinely slower than that.`,
368
+ );
369
+ }
370
+
371
+ function validateRepository(repoRoot, repo, knownRepositories) {
372
+ if (!knownRepositories.has(repo)) {
373
+ throw new Error(
374
+ `source repository "${repo}" is not in the explicit distribution source repository set`,
375
+ );
376
+ }
377
+ const directory = resolve(repoRoot, repo);
378
+ const rel = relative(repoRoot, directory);
379
+ if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`)) {
380
+ throw new Error(`source repository "${repo}" resolves outside the aggregator`);
381
+ }
382
+ if (!existsSync(directory)) {
383
+ throw new Error(
384
+ `source repository "${repo}" is missing at ${directory}; source resolution never falls back to another authority`,
385
+ );
386
+ }
387
+ const actualTopLevel = realpathSync(
388
+ git(directory, ['rev-parse', '--show-toplevel']),
389
+ );
390
+ if (actualTopLevel !== realpathSync(directory)) {
391
+ throw new Error(
392
+ `source repository "${repo}" resolves to ${directory}, but Git reports top-level ${actualTopLevel}`,
393
+ );
394
+ }
395
+ return directory;
396
+ }
397
+
398
+ function workspaceSnapshotCommit(directory, repo) {
399
+ const head = exactCommit(directory, 'HEAD', `workspace repository "${repo}"`);
400
+ const temporary = mkdtempSync(join(tmpdir(), 'xema-distribution-source-'));
401
+ const indexPath = join(temporary, 'index');
402
+ const env = { ...process.env, GIT_INDEX_FILE: indexPath };
403
+ try {
404
+ git(directory, ['read-tree', head], { env });
405
+ git(directory, ['add', '-A', '--', '.'], { env });
406
+ const tree = git(directory, ['write-tree'], { env });
407
+ const headTree = git(directory, ['rev-parse', `${head}^{tree}`]);
408
+ if (tree === headTree) {return { commit: head, head, dirty: false };}
409
+ const commit = execFileSync(
410
+ 'git',
411
+ ['-C', directory, 'commit-tree', tree, '-p', head],
412
+ {
413
+ encoding: 'utf8',
414
+ input: 'xema local workspace source snapshot\n',
415
+ stdio: ['pipe', 'pipe', 'pipe'],
416
+ env: {
417
+ ...process.env,
418
+ GIT_AUTHOR_NAME: 'Xema Workspace Source',
419
+ GIT_AUTHOR_EMAIL: 'workspace-source@xema.invalid',
420
+ GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z',
421
+ GIT_COMMITTER_NAME: 'Xema Workspace Source',
422
+ GIT_COMMITTER_EMAIL: 'workspace-source@xema.invalid',
423
+ GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z',
424
+ },
425
+ },
426
+ ).trim();
427
+ return { commit, head, dirty: true };
428
+ } finally {
429
+ rmSync(temporary, { recursive: true, force: true });
430
+ }
431
+ }
432
+
433
+ export function parseSourceModeArgs(argv) {
434
+ const remaining = [];
435
+ let mode = DistributionSourceMode.Published;
436
+ let declared = false;
437
+ for (let index = 0; index < argv.length; index += 1) {
438
+ if (argv[index] !== '--source') {
439
+ remaining.push(argv[index]);
440
+ continue;
441
+ }
442
+ if (declared) {throw new Error('--source may be declared only once');}
443
+ const value = argv[index + 1];
444
+ if (!Object.values(DistributionSourceMode).includes(value)) {
445
+ throw new Error('--source requires exactly "published" or "workspace"');
446
+ }
447
+ mode = value;
448
+ declared = true;
449
+ index += 1;
450
+ }
451
+ return { mode, remaining };
452
+ }
453
+
454
+ /**
455
+ * The delegate a pure-record authority is given: one that cannot answer.
456
+ *
457
+ * Its message names the repository AND says what asking would have cost, so a
458
+ * reader meeting it in a stack trace does not have to find this file to learn
459
+ * why a resolution that works everywhere else is refused here.
460
+ */
461
+ function refusingDelegate() {
462
+ const refuse = (repo) => {
463
+ throw new Error(
464
+ `source repository "${repo}" reached a LIVE resolution from an artifact ` +
465
+ `whose commits are decided by the committed pins. Nothing downstream of ` +
466
+ `runtime-image-source-index.json may ask the world what a repository's ` +
467
+ `commit is: that is a second authority, and it moves pins nobody asked ` +
468
+ `to move. Refresh the pin itself (build-runtime-image-source-index.mjs ` +
469
+ `--trusted-write --repo ${repo}), or declare the live resolution ` +
470
+ `explicitly with --unpinned.`,
471
+ );
472
+ };
473
+ return Object.freeze({
474
+ mode: DistributionSourceMode.Published,
475
+ commitForRepository: refuse,
476
+ vendoredSourceCommitForRepository: refuse,
477
+ });
478
+ }
479
+
480
+ /**
481
+ * A source authority that RESOLVES exactly one repository and answers every
482
+ * other from a set of pins already decided.
483
+ *
484
+ * ── WHY THE PIN SET IS THE AUTHORITY AND NOT A CACHE ──
485
+ *
486
+ * `commitForRepository` asks the world a live question — "what is the newest
487
+ * commit of this repository with published image evidence?" — and the answer
488
+ * moves whenever a build lands. Every derived artifact that calls it therefore
489
+ * re-decides every repository's pin on every run, and two artifacts derived
490
+ * minutes apart can disagree. `resolve-images` already refuses that
491
+ * disagreement: it re-reads each biome manifest at the RUNTIME INDEX's pin and
492
+ * fails when the biome index was built from a different commit.
493
+ *
494
+ * So the pins are not an optimisation. They are the DECISION, and a refresh
495
+ * that means to move one of them must leave the rest exactly where they are —
496
+ * otherwise shipping a fix in one repository silently re-pins every other,
497
+ * and every subject derived from a moved pin becomes a coordinate the registry
498
+ * must already be serving.
499
+ *
500
+ * A repository with neither a pin nor the refresh scope is a REFUSAL. Falling
501
+ * through to a live resolution would reintroduce the drift by the back door,
502
+ * in the one case nobody is looking at — a repository that JOINED the index
503
+ * since the pins were written.
504
+ */
505
+ export function createPinnedSourceAuthority({
506
+ delegate,
507
+ refreshRepository,
508
+ pins,
509
+ }) {
510
+ // A refresh scope is the ONLY thing that can reach the live resolver, so it is
511
+ // the only thing that needs one. Omitting the delegate is the ordinary case
512
+ // for a CONSUMER artifact — one whose commits are all decided already — and it
513
+ // is substituted with a delegate that THROWS rather than left undefined.
514
+ //
515
+ // That substitution is the point rather than a convenience. "No repository was
516
+ // resolved live" is otherwise a property of the CALL SITE, provable only by
517
+ // reading it and re-provable after every edit; with a refusing delegate it is
518
+ // a property of the OBJECT, and a future edit that reaches for a live answer
519
+ // fails loudly at the moment it does. It is the positive control that proved
520
+ // `biome-index.json` is already pin-derived, kept rather than thrown away.
521
+ if (refreshRepository !== undefined && !delegate) {
522
+ throw new Error(
523
+ 'a pinned source authority with a refresh scope needs a delegate to resolve it',
524
+ );
525
+ }
526
+ const live = delegate ?? refusingDelegate();
527
+ const byRepo = new Map();
528
+ for (const pin of pins ?? []) {byRepo.set(pin.repo, pin);}
529
+ if (byRepo.size === 0) {
530
+ throw new Error('a pinned source authority needs at least one pin');
531
+ }
532
+ if (refreshRepository !== undefined && !byRepo.has(refreshRepository)) {
533
+ throw new Error(
534
+ `refresh scope "${refreshRepository}" has no pin to refresh. A repository ` +
535
+ `joining the index is a full rebuild, not a scoped refresh.`,
536
+ );
537
+ }
538
+ const pinned = (repo, field) => {
539
+ const pin = byRepo.get(repo);
540
+ if (!pin) {
541
+ throw new Error(
542
+ `source repository "${repo}" has no committed pin and is not the ` +
543
+ `refresh scope. Refusing to resolve it live: that is how a scoped ` +
544
+ `refresh silently re-pins a repository nobody asked about.`,
545
+ );
546
+ }
547
+ return pin[field];
548
+ };
549
+ return Object.freeze({
550
+ // A record of pins carries no mode of its own, and only PUBLISHED mode can
551
+ // have produced one: a workspace snapshot is local-only and unpublishable by
552
+ // construction, so it is never written to this index. Stated rather than
553
+ // defaulted — a reader asking a pure-record authority for its mode is asking
554
+ // what produced the pins, and the answer is not "whatever the caller had".
555
+ mode: delegate?.mode ?? DistributionSourceMode.Published,
556
+ commitForRepository(repo) {
557
+ if (repo === refreshRepository) {return live.commitForRepository(repo);}
558
+ return pinned(repo, 'sourceCommit');
559
+ },
560
+ vendoredSourceCommitForRepository(repo) {
561
+ if (repo === refreshRepository) {
562
+ return live.vendoredSourceCommitForRepository(repo);
563
+ }
564
+ return pinned(repo, 'vendoredSourceCommit');
565
+ },
566
+ });
567
+ }
568
+
569
+ /**
570
+ * Pin ONE repository at an EXACT named commit, and answer every other from the
571
+ * delegate.
572
+ *
573
+ * WHY A NAMED COMMIT AND NOT "THE NEWEST BUILT ONE". `publishedSourceCommit`
574
+ * asks the world a question whose answer moves whenever a build lands, so a
575
+ * refresh that means to ship a SPECIFIC fix is a race against the fleet: the
576
+ * operator names the commit that carries the fix, the resolver picks whatever is
577
+ * newest at the instant it runs, and the two are the same value only until they
578
+ * are not. The ruling this plane now follows says the pin is an EXACT SOURCE
579
+ * REF; this is how an operator states one.
580
+ *
581
+ * It is a NARROWING, never a widening. The commit must exist, must be a real
582
+ * commit object, and must be CONTAINED IN the published ref — so this cannot pin
583
+ * a topic branch, a commit that was force-pushed away, or anything a consumer
584
+ * could not fetch. What it can do is name a commit OLDER than the newest built
585
+ * one, which is the whole point: that is a deliberate hold-back or a deliberate
586
+ * choice of the commit that carries a fix.
587
+ *
588
+ * It does NOT verify the commit was built. `publishedSourceCommit` exists to
589
+ * stop an ARBITRARY tip being pinned, and its refusal text explains what that
590
+ * cost; an operator naming a commit by hand has taken that decision explicitly,
591
+ * and the coordinate is proven by `dist:verify-platforms` against the real
592
+ * registry either way. Substituting the resolver's judgement for the operator's
593
+ * here would make the flag a suggestion.
594
+ */
595
+ export function createExactCommitSourceAuthority({
596
+ delegate,
597
+ repo,
598
+ commit,
599
+ repoRoot = sourcesRoot(),
600
+ }) {
601
+ if (!delegate) {throw new Error('an exact-commit authority needs a delegate');}
602
+ if (!/^[0-9a-f]{40}$/u.test(String(commit ?? ''))) {
603
+ throw new Error(
604
+ `--at requires a full 40-character commit sha, not "${String(commit)}". ` +
605
+ `An abbreviation is ambiguous across repositories and a ref is not exact.`,
606
+ );
607
+ }
608
+ const directory = resolve(repoRoot, repo);
609
+ let resolved;
610
+ try {
611
+ resolved = git(directory, ['rev-parse', '--verify', `${commit}^{commit}`]);
612
+ } catch {
613
+ throw new Error(
614
+ `source repository "${repo}" has no commit ${commit}. Fetch it before pinning.`,
615
+ );
616
+ }
617
+ if (resolved !== commit) {
618
+ throw new Error(
619
+ `${commit} resolves to ${resolved} in "${repo}" — pass the commit itself.`,
620
+ );
621
+ }
622
+ try {
623
+ git(directory, [
624
+ 'merge-base',
625
+ '--is-ancestor',
626
+ commit,
627
+ PUBLISHED_SOURCE_REF,
628
+ ]);
629
+ } catch {
630
+ throw new Error(
631
+ `${commit} is not contained in ${PUBLISHED_SOURCE_REF} of "${repo}". A ` +
632
+ `distribution pin must name a PUBLISHED commit: anything else cannot be ` +
633
+ `fetched by a consumer and can be force-pushed away under the lock.`,
634
+ );
635
+ }
636
+ return Object.freeze({
637
+ mode: delegate.mode,
638
+ commitForRepository(target) {
639
+ return target === repo ? commit : delegate.commitForRepository(target);
640
+ },
641
+ vendoredSourceCommitForRepository(target) {
642
+ // BOTH axes. The two-axis split exists because a content-only biome can
643
+ // never earn image evidence and so must be read at the SOURCE tip rather
644
+ // than at a possibly-much-older image commit. An operator naming an exact
645
+ // commit has answered both questions at once, and leaving the source axis
646
+ // on the live tip would put a moving value back into a pin the operator
647
+ // asked to be exact — half a race is still a race.
648
+ return target === repo
649
+ ? commit
650
+ : delegate.vendoredSourceCommitForRepository(target);
651
+ },
652
+ });
653
+ }
654
+
655
+ export function createSourceAuthority({
656
+ mode = DistributionSourceMode.Published,
657
+ repoRoot = sourcesRoot(),
658
+ knownRepositories,
659
+ log = console.log,
660
+ } = {}) {
661
+ if (!Object.values(DistributionSourceMode).includes(mode)) {
662
+ throw new Error(`unknown distribution source mode "${String(mode)}"`);
663
+ }
664
+ if (
665
+ !knownRepositories ||
666
+ typeof knownRepositories === 'string' ||
667
+ typeof knownRepositories[Symbol.iterator] !== 'function'
668
+ ) {
669
+ throw new Error(
670
+ 'source authority requires the caller to declare its source repositories',
671
+ );
672
+ }
673
+ const known = new Set(knownRepositories);
674
+ if (known.size === 0) {
675
+ throw new Error('source authority repository declaration must not be empty');
676
+ }
677
+ const resolvedRoot = realpathSync(repoRoot);
678
+ const identities = new Map();
679
+ const vendored = new Map();
680
+ return Object.freeze({
681
+ mode,
682
+ /**
683
+ * The SOURCE-provenance revision — see {@link publishedSourceTip} for why
684
+ * this is a different question from `commitForRepository`.
685
+ *
686
+ * In WORKSPACE mode the two deliberately coincide: a workspace snapshot is
687
+ * local-only and unpublishable by construction, so there is no image-
688
+ * evidence constraint to diverge from and no second fact to resolve.
689
+ */
690
+ vendoredSourceCommitForRepository(repo) {
691
+ const cached = vendored.get(repo);
692
+ if (cached) {return cached;}
693
+ const directory = validateRepository(resolvedRoot, repo, known);
694
+ if (mode !== DistributionSourceMode.Published) {
695
+ return this.commitForRepository(repo);
696
+ }
697
+ const commit = publishedSourceTip(directory, repo);
698
+ vendored.set(repo, commit);
699
+ log(
700
+ `[source-authority] vendored-source ${repo}@${commit} (${PUBLISHED_SOURCE_REF} tip)`,
701
+ );
702
+ return commit;
703
+ },
704
+ commitForRepository(repo) {
705
+ const cached = identities.get(repo);
706
+ if (cached) {return cached.commit;}
707
+ const directory = validateRepository(resolvedRoot, repo, known);
708
+ if (mode === DistributionSourceMode.Published) {
709
+ const commit = publishedSourceCommit(directory, repo);
710
+ identities.set(repo, { commit });
711
+ log(
712
+ `[source-authority] published ${repo}@${commit} (${PUBLISHED_SOURCE_REF})`,
713
+ );
714
+ return commit;
715
+ }
716
+ const snapshot = workspaceSnapshotCommit(directory, repo);
717
+ identities.set(repo, snapshot);
718
+ log(
719
+ `[source-authority] workspace ${repo}@${snapshot.commit} ` +
720
+ `(HEAD ${snapshot.head}; ${snapshot.dirty ? 'dirty snapshot — local only, do not publish' : 'clean'})`,
721
+ );
722
+ return snapshot.commit;
723
+ },
724
+ });
725
+ }