@xemahq/xema 0.54.0 → 0.55.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 (36) hide show
  1. package/dist/edition/verify-image-evidence.bundle.mjs +1989 -1008
  2. package/dist/edition/verify-image-evidence.bundle.provenance.json +583 -499
  3. package/dist/runtime/lock-loader.d.ts +2 -1
  4. package/dist/runtime/lock-loader.d.ts.map +1 -1
  5. package/dist/runtime/lock-loader.js +15 -5
  6. package/dist/runtime/lock-loader.js.map +1 -1
  7. package/edition/README.md +8 -3
  8. package/edition/check-committed-image-evidence.mjs +44 -15
  9. package/edition/customer-edition-verbs.mjs +15 -4
  10. package/edition/develop-current-resolution.mjs +29 -0
  11. package/edition/develop-edition-resolve.mjs +4 -3
  12. package/edition/develop-edition.mjs +30 -9
  13. package/edition/develop-freeze.mjs +24 -1
  14. package/edition/develop-graph.mjs +5 -3
  15. package/edition/develop-image-probe.mjs +6 -4
  16. package/edition/develop-image-record.mjs +194 -0
  17. package/edition/develop-record-ancestry.mjs +23 -5
  18. package/edition/develop-select-records.mjs +29 -21
  19. package/edition/develop-selection-line.mjs +30 -0
  20. package/edition/develop-selection-pins.mjs +30 -0
  21. package/edition/dist-check.mjs +45 -9
  22. package/edition/distribution-evidence-set.mjs +14 -9
  23. package/edition/edition-image-seal.mjs +2 -1
  24. package/edition/edition-protocol.mjs +6 -20
  25. package/edition/image-evidence.mjs +6 -2
  26. package/edition/image-lock-service-rows.mjs +4 -4
  27. package/edition/image-subject-identity.mjs +24 -0
  28. package/edition/images-from-record.mjs +46 -110
  29. package/edition/lib.mjs +24 -26
  30. package/edition/resolve-customer-edition.mjs +1 -1
  31. package/edition/resolve-edition.mjs +2 -1
  32. package/edition/resolve-images.mjs +224 -12
  33. package/edition/seal-image-digests.mjs +24 -13
  34. package/edition/verify-image-platforms.mjs +2 -0
  35. package/edition/workspace-sections.mjs +11 -1
  36. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { type DistributionLockReadView, type RuntimeImagePlatform } from '@xemahq/contracts/distribution';
1
+ import { type DistributionLockReadView, type RuntimeImagePlatform, type RuntimeImageSubject } from '@xemahq/contracts/distribution';
2
2
  import { type ArtifactResolution } from './paths';
3
3
  export interface LoadedDistributionLock {
4
4
  readonly lock: DistributionLockReadView;
@@ -20,4 +20,5 @@ export interface LoadedImageLock {
20
20
  readonly resolution: ArtifactResolution;
21
21
  }
22
22
  export declare function loadImageLock(opts: LoadLockOptions): LoadedImageLock;
23
+ export declare function subjectIdentity(subject: RuntimeImageSubject): string;
23
24
  //# sourceMappingURL=lock-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lock-loader.d.ts","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EAE1B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAqD,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAarG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,GAAG,sBAAsB,CASlF;AAQD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAQ7B,QAAQ,CAAC,iBAAiB,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,eAAe;IAQ9B,QAAQ,CAAC,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC1D,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AA0BD,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CAwBpE"}
1
+ {"version":3,"file":"lock-loader.d.ts","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACzB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAqD,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAarG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,GAAG,sBAAsB,CASlF;AAQD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAQ7B,QAAQ,CAAC,iBAAiB,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,eAAe;IAQ9B,QAAQ,CAAC,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC1D,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AA0BD,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CA+BpE;AASD,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAIpE"}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadDistributionLock = loadDistributionLock;
4
4
  exports.loadImageLock = loadImageLock;
5
+ exports.subjectIdentity = subjectIdentity;
5
6
  const node_fs_1 = require("node:fs");
6
7
  const distribution_1 = require("@xemahq/contracts/distribution");
7
8
  const paths_1 = require("./paths");
@@ -20,14 +21,21 @@ function loadImageLock(opts) {
20
21
  const lock = (0, distribution_1.parseImageLock)(readJson(resolution.path));
21
22
  const subjectsByKey = new Map();
22
23
  const imagesByService = new Map();
24
+ const subjectsByName = new Map();
23
25
  for (const subject of lock.subjects) {
24
- subjectsByKey.set(subjectKey(subject.kind, subject.id), subject);
26
+ subjectsByKey.set(subjectIdentity(subject), subject);
25
27
  }
26
- for (const [service, subject] of (0, distribution_1.serviceImageSubjects)(lock.subjects)) {
28
+ for (const subject of subjectsByKey.values()) {
29
+ const named = subjectsByName.get(`${subject.kind}:${subject.id}`);
30
+ if (named === undefined || (subject.generation ?? 0) >= (named.generation ?? 0)) {
31
+ subjectsByName.set(`${subject.kind}:${subject.id}`, subject);
32
+ }
33
+ }
34
+ for (const [service, subject] of (0, distribution_1.dedicatedServiceImageSubjects)(lock)) {
27
35
  imagesByService.set(service, toImageEntry(subject));
28
36
  }
29
37
  for (const binding of lock.platformServiceBindings) {
30
- const subject = subjectsByKey.get(subjectKey(binding.subjectKind, binding.subjectId));
38
+ const subject = subjectsByName.get(`${binding.subjectKind}:${binding.subjectId}`);
31
39
  if (subject === undefined) {
32
40
  throw new Error(`Invariant violated reading ${resolution.path}: platform service ` +
33
41
  `"${binding.platformService}" binds the ${binding.subjectKind} subject ` +
@@ -37,8 +45,10 @@ function loadImageLock(opts) {
37
45
  }
38
46
  return { imagesByService, resolution };
39
47
  }
40
- function subjectKey(kind, id) {
41
- return `${kind}:${id}`;
48
+ function subjectIdentity(subject) {
49
+ return subject.kind === distribution_1.RuntimeImageSubjectKind.ComposedProcess
50
+ ? `${subject.kind}:${subject.imageRef}:${subject.contentHash}`
51
+ : `${subject.kind}:${subject.id}`;
42
52
  }
43
53
  function toImageEntry(subject) {
44
54
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"lock-loader.js","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lock-loader.js","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"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"}
package/edition/README.md CHANGED
@@ -82,7 +82,8 @@ xema edition artifact <id> --channel release --registry <ns> --coordinate <tag>
82
82
  xema edition develop <freeze-sources|resolve|target|check-existing> ...
83
83
  # develop tag = sha256 of { cli, engine, pins, sourceImages }; target and check-existing take --engine-image, and a customer's --source-image
84
84
  xema edition develop coverage <id> --pins <file> --cosign <abs path> [--json] # read-only forecast (policy held to its pin, this CLI's own verifier code): is each develop image producer-signed? exit 0 covered, 1 not, 2 error
85
- xema edition develop select-records --branch develop --registry <root> --cosign <abs path> --out <file> [--at <ISO-8601 UTC>] [--published-pins <file>] # newest signed build record per graph member (the newest COMMIT on develop, every record verified; --published-pins keeps a published commit the records are not ahead of, part 07 passes it) -> { pins, snapshot, images }, written once; any bad record refuses the run (needs dataRootRepositoryUrl in edition-workspace.json; anyone who can delete registry tags can remove newer records and this then pins the newest remaining valid one, and the guard is part 07's: deploy-latest's owner refuses a snapshot that goes backwards)
85
+ xema edition develop select-records --branch develop --registry <root> --cosign <abs path> --out <file> [--at <ISO-8601 UTC>] [--published-pins <file>] # newest signed build record per graph member (the newest COMMIT on develop, every record verified; --published-pins keeps a published commit the records are not ahead of, part 07 passes it) -> { pins, snapshot, behind, images, currentResolution }, written once (a member whose firstPartySources entry declares `currentResolution: "<ticket>"`, xema-docs until XEMA-2373, is skipped with no registry read, listed in currentResolution and printed as `xema-docs: current resolution (leaves the edition, XEMA-2373)`) (behind = { <repo>: { head, commits } }: develop's head and the commits the pinned one trails it by; stdout gets one `<short name>: record <commit>, <N> commits behind HEAD` line per member, always `commits`, short name = the repo's last path segment and unambiguous; images[] entries carry `member`); any bad record refuses the run (needs dataRootRepositoryUrl in edition-workspace.json; anyone who can delete registry tags can remove newer records and this then pins the newest remaining valid one, and the guard is part 07's: deploy-latest's owner refuses a snapshot that goes backwards)
86
+ xema edition develop image-record <id> --selection <file> --pins <file> --out <file> # the edition's image build record (what `resolve --images-from` reads) from a select-records selection: subjects are the checked-out snapshot's first-party image subjects; a current-resolution member's (xema-docs) is probed and goes in `images`, every other takes the selection's image of its develop repository (selected commit must equal the snapshot's) and goes in `satellites`; platforms read by digest; any gap refuses and writes nothing; written once
86
87
  xema edition develop verify-image-record --image <name>@sha256:<digest> --producer <owner/repo> --registry <root> --cosign <abs path> # is the digest bound to the producer by an authenticated develop build record (shared verify core with select-records). Records are read newest run first (listBuildRecordRuns sorts by compareBuildRecordRuns: run, then attempt, highest first), a verified record of a workflow other than the producer's `.github/workflows/ci.yaml` is skipped, and the answer is the newest ci.yaml record listing the exact image -> { image, digest, producer, commit }. `commit` is the commit of the newest verified ci.yaml record (by run number, then attempt) that lists the digest; it can be an older commit when a later run targeted it, and for an image the producer reused by content hash it is NOT the commit that built it. This is safe because the digest is what binds the bytes. Refuses when no ci.yaml record lists the image, the record names another producer, or cosign refuses the identity
87
88
  xema edition develop check-overrides --overrides <json> --data-root <dir> # data root only
88
89
  xema edition ceiling --distribution <id> --at <distributions sha> --producer-commit <owner/repo>=<sha> [...] --data-root <dir> --sources-root <dir> --kernel <dir> # the dev ceiling (DevelopCeilingSchema from @xemahq/contracts/develop-ceiling): selection, origin, repo and trustCeiling from the definition and indexes at --at; version, services, runtimeComponents and serviceProcessFacts from each producer's xema-biome.json and xema-biome.index.json at its --producer-commit; a missing commit or manifest refuses, with no fallback
@@ -126,7 +127,7 @@ The develop graph is the data root's first-party repositories, declared in `edit
126
127
 
127
128
  - Each key is a repository handle (`repos/<x>` or `submodules/<x>`); `sourceId` is its kebab-case source id; `repositoryUrl` is canonical HTTPS under `sourceRepositoryUrlPrefix`. Both are unique. The retired string form (`"repos/x": "x"`) is refused with `RetiredFirstPartySourceShapeError`.
128
129
  - `imageOrigin` says where the source's images come from on the everyday lane: `workspace` (built by the job in the aggregator workspace) or `satellite` (a producer outside it, such as `submodules/xema-host-web`, that publishes its own signed develop digest). The parse accepts a source without it and fills in no default; `--images-from` refuses such a source (see below).
129
- - `xema edition develop freeze-graph` pins every declared repository at the `develop` head of its declared URL; overrides may name any of them. The snapshot's `repositoryPins` (`develop resolve`, `checkout-indexed-source-repositories.mjs --snapshot-pins`) are exactly these repositories. The runtime image source index's `repositoryPins` are release pins and are not read on develop.
130
+ - `xema edition develop freeze-graph` pins every declared repository at the `develop` head of its declared URL; overrides may name any of them. With `--member-pins <selection file>` (a select-records selection; only `.pins`, 40-hex, is read) each member is pinned at its selected record commit instead: the selection must pin exactly the graph minus its `currentResolution` members (equal to the graph members' declared `currentResolution`; a missing or extra member is refused by name), a `currentResolution` member (xema-docs, XEMA-2373) is pinned at its develop head, and `--member-pins` with `--overrides` is refused. The snapshot's `repositoryPins` (`develop resolve`, `checkout-indexed-source-repositories.mjs --snapshot-pins`) are exactly these repositories. The runtime image source index's `repositoryPins` are release pins and are not read on develop.
130
131
  - A first-party repository is in the develop graph before any release pins it. The converse is checked offline: every `repositoryPins` entry must be declared first-party at the same `repositoryUrl`.
131
132
 
132
133
  ## Develop images from a build record
@@ -177,9 +178,13 @@ The edition library builds, validates and verifies evidence statements; it never
177
178
  - `xema edition evidence statement catalogue stage|accept` (vendor) derives the E4 statement from `--artifact oci://<repository>@sha256:<digest>` and `--offering <id>`, and `accept` attaches the signed statement and bundle to the artifact as a referrer. A release catalogue is verified before it is read, by the data root's pinned built verifier (`catalogue-evidence arming|verify` mode of the bundle, which uses its own embedded kernel because the catalogue's kernel is not adopted yet); `xema edition release resolve` and `check-existing` take `--cosign <absolute path>` for it.
178
179
  - `oras` is located with `XEMA_ORAS_BIN` or `PATH` and must hash to a pinned digest. It runs with a scrubbed environment, so proxy and CA variables do not reach it.
179
180
 
181
+ ## Previous seal
182
+
183
+ A write of `images.lock.json` (`resolve-images`, `xema edition develop resolve`) takes exactly one of `--previous-lock <lock> --previous-lock-sha256 <hex>` or `--first-seal`; the CLI refuses unless the lock's bytes hash to the digest, which the caller takes from the images.lock layer of the signed, published edition (the latest, under the per-edition concurrency group). The in-repo resolve and pin/unpin take the committed `images.lock.json` at the target path as the previous lock (first seal when absent). The the composed artifacts' generations carry forward from it.
184
+
180
185
  ## Kernel protocol and pins
181
186
 
182
- The CLI is coupled to the kernel's edition protocol, not to the kernel package. `@xemahq/contracts` exports the protocol identity as data (`dist/edition-protocol/edition-protocol.json`: `{contentDigest, provisional, version}`). This CLI supports protocols v28, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39 and v40 (no published kernel declares v29; a CLI lists a protocol only while every kernel published under it exports what the CLI imports; v27 and older kernels lack `serviceImageSubjects`, so a data root whose pinned kernel speaks v27 or older stays on an earlier CLI), pinned to its digest and vendored, and REFUSES an identity with `provisional: true`; every loader that executes the kernel, `xema edition check` first, and the evidence verifier compare only that identity and refuse an unsupported one, naming the supported and the declared versions. A protocol change needs a CLI release first; only then does the data root repin its kernel authority.
187
+ The CLI is coupled to the kernel's edition protocol, not to the kernel package. `@xemahq/contracts` exports the protocol identity as data (`dist/edition-protocol/edition-protocol.json`: `{contentDigest, provisional, version}`). This CLI supports protocols v28, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40 and v41 (no published kernel declares v29; a CLI lists a protocol only while every kernel published under it exports what the CLI imports; v27 and older kernels lack `serviceImageSubjects`, so a data root whose pinned kernel speaks v27 or older stays on an earlier CLI), pinned to its digest and vendored, and REFUSES an identity with `provisional: true`; every loader that executes the kernel, `xema edition check` first, and the evidence verifier compare only that identity and refuse an unsupported one, naming the supported and the declared versions. A protocol change needs a CLI release first; only then does the data root repin its kernel authority.
183
188
 
184
189
  A develop edition does not take the kernel's develop head blindly: `xema edition develop choose-kernel --kernel <package dir> [--kernel-pin <40-hex>] --out <file>` reads the declared protocol of each commit on the checkout's first-parent history (from source, unbuilt) and chooses the newest one this CLI supports. It writes `{chosenSha, headSha, held, reason}` (`reason`: `head-supported` | `head-ahead` | `explicit-pin`; `held` is `head-ahead`), and when held prints `kernel held at v<N> (<sha12>), head at v<M> (<sha12>): raise the CLI` (stdout only, never persisted). The head is the checkout's `refs/remotes/origin/develop` (a full-history checkout). An explicit pin wins but is still checked, and must be on that head's first-parent history (`not-on-develop` otherwise); when no commit is supported it refuses (`EditionProtocolMismatchError`, `no-supported-commit`; the walk ends at the first commit with no declaration file) and never falls back to the head; a declaration that exists but does not parse, at the head or any walked commit, is refused by name (`unparseable`), never skipped. `freeze-graph` applies the same check to `--kernel-sha`.
185
190
 
@@ -66,6 +66,7 @@ import {
66
66
  evidenceDirectoryForDigest,
67
67
  sha256Digest,
68
68
  } from './image-evidence.mjs';
69
+ import { evidenceSubjectKey } from './image-subject-identity.mjs';
69
70
  import { loadKernelDistributionSchemas } from './lib.mjs';
70
71
  import {
71
72
  assertProducerInput,
@@ -124,6 +125,29 @@ function assertContainedRelativePath(value, label) {
124
125
  *
125
126
  * @returns {{ evidenceDirectory: string, reusable: boolean, reason: string }}
126
127
  */
128
+ /**
129
+ * The key of an index entry: its subject's evidence identity, which for a composed artifact names the generation, so it is read
130
+ * from the statement the entry points at (the index states only kind and id). An entry whose statement cannot be read keeps
131
+ * `kind\0id`; the unusable statement is reported below.
132
+ */
133
+ export function entryKey(entry, absoluteDirectory) {
134
+ try {
135
+ const { predicate } = JSON.parse(
136
+ readBoundedFile(join(absoluteDirectory, assertContainedRelativePath(entry.evidence.statementPath, 'statementPath'))).toString('utf8'),
137
+ );
138
+ return evidenceSubjectKey({
139
+ kind: entry.subjectKind,
140
+ id: entry.subjectId,
141
+ imageRef: predicate.artifact.imageRepository,
142
+ contentHash: predicate.source.sourceTreeHash,
143
+ });
144
+ } catch {
145
+ return `${entry.subjectKind}\0${entry.subjectId}`;
146
+ }
147
+ }
148
+
149
+ const printable = (key) => key.replaceAll('\0', ':');
150
+
127
151
  export async function assessCommittedEvidence({
128
152
  input,
129
153
  repositoryRoot,
@@ -190,7 +214,12 @@ export async function assessCommittedEvidence({
190
214
  }
191
215
  const expectedByKey = new Map(
192
216
  expectedStatements.map((statement) => [
193
- `${statement.predicate.source.kind}:${statement.predicate.source.id}`,
217
+ evidenceSubjectKey({
218
+ kind: statement.predicate.source.kind,
219
+ id: statement.predicate.source.id,
220
+ imageRef: statement.predicate.artifact.imageRepository,
221
+ contentHash: statement.predicate.source.sourceTreeHash,
222
+ }),
194
223
  statement,
195
224
  ]),
196
225
  );
@@ -200,20 +229,20 @@ export async function assessCommittedEvidence({
200
229
  if (entry === null || typeof entry !== 'object') {
201
230
  return unusable('producer-evidence.json carries a non-object entry');
202
231
  }
203
- const key = `${entry.subjectKind}:${entry.subjectId}`;
232
+ const key = entryKey(entry, absoluteDirectory);
204
233
  if (seen.has(key)) {
205
- return unusable(`producer-evidence.json duplicates subject ${key}`);
234
+ return unusable(`producer-evidence.json duplicates subject ${printable(key)}`);
206
235
  }
207
236
  seen.add(key);
208
237
  const expected = expectedByKey.get(key);
209
238
  if (expected === undefined) {
210
239
  return unusable(
211
- `committed evidence carries subject ${key}, which this input does not evidence`,
240
+ `committed evidence carries subject ${printable(key)}, which this input does not evidence`,
212
241
  );
213
242
  }
214
243
  const descriptor = entry.evidence;
215
244
  if (descriptor === null || typeof descriptor !== 'object') {
216
- return unusable(`subject ${key} has no evidence descriptor`);
245
+ return unusable(`subject ${printable(key)} has no evidence descriptor`);
217
246
  }
218
247
  let statementBytes;
219
248
  let bundleBytes;
@@ -223,7 +252,7 @@ export async function assessCommittedEvidence({
223
252
  absoluteDirectory,
224
253
  assertContainedRelativePath(
225
254
  descriptor.statementPath,
226
- `subject ${key} statementPath`,
255
+ `subject ${printable(key)} statementPath`,
227
256
  ),
228
257
  ),
229
258
  );
@@ -232,25 +261,25 @@ export async function assessCommittedEvidence({
232
261
  absoluteDirectory,
233
262
  assertContainedRelativePath(
234
263
  descriptor.bundlePath,
235
- `subject ${key} bundlePath`,
264
+ `subject ${printable(key)} bundlePath`,
236
265
  ),
237
266
  ),
238
267
  );
239
268
  } catch (error) {
240
- return unusable(`subject ${key} artifacts are unreadable: ${error.message}`);
269
+ return unusable(`subject ${printable(key)} artifacts are unreadable: ${error.message}`);
241
270
  }
242
271
  if (
243
272
  sha256Digest(statementBytes) !== descriptor.statementDigest ||
244
273
  sha256Digest(bundleBytes) !== descriptor.bundleDigest
245
274
  ) {
246
275
  return unusable(
247
- `subject ${key} artifacts do not match the digests producer-evidence.json records`,
276
+ `subject ${printable(key)} artifacts do not match the digests producer-evidence.json records`,
248
277
  );
249
278
  }
250
279
  try {
251
280
  validateProducedBundle(statementBytes, bundleBytes);
252
281
  } catch (error) {
253
- return unusable(`subject ${key} bundle is malformed: ${error.message}`);
282
+ return unusable(`subject ${printable(key)} bundle is malformed: ${error.message}`);
254
283
  }
255
284
  let committed;
256
285
  try {
@@ -259,7 +288,7 @@ export async function assessCommittedEvidence({
259
288
  );
260
289
  } catch (error) {
261
290
  return unusable(
262
- `subject ${key} statement does not parse under the kernel schema: ${error.message}`,
291
+ `subject ${printable(key)} statement does not parse under the kernel schema: ${error.message}`,
263
292
  );
264
293
  }
265
294
  // producerRun is deliberately fresh on every execution (run id, attempt,
@@ -269,24 +298,24 @@ export async function assessCommittedEvidence({
269
298
  // validate the existing run's own signature and lifetime independently.
270
299
  if (!producerStatementMatchesInput(committed, expected)) {
271
300
  return unusable(
272
- `subject ${key} committed statement is not bound to this producer input ` +
301
+ `subject ${printable(key)} committed statement is not bound to this producer input ` +
273
302
  '(authority, source, build, artifact or platform set changed)',
274
303
  );
275
304
  }
276
305
  const expiresAt = Date.parse(committed.predicate.producerRun.expiresAt);
277
306
  if (!Number.isFinite(expiresAt)) {
278
- return unusable(`subject ${key} statement has an unreadable expiresAt`);
307
+ return unusable(`subject ${printable(key)} statement has an unreadable expiresAt`);
279
308
  }
280
309
  const remainingSeconds = Math.floor((expiresAt - now) / 1000);
281
310
  if (remainingSeconds <= 0) {
282
311
  return unusable(
283
- `subject ${key} evidence EXPIRED at ${committed.predicate.producerRun.expiresAt} — ` +
312
+ `subject ${printable(key)} evidence EXPIRED at ${committed.predicate.producerRun.expiresAt} — ` +
284
313
  'it can never verify again and must be re-produced',
285
314
  );
286
315
  }
287
316
  if (remainingSeconds < MIN_REMAINING_TTL_SECONDS) {
288
317
  return unusable(
289
- `subject ${key} evidence expires in ${remainingSeconds}s, under the ` +
318
+ `subject ${printable(key)} evidence expires in ${remainingSeconds}s, under the ` +
290
319
  `${MIN_REMAINING_TTL_SECONDS}s reuse floor — refreshing it now`,
291
320
  );
292
321
  }
@@ -183,15 +183,20 @@ const ENGINE_IMAGE_FLAG = '--engine-image';
183
183
  const ENGINE_REPOSITORY_FLAG = '--engine-repository';
184
184
  const ENGINE_SIGNER_FLAG = '--engine-signer';
185
185
  export const IMAGES_FROM_FLAG = '--images-from';
186
+ export const PREVIOUS_LOCK_FLAG = '--previous-lock';
187
+ export const PREVIOUS_LOCK_SHA256_FLAG = '--previous-lock-sha256';
188
+ export const FIRST_SEAL_FLAG = '--first-seal';
186
189
 
187
190
  const COMMANDS = Object.freeze({
188
191
  resolve: {
189
192
  positional: true,
190
193
  // --images-from: the everyday job's image build record (images-from-record.mjs) instead of the registry probe; vendor only.
191
- values: ['--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG, IMAGES_FROM_FLAG],
192
- switches: ['--local'],
194
+ // --previous-lock with --previous-lock-sha256 (the digest of the latest signed, published edition's images.lock layer, passed by the workflow under
195
+ // the per-edition concurrency group) or --first-seal: the previous seal whose composed artifacts carry their generation forward; vendor only.
196
+ values: ['--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG, IMAGES_FROM_FLAG, PREVIOUS_LOCK_FLAG, PREVIOUS_LOCK_SHA256_FLAG],
197
+ switches: ['--local', FIRST_SEAL_FLAG],
193
198
  required: ['--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG],
194
- customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG, COSIGN_FLAG, ENGINE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [IMAGES_FROM_FLAG], cosign: true },
199
+ customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG, COSIGN_FLAG, ENGINE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [IMAGES_FROM_FLAG, PREVIOUS_LOCK_FLAG, PREVIOUS_LOCK_SHA256_FLAG, FIRST_SEAL_FLAG], cosign: true },
195
200
  // A vendor resolve finds its engine itself; the old flag is refused by name, never read as a customer flag.
196
201
  removedFromVendor: { [ENGINE_IMAGE_FLAG]: `use ${ENGINE_REPOSITORY_FLAG} and ${ENGINE_SIGNER_FLAG}` },
197
202
  },
@@ -210,7 +215,7 @@ const COMMANDS = Object.freeze({
210
215
  },
211
216
  'freeze-graph': {
212
217
  positional: false,
213
- values: ['--data-sha', '--kernel-sha', '--deploy-sha', '--editions-root-url', '--overrides', '--out'],
218
+ values: ['--data-sha', '--kernel-sha', '--deploy-sha', '--editions-root-url', '--overrides', '--member-pins', '--out'],
214
219
  switches: [],
215
220
  required: ['--data-sha', '--kernel-sha', '--deploy-sha', '--out'],
216
221
  },
@@ -233,6 +238,12 @@ const COMMANDS = Object.freeze({
233
238
  switches: [],
234
239
  required: ['--branch', '--registry', '--cosign', '--out'],
235
240
  },
241
+ 'image-record': {
242
+ positional: true,
243
+ values: ['--selection', '--pins', '--out'],
244
+ switches: [],
245
+ required: ['--selection', '--pins', '--out'],
246
+ },
236
247
  'verify-image-record': {
237
248
  positional: false,
238
249
  values: ['--image', '--producer', '--registry', '--cosign'],
@@ -0,0 +1,29 @@
1
+ // develop-current-resolution — the members that keep the CURRENT develop resolution instead of a signed build record.
2
+ //
3
+ // A member keeps its current resolution (develop HEAD freeze plus the registry probe) only when the data root declares
4
+ // it: `currentResolution: "<ticket>"` on its `firstPartySources` entry in edition-workspace.json, the ticket that
5
+ // removes the exception (today xema-docs, whose develop lane is unsigned by owner decision and which leaves the
6
+ // edition). It is a declared exception, not a fallback: absent means none, and every other member without a record
7
+ // still refuses.
8
+
9
+ import { compareStrings } from './lib.mjs';
10
+
11
+ /** Whether a member (or its `firstPartySources` entry) keeps its current resolution. */
12
+ export const keepsCurrentResolution = (member) => Object.hasOwn(member, 'currentResolution');
13
+
14
+ /** The graph `members` that declare a current resolution, repo -> ticket, keys sorted: what a selection records. */
15
+ export function currentResolutionOf(members) {
16
+ return Object.fromEntries(
17
+ members
18
+ .filter(keepsCurrentResolution)
19
+ .map(({ repo, currentResolution }) => [repo, currentResolution])
20
+ .sort(([a], [b]) => compareStrings(a, b)),
21
+ );
22
+ }
23
+
24
+ /** Why a selection's `currentResolution` (absent = none) is not exactly the one the graph `members` declare; null when it is. */
25
+ export function currentResolutionProblem(declared, members) {
26
+ const expected = currentResolutionOf(members);
27
+ if (JSON.stringify(declared ?? {}) === JSON.stringify(expected)) {return null;}
28
+ return `"currentResolution" is ${JSON.stringify(declared)}, not ${JSON.stringify(expected)}, the members the data root declares keep their current resolution`;
29
+ }
@@ -79,8 +79,9 @@ export async function resolveDevelopSnapshot({ id, pins, steps = DEVELOP_EDITION
79
79
  * @param {object} request
80
80
  * @param {object} [request.imagesFrom] a parsed image build record (images-from-record.mjs): the digests are the record's,
81
81
  * with no probe and no rehash, and `verifySatellites` (createSatelliteVerifier) admits its satellite entries
82
+ * @param {object|null} [request.previousLock] the previous edition's parsed images lock: its composed artifacts carry their generation forward (null: generation 1)
82
83
  */
83
- export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, env = process.env, sealOptions = {}, fetchKernelTarball, fetchManifest = fetchRegistryManifest, progress = stderrProgress, steps = DEVELOP_EDITION_STEPS, imagesFrom, verifySatellites }) {
84
+ export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, env = process.env, sealOptions = {}, fetchKernelTarball, fetchManifest = fetchRegistryManifest, progress = stderrProgress, steps = DEVELOP_EDITION_STEPS, imagesFrom, verifySatellites, previousLock = null }) {
84
85
  if ((imagesFrom === undefined) !== (verifySatellites === undefined)) {
85
86
  throw new Error('resolveDevelopEdition takes an image build record and its satellite verifier together, or neither');
86
87
  }
@@ -135,7 +136,7 @@ export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, en
135
136
  let recorded;
136
137
  const imageLock = await checked(DevelopResolveCheck.DevelopImages, async () => {
137
138
  if (imagesFrom !== undefined) {
138
- const fromRecord = await resolveImagesFromRecord({ id, inputs, lock, record: imagesFrom, resolveImages: steps.resolveImages, verifySatellites });
139
+ const fromRecord = await resolveImagesFromRecord({ id, inputs, lock, record: imagesFrom, resolveImages: steps.resolveImages, verifySatellites, previousLock });
139
140
  ({ recorded } = fromRecord);
140
141
  phase(`image subjects taken from the build record (${fromRecord.imageLock.subjects.length} subjects); satellites verified`);
141
142
  return fromRecord.imageLock;
@@ -148,7 +149,7 @@ export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, en
148
149
  ...(sealOptions.run === undefined ? {} : { run: sealOptions.run }),
149
150
  progress,
150
151
  });
151
- const resolved = await steps.resolveImages(id, { inputs, lock, onSubjectHashed: probe.observe });
152
+ const resolved = await steps.resolveImages(id, { inputs, lock, onSubjectHashed: probe.observe, previousLock });
152
153
  phase(`image content hashes computed (${resolved.imageLock.subjects.length} subjects)`);
153
154
  await probe.settle();
154
155
  phase('every develop image is present');
@@ -41,10 +41,14 @@
41
41
  // the same for a customer edition (id = the definition's; the tag covers the pinned catalogue digest).
42
42
  // freeze-graph --data-sha <sha> --kernel-sha <sha>
43
43
  // --deploy-sha <sha> [--editions-root-url <https url>]
44
- // [--overrides <json>] --out <file>
44
+ // [--overrides <json> | --member-pins <selection file>] --out <file>
45
45
  // resolve refs/heads/develop once per develop-graph repository (the data root's
46
46
  // firstPartySources, at each declared URL) and the editions root; the three checked-out
47
- // repositories are pinned at what the workflow checked out; write the pins. --kernel-sha is refused
47
+ // repositories are pinned at what the workflow checked out; write the pins. --member-pins takes a select-records
48
+ // selection (only .pins, 40-hex, is read) and pins each graph member at its selected record commit instead of its
49
+ // develop head: the selection must pin exactly the graph minus its currentResolution members (which must equal
50
+ // the graph members' declared currentResolution), a missing or extra member is refused by name, and a currentResolution
51
+ // member is pinned at its develop head. --member-pins together with --overrides is refused. --kernel-sha is refused
48
52
  // unless its declared edition protocol is one this CLI supports (the check choose-kernel applies to a pin).
49
53
  // check-existing <id> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
50
54
  // refuse to reuse a pulled artifact whose descriptor disagrees with this snapshot.
@@ -71,7 +75,7 @@
71
75
  // for every develop-graph member, take the newest build record the data root's record workflow signed for
72
76
  // the branch (<root>/build-records/<name>, verified offline by digest against the pinned trusted root, as
73
77
  // signed by <data root>/.github/workflows/record-build.yaml at refs/heads/<branch>, called by the member at the
74
- // record's own commit) and write { pins, snapshot, images }, once (wx). Any missing, unsigned, mismatched or
78
+ // record's own commit) and write { pins, snapshot, behind, images, currentResolution }, once (wx); a member that keeps its current resolution (a declared currentResolution, develop-current-resolution.mjs) is skipped with no registry read, listed in currentResolution, and printed as `xema-docs: current resolution (leaves the edition, XEMA-2373)`; behind is { <repo>: { head, commits } }, develop's head and how many commits the selected one trails it by, and one line per member `<short name>: record <commit>, <N> commits behind HEAD` goes to stdout (always `commits`; short name = the repo's last path segment, refused when two members share one); images[] entries carry `member`, the owning repo handle. Any missing, unsigned, mismatched or
75
79
  // unreadable record refuses the whole run and writes nothing: a newer bad record is never skipped for an older
76
80
  // one. --at takes the newest record completed at or before that time. Only `develop` has a declared signer; the
77
81
  // data root must declare dataRootRepositoryUrl. Takes the roots `resolve` takes (no registry root is implied).
@@ -83,6 +87,14 @@
83
87
  // part 07 passes the flag.
84
88
  // Residual: anyone who can delete registry tags can remove newer records, and this then pins the newest remaining
85
89
  // valid one; the guard is part 07's, deploy-latest's owner refuses a snapshot that goes backwards.
90
+ // image-record <id> --selection <file> --pins <file> --out <file>
91
+ // write the edition's image build record (the file `resolve --images-from` reads) from a select-records selection:
92
+ // the subjects are the checked-out snapshot's first-party image subjects (enumerated as `coverage` does). A subject of
93
+ // a member that keeps its current resolution (a declared currentResolution, develop-current-resolution.mjs) is probed as resolve probes it
94
+ // and goes in `images`; every other first-party subject takes the selection's image of its develop repository, whose
95
+ // selected commit must be the snapshot's, and goes in `satellites`. Platforms are read from the registry by digest.
96
+ // Any subject without an image, any commit mismatch refuses the whole run and writes nothing, once (wx). Third-party
97
+ // subjects are ignored. Takes the roots `resolve` takes.
86
98
  // choose-kernel [--kernel-pin <40-hex>] --out <file>
87
99
  // choose the develop kernel commit in the --kernel checkout (full history; its refs/remotes/origin/develop is the develop head):
88
100
  // the newest first-parent commit whose declared edition protocol this CLI supports
@@ -106,14 +118,17 @@
106
118
  import { resolve } from 'node:path';
107
119
 
108
120
  import { writeCatalogue } from './catalogue.mjs';
109
- import { CUSTOMER_ROOTS, IMAGES_FROM_FLAG, headResolver, parseEditionVerbArgs, runCustomerVerb, writeOnce } from './customer-edition-verbs.mjs';
121
+ import { CUSTOMER_ROOTS, FIRST_SEAL_FLAG, IMAGES_FROM_FLAG, PREVIOUS_LOCK_FLAG, PREVIOUS_LOCK_SHA256_FLAG, headResolver, parseEditionVerbArgs, runCustomerVerb, writeOnce } from './customer-edition-verbs.mjs';
110
122
  import { DEFINITION_FLAG } from './customer-flags.mjs';
111
123
  import { runDevelopCoverage } from './develop-coverage-run.mjs';
112
124
  import { CoverageExit } from './develop-coverage.mjs';
113
125
  import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
114
126
  import { resolveDevelopEngineImage } from './develop-engine-image.mjs';
115
127
  import { assertNoToolingOverride, freezeGraph, freezeSources } from './develop-freeze.mjs';
128
+ import { developGraphMembers } from './develop-graph.mjs';
129
+ import { runImageRecord } from './develop-image-record.mjs';
116
130
  import { runSelectRecords } from './develop-select-records.mjs';
131
+ import { readSelectionPins } from './develop-selection-pins.mjs';
117
132
  import { runVerifyImageRecord } from './develop-verify-image-record.mjs';
118
133
  import { LOCAL_WORKFLOW, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
119
134
  import { EditionChannel, EditionKind } from './edition-constants.mjs';
@@ -124,6 +139,7 @@ import { assertKernelCommitSupported, chooseKernelCommit } from './kernel-protoc
124
139
  import { loadKernelDistributionSchemas } from './lib.mjs';
125
140
  import { snapshotPinSource } from './pin-source.mjs';
126
141
  import { composeEdition } from './resolve-distribution.mjs';
142
+ import { resolvePreviousSeal } from './resolve-images.mjs';
127
143
  import { readStrictJsonFile } from './strict-json.mjs';
128
144
 
129
145
  // The roots a verb takes. A customer form (it names --definition) takes fewer, and never the kernel (the
@@ -167,8 +183,8 @@ function parseOverrides(text) {
167
183
  }
168
184
 
169
185
  /** The target-tag input of `--images-from`: the record's digest, or nothing on the probe path. */
170
- function imageBuildRecordTagInput(options) {
171
- return options[IMAGES_FROM_FLAG] === undefined ? {} : { imageBuildRecordDigest: imageBuildRecordDigest(readImageBuildRecord(options[IMAGES_FROM_FLAG])) };
186
+ async function imageBuildRecordTagInput(options) {
187
+ return options[IMAGES_FROM_FLAG] === undefined ? {} : { imageBuildRecordDigest: imageBuildRecordDigest(await readImageBuildRecord(options[IMAGES_FROM_FLAG])) };
172
188
  }
173
189
 
174
190
  async function run(argv, env) {
@@ -176,7 +192,7 @@ async function run(argv, env) {
176
192
  switch (command) {
177
193
  case 'target': {
178
194
  if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
179
- console.log(developEditionTarget(id, readStrictJsonFile(options['--pins']), { registry: options['--registry'], engineImage: options['--engine-image'], ...imageBuildRecordTagInput(options) }));
195
+ console.log(developEditionTarget(id, readStrictJsonFile(options['--pins']), { registry: options['--registry'], engineImage: options['--engine-image'], ...(await imageBuildRecordTagInput(options)) }));
180
196
  return;
181
197
  }
182
198
  case 'resolve': {
@@ -190,12 +206,13 @@ async function run(argv, env) {
190
206
  env,
191
207
  engine: { repository: options['--engine-repository'], signer: options['--engine-signer'] },
192
208
  cosignBinary: options['--cosign'],
209
+ previousLock: await resolvePreviousSeal({ distributionId: id, previousLockPath: options[PREVIOUS_LOCK_FLAG], previousLockSha256: options[PREVIOUS_LOCK_SHA256_FLAG], firstSeal: Boolean(options[FIRST_SEAL_FLAG]) }),
193
210
  ...(options[IMAGES_FROM_FLAG] === undefined ? {} : {
194
- imagesFrom: readImageBuildRecord(options[IMAGES_FROM_FLAG]),
211
+ imagesFrom: await readImageBuildRecord(options[IMAGES_FROM_FLAG]),
195
212
  verifySatellites: createSatelliteVerifier({ registry: options['--registry'], cosignBinary: options['--cosign'], env }),
196
213
  }),
197
214
  });
198
- const target = developEditionTarget(id, pins, { registry: options['--registry'], engineImage, ...imageBuildRecordTagInput(options) });
215
+ const target = developEditionTarget(id, pins, { registry: options['--registry'], engineImage, ...(await imageBuildRecordTagInput(options)) });
199
216
  const outDir = resolve(options['--out']);
200
217
  writeEditionArtifact({
201
218
  outDir,
@@ -232,6 +249,9 @@ async function run(argv, env) {
232
249
  case 'select-records':
233
250
  await runSelectRecords({ options, env });
234
251
  return;
252
+ case 'image-record':
253
+ await runImageRecord({ id, options, env });
254
+ return;
235
255
  case 'verify-image-record':
236
256
  await runVerifyImageRecord({ options, env });
237
257
  return;
@@ -265,6 +285,7 @@ async function run(argv, env) {
265
285
  deploySha: options['--deploy-sha'],
266
286
  editionsRootUrl: options['--editions-root-url'],
267
287
  overrides: parseOverrides(options['--overrides']),
288
+ ...(options['--member-pins'] === undefined ? {} : { memberPins: readSelectionPins(options['--member-pins'], '--member-pins', developGraphMembers()) }),
268
289
  ...headResolver(EditionChannel.Develop, env),
269
290
  });
270
291
  writeOnce(options['--out'], pins);
@@ -9,6 +9,9 @@
9
9
  // freezeGraph the graph repositories, the data root, the kernel, the deploy checkout, the editions root
10
10
  // freezeSources one pin per `sources[]` entry the chain declares
11
11
  //
12
+ // With `memberPins` (a select-records selection) the members are pinned at the selected build records instead; the one
13
+ // declared exception, a member that keeps its current resolution, is pinned at its develop head.
14
+ //
12
15
  // `resolveHead({ name, url })` is the one network seam (default: `git ls-remote` through
13
16
  // git-source-auth: the data root's token only for a repository under its
14
17
  // `sourceRepositoryUrlPrefix`, no credential for any other, never argv).
@@ -16,6 +19,7 @@
16
19
  import { execFileSync } from 'node:child_process';
17
20
 
18
21
  import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
22
+ import { currentResolutionOf } from './develop-current-resolution.mjs';
19
23
  import { developGraphMembers } from './develop-graph.mjs';
20
24
  import { EditionChannel } from './edition-channel.mjs';
21
25
  import { assertCanonicalSourceUrl, assertSourceDeclaresNoCredential, isUnderSourcePrefix, withSourceGitEnv } from './git-source-auth.mjs';
@@ -124,6 +128,17 @@ function assertOverrides(overrides, allowed) {
124
128
  }
125
129
  }
126
130
 
131
+ /** The member pins name exactly the graph minus the current-resolution members; a missing or extra member is refused by name. */
132
+ function assertMemberPinsAreTheGraph(memberPins, members) {
133
+ const current = new Set(Object.keys(currentResolutionOf(members)));
134
+ const expected = members.map(({ repo }) => repo).filter((repo) => !current.has(repo));
135
+ const missing = expected.filter((repo) => !Object.hasOwn(memberPins, repo));
136
+ const extra = Object.keys(memberPins).filter((repo) => !expected.includes(repo));
137
+ if (missing.length > 0 || extra.length > 0) {
138
+ throw new Error(`--member-pins must pin exactly the develop graph minus ${JSON.stringify([...current])}: missing ${JSON.stringify(missing)}, extra ${JSON.stringify(extra)}`);
139
+ }
140
+ }
141
+
127
142
  const GRAPH_PHASE = new Set([
128
143
  SnapshotPinNamespace.Graph,
129
144
  SnapshotPinNamespace.Distributions,
@@ -149,6 +164,9 @@ function assertCheckedOutPin({ key, sha, flag, what, overrides }) {
149
164
  * @param {string} request.kernelSha the commit the kernel checkout is at
150
165
  * @param {string} request.deploySha the commit the deploy checkout is at
151
166
  * @param {string} [request.editionsRootUrl] https URL of the editions root; absent = this repository
167
+ * @param {Record<string, string>} [request.memberPins] the graph members' pins from a selection (`--member-pins`): exactly the
168
+ * graph minus the members that keep their current resolution (develop-current-resolution.mjs), which are pinned at their
169
+ * develop head; refused together with overrides
152
170
  * @param {(source: {name: string, url: string}) => void} request.holdSource the hold of one source; runs for ALL graph sources before the first head
153
171
  */
154
172
  export function freezeGraph({
@@ -157,9 +175,13 @@ export function freezeGraph({
157
175
  deploySha,
158
176
  editionsRootUrl,
159
177
  overrides = {},
178
+ memberPins,
160
179
  resolveHead,
161
180
  holdSource,
162
181
  }) {
182
+ if (memberPins !== undefined && Object.keys(overrides).length > 0) {
183
+ throw new Error('--member-pins and --overrides are exclusive: the member pins are the selected build records, never overridden');
184
+ }
163
185
  assertNoToolingOverride(overrides);
164
186
  assertOverrides(overrides, GRAPH_PHASE);
165
187
  const head = memoizedHeads(resolveHead);
@@ -171,6 +193,7 @@ export function freezeGraph({
171
193
  throw new Error(`override names "${key}", which is not a repository in the graph`);
172
194
  }
173
195
  }
196
+ if (memberPins !== undefined) {assertMemberPinsAreTheGraph(memberPins, members);}
174
197
  const checkedOut = [
175
198
  { key: dataPinKey(), sha: dataSha, flag: '--data-sha', what: 'the data root checkout' },
176
199
  { key: kernelPinKey(), sha: kernelSha, flag: '--kernel-sha', what: 'the kernel checkout' },
@@ -188,7 +211,7 @@ export function freezeGraph({
188
211
  }
189
212
  const pins = Object.fromEntries(checkedOut.map(({ key, sha }) => [key, sha]));
190
213
  for (const member of members) {
191
- pins[member.repo] = overrides[member.repo] ?? head({ name: member.repo, url: member.repositoryUrl });
214
+ pins[member.repo] = overrides[member.repo] ?? memberPins?.[member.repo] ?? head({ name: member.repo, url: member.repositoryUrl });
192
215
  }
193
216
  if (editionsRootUrl !== undefined) {
194
217
  const url = canonicalPublicHttpsRepositoryUrl(editionsRootUrl, '--editions-root-url');
@@ -13,14 +13,16 @@ import { compareStrings } from './lib.mjs';
13
13
 
14
14
  /**
15
15
  * The develop graph: `{ repo, repositoryUrl }` per declared first-party
16
- * repository, sorted by repo handle.
16
+ * repository, sorted by repo handle, plus `currentResolution` (its ticket) on a
17
+ * member the data root declares one for (develop-current-resolution.mjs).
17
18
  *
18
- * @returns {readonly { repo: string, repositoryUrl: string }[]}
19
+ * @returns {readonly { repo: string, repositoryUrl: string, currentResolution?: string }[]}
19
20
  */
20
21
  export function developGraphMembers() {
21
22
  return Object.freeze(
22
23
  Object.entries(firstPartySources('the develop graph'))
23
- .map(([repo, { repositoryUrl }]) => Object.freeze({ repo, repositoryUrl }))
24
+ .map(([repo, { repositoryUrl, currentResolution }]) =>
25
+ Object.freeze({ repo, repositoryUrl, ...(currentResolution === undefined ? {} : { currentResolution }) }))
24
26
  .sort((a, b) => compareStrings(a.repo, b.repo)),
25
27
  );
26
28
  }