@xemahq/xema 0.58.0 → 0.59.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,280 @@
1
+ // develop-catalogue-free-resolve — the catalogue-free CUSTOMER develop resolve:
2
+ //
3
+ // xema edition develop resolve --definition-at <owner>/<repo>,<path>,<dataRoot> --offering <owner>/<repo>,<path>,<dataRoot>
4
+ // --pins <file> --out <dir> --registry <root> --engine-repository <repository> --engine-signer <certificate identity>
5
+ // --cosign <abs path> --offering-previous-lock <file> --offering-previous-lock-sha256 <hex>
6
+ // (--previous-lock <file> --previous-lock-sha256 <hex> | --first-seal) [--source-image <source>=<registry>/<prefix>]... [--local]
7
+ //
8
+ // A dev deployment of a customer definition resolves the vendor offering it builds on IN THE SAME CALL instead of
9
+ // pinning a published catalogue: no catalogue.pin.json is read (its presence beside the definition is refused, never
10
+ // a fallback), and no public flag substitutes a catalogue file. Two workspaces, two steps, one CLI:
11
+ //
12
+ // vendor a child `develop resolve <offering id> --pins <vendor pins> --local` of this CLI on the VENDOR roots
13
+ // (the roots this call was given), unchanged: it writes the vendor artifact and catalogue.json into a
14
+ // scratch directory. --offering names the offering's directory inside the vendor data checkout, held to
15
+ // it like follow's --definition-at (the checkout is the declared repository, clean at the vendor data
16
+ // pin, the data root and the directory where the triple says). Never defaulted.
17
+ // customer the definition repository fetched at --pins "definition-repository" (the walk's git source fetch, by
18
+ // exact commit) and its tree at that commit checked out from the object store into this call's private
19
+ // scratch before the vendor step runs (a temporary index, symlinks as plain files): the shared fetch cache
20
+ // is never read again, so a cache tree that moves or changes mid-run cannot change what is resolved. The
21
+ // workspace is re-stated on the scratch copy's roots (data = <copy>/<dataRoot>, editions = the parent of
22
+ // <copy>/<path>, a fresh sources root, the same cache), and the customer resolve run against the catalogue
23
+ // the vendor step wrote. The edition's pins are the catalogue-free form
24
+ // (edition-contract.json provenancePins.customerForms): the vendor pins, the customer's own
25
+ // sources/<name> commits, "definition-repository" and the definition digest, never a catalogue pin.
26
+ // The catalogue CONTENT digest is sha256 of the EXACT catalogue.json bytes the vendor step wrote (parsed
27
+ // strictly, so a duplicate key refuses), the convention of the catalogue layer digest of an offering
28
+ // artifact (produce-edition-catalogue-evidence's predicate catalogueDigest). It goes into the distribution
29
+ // lock only, as catalogueContentDigest (kernel edition protocol v37), never catalogueDigest: no catalogue
30
+ // artifact exists, so the lock never names one (catalogue-lock-field.mjs).
31
+ //
32
+ // --pins is the COMPLETE frozen set; nothing here reads a head. It is split once, before anything is fetched: a
33
+ // sources/<name> key the definition declares is the customer's own, every other key but "definition-repository" goes
34
+ // to the vendor step (which refuses what its offering does not declare). A source name both the offering and the
35
+ // definition declare is refused before the vendor step: its one pin could not say whose source it is.
36
+
37
+ import { execFileSync, spawnSync } from 'node:child_process';
38
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
39
+ import { tmpdir } from 'node:os';
40
+ import { basename, dirname, join, resolve } from 'node:path';
41
+ import { fileURLToPath } from 'node:url';
42
+
43
+ import { sha256Digest } from './canonical-evidence-json.mjs';
44
+ import { CATALOGUE_PIN_FILE } from './catalogue-pin.mjs';
45
+ import { CATALOGUE_FILE, parseCatalogue } from './catalogue.mjs';
46
+ import { customerPreviousSeal, customerSourceImages, declaredSourceNamesOf, resolveCustomerChannelEdition } from './customer-edition-verbs.mjs';
47
+ import { DEFINITION_AT_FLAG, OFFERING_FLAG, OFFERING_PREVIOUS_LOCK_FLAG, OFFERING_PREVIOUS_LOCK_SHA256_FLAG } from './customer-flags.mjs';
48
+ import { assertDefinitionCheckout, parseFollowDefinition } from './develop-follow.mjs';
49
+ import { customerDevelopGitBundleVerifier } from './develop-git-bundles.mjs';
50
+ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
51
+ import { LOCAL_WORKFLOW, parseEditionDescriptor, snapshotEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
52
+ import { resetEditionWorkspaceConfig } from './edition-config.mjs';
53
+ import { CATALOGUE_PIN_KEY, DEFINITION_PIN_KEY, DEFINITION_REPOSITORY_PIN_KEY, EDITION_DESCRIPTOR_FILE, EditionChannel, EditionKind, VENDOR_MEMBER_PIN_PATTERN } from './edition-constants.mjs';
54
+ import { WorkspaceRoot, cacheDir, configureEditionWorkspace, configuredWorkspace, editionsRoot, workspaceArgv } from './edition-workspace.mjs';
55
+ import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
56
+ import { materializeGitSource } from './external-source-walk.mjs';
57
+ import { serializeJson } from './lib.mjs';
58
+ import { dataPinKey, isSourcePinKey, sortedSnapshotPins, sourceNameOfPinKey } from './pin-source.mjs';
59
+ import { parseStrictJson, readStrictJsonFile } from './strict-json.mjs';
60
+
61
+ export { DEFINITION_REPOSITORY_PIN_KEY };
62
+ /** The definition file in a definition directory (the editions-root layout). */
63
+ const DEFINITION_FILE = 'xema-distribution.json';
64
+ const SHA_PATTERN = /^[0-9a-f]{40}$/;
65
+ const ENTRY = join(dirname(fileURLToPath(import.meta.url)), 'develop-edition.mjs');
66
+
67
+ /**
68
+ * `sha256:<hex>` of the exact catalogue.json bytes the vendor step wrote (the catalogue layer digest convention): the
69
+ * lock's catalogueContentDigest, never a pin.
70
+ */
71
+ export const catalogueContentDigestOf = (bytes) => sha256Digest(bytes);
72
+
73
+ /**
74
+ * --pins checked and split once, before anything is fetched: `{ definitionRepositoryCommit, members }`, the members
75
+ * every other key. "catalogue" and "definition" are computed, never input.
76
+ */
77
+ export function splitCatalogueFreePins(pins) {
78
+ if (pins === null || typeof pins !== 'object' || Array.isArray(pins)) {throw new Error('--pins must be an object mapping pin key -> 40-hex commit');}
79
+ for (const computed of [CATALOGUE_PIN_KEY, DEFINITION_PIN_KEY]) {
80
+ if (computed in pins) {throw new Error(`--pins names "${computed}": a catalogue-free resolve computes the definition digest and pins no catalogue`);}
81
+ }
82
+ const { [DEFINITION_REPOSITORY_PIN_KEY]: definitionRepositoryCommit, ...members } = pins;
83
+ if (typeof definitionRepositoryCommit !== 'string' || !SHA_PATTERN.test(definitionRepositoryCommit)) {
84
+ throw new Error(`--pins "${DEFINITION_REPOSITORY_PIN_KEY}" must be the 40-hex commit the definition repository is read at, got ${JSON.stringify(definitionRepositoryCommit)}`);
85
+ }
86
+ if (!Object.keys(members).some((key) => VENDOR_MEMBER_PIN_PATTERN.test(key))) {
87
+ throw new Error('--pins carries no vendor member pin (repos/<x> or submodules/<x>): a catalogue-free resolve resolves the vendor offering from them');
88
+ }
89
+ return { definitionRepositoryCommit, members };
90
+ }
91
+
92
+ /**
93
+ * The members of --pins between the two steps: `{ own, vendor }`, own the sources/<name> keys the definition declares.
94
+ * `offering` and `definition` are `{ label, sourceNames }`: a source name both declare is refused, naming both.
95
+ */
96
+ export function partitionCatalogueFreePins(members, { offering, definition }) {
97
+ const vendorNames = new Set(offering.sourceNames);
98
+ const shared = [...new Set(definition.sourceNames)].filter((name) => vendorNames.has(name));
99
+ if (shared.length > 0) {
100
+ throw new Error(`the source${shared.length === 1 ? '' : 's'} ${shared.map((name) => `"${name}"`).join(', ')} ${shared.length === 1 ? 'is' : 'are'} declared by both the offering ${offering.label} and the definition ${definition.label}: a catalogue-free resolve splits each sources/<name> pin to one of them, so a source name is declared once`);
101
+ }
102
+ const ownNames = new Set(definition.sourceNames);
103
+ const own = {};
104
+ const vendor = {};
105
+ for (const [key, commit] of Object.entries(members)) {
106
+ if (isSourcePinKey(key) && ownNames.has(sourceNameOfPinKey(key))) {own[key] = commit;} else {vendor[key] = commit;}
107
+ }
108
+ return { own, vendor: sortedSnapshotPins(vendor) };
109
+ }
110
+
111
+ /** The definition repository at `commit`, through the source walk's fetch by exact commit; returns its (shared cache) directory. */
112
+ export function fetchDefinitionRepository({ repository, commit, env }) {
113
+ const source = { kind: 'git', name: DEFINITION_REPOSITORY_PIN_KEY, url: `https://github.com/${repository}.git`, ref: commit };
114
+ return materializeGitSource({ source, cacheRoot: join(cacheDir(), 'definition-repositories', ...repository.split('/')), env }).directory;
115
+ }
116
+
117
+ /**
118
+ * The tree of `commit` in the repository at `repository`, written into the new directory `into` from the object store
119
+ * (a temporary index beside `into`, never the repository's own index or worktree; symlinks as plain files, so nothing
120
+ * read from `into` resolves outside it). Returns `into`.
121
+ */
122
+ export function materializeDefinitionCommit({ repository, commit, into }) {
123
+ mkdirSync(into);
124
+ const env = { ...process.env, GIT_INDEX_FILE: `${into}.index`, GIT_NO_LAZY_FETCH: '1' };
125
+ const git = (args) => execFileSync('git', ['-C', repository, '-c', 'core.symlinks=false', ...args], { env, stdio: ['ignore', 'pipe', 'pipe'] });
126
+ try {
127
+ git(['read-tree', `${commit}^{tree}`]);
128
+ git(['checkout-index', '--all', `--prefix=${into}/`]);
129
+ } finally {
130
+ rmSync(`${into}.index`, { force: true });
131
+ }
132
+ return into;
133
+ }
134
+
135
+ /** The argv of the vendor step's child `develop resolve` (after the node binary). */
136
+ export function vendorResolveArgs({ offeringId, pinsFile, outDir, options, roots }) {
137
+ // Always --local: the child is a step of this call, not a release workflow; its descriptor never leaves the scratch
138
+ // directory, and the edition this call writes records this call's own workflow (LOCAL_WORKFLOW only under --local).
139
+ // The child finds and proves the engine itself (--engine-repository/--engine-signer), and seals against the OFFERING's
140
+ // own previous seal (the images lock `develop follow` froze), held by the child to its sha256 and to the offering's id.
141
+ return [
142
+ ENTRY, 'resolve', offeringId, '--pins', pinsFile, '--out', outDir, '--registry', options['--registry'],
143
+ '--engine-repository', options['--engine-repository'], '--engine-signer', options['--engine-signer'], '--cosign', options['--cosign'],
144
+ '--previous-lock', options[OFFERING_PREVIOUS_LOCK_FLAG], '--previous-lock-sha256', options[OFFERING_PREVIOUS_LOCK_SHA256_FLAG],
145
+ '--local', ...roots,
146
+ ];
147
+ }
148
+
149
+ /** The vendor step: a child `develop resolve` of this CLI on the vendor roots, writing into `outDir`. */
150
+ export function runVendorResolve({ offeringId, pinsFile, outDir, options, env, roots }) {
151
+ const result = spawnSync(process.execPath, vendorResolveArgs({ offeringId, pinsFile, outDir, options, roots }), { env, stdio: ['ignore', 'ignore', 'inherit'] });
152
+ if (result.error !== undefined || result.status !== 0) {
153
+ throw new Error(`the vendor resolve of the offering "${offeringId}" failed (${result.error?.message ?? `exit ${result.status ?? result.signal}`}); its refusal is above`);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * The catalogue the vendor step wrote, its digest, the engine it proved, and the proof the vendor artifact is that offering
159
+ * at those pins. (The digest is of `bytes` as read, the same bytes parsed: never a re-serialisation.)
160
+ */
161
+ export function readVendorCatalogue({ outDir, offeringId, vendorPins }) {
162
+ const path = join(outDir, CATALOGUE_FILE);
163
+ const bytes = readFileSync(path);
164
+ const catalogue = parseCatalogue(parseStrictJson(bytes.toString('utf8'), path));
165
+ if (catalogue.offeringId !== offeringId || catalogue.channel !== EditionChannel.Develop) {
166
+ throw new Error(`the vendor step wrote the catalogue of "${catalogue.offeringId}" (${catalogue.channel}), not of the develop offering "${offeringId}"`);
167
+ }
168
+ const descriptor = parseEditionDescriptor(readStrictJsonFile(join(outDir, EDITION_DESCRIPTOR_FILE)));
169
+ if (serializeJson(sortedSnapshotPins(descriptor.provenance.pins)) !== serializeJson(vendorPins)) {
170
+ throw new Error(`the vendor step resolved "${offeringId}" at other pins than the vendor pins it was given`);
171
+ }
172
+ return { catalogue, catalogueContentDigest: catalogueContentDigestOf(bytes), engineImage: descriptor.engine.image };
173
+ }
174
+
175
+ /** Run `body` with the workspace on `roots`, restoring the previous roots after. */
176
+ async function onRoots(roots, body) {
177
+ const restored = configuredWorkspace();
178
+ resetEditionWorkspaceConfig();
179
+ configureEditionWorkspace(roots);
180
+ try {
181
+ return await body();
182
+ } finally {
183
+ resetEditionWorkspaceConfig();
184
+ configureEditionWorkspace(restored);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * The catalogue-free customer develop resolve (see the header). Writes the artifact to --out and returns
190
+ * `{ id, pins, target, catalogueContentDigest, engineImage }`. The edition records the engine the vendor step proved, and
191
+ * carries the customer's own composed generations from --previous-lock (or --first-seal) under the customer's id.
192
+ * `fetchDefinition`, `vendorResolve`, `resolveCustomer`, `evidenceTrust` and `write` are replaced by tests; `print`
193
+ * receives the target.
194
+ */
195
+ export async function resolveCatalogueFree({
196
+ options,
197
+ env = process.env,
198
+ fetchDefinition = fetchDefinitionRepository,
199
+ vendorResolve = runVendorResolve,
200
+ resolveCustomer = resolveCustomerChannelEdition,
201
+ evidenceTrust = descriptorEvidenceTrust,
202
+ write = writeEditionArtifact,
203
+ print = (line) => console.log(line),
204
+ }) {
205
+ const definition = parseFollowDefinition(options[DEFINITION_AT_FLAG], DEFINITION_AT_FLAG);
206
+ const offering = parseFollowDefinition(options[OFFERING_FLAG], OFFERING_FLAG);
207
+ const rawPins = readStrictJsonFile(options['--pins']);
208
+ const offeringId = assertDefinitionCheckout({ definition: offering, dataSha: rawPins?.[dataPinKey()], flag: OFFERING_FLAG, shaLabel: `--pins "${dataPinKey()}"` });
209
+ const { definitionRepositoryCommit, members } = splitCatalogueFreePins(rawPins);
210
+ const offeringLabel = `${offering.repository}:${offering.path}/${DEFINITION_FILE}`;
211
+ const offeringSourceNames = declaredSourceNamesOf(readStrictJsonFile(join(editionsRoot(), offeringId, DEFINITION_FILE)));
212
+ const vendorRoots = configuredWorkspace();
213
+ const scratch = mkdtempSync(join(tmpdir(), 'xema-catalogue-free-'));
214
+ try {
215
+ const cached = fetchDefinition({ repository: definition.repository, commit: definitionRepositoryCommit, env });
216
+ const checkout = materializeDefinitionCommit({ repository: cached, commit: definitionRepositoryCommit, into: join(scratch, DEFINITION_REPOSITORY_PIN_KEY) });
217
+ const definitionDir = join(checkout, definition.path);
218
+ const definitionLabel = `${definition.repository}:${definition.path}/${DEFINITION_FILE}`;
219
+ if (existsSync(join(definitionDir, CATALOGUE_PIN_FILE))) {
220
+ throw new Error(`${definition.repository}:${definition.path} carries ${CATALOGUE_PIN_FILE}: a definition is resolved against its pinned catalogue (--definition <file>) or catalogue-free, never both`);
221
+ }
222
+ const rawDefinition = readStrictJsonFile(join(definitionDir, DEFINITION_FILE));
223
+ assertCanonicalDistributionId(rawDefinition?.id);
224
+ if (rawDefinition.id !== basename(definition.path)) {
225
+ throw new Error(`${definitionLabel} is "${rawDefinition.id}", not the directory's "${basename(definition.path)}"`);
226
+ }
227
+ const split = partitionCatalogueFreePins(members, {
228
+ offering: { label: offeringLabel, sourceNames: offeringSourceNames },
229
+ definition: { label: definitionLabel, sourceNames: declaredSourceNamesOf(rawDefinition) },
230
+ });
231
+ const sourceImages = customerSourceImages(options, rawDefinition);
232
+ const vendorOut = join(scratch, 'vendor');
233
+ const pinsFile = join(scratch, 'vendor-pins.json');
234
+ writeFileSync(pinsFile, serializeJson(split.vendor));
235
+ vendorResolve({ offeringId, pinsFile, outDir: vendorOut, options, env, roots: workspaceArgv() });
236
+ const { catalogue, catalogueContentDigest, engineImage } = readVendorCatalogue({ outDir: vendorOut, offeringId, vendorPins: split.vendor });
237
+ const sources = join(scratch, 'sources');
238
+ mkdirSync(sources);
239
+ const customerRoots = {
240
+ [WorkspaceRoot.Data]: join(checkout, definition.dataRoot),
241
+ [WorkspaceRoot.Editions]: dirname(definitionDir),
242
+ [WorkspaceRoot.Sources]: sources,
243
+ [WorkspaceRoot.Cache]: vendorRoots[WorkspaceRoot.Cache],
244
+ };
245
+ return await onRoots(customerRoots, async () => {
246
+ const { id, pins, ...resolved } = await resolveCustomer({
247
+ channel: EditionChannel.Develop,
248
+ rawDefinition,
249
+ catalogue,
250
+ catalogueContentDigest,
251
+ pins: split.own,
252
+ vendorPins: { ...split.vendor, [DEFINITION_REPOSITORY_PIN_KEY]: definitionRepositoryCommit },
253
+ previousSeal: customerPreviousSeal(EditionChannel.Develop, options),
254
+ sourceImages,
255
+ env,
256
+ // The customer's git-sourced OCI bundles, verified under the definition's own pinned policy (its data root).
257
+ developGitBundles: customerDevelopGitBundleVerifier({ channel: EditionChannel.Develop, cosignBinary: options['--cosign'], env }),
258
+ });
259
+ const evidenceTrustPolicy = await evidenceTrust({ kind: EditionKind.Resolved, channel: EditionChannel.Develop, consumesCatalogue: true });
260
+ const target = snapshotEditionTarget(EditionChannel.Develop, id, pins, { registry: options['--registry'], engineImage, sourceImages });
261
+ write({
262
+ outDir: resolve(options['--out']),
263
+ channel: EditionChannel.Develop,
264
+ distributionId: id,
265
+ pins,
266
+ workflow: options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env),
267
+ evidenceTrustPolicy,
268
+ engineImage,
269
+ distributionLock: resolved.distributionLock,
270
+ imageLock: resolved.imageLock,
271
+ authorizationLock: resolved.authorizationLock,
272
+ chartSetLock: resolved.chartSetLock,
273
+ });
274
+ print(target);
275
+ return { id, pins, target, catalogueContentDigest, engineImage };
276
+ });
277
+ } finally {
278
+ rmSync(scratch, { recursive: true, force: true });
279
+ }
280
+ }
@@ -23,6 +23,23 @@
23
23
  // declares (`freeze-sources --definition` writes it); the catalogue digest is frozen into the descriptor's
24
24
  // provenance pins as `catalogue`. The id is the definition's. Takes --data-root, --editions-root,
25
25
  // --sources-root, --cache-dir (no --deploy-root, no --kernel: the kernel is the catalogue's, from npm).
26
+ // It takes the customer edition's OWN previous seal, exactly one of --previous-lock <file> --previous-lock-sha256 <hex>
27
+ // (the customer edition's last images lock: sha256 of its bytes, its id the definition's) or --first-seal; it carries
28
+ // the generations of the customer's own composed artifacts forward (vendor rows are the catalogue's, unchanged).
29
+ // resolve --definition-at <owner>/<repo>,<path>,<dataRoot> --offering <owner>/<repo>,<path>,<dataRoot>
30
+ // --pins <file> --out <dir> --registry <root> --engine-repository <repository> --engine-signer <certificate identity>
31
+ // --cosign <absolute-path> --offering-previous-lock <file> --offering-previous-lock-sha256 <hex>
32
+ // (--previous-lock <file> --previous-lock-sha256 <hex> | --first-seal) [--source-image <source>=<registry>/<prefix>]... [--local]
33
+ // a CATALOGUE-FREE customer develop edition (develop-catalogue-free-resolve.mjs): the vendor offering named by
34
+ // --offering (its directory in the vendor data checkout) is resolved first by a child `resolve` on the vendor
35
+ // roots, sealed against the OFFERING's previous seal (--offering-previous-lock, as `follow` froze it, held to its
36
+ // sha256 and the offering's id) with the engine it finds and proves; then the definition, read from its repository at
37
+ // --pins "definition-repository", is resolved against the catalogue that child wrote, on its own roots, carrying the
38
+ // customer's own composed generations from --previous-lock (or --first-seal). --pins is the complete frozen set (the
39
+ // vendor pins, the definition's sources/<name>, "definition-repository"); no head is read and nothing is pushed. The
40
+ // edition pins those and the definition digest, no catalogue (the catalogue's content digest is in the distribution
41
+ // lock only), and records the vendor step's engine. catalogue.pin.json beside the definition is refused.
42
+ // Takes the vendor roots.
26
43
  // engine <id> --pins <file> --engine-repository <repository> --engine-signer <certificate identity> --cosign <absolute-path>
27
44
  // print <repository>@sha256:<digest>: the engine xema-deploy CI published (tag sha-<12 hex>) for the
28
45
  // snapshot's xema-deploy commit, its signature verified offline against the pinned trust root as made by
@@ -87,6 +104,22 @@
87
104
  // part 07 passes the flag.
88
105
  // Residual: anyone who can delete registry tags can remove newer records, and this then pins the newest remaining
89
106
  // valid one; the guard is part 07's, deploy-latest's owner refuses a snapshot that goes backwards.
107
+ // follow --definition-at <owner>/<repo>,<path>,<dataRoot> --data-sha <sha> --kernel-sha <sha> --deploy-sha <upper bound>
108
+ // --engine-repository <registry>/<path> --registry <root> --cosign <absolute-path> (--first | --current-pins <file>) --out <file>
109
+ // the pins a dev deployment's follow policy selects (develop-follow.mjs), written once (wx) as the COMPLETE frozen
110
+ // set `resolve` reads; nothing else is read or written. The data root checkout is the definition: the data root's
111
+ // declared repository, clean at --data-sha, its data root and distribution directory at dataRoot and path, and the
112
+ // editions root that same repository; the kernel checkout is at --kernel-sha. Members: select-records' newest-commit
113
+ // rule over their signed build records (the same verifier), with the --current-pins as the published pins (behind or
114
+ // equal keeps the current pin, diverged is refused by name), pinned as freeze-graph --member-pins pins them (a member
115
+ // that keeps its current resolution at its develop head). Deploy root: --deploy-sha is an UPPER BOUND, the pin the
116
+ // newest first-parent commit of the deploy checkout at or below it whose engine <--engine-repository>:sha-<12 hex> is
117
+ // published (unpublished skipped and counted, none within the walk limit refused by name). Tooling roots:
118
+ // freeze-graph's rules, then each classified against its current pin by ancestry in its own checkout (behind,
119
+ // diverged or absent from the checkout is refused by type). Declared sources: their develop heads, newer-wins
120
+ // against the current pin by the same ancestry rule. Prints {distributionId, definition, follow: first |
121
+ // from-current, tooling, advanced, kept, added, dropped, deploy: {deploySha, upperBound, skipped}}: --first says so
122
+ // and classifies nothing. Takes the roots `resolve` takes.
90
123
  // image-record <id> --selection <file> --pins <file> --out <file>
91
124
  // write the edition's image build record (the file `resolve --images-from` reads) from a select-records selection:
92
125
  // the subjects are the checked-out snapshot's first-party image subjects (enumerated as `coverage` does). A subject of
@@ -120,10 +153,12 @@ import { resolve } from 'node:path';
120
153
  import { writeCatalogue } from './catalogue.mjs';
121
154
  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';
122
155
  import { DEFINITION_FLAG } from './customer-flags.mjs';
156
+ import { resolveCatalogueFree } from './develop-catalogue-free-resolve.mjs';
123
157
  import { runDevelopCoverage } from './develop-coverage-run.mjs';
124
158
  import { CoverageExit } from './develop-coverage.mjs';
125
159
  import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
126
160
  import { resolveDevelopEngineImage } from './develop-engine-image.mjs';
161
+ import { runFollow } from './develop-follow.mjs';
127
162
  import { assertNoToolingOverride, freezeGraph, freezeSources } from './develop-freeze.mjs';
128
163
  import { developGraphMembers } from './develop-graph.mjs';
129
164
  import { runImageRecord } from './develop-image-record.mjs';
@@ -189,7 +224,7 @@ async function imageBuildRecordTagInput(options) {
189
224
  }
190
225
 
191
226
  async function run(argv, env) {
192
- const { command, id, options, customer } = parseDevelopEditionArgs(argv);
227
+ const { command, id, options, customer, catalogueFree } = parseDevelopEditionArgs(argv);
193
228
  switch (command) {
194
229
  case 'target': {
195
230
  if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
@@ -198,6 +233,10 @@ async function run(argv, env) {
198
233
  }
199
234
  case 'resolve': {
200
235
  if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
236
+ if (catalogueFree) {
237
+ await resolveCatalogueFree({ options, env });
238
+ return;
239
+ }
201
240
  const pins = readStrictJsonFile(options['--pins']);
202
241
  const workflow = options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env);
203
242
  const evidenceTrustPolicy = await descriptorEvidenceTrust({ kind: EditionKind.Resolved, channel: EditionChannel.Develop });
@@ -262,6 +301,9 @@ async function run(argv, env) {
262
301
  json: options['--json'] === true,
263
302
  env,
264
303
  });
304
+ case 'follow':
305
+ await runFollow({ options, env });
306
+ return;
265
307
  case 'select-records':
266
308
  await runSelectRecords({ options, env });
267
309
  return;