@geonosis/release 2.0.0 → 2.2.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.
@@ -1,3 +1,6 @@
1
+ // src/phases.ts
2
+ var PHASES = ["typecheck", "lint", "test", "doctor"];
3
+
1
4
  // src/config.ts
2
5
  import { existsSync, readFileSync } from "fs";
3
6
  import { resolve } from "path";
@@ -95,12 +98,39 @@ var parseSchema = (value2) => {
95
98
  };
96
99
  };
97
100
  var SMOKE_KEYS = "expected { exclude?, snapshots? }";
101
+ var SNAPSHOT_KEYS = "expected { commands?, install?, name }";
102
+ var SMOKE_PHASES = PHASES;
103
+ var parseCommands = (value2, at) => {
104
+ if (value2 === void 0) return {};
105
+ if (!isRecord(value2)) {
106
+ throw new CannotRun(
107
+ `${at}.commands must be an object \u2014 expected one command per phase: ${PHASES.map((one) => `${one}?`).join(", ")}`
108
+ );
109
+ }
110
+ const unknown = Object.keys(value2).find((key) => !SMOKE_PHASES.includes(key));
111
+ if (unknown !== void 0) {
112
+ throw new CannotRun(
113
+ `${at}.commands.${unknown} is not a phase the smoke runs \u2014 it runs ${SMOKE_PHASES.join(", ")}`
114
+ );
115
+ }
116
+ const commands = {};
117
+ for (const phase of SMOKE_PHASES) {
118
+ const command = value2[phase];
119
+ if (command === void 0) continue;
120
+ if (typeof command !== "string" || command === "") {
121
+ throw new CannotRun(`${at}.commands.${phase} must be the command that runs their ${phase}`);
122
+ }
123
+ commands[phase] = command;
124
+ }
125
+ return commands;
126
+ };
98
127
  var oneSnapshot = (value2, index) => {
99
128
  const at = `release.smoke.snapshots[${index}]`;
100
- if (!isRecord(value2)) throw new CannotRun(`${at} must be an object \u2014 expected { name, install? }`);
101
- const unknown = Object.keys(value2).find((key) => key !== "install" && key !== "name");
129
+ if (!isRecord(value2)) throw new CannotRun(`${at} must be an object \u2014 ${SNAPSHOT_KEYS}`);
130
+ const known = /* @__PURE__ */ new Set(["commands", "install", "name"]);
131
+ const unknown = Object.keys(value2).find((key) => !known.has(key));
102
132
  if (unknown !== void 0) {
103
- throw new CannotRun(`${at}.${unknown} is not a key it takes \u2014 expected { name, install? }`);
133
+ throw new CannotRun(`${at}.${unknown} is not a key it takes \u2014 ${SNAPSHOT_KEYS}`);
104
134
  }
105
135
  const name = value2["name"];
106
136
  if (typeof name !== "string" || name === "") {
@@ -110,7 +140,11 @@ var oneSnapshot = (value2, index) => {
110
140
  if (install !== void 0 && typeof install !== "string") {
111
141
  throw new CannotRun(`${at}.install must be the command that installs that tree`);
112
142
  }
113
- return { ...install === void 0 ? {} : { install }, name };
143
+ return {
144
+ commands: parseCommands(value2["commands"], at),
145
+ ...install === void 0 ? {} : { install },
146
+ name
147
+ };
114
148
  };
115
149
  var parseSmoke = (value2) => {
116
150
  if (value2 === void 0) return { exclude: [], snapshots: [] };
@@ -121,7 +155,7 @@ var parseSmoke = (value2) => {
121
155
  }
122
156
  const snapshots = value2["snapshots"];
123
157
  if (snapshots !== void 0 && !Array.isArray(snapshots)) {
124
- throw new CannotRun("release.smoke.snapshots must be a list of { name, install? }");
158
+ throw new CannotRun(`release.smoke.snapshots must be a list \u2014 ${SNAPSHOT_KEYS}, per entry`);
125
159
  }
126
160
  return {
127
161
  exclude: strings(value2["exclude"], "release.smoke.exclude"),
@@ -175,239 +209,952 @@ var readReleaseConfig = (root) => {
175
209
  }
176
210
  };
177
211
 
178
- // src/wrangler.ts
179
- import { readFileSync as readFileSync2 } from "fs";
180
- import { resolve as resolve2 } from "path";
181
- var isRecord2 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
182
- var parseJsonc = (source) => {
183
- let out = "";
184
- for (let index = 0; index < source.length; index += 1) {
185
- const char = source[index] ?? "";
186
- if (char === '"') {
187
- const start = index;
188
- index += 1;
189
- for (; index < source.length; index += 1) {
190
- if (source[index] === "\\") {
191
- index += 1;
192
- continue;
193
- }
194
- if (source[index] === '"') break;
195
- }
196
- out += source.slice(start, index + 1);
197
- continue;
198
- }
199
- if (char === "/" && source[index + 1] === "/") {
200
- const end = source.indexOf("\n", index);
201
- index = end === -1 ? source.length : end - 1;
202
- continue;
203
- }
204
- if (char === "/" && source[index + 1] === "*") {
205
- const end = source.indexOf("*/", index + 2);
206
- index = end === -1 ? source.length : end + 1;
207
- continue;
208
- }
209
- out += char;
212
+ // src/envelope.ts
213
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
214
+ import { dirname, join, resolve as resolve2 } from "path";
215
+ import { fileURLToPath } from "url";
216
+ var ENVELOPES_DIR = ".geonosis/envelopes";
217
+ var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
218
+ var UnbalancedEnvelope = class extends Error {
219
+ constructor(message) {
220
+ super(message);
221
+ this.name = "UnbalancedEnvelope";
210
222
  }
211
- return JSON.parse(out.replaceAll(/,(\s*[\]}])/g, "$1"));
212
- };
213
- var literal = (text) => {
214
- const value2 = text.trim();
215
- if (value2.startsWith('"') || value2.startsWith("'")) return value2.slice(1, -1);
216
- if (value2 === "true") return true;
217
- if (value2 === "false") return false;
218
- const number = Number(value2);
219
- return Number.isNaN(number) ? value2 : number;
220
223
  };
221
- var split = (text, delimiter) => {
222
- const parts = [];
223
- let depth = 0;
224
- let quote = "";
225
- let current = "";
226
- for (const char of text) {
227
- if (quote !== "") {
228
- current += char;
229
- if (char === quote) quote = "";
230
- continue;
231
- }
232
- if (char === '"' || char === "'") quote = char;
233
- if (char === "[" || char === "{") depth += 1;
234
- if (char === "]" || char === "}") depth -= 1;
235
- if (char === delimiter && depth === 0) {
236
- parts.push(current);
237
- current = "";
238
- continue;
239
- }
240
- current += char;
241
- }
242
- parts.push(current);
243
- return parts.filter((one) => one.trim() !== "");
224
+ var isCount = (value2) => Number.isSafeInteger(value2) && value2 >= 0;
225
+ var unbalancedMessage = (envelope, next) => `${envelope.tool}: considered ${envelope.considered} but accounts for ${envelope.read + envelope.refused.length + envelope.excused.length} \u2014 ${envelope.read} read + ${envelope.refused.length} refused + ${envelope.excused.length} excused. A run that has lost count of its own inputs cannot say what it measured, so no verdict was rendered and no envelope was written. Next: ${next}`;
226
+ var FORBIDDEN_ROOT = "GEONOSIS_ENVELOPES_FORBIDDEN_ROOT";
227
+ var refuseForbiddenRoot = (root) => {
228
+ const forbidden = process.env[FORBIDDEN_ROOT];
229
+ if (forbidden === void 0 || resolve2(forbidden) !== resolve2(root)) return;
230
+ throw new UnbalancedEnvelope(
231
+ `${root} is off limits to envelope writers in this process (${FORBIDDEN_ROOT}) \u2014 a run that writes one into a shared root races every other run reading it, and leaves a file the next one takes for real. Point this at a scratch root of its own: tooling/scratch-dir.ts.`
232
+ );
244
233
  };
245
- var value = (text) => {
246
- const trimmed = text.trim();
247
- if (trimmed.startsWith("[")) return split(trimmed.slice(1, -1), ",").map(value);
248
- if (trimmed.startsWith("{")) {
249
- return Object.fromEntries(
250
- split(trimmed.slice(1, -1), ",").map((pair) => {
251
- const at = pair.indexOf("=");
252
- return [pair.slice(0, at).trim(), value(pair.slice(at + 1))];
253
- })
234
+ var writeEnvelope = ({
235
+ envelope,
236
+ next,
237
+ root
238
+ }) => {
239
+ if (envelope.tool.trim() === "") {
240
+ throw new UnbalancedEnvelope(
241
+ `an envelope with no tool name cannot be filed or reported against. Next: ${next}`
254
242
  );
255
243
  }
256
- return literal(trimmed);
257
- };
258
- var put = (into, path, leaf) => {
259
- let here = into;
260
- for (const key of path.slice(0, -1)) {
261
- if (!isRecord2(here[key])) here[key] = {};
262
- here = here[key];
263
- }
264
- here[path.at(-1) ?? ""] = leaf;
265
- };
266
- var table = (into, path) => {
267
- let here = into;
268
- for (const key of path) {
269
- if (!isRecord2(here[key])) here[key] = {};
270
- here = here[key];
244
+ if (envelope.version.trim() === "") {
245
+ throw new UnbalancedEnvelope(
246
+ `${envelope.tool}: an envelope that cannot name the build that wrote it dates nothing, and a stale one reads exactly like a fresh one. Next: ${next}`
247
+ );
271
248
  }
272
- return here;
273
- };
274
- var arrayTable = (into, path) => {
275
- let here = into;
276
- for (const key of path.slice(0, -1)) {
277
- if (!isRecord2(here[key])) here[key] = {};
278
- here = here[key];
249
+ if (!isCount(envelope.considered) || !isCount(envelope.read)) {
250
+ throw new UnbalancedEnvelope(
251
+ `${envelope.tool}: considered ${envelope.considered} and read ${envelope.read} \u2014 a census is a whole number of things, and arithmetic over anything else balances by accident. Next: ${next}`
252
+ );
279
253
  }
280
- const last = path.at(-1) ?? "";
281
- if (!Array.isArray(here[last])) here[last] = [];
282
- const list = here[last];
283
- const entry = {};
284
- list.push(entry);
285
- return entry;
286
- };
287
- var parseToml = (source) => {
288
- const out = {};
289
- let here = out;
290
- const lines2 = source.split("\n");
291
- for (let index = 0; index < lines2.length; index += 1) {
292
- const line = (lines2[index] ?? "").split("#")[0]?.trim() ?? "";
293
- if (line === "") continue;
294
- if (line.startsWith("[[") && line.endsWith("]]")) {
295
- here = arrayTable(out, line.slice(2, -2).trim().split("."));
296
- continue;
297
- }
298
- if (line.startsWith("[") && line.endsWith("]")) {
299
- here = table(out, line.slice(1, -1).trim().split("."));
300
- continue;
301
- }
302
- const at = line.indexOf("=");
303
- if (at === -1) continue;
304
- let text = line.slice(at + 1);
305
- while ([...text].filter((one) => one === "[").length > [...text].filter((one) => one === "]").length) {
306
- index += 1;
307
- if (index >= lines2.length) throw new CannotRun("an unterminated array in the TOML config");
308
- text += `
309
- ${(lines2[index] ?? "").split("#")[0] ?? ""}`;
310
- }
311
- put(here, line.slice(0, at).trim().split("."), value(text));
254
+ if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
255
+ throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
312
256
  }
313
- return out;
314
- };
315
- var BINDING_LISTS = [
316
- "ai",
317
- "analytics_engine_datasets",
318
- "browser",
319
- "d1_databases",
320
- "dispatch_namespaces",
321
- "durable_objects",
322
- "hyperdrive",
323
- "kv_namespaces",
324
- "mtls_certificates",
325
- "queues",
326
- "r2_buckets",
327
- "send_email",
328
- "services",
329
- "vectorize",
330
- "version_metadata",
331
- "workflows"
332
- ];
333
- var bindingsOf = (found) => {
334
- if (Array.isArray(found)) return found.flatMap(bindingsOf);
335
- if (!isRecord2(found)) return [];
336
- const named = found["binding"] ?? found["name"];
337
- const here = typeof named === "string" ? [named] : [];
338
- const nested = Object.entries(found).filter(([key]) => key === "bindings" || key === "producers" || key === "consumers").flatMap(([, one]) => bindingsOf(one));
339
- return [...here, ...nested];
340
- };
341
- var declaredIn = (config) => {
342
- const triggers = config["triggers"];
343
- const crons = isRecord2(triggers) && Array.isArray(triggers["crons"]) ? triggers["crons"] : [];
344
- const routes = config["routes"];
345
- const one = config["route"];
346
- const listed = [
347
- ...Array.isArray(routes) ? routes : [],
348
- ...typeof one === "string" ? [one] : []
349
- ];
350
- return {
351
- bindings: BINDING_LISTS.flatMap((key) => bindingsOf(config[key])).toSorted(),
352
- crons: crons.filter((cron) => typeof cron === "string").toSorted(),
353
- routes: listed.map((route) => isRecord2(route) ? route["pattern"] : route).filter((pattern) => typeof pattern === "string").toSorted()
354
- };
257
+ refuseForbiddenRoot(root);
258
+ const at = envelopePath(root, envelope.tool);
259
+ mkdirSync(dirname(at), { recursive: true });
260
+ writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
261
+ `);
262
+ return at;
355
263
  };
356
- var readWrangler = (root, relative5, env) => {
357
- const path = resolve2(root, relative5);
358
- let parsed;
264
+ var UNKNOWN = "unknown";
265
+ var versionIn = (dir) => {
359
266
  try {
360
- const source = readFileSync2(path, "utf8");
361
- parsed = relative5.endsWith(".toml") ? parseToml(source) : parseJsonc(source);
362
- } catch (error) {
363
- throw new CannotRun(`${relative5} could not be read: ${error.message}`);
267
+ const manifest = JSON.parse(readFileSync2(join(dir, "package.json"), "utf8"));
268
+ return typeof manifest.version === "string" ? manifest.version : void 0;
269
+ } catch {
270
+ return void 0;
364
271
  }
365
- if (!isRecord2(parsed)) throw new CannotRun(`${relative5} is not a wrangler configuration`);
366
- const environments = parsed["env"];
367
- const block = env !== void 0 && isRecord2(environments) && isRecord2(environments[env]) ? environments[env] : parsed;
368
- return declaredIn(block);
369
272
  };
370
-
371
- // src/deployed.ts
372
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
373
- import { resolve as resolve3 } from "path";
374
- var DEPLOYED_FILE = ".geonosis/deployed.json";
375
- var NOTHING_SAYS = "nothing here says what is deployed \u2014 .geonosis/deployed.json is written by the pipeline after promote, and its absence is not a pass";
376
- var listOf = (found) => Array.isArray(found) ? found.filter((one) => typeof one === "string") : [];
377
- var isRecord3 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
378
- var readDeployed = (root) => {
379
- const path = resolve3(root, DEPLOYED_FILE);
380
- if (!existsSync2(path)) throw new CannotRun(NOTHING_SAYS);
381
- let parsed;
382
- try {
383
- parsed = JSON.parse(readFileSync3(path, "utf8"));
384
- } catch (error) {
385
- throw new CannotRun(`${DEPLOYED_FILE} is not readable JSON: ${error.message}`);
273
+ var versionOf = (moduleUrl) => {
274
+ let dir = dirname(fileURLToPath(moduleUrl));
275
+ for (; ; ) {
276
+ const found = versionIn(dir);
277
+ if (found !== void 0) return found;
278
+ const up = dirname(dir);
279
+ if (up === dir) return UNKNOWN;
280
+ dir = up;
386
281
  }
387
- if (!isRecord3(parsed)) throw new CannotRun(`${DEPLOYED_FILE} is not an object`);
388
- const triggers = isRecord3(parsed["triggers"]) ? parsed["triggers"] : {};
389
- return {
390
- ...typeof parsed["at"] === "string" ? { at: parsed["at"] } : {},
391
- deployed: {
392
- bindings: listOf(parsed["bindings"]),
393
- crons: listOf(triggers["crons"]),
394
- routes: listOf(triggers["routes"]),
395
- secrets: listOf(parsed["secrets"])
396
- }
397
- };
398
- };
399
- var missingBetween = (kind, declared, deployed) => {
400
- const missing = declared.filter((one) => !deployed.includes(one));
401
- const extra = deployed.filter((one) => !declared.includes(one));
402
- return missing.length === 0 && extra.length === 0 ? [] : [{ extra, kind, missing }];
403
282
  };
404
- var driftBetween = (declared, deployed) => [
405
- ...missingBetween("crons", declared.crons, deployed.crons),
406
- ...missingBetween("routes", declared.routes, deployed.routes),
407
- ...missingBetween("bindings", declared.bindings, deployed.bindings),
408
- ...missingBetween("secrets", declared.secrets, deployed.secrets)
409
- ];
410
- var runDeployed = (input) => {
283
+ var MIGRATIONS_TOOL = "release-migrations";
284
+ var MIGRATIONS_NEXT = "geonosis-release migrations --since <ref> --json and compare `files` against `unreadable` \u2014 every added file leaves by exactly one door, and a file in one list and not the other is the accounting bug this refuses over";
285
+ var migrationsEnvelope = (report, durationMs) => ({
286
+ considered: report.files.length,
287
+ durationMs,
288
+ excused: report.files.filter((one) => one.state === "excused").map((one) => ({
289
+ path: one.path,
290
+ reason: "a contract-migration marker this gate could believe"
291
+ })),
292
+ findings: [...report.refusals, ...report.markers, ...report.squawk],
293
+ read: report.files.filter((one) => one.state !== "excused" && one.state !== "unreadable").length,
294
+ refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
295
+ tool: MIGRATIONS_TOOL,
296
+ version: versionOf(import.meta.url)
297
+ });
298
+ var SCHEMA_TOOL = "release-schema";
299
+ var SCHEMA_NEXT = "geonosis-release schema --json and compare `files` against `unreadable` \u2014 the denominator is every migration under every declared domain directory, and a file in neither list is one nothing read (#168)";
300
+ var schemaEnvelope = (report, durationMs) => ({
301
+ considered: report.files.length,
302
+ durationMs,
303
+ excused: [],
304
+ findings: report.findings,
305
+ read: report.read,
306
+ refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
307
+ tool: SCHEMA_TOOL,
308
+ version: versionOf(import.meta.url)
309
+ });
310
+ var PUBLISHED_TOOL = "release-published";
311
+ var PUBLISHED_NEXT = "geonosis-release published --group --json and compare `considered` against `lines` + `excused` \u2014 every manifest in the tree leaves by exactly one door, and a group announced off a sample is the failure this counts against (#147)";
312
+ var publishedEnvelope = (report, durationMs) => ({
313
+ considered: report.considered,
314
+ durationMs,
315
+ excused: report.excused,
316
+ findings: report.lines.filter((one) => one.verdict !== "MATCH"),
317
+ read: report.lines.filter((one) => one.verdict !== "UNREACHABLE").length,
318
+ refused: report.lines.filter((one) => one.verdict === "UNREACHABLE").map((one) => ({ path: one.at, reason: one.why })),
319
+ tool: PUBLISHED_TOOL,
320
+ version: versionOf(import.meta.url)
321
+ });
322
+
323
+ // src/registry.ts
324
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
325
+ var TIMEOUT_MS = 15e3;
326
+ var registryPathOf = (name) => name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
327
+ var versionsIn = (body) => {
328
+ const versions = body.versions;
329
+ return typeof versions === "object" && versions !== null ? Object.keys(versions) : [];
330
+ };
331
+ var latestIn = (body) => {
332
+ const tags = body["dist-tags"];
333
+ const latest = typeof tags === "object" && tags !== null ? tags.latest : void 0;
334
+ return typeof latest === "string" ? latest : void 0;
335
+ };
336
+ var askRegistry = async ({
337
+ name,
338
+ registry = DEFAULT_REGISTRY,
339
+ timeoutMs = TIMEOUT_MS
340
+ }) => {
341
+ const url = `${registry.replace(/\/+$/, "")}/${registryPathOf(name)}`;
342
+ let response;
343
+ try {
344
+ response = await fetch(url, {
345
+ headers: { accept: "application/json" },
346
+ signal: AbortSignal.timeout(timeoutMs)
347
+ });
348
+ } catch (error) {
349
+ return { kind: "unreachable", name, why: `GET ${url} \u2014 ${error.message}` };
350
+ }
351
+ if (response.status === 404) return { kind: "absent", name };
352
+ if (!response.ok) {
353
+ return { kind: "unreachable", name, why: `GET ${url} \u2014 HTTP ${response.status}` };
354
+ }
355
+ let body;
356
+ try {
357
+ body = await response.json();
358
+ } catch (error) {
359
+ return {
360
+ kind: "unreachable",
361
+ name,
362
+ why: `GET ${url} answered ${response.status} with something that is not JSON \u2014 ${error.message}`
363
+ };
364
+ }
365
+ return { kind: "present", latest: latestIn(body), name, versions: versionsIn(body) };
366
+ };
367
+
368
+ // src/published.ts
369
+ import { readdirSync, readFileSync as readFileSync3 } from "fs";
370
+ import { join as join2, relative, sep } from "path";
371
+ var NEVER_WALKED = /* @__PURE__ */ new Set(["build", "coverage", "dist", "node_modules", "storybook-static"]);
372
+ var pathOf = (root, path) => relative(root, path).split(sep).join("/");
373
+ var manifestsUnder = (root) => {
374
+ const found = [];
375
+ const walk = (dir) => {
376
+ let entries;
377
+ try {
378
+ entries = readdirSync(dir, { withFileTypes: true });
379
+ } catch {
380
+ return;
381
+ }
382
+ for (const entry of entries) {
383
+ if (entry.isDirectory()) {
384
+ if (!entry.name.startsWith(".") && !NEVER_WALKED.has(entry.name))
385
+ walk(join2(dir, entry.name));
386
+ continue;
387
+ }
388
+ if (entry.name === "package.json") found.push(join2(dir, entry.name));
389
+ }
390
+ };
391
+ walk(root);
392
+ return found;
393
+ };
394
+ var censusOf = (root) => {
395
+ const excused = [];
396
+ const locals = [];
397
+ for (const path of manifestsUnder(root)) {
398
+ const at = pathOf(root, path);
399
+ let manifest;
400
+ try {
401
+ manifest = JSON.parse(readFileSync3(path, "utf8"));
402
+ } catch (error) {
403
+ excused.push({ path: at, reason: `it does not parse: ${error.message}` });
404
+ continue;
405
+ }
406
+ if (manifest.private === true) {
407
+ excused.push({ path: at, reason: "private: true \u2014 nothing publishes it" });
408
+ continue;
409
+ }
410
+ if (typeof manifest.name !== "string" || manifest.name === "") {
411
+ excused.push({ path: at, reason: "it names no package" });
412
+ continue;
413
+ }
414
+ if (typeof manifest.version !== "string" || manifest.version === "") {
415
+ excused.push({ path: at, reason: "it declares no version" });
416
+ continue;
417
+ }
418
+ locals.push({ at, name: manifest.name, version: manifest.version });
419
+ }
420
+ return { excused, locals: locals.toSorted((a, b) => a.name.localeCompare(b.name)) };
421
+ };
422
+ var EXISTS_BUT = "exists but no version matching";
423
+ var lineFor = async (local, registry) => {
424
+ const answer = await askRegistry({ name: local.name, registry });
425
+ const base = { at: local.at, local: local.version, name: local.name };
426
+ if (answer.kind === "unreachable") {
427
+ return { ...base, latest: void 0, verdict: "UNREACHABLE", why: answer.why };
428
+ }
429
+ if (answer.kind === "absent") {
430
+ return {
431
+ ...base,
432
+ latest: void 0,
433
+ verdict: "ABSENT",
434
+ why: "npm has never heard of this name \u2014 a name published moments ago 404s for a few minutes; --wait <seconds> polls through that window"
435
+ };
436
+ }
437
+ if (answer.latest === local.version) {
438
+ return { ...base, latest: answer.latest, verdict: "MATCH", why: "on npm" };
439
+ }
440
+ return {
441
+ ...base,
442
+ latest: answer.latest,
443
+ verdict: "BEHIND",
444
+ why: answer.versions.includes(local.version) ? `npm has ${local.version}, and its latest tag is ${answer.latest ?? "nothing"} \u2014 the group has not landed on one version` : `the package ${EXISTS_BUT} ${local.version}; npm answers ${answer.latest ?? "nothing"}. Minutes after a publish, suspect the package manager's manifest cache before the registry`
445
+ };
446
+ };
447
+ var sleep = (ms) => new Promise((done) => {
448
+ setTimeout(done, ms);
449
+ });
450
+ var POLL_MS = 5e3;
451
+ var runPublished = async ({
452
+ pollMs = POLL_MS,
453
+ registry = DEFAULT_REGISTRY,
454
+ root,
455
+ waitSeconds = 0
456
+ }) => {
457
+ const { excused, locals } = censusOf(root);
458
+ const startedAt = Date.now();
459
+ const budgetMs = Math.max(0, waitSeconds * 1e3);
460
+ let lines2 = [];
461
+ let sweeps = 0;
462
+ for (; ; ) {
463
+ lines2 = [];
464
+ for (const local of locals) lines2.push(await lineFor(local, registry));
465
+ sweeps += 1;
466
+ const settled = lines2.every((one) => one.verdict === "MATCH");
467
+ const stuck = lines2.some((one) => one.verdict === "UNREACHABLE");
468
+ const left = budgetMs - (Date.now() - startedAt);
469
+ if (settled || stuck || left <= 0) break;
470
+ await sleep(Math.min(pollMs, left));
471
+ }
472
+ const unreachable = lines2.filter((one) => one.verdict === "UNREACHABLE").length;
473
+ return {
474
+ considered: lines2.length + excused.length,
475
+ excused: excused.toSorted((a, b) => a.path.localeCompare(b.path)),
476
+ lines: lines2,
477
+ ok: lines2.length > 0 && lines2.every((one) => one.verdict === "MATCH"),
478
+ registry,
479
+ sweeps,
480
+ unreachable,
481
+ waitedMs: Date.now() - startedAt
482
+ };
483
+ };
484
+ var NOTHING_TO_CHECK = "published FAIL \u2014 no workspace in this tree declares a package a publish could be about, and a check that considered nothing has not passed";
485
+ var formatPublished = (report) => {
486
+ const width = Math.max(1, ...report.lines.map((one) => one.name.length));
487
+ const body = report.lines.map((one) => ` ${one.verdict.padEnd(11)} ${one.name.padEnd(width)} ${one.local} ${one.why}`).join("\n");
488
+ const head = report.lines.length === 0 ? NOTHING_TO_CHECK : `published ${report.ok ? "PASS" : "FAIL"} \u2014 ${report.lines.filter((one) => one.verdict === "MATCH").length}/${report.lines.length} of the group on ${report.registry}`;
489
+ const tail = [
490
+ `considered ${report.considered} manifests: ${report.lines.length} read, ${report.excused.length} excused`,
491
+ report.sweeps > 1 ? `swept ${report.sweeps} times over ${report.waitedMs} ms` : ""
492
+ ].filter((one) => one !== "");
493
+ return `${[head, body, ...tail].filter((one) => one !== "").join("\n")}
494
+ `;
495
+ };
496
+
497
+ // src/smoke.ts
498
+ import { spawnSync } from "child_process";
499
+ import {
500
+ cpSync,
501
+ existsSync as existsSync2,
502
+ mkdirSync as mkdirSync2,
503
+ readdirSync as readdirSync2,
504
+ readFileSync as readFileSync4,
505
+ rmSync,
506
+ statSync,
507
+ writeFileSync as writeFileSync2
508
+ } from "fs";
509
+ import { join as join3, relative as relative2, resolve as resolve3 } from "path";
510
+ var SNAPSHOTS_DIR = ".geonosis/consumer-snapshots";
511
+ var LOCKFILES = [
512
+ { file: "bun.lock", manager: "bun" },
513
+ { file: "bun.lockb", manager: "bun" },
514
+ { file: "pnpm-lock.yaml", manager: "pnpm" }
515
+ ];
516
+ var UNMEASURED = [
517
+ { file: "package-lock.json", manager: "npm" },
518
+ { file: "yarn.lock", manager: "yarn" }
519
+ ];
520
+ var EXCLUDED_DIRS = [
521
+ ".cache",
522
+ ".claude",
523
+ ".geonosis",
524
+ ".git",
525
+ ".next",
526
+ ".turbo",
527
+ ".wrangler",
528
+ "coverage",
529
+ "dist",
530
+ "node_modules",
531
+ "storybook-static"
532
+ ];
533
+ var headOf = (from) => {
534
+ const done = spawnSync("git", ["-C", from, "rev-parse", "HEAD"], { encoding: "utf8" });
535
+ if (done.error !== void 0) {
536
+ return { why: `git could not be run here: ${done.error.message}` };
537
+ }
538
+ if (done.status !== 0) {
539
+ return { why: `git says nothing about ${from}: ${done.stderr.trim()}` };
540
+ }
541
+ return { sha: done.stdout.trim() };
542
+ };
543
+ var DOCTOR_PACKAGE = "@geonosis/doctor";
544
+ var DOCTOR_BIN = "geonosis-doctor";
545
+ var DOCTOR_DOORS = ["@geonosis/cli", "geonosis"];
546
+ var DOOR_BIN = "geonosis doctor";
547
+ var isRecord2 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
548
+ var readManifest = (path) => {
549
+ try {
550
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
551
+ return isRecord2(parsed) ? parsed : {};
552
+ } catch (error) {
553
+ throw new CannotRun(`${path} is not readable JSON: ${error.message}`);
554
+ }
555
+ };
556
+ var versionsIn2 = (manifest) => {
557
+ const found = {};
558
+ for (const key of ["dependencies", "devDependencies"]) {
559
+ const block = manifest[key];
560
+ if (!isRecord2(block)) continue;
561
+ for (const [name, range] of Object.entries(block)) {
562
+ if (typeof range === "string") found[name] = range;
563
+ }
564
+ }
565
+ return Object.fromEntries(Object.entries(found).toSorted(([a], [b]) => a.localeCompare(b)));
566
+ };
567
+ var scriptsIn = (manifest) => {
568
+ const scripts = manifest["scripts"];
569
+ if (!isRecord2(scripts)) return {};
570
+ return Object.fromEntries(
571
+ Object.entries(scripts).filter((one) => typeof one[1] === "string")
572
+ );
573
+ };
574
+ var copyInto = (from, to, excluded) => {
575
+ let bytes = 0;
576
+ let files = 0;
577
+ const walk = (dir, into) => {
578
+ mkdirSync2(into, { recursive: true });
579
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
580
+ if (excluded.has(entry.name)) continue;
581
+ const at = join3(dir, entry.name);
582
+ if (entry.isDirectory()) {
583
+ walk(at, join3(into, entry.name));
584
+ continue;
585
+ }
586
+ if (!entry.isFile()) continue;
587
+ cpSync(at, join3(into, entry.name));
588
+ bytes += statSync(at).size;
589
+ files += 1;
590
+ }
591
+ };
592
+ walk(from, to);
593
+ return { bytes, files };
594
+ };
595
+ var manifestsUnder2 = (root) => {
596
+ const found = [];
597
+ const walk = (dir) => {
598
+ for (const entry of readdirSync2(dir, { withFileTypes: true }).toSorted(
599
+ (a, b) => a.name.localeCompare(b.name)
600
+ )) {
601
+ const at = join3(dir, entry.name);
602
+ if (entry.isDirectory()) {
603
+ walk(at);
604
+ continue;
605
+ }
606
+ if (entry.name !== "package.json") continue;
607
+ found.push({ path: relative2(root, at), versions: versionsIn2(readManifest(at)) });
608
+ }
609
+ };
610
+ walk(root);
611
+ return found.toSorted((a, b) => a.path.localeCompare(b.path));
612
+ };
613
+ var managerOf = (from) => {
614
+ const found = LOCKFILES.find((one) => existsSync2(join3(from, one.file)));
615
+ if (found !== void 0) return { lockfile: found.file, manager: found.manager };
616
+ const unmeasured = UNMEASURED.find((one) => existsSync2(join3(from, one.file)));
617
+ if (unmeasured !== void 0) {
618
+ throw new CannotRun(
619
+ `${from} is a ${unmeasured.manager} tree (${unmeasured.file}), and the pack \u2192 rewrite \u2192 install path has never been measured under ${unmeasured.manager} here. It reads ${LOCKFILES.map((one) => one.file).join(", ")}.`
620
+ );
621
+ }
622
+ throw new CannotRun(
623
+ `nothing in ${from} says which runtime installs it \u2014 looked for ${[...LOCKFILES, ...UNMEASURED].map((one) => one.file).join(", ")}. The smoke installs the way the consumer installs, so the manager is read, never assumed.`
624
+ );
625
+ };
626
+ var commandsFor = (manifest, manifests, named) => {
627
+ const scripts = scriptsIn(manifest);
628
+ const installs = (name) => manifests.some((one) => one.versions[name] !== void 0);
629
+ const doctorCommand = installs(DOCTOR_PACKAGE) ? { exec: DOCTOR_BIN } : DOCTOR_DOORS.some(installs) ? { exec: DOOR_BIN } : {
630
+ why: `no manifest in this tree installs ${DOCTOR_PACKAGE} or a door that brings it (${DOCTOR_DOORS.join(", ")}), so nothing here can run ${DOCTOR_BIN}`
631
+ };
632
+ const phase = (name, fallback) => {
633
+ const override = named[name];
634
+ if (override !== void 0) return { exec: override };
635
+ if (scripts[name] !== void 0) return { run: name };
636
+ return fallback;
637
+ };
638
+ return {
639
+ doctor: phase("doctor", doctorCommand),
640
+ lint: phase("lint", { why: 'the tree has no "lint" script and no --lint command was named' }),
641
+ test: phase("test", { why: 'the tree has no "test" script and no --test command was named' }),
642
+ typecheck: phase("typecheck", {
643
+ why: 'the tree has no "typecheck" script and no --typecheck command was named'
644
+ })
645
+ };
646
+ };
647
+ var asPattern = (glob) => new RegExp(
648
+ `^${glob.split("/").map(
649
+ (part) => part === "**" ? ".*" : part.replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`).replaceAll("*", "[^/]*")
650
+ ).join("/").replaceAll(".*/", "(?:.*/)?")}$`
651
+ );
652
+ var filesUnder = (root, at = root) => readdirSync2(at, { withFileTypes: true }).flatMap((entry) => {
653
+ const full = join3(at, entry.name);
654
+ if (entry.isDirectory()) return filesUnder(root, full);
655
+ return entry.isFile() ? [full.slice(root.length + 1)] : [];
656
+ });
657
+ var seamsIn = (tree) => {
658
+ const at = join3(tree, "geonosis.json");
659
+ if (!existsSync2(at)) return void 0;
660
+ let globs;
661
+ try {
662
+ const declared = JSON.parse(readFileSync4(at, "utf8")).adoption?.seams;
663
+ globs = Array.isArray(declared) ? declared.filter((one) => typeof one === "string") : [];
664
+ } catch {
665
+ return void 0;
666
+ }
667
+ if (globs.length === 0) return void 0;
668
+ const patterns = globs.map(asPattern);
669
+ const lines2 = filesUnder(tree).filter((one) => patterns.some((pattern) => pattern.test(one))).reduce((total, one) => {
670
+ const body = readFileSync4(join3(tree, one), "utf8");
671
+ return total + (body === "" ? 0 : body.replace(/\n$/, "").split("\n").length);
672
+ }, 0);
673
+ return { globs, lines: lines2 };
674
+ };
675
+ var snapshotDir = (root, name) => join3(root, SNAPSHOTS_DIR, name);
676
+ var readSnapshot = (root, name) => {
677
+ const at = join3(snapshotDir(root, name), "snapshot.json");
678
+ if (!existsSync2(at)) {
679
+ throw new CannotRun(
680
+ `there is no snapshot called "${name}" here \u2014 ${at} does not exist. Record one with: geonosis-release smoke snapshot ${name} --from <their tree>`
681
+ );
682
+ }
683
+ return JSON.parse(readFileSync4(at, "utf8"));
684
+ };
685
+ var wouldBeCommitted = (root, path) => {
686
+ const inside = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root });
687
+ if (inside.error !== void 0 || inside.status !== 0) return false;
688
+ return spawnSync("git", ["check-ignore", "-q", path], { cwd: root }).status !== 0;
689
+ };
690
+ var NAME_SHAPE = /^[a-z0-9][\w.-]*$/i;
691
+ var runSnapshot = (input) => {
692
+ if (!NAME_SHAPE.test(input.name)) {
693
+ throw new CannotRun(
694
+ `"${input.name}" is not a snapshot name \u2014 a name is letters, digits, dots, dashes and underscores, so that ${SNAPSHOTS_DIR}/<name> is the only place the bytes can land`
695
+ );
696
+ }
697
+ const from = resolve3(input.from);
698
+ if (!existsSync2(join3(from, "package.json"))) {
699
+ throw new CannotRun(`${from} has no package.json \u2014 that is not a tree a consumer installs`);
700
+ }
701
+ const at = snapshotDir(input.root, input.name);
702
+ if (wouldBeCommitted(input.root, join3(SNAPSHOTS_DIR, input.name))) {
703
+ throw new CannotRun(
704
+ `git here does not ignore ${SNAPSHOTS_DIR}/ \u2014 a consumer's tree is private (D-048) and a copy that reaches a commit cannot be taken back. Add ".geonosis/" to .gitignore and run this again.`
705
+ );
706
+ }
707
+ if (existsSync2(at) && !input.replace) {
708
+ throw new CannotRun(
709
+ `${at} is already a snapshot, and a baseline recorded against other bytes is worse than none \u2014 pass --replace to overwrite it`
710
+ );
711
+ }
712
+ const { lockfile, manager } = managerOf(from);
713
+ const excluded = [.../* @__PURE__ */ new Set([...EXCLUDED_DIRS, ...input.exclude ?? []])].toSorted();
714
+ rmSync(at, { force: true, recursive: true });
715
+ const tree = join3(at, "tree");
716
+ const { bytes, files } = copyInto(from, tree, new Set(excluded));
717
+ const manifests = manifestsUnder2(tree);
718
+ const seams = seamsIn(tree);
719
+ const record = {
720
+ at: (/* @__PURE__ */ new Date()).toISOString(),
721
+ bytes,
722
+ ...seams === void 0 ? {} : { seams },
723
+ commit: headOf(from),
724
+ commands: commandsFor(readManifest(join3(tree, "package.json")), manifests, input.named),
725
+ excluded,
726
+ files,
727
+ from,
728
+ lockfile,
729
+ manager,
730
+ manifests,
731
+ name: input.name
732
+ };
733
+ writeFileSync2(join3(at, "snapshot.json"), `${JSON.stringify(record, void 0, 2)}
734
+ `);
735
+ return record;
736
+ };
737
+ var describeCommand = (command) => {
738
+ if ("run" in command) return `run ${command.run}`;
739
+ if ("exec" in command) return command.exec;
740
+ return `nothing \u2014 ${command.why}`;
741
+ };
742
+ var describeCommit = (commit) => {
743
+ if (commit === void 0) return "nothing recorded its commit";
744
+ if ("sha" in commit) return commit.sha;
745
+ return commit.why;
746
+ };
747
+ var formatSnapshot = (record) => [
748
+ `OK snapshot ${record.name}: ${record.files} files, ${record.bytes} bytes, ${record.manifests.length} manifests
749
+ `,
750
+ ` from ${record.from} \u2014 ${record.manager} (${record.lockfile})
751
+ `,
752
+ ` at ${describeCommit(record.commit)}
753
+ `,
754
+ ...PHASES.map((phase) => ` ${phase}: ${describeCommand(record.commands[phase])}
755
+ `)
756
+ ].join("");
757
+
758
+ // src/adoption.ts
759
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
760
+ import { join as join4 } from "path";
761
+ var groupsOf = (root) => {
762
+ const at = join4(root, ".changeset/config.json");
763
+ if (!existsSync3(at)) return [];
764
+ let parsed;
765
+ try {
766
+ parsed = JSON.parse(readFileSync5(at, "utf8"));
767
+ } catch (error) {
768
+ throw new CannotRun(`${at} is not readable JSON: ${error.message}`);
769
+ }
770
+ const fixed = parsed.fixed;
771
+ return Array.isArray(fixed) ? fixed.filter((one) => Array.isArray(one) && one.every(isString)) : [];
772
+ };
773
+ var isString = (value2) => typeof value2 === "string";
774
+ var RANGE_PREFIX = /^[\^~>=<v\s]+/;
775
+ var RELEASE_NUMBER = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/;
776
+ var numberIn = (spec) => {
777
+ const bare = spec.replace(RANGE_PREFIX, "").trim();
778
+ return RELEASE_NUMBER.test(bare) ? bare : void 0;
779
+ };
780
+ var declarationsIn = (record, kit) => record.manifests.flatMap(
781
+ (manifest) => Object.entries(manifest.versions).filter(([name]) => kit.has(name)).map(([name, spec]) => ({ name, path: manifest.path, spec }))
782
+ );
783
+ var groupName = (index, total) => total > 1 ? `fixed group ${index + 1}` : "the fixed group";
784
+ var incoherence = (consumer, groups, declarations) => groups.flatMap((group, index) => {
785
+ const members = new Set(group);
786
+ const versions = [
787
+ ...new Set(
788
+ declarations.filter((one) => members.has(one.name)).flatMap((one) => {
789
+ const number = numberIn(one.spec);
790
+ return number === void 0 ? [] : [number];
791
+ })
792
+ )
793
+ ].toSorted();
794
+ if (versions.length < 2) return [];
795
+ const name = groupName(index, groups.length);
796
+ return [
797
+ {
798
+ consumer,
799
+ detail: `${name} at ${versions.join(", ")} \u2014 they are published as one number, so this tree is a bump that landed halfway`,
800
+ name,
801
+ verdict: "INCOHERENT"
802
+ }
803
+ ];
804
+ });
805
+ var distance = (consumer, declarations, current) => declarations.flatMap((one) => {
806
+ const now = current.get(one.name);
807
+ if (now === void 0) return [];
808
+ const declared = numberIn(one.spec);
809
+ if (declared === void 0) {
810
+ return [
811
+ {
812
+ consumer,
813
+ detail: `${one.path} names it "${one.spec}", which is not a release number this can compare`,
814
+ name: one.name,
815
+ verdict: "UNJUDGED"
816
+ }
817
+ ];
818
+ }
819
+ if (declared === now) return [];
820
+ return [{ consumer, detail: `${declared} \u2192 ${now}`, name: one.name, verdict: "BEHIND" }];
821
+ });
822
+ var undeclaredFloors = (consumer, floors, declarations) => {
823
+ const declared = new Set(declarations.map((one) => one.name));
824
+ return floors.filter((name) => !declared.has(name)).map((name) => ({
825
+ consumer,
826
+ detail: "nothing in this tree declares it \u2014 informational, a floor is opted into",
827
+ name,
828
+ verdict: "FLOOR UNDECLARED"
829
+ }));
830
+ };
831
+ var currentVersions = async (root, registry) => {
832
+ const locals = censusOf(root).locals;
833
+ const current = new Map(locals.map((one) => [one.name, one.version]));
834
+ if (registry === void 0) return current;
835
+ for (const one of locals) {
836
+ const answer = await askRegistry({ name: one.name, registry });
837
+ if (answer.kind === "present" && answer.latest !== void 0)
838
+ current.set(one.name, answer.latest);
839
+ }
840
+ return current;
841
+ };
842
+ var runAdoption = async ({
843
+ declared,
844
+ registry,
845
+ root
846
+ }) => {
847
+ if (declared.length === 0) {
848
+ throw new CannotRun(
849
+ "nothing here names a consumer to measure \u2014 geonosis.json \u2192 release.smoke.snapshots names them. An adoption sweep over zero consumers is not a scoreboard."
850
+ );
851
+ }
852
+ const current = await currentVersions(root, registry);
853
+ const groups = groupsOf(root);
854
+ const grouped = new Set(groups.flat());
855
+ const floors = [...current.keys()].filter((name) => !grouped.has(name)).toSorted();
856
+ const consumers = [];
857
+ const excused = [];
858
+ for (const one of declared) {
859
+ if (!existsSync3(join4(snapshotDir(root, one.name), "snapshot.json"))) {
860
+ excused.push({
861
+ path: one.name,
862
+ reason: `no recording of it here \u2014 record one with: geonosis-release smoke snapshot ${one.name} --from <their tree>`
863
+ });
864
+ continue;
865
+ }
866
+ const record = readSnapshot(root, one.name);
867
+ const declarations = declarationsIn(record, new Set(current.keys()));
868
+ const seams = record.seams;
869
+ consumers.push({
870
+ declared: declarations.length,
871
+ ...seams === void 0 ? {} : { seams },
872
+ findings: [
873
+ ...distance(one.name, declarations, current),
874
+ ...incoherence(one.name, groups, declarations),
875
+ ...undeclaredFloors(one.name, floors, declarations)
876
+ ],
877
+ name: one.name
878
+ });
879
+ }
880
+ return {
881
+ considered: declared.length,
882
+ consumers,
883
+ excused,
884
+ registry,
885
+ versions: Object.fromEntries([...current.entries()].toSorted(([a], [b]) => a.localeCompare(b)))
886
+ };
887
+ };
888
+ var NOT_A_FAULT = /* @__PURE__ */ new Set(["FLOOR UNDECLARED"]);
889
+ var lineOf = (finding) => {
890
+ if (finding.verdict === "INCOHERENT") return ` INCOHERENT ${finding.consumer}: ${finding.detail}`;
891
+ if (finding.verdict === "FLOOR UNDECLARED") {
892
+ return ` FLOOR UNDECLARED ${finding.consumer} ${finding.name}`;
893
+ }
894
+ return ` ${finding.verdict} ${finding.consumer} ${finding.name} ${finding.detail}`;
895
+ };
896
+ var blockFor = (consumer) => {
897
+ const faults = consumer.findings.filter((one) => !NOT_A_FAULT.has(one.verdict));
898
+ const head = faults.length === 0 ? [
899
+ ` MATCH ${consumer.name} \u2014 ${consumer.declared} spec(s) on the versions this release is cut at`
900
+ ] : [];
901
+ const seams = consumer.seams === void 0 ? [] : [
902
+ ` SEAMS ${consumer.name} \u2014 ${consumer.seams.lines} lines in ${consumer.seams.globs.join(", ")} at this recording; a floor is adopted the day it deletes more than it adds`
903
+ ];
904
+ return [...head, ...seams, ...new Set(consumer.findings.map(lineOf))];
905
+ };
906
+ var formatAdoption = (report) => [
907
+ `adoption \u2014 ${report.considered} declared, ${report.consumers.length} read, ${report.excused.length} excused; against ${report.registry ?? "this workspace"}`,
908
+ ...report.consumers.flatMap(blockFor),
909
+ ...report.excused.map((one) => ` SKIP ${one.path}: ${one.reason}`),
910
+ ""
911
+ ].join("\n");
912
+ var ADOPTION_TOOL = "release-adoption";
913
+ var ADOPTION_NEXT = "geonosis-release adoption --json and read `consumers` \u2014 every declared consumer leaves by exactly one door, read or excused, and a release review carries one row per consumer from it (D-061)";
914
+ var adoptionEnvelope = (report, durationMs) => ({
915
+ considered: report.considered,
916
+ durationMs,
917
+ excused: report.excused,
918
+ findings: report.consumers.flatMap((one) => one.findings),
919
+ read: report.consumers.length,
920
+ refused: [],
921
+ tool: ADOPTION_TOOL,
922
+ version: versionOf(import.meta.url)
923
+ });
924
+
925
+ // src/wrangler.ts
926
+ import { readFileSync as readFileSync6 } from "fs";
927
+ import { resolve as resolve4 } from "path";
928
+ var isRecord3 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
929
+ var parseJsonc = (source) => {
930
+ let out = "";
931
+ for (let index = 0; index < source.length; index += 1) {
932
+ const char = source[index] ?? "";
933
+ if (char === '"') {
934
+ const start = index;
935
+ index += 1;
936
+ for (; index < source.length; index += 1) {
937
+ if (source[index] === "\\") {
938
+ index += 1;
939
+ continue;
940
+ }
941
+ if (source[index] === '"') break;
942
+ }
943
+ out += source.slice(start, index + 1);
944
+ continue;
945
+ }
946
+ if (char === "/" && source[index + 1] === "/") {
947
+ const end = source.indexOf("\n", index);
948
+ index = end === -1 ? source.length : end - 1;
949
+ continue;
950
+ }
951
+ if (char === "/" && source[index + 1] === "*") {
952
+ const end = source.indexOf("*/", index + 2);
953
+ index = end === -1 ? source.length : end + 1;
954
+ continue;
955
+ }
956
+ out += char;
957
+ }
958
+ return JSON.parse(out.replaceAll(/,(\s*[\]}])/g, "$1"));
959
+ };
960
+ var literal = (text) => {
961
+ const value2 = text.trim();
962
+ if (value2.startsWith('"') || value2.startsWith("'")) return value2.slice(1, -1);
963
+ if (value2 === "true") return true;
964
+ if (value2 === "false") return false;
965
+ const number = Number(value2);
966
+ return Number.isNaN(number) ? value2 : number;
967
+ };
968
+ var split = (text, delimiter) => {
969
+ const parts = [];
970
+ let depth = 0;
971
+ let quote = "";
972
+ let current = "";
973
+ for (const char of text) {
974
+ if (quote !== "") {
975
+ current += char;
976
+ if (char === quote) quote = "";
977
+ continue;
978
+ }
979
+ if (char === '"' || char === "'") quote = char;
980
+ if (char === "[" || char === "{") depth += 1;
981
+ if (char === "]" || char === "}") depth -= 1;
982
+ if (char === delimiter && depth === 0) {
983
+ parts.push(current);
984
+ current = "";
985
+ continue;
986
+ }
987
+ current += char;
988
+ }
989
+ parts.push(current);
990
+ return parts.filter((one) => one.trim() !== "");
991
+ };
992
+ var value = (text) => {
993
+ const trimmed = text.trim();
994
+ if (trimmed.startsWith("[")) return split(trimmed.slice(1, -1), ",").map(value);
995
+ if (trimmed.startsWith("{")) {
996
+ return Object.fromEntries(
997
+ split(trimmed.slice(1, -1), ",").map((pair) => {
998
+ const at = pair.indexOf("=");
999
+ return [pair.slice(0, at).trim(), value(pair.slice(at + 1))];
1000
+ })
1001
+ );
1002
+ }
1003
+ return literal(trimmed);
1004
+ };
1005
+ var put = (into, path, leaf) => {
1006
+ let here = into;
1007
+ for (const key of path.slice(0, -1)) {
1008
+ if (!isRecord3(here[key])) here[key] = {};
1009
+ here = here[key];
1010
+ }
1011
+ here[path.at(-1) ?? ""] = leaf;
1012
+ };
1013
+ var table = (into, path) => {
1014
+ let here = into;
1015
+ for (const key of path) {
1016
+ if (!isRecord3(here[key])) here[key] = {};
1017
+ here = here[key];
1018
+ }
1019
+ return here;
1020
+ };
1021
+ var arrayTable = (into, path) => {
1022
+ let here = into;
1023
+ for (const key of path.slice(0, -1)) {
1024
+ if (!isRecord3(here[key])) here[key] = {};
1025
+ here = here[key];
1026
+ }
1027
+ const last = path.at(-1) ?? "";
1028
+ if (!Array.isArray(here[last])) here[last] = [];
1029
+ const list = here[last];
1030
+ const entry = {};
1031
+ list.push(entry);
1032
+ return entry;
1033
+ };
1034
+ var parseToml = (source) => {
1035
+ const out = {};
1036
+ let here = out;
1037
+ const lines2 = source.split("\n");
1038
+ for (let index = 0; index < lines2.length; index += 1) {
1039
+ const line = (lines2[index] ?? "").split("#")[0]?.trim() ?? "";
1040
+ if (line === "") continue;
1041
+ if (line.startsWith("[[") && line.endsWith("]]")) {
1042
+ here = arrayTable(out, line.slice(2, -2).trim().split("."));
1043
+ continue;
1044
+ }
1045
+ if (line.startsWith("[") && line.endsWith("]")) {
1046
+ here = table(out, line.slice(1, -1).trim().split("."));
1047
+ continue;
1048
+ }
1049
+ const at = line.indexOf("=");
1050
+ if (at === -1) continue;
1051
+ let text = line.slice(at + 1);
1052
+ while ([...text].filter((one) => one === "[").length > [...text].filter((one) => one === "]").length) {
1053
+ index += 1;
1054
+ if (index >= lines2.length) throw new CannotRun("an unterminated array in the TOML config");
1055
+ text += `
1056
+ ${(lines2[index] ?? "").split("#")[0] ?? ""}`;
1057
+ }
1058
+ put(here, line.slice(0, at).trim().split("."), value(text));
1059
+ }
1060
+ return out;
1061
+ };
1062
+ var BINDING_LISTS = [
1063
+ "ai",
1064
+ "analytics_engine_datasets",
1065
+ "browser",
1066
+ "d1_databases",
1067
+ "dispatch_namespaces",
1068
+ "durable_objects",
1069
+ "hyperdrive",
1070
+ "kv_namespaces",
1071
+ "mtls_certificates",
1072
+ "queues",
1073
+ "r2_buckets",
1074
+ "send_email",
1075
+ "services",
1076
+ "vectorize",
1077
+ "version_metadata",
1078
+ "workflows"
1079
+ ];
1080
+ var bindingsOf = (found) => {
1081
+ if (Array.isArray(found)) return found.flatMap(bindingsOf);
1082
+ if (!isRecord3(found)) return [];
1083
+ const named = found["binding"] ?? found["name"];
1084
+ const here = typeof named === "string" ? [named] : [];
1085
+ const nested = Object.entries(found).filter(([key]) => key === "bindings" || key === "producers" || key === "consumers").flatMap(([, one]) => bindingsOf(one));
1086
+ return [...here, ...nested];
1087
+ };
1088
+ var declaredIn = (config) => {
1089
+ const triggers = config["triggers"];
1090
+ const crons = isRecord3(triggers) && Array.isArray(triggers["crons"]) ? triggers["crons"] : [];
1091
+ const routes = config["routes"];
1092
+ const one = config["route"];
1093
+ const listed = [
1094
+ ...Array.isArray(routes) ? routes : [],
1095
+ ...typeof one === "string" ? [one] : []
1096
+ ];
1097
+ return {
1098
+ bindings: BINDING_LISTS.flatMap((key) => bindingsOf(config[key])).toSorted(),
1099
+ crons: crons.filter((cron) => typeof cron === "string").toSorted(),
1100
+ routes: listed.map((route) => isRecord3(route) ? route["pattern"] : route).filter((pattern) => typeof pattern === "string").toSorted()
1101
+ };
1102
+ };
1103
+ var readWrangler = (root, relative5, env) => {
1104
+ const path = resolve4(root, relative5);
1105
+ let parsed;
1106
+ try {
1107
+ const source = readFileSync6(path, "utf8");
1108
+ parsed = relative5.endsWith(".toml") ? parseToml(source) : parseJsonc(source);
1109
+ } catch (error) {
1110
+ throw new CannotRun(`${relative5} could not be read: ${error.message}`);
1111
+ }
1112
+ if (!isRecord3(parsed)) throw new CannotRun(`${relative5} is not a wrangler configuration`);
1113
+ const environments = parsed["env"];
1114
+ const block = env !== void 0 && isRecord3(environments) && isRecord3(environments[env]) ? environments[env] : parsed;
1115
+ return declaredIn(block);
1116
+ };
1117
+
1118
+ // src/deployed.ts
1119
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
1120
+ import { resolve as resolve5 } from "path";
1121
+ var DEPLOYED_FILE = ".geonosis/deployed.json";
1122
+ var NOTHING_SAYS = "nothing here says what is deployed \u2014 .geonosis/deployed.json is written by the pipeline after promote, and its absence is not a pass";
1123
+ var listOf = (found) => Array.isArray(found) ? found.filter((one) => typeof one === "string") : [];
1124
+ var isRecord4 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1125
+ var readDeployed = (root) => {
1126
+ const path = resolve5(root, DEPLOYED_FILE);
1127
+ if (!existsSync4(path)) throw new CannotRun(NOTHING_SAYS);
1128
+ let parsed;
1129
+ try {
1130
+ parsed = JSON.parse(readFileSync7(path, "utf8"));
1131
+ } catch (error) {
1132
+ throw new CannotRun(`${DEPLOYED_FILE} is not readable JSON: ${error.message}`);
1133
+ }
1134
+ if (!isRecord4(parsed)) throw new CannotRun(`${DEPLOYED_FILE} is not an object`);
1135
+ const triggers = isRecord4(parsed["triggers"]) ? parsed["triggers"] : {};
1136
+ return {
1137
+ ...typeof parsed["at"] === "string" ? { at: parsed["at"] } : {},
1138
+ deployed: {
1139
+ bindings: listOf(parsed["bindings"]),
1140
+ crons: listOf(triggers["crons"]),
1141
+ routes: listOf(triggers["routes"]),
1142
+ secrets: listOf(parsed["secrets"])
1143
+ }
1144
+ };
1145
+ };
1146
+ var missingBetween = (kind, declared, deployed) => {
1147
+ const missing = declared.filter((one) => !deployed.includes(one));
1148
+ const extra = deployed.filter((one) => !declared.includes(one));
1149
+ return missing.length === 0 && extra.length === 0 ? [] : [{ extra, kind, missing }];
1150
+ };
1151
+ var driftBetween = (declared, deployed) => [
1152
+ ...missingBetween("crons", declared.crons, deployed.crons),
1153
+ ...missingBetween("routes", declared.routes, deployed.routes),
1154
+ ...missingBetween("bindings", declared.bindings, deployed.bindings),
1155
+ ...missingBetween("secrets", declared.secrets, deployed.secrets)
1156
+ ];
1157
+ var runDeployed = (input) => {
411
1158
  const config = readReleaseConfig(input.root);
412
1159
  const configs = config.wrangler ?? [];
413
1160
  const secrets = config.secrets ?? [];
@@ -441,7 +1188,7 @@ var formatDeployed = (report) => {
441
1188
  };
442
1189
 
443
1190
  // src/added.ts
444
- import { execFileSync, spawnSync } from "child_process";
1191
+ import { execFileSync, spawnSync as spawnSync2 } from "child_process";
445
1192
  var git = (root, args) => {
446
1193
  try {
447
1194
  return execFileSync("git", [...args], { cwd: root, encoding: "utf8", stdio: "pipe" });
@@ -457,7 +1204,7 @@ var addedSince = (root, since) => {
457
1204
  return [.../* @__PURE__ */ new Set([...committed, ...staged, ...untracked])].toSorted();
458
1205
  };
459
1206
  var gitOk = (root, args) => {
460
- const run = spawnSync("git", [...args], { cwd: root, encoding: "utf8" });
1207
+ const run = spawnSync2("git", [...args], { cwd: root, encoding: "utf8" });
461
1208
  return run.error === void 0 && run.status === 0;
462
1209
  };
463
1210
 
@@ -647,32 +1394,32 @@ var NARROWING = [
647
1394
  ["SET NOT NULL", /\bSET\s+NOT\s+NULL\b/i]
648
1395
  ];
649
1396
  var statementsOf = (sql) => sql.replaceAll(/'[^']*'/g, (found) => `'${found.slice(1, -1).replaceAll(/[^\n]/g, " ")}'`);
650
- var lineOf = (body, index) => body.slice(0, index).split("\n").length;
1397
+ var lineOf2 = (body, index) => body.slice(0, index).split("\n").length;
651
1398
  var refusalsIn = (path, body, firstLine = 1) => {
652
1399
  const stripped = statementsOf(body);
653
1400
  return NARROWING.flatMap(([verb, pattern]) => {
654
1401
  const found = pattern.exec(stripped);
655
- return found === null ? [] : [{ line: lineOf(stripped, found.index) + firstLine - 1, path, verb }];
1402
+ return found === null ? [] : [{ line: lineOf2(stripped, found.index) + firstLine - 1, path, verb }];
656
1403
  });
657
1404
  };
658
1405
 
659
1406
  // src/migrations.ts
660
- import { mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
1407
+ import { mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
661
1408
  import { tmpdir } from "os";
662
- import { basename, join, resolve as resolve5 } from "path";
1409
+ import { basename, join as join5, resolve as resolve7 } from "path";
663
1410
 
664
1411
  // src/squawk.ts
665
- import { spawnSync as spawnSync2 } from "child_process";
666
- import { existsSync as existsSync3 } from "fs";
667
- import { dirname, resolve as resolve4 } from "path";
668
- import { fileURLToPath } from "url";
669
- var HERE = dirname(fileURLToPath(import.meta.url));
1412
+ import { spawnSync as spawnSync3 } from "child_process";
1413
+ import { existsSync as existsSync5 } from "fs";
1414
+ import { dirname as dirname2, resolve as resolve6 } from "path";
1415
+ import { fileURLToPath as fileURLToPath2 } from "url";
1416
+ var HERE = dirname2(fileURLToPath2(import.meta.url));
670
1417
  var PINNED = "2.63.0";
671
1418
  var findSquawk = (from = HERE) => {
672
- for (let dir = from; ; dir = dirname(dir)) {
673
- const candidate = resolve4(dir, "node_modules/.bin/squawk");
674
- if (existsSync3(candidate)) return candidate;
675
- if (dirname(dir) === dir) break;
1419
+ for (let dir = from; ; dir = dirname2(dir)) {
1420
+ const candidate = resolve6(dir, "node_modules/.bin/squawk");
1421
+ if (existsSync5(candidate)) return candidate;
1422
+ if (dirname2(dir) === dir) break;
676
1423
  }
677
1424
  throw new CannotRun(
678
1425
  `squawk-cli is not installed beside this package, and the postgres and mikro-orm-ts dialects cannot be measured without it \u2014 a gate that cannot measure has not passed. Install it:
@@ -685,7 +1432,7 @@ It is an optional peer: a repo whose release.migrations names only the sqlite di
685
1432
  var squawkOn = (input) => {
686
1433
  const { cwd, exclude, files } = input;
687
1434
  if (files.length === 0) return { findings: "", ok: true };
688
- const run = spawnSync2(
1435
+ const run = spawnSync3(
689
1436
  input.bin ?? findSquawk(),
690
1437
  [...exclude.length === 0 ? [] : [`--exclude=${exclude.join(",")}`], ...files],
691
1438
  { cwd, encoding: "utf8" }
@@ -752,7 +1499,7 @@ var runMigrations = (input) => {
752
1499
  const forSquawk = [];
753
1500
  const states = /* @__PURE__ */ new Map();
754
1501
  for (const { entry, file } of matched) {
755
- const source = readFileSync4(resolve5(input.root, file), "utf8");
1502
+ const source = readFileSync8(resolve7(input.root, file), "utf8");
756
1503
  const found = markersIn(source);
757
1504
  const bad = found.flatMap((one) => {
758
1505
  const why = judge(one, input.root, input.since);
@@ -777,11 +1524,11 @@ var runMigrations = (input) => {
777
1524
  }
778
1525
  let clean = true;
779
1526
  if (forSquawk.length > 0) {
780
- const staging = mkdtempSync(join(tmpdir(), "geonosis-release-squawk-"));
1527
+ const staging = mkdtempSync(join5(tmpdir(), "geonosis-release-squawk-"));
781
1528
  try {
782
1529
  for (const one of forSquawk) {
783
- const path = one.entry.dialect === "mikro-orm-ts" ? join(staging, `${basename(one.file, ".ts")}.sql`) : resolve5(input.root, one.file);
784
- if (one.entry.dialect === "mikro-orm-ts") writeFileSync(path, `${one.sql}
1530
+ const path = one.entry.dialect === "mikro-orm-ts" ? join5(staging, `${basename(one.file, ".ts")}.sql`) : resolve7(input.root, one.file);
1531
+ if (one.entry.dialect === "mikro-orm-ts") writeFileSync3(path, `${one.sql}
785
1532
  `);
786
1533
  const found = squawkOn({
787
1534
  cwd: input.root,
@@ -795,7 +1542,7 @@ var runMigrations = (input) => {
795
1542
  }
796
1543
  }
797
1544
  } finally {
798
- rmSync(staging, { force: true, recursive: true });
1545
+ rmSync2(staging, { force: true, recursive: true });
799
1546
  }
800
1547
  }
801
1548
  return {
@@ -925,7 +1672,7 @@ var Runner = class {
925
1672
  if (this.closed !== void 0) {
926
1673
  throw new CannotRun(`the runner is gone before "${request.step}": ${this.closed}`);
927
1674
  }
928
- const line = await new Promise((resolve9, reject) => {
1675
+ const line = await new Promise((resolve10, reject) => {
929
1676
  const timer = setTimeout(() => {
930
1677
  reject(
931
1678
  new CannotRun(
@@ -935,7 +1682,7 @@ var Runner = class {
935
1682
  }, timeoutMs);
936
1683
  this.waiting.push((answer) => {
937
1684
  clearTimeout(timer);
938
- resolve9(answer);
1685
+ resolve10(answer);
939
1686
  });
940
1687
  this.child.on("close", () => {
941
1688
  clearTimeout(timer);
@@ -959,8 +1706,8 @@ var Runner = class {
959
1706
  };
960
1707
 
961
1708
  // src/prove.ts
962
- import { existsSync as existsSync4 } from "fs";
963
- import { fileURLToPath as fileURLToPath2 } from "url";
1709
+ import { existsSync as existsSync6 } from "fs";
1710
+ import { fileURLToPath as fileURLToPath3 } from "url";
964
1711
  var stringAt = (reply, key) => typeof reply[key] === "string" ? reply[key] : void 0;
965
1712
  var proveOver = async (input) => {
966
1713
  const runner = new Runner(input.command, input.args ?? [], input.cwd);
@@ -987,264 +1734,90 @@ var proveOver = async (input) => {
987
1734
  }
988
1735
  steps.push("promote");
989
1736
  const promoted = await runner.ask({ step: "promote", versionId }, input.timeoutMs);
990
- if (promoted["ok"] !== true) return refuse(`the promote of ${versionId} did not pass`);
991
- const went = stringAt(promoted, "promotedVersionId");
992
- if (went !== void 0 && went !== versionId) {
993
- return refuse(`${versionId} was proved and ${went} was promoted \u2014 nothing proved ${went}`);
994
- }
995
- return { ok: true, steps };
996
- } finally {
997
- runner.close();
998
- }
999
- };
1000
- var PLANTS = [
1001
- {
1002
- name: "answered",
1003
- says: "a smoke that answered a version other than the one it overrode to is refused"
1004
- },
1005
- { name: "failed", says: "a smoke that did not pass is refused" },
1006
- { name: "promoted", says: "a promote of a version nothing proved is refused" }
1007
- ];
1008
- var stubPath = () => {
1009
- const path = fileURLToPath2(new URL("./stub-runner.js", import.meta.url));
1010
- if (!existsSync4(path)) throw new CannotRun(`${path} is missing \u2014 run pnpm build first`);
1011
- return path;
1012
- };
1013
- var prove = async (cwd, stub = stubPath()) => {
1014
- const lines2 = [];
1015
- let ok = true;
1016
- for (const plant of PLANTS) {
1017
- const verdict = await proveOver({
1018
- args: [stub, `--plant=${plant.name}`],
1019
- command: process.execPath,
1020
- cwd,
1021
- timeoutMs: 3e4
1022
- });
1023
- if (verdict.ok) ok = false;
1024
- lines2.push(` ${verdict.ok ? "MISSED" : "PROVEN"} ${plant.says}`);
1025
- if (plant.name !== "promoted" && verdict.steps.includes("promote")) {
1026
- ok = false;
1027
- lines2.push(" MISSED a refused smoke was followed by a promote request");
1028
- }
1029
- }
1030
- const honest = await proveOver({
1031
- args: [stub, "--plant=good"],
1032
- command: process.execPath,
1033
- cwd,
1034
- timeoutMs: 3e4
1035
- });
1036
- if (!honest.ok) ok = false;
1037
- lines2.push(
1038
- ` ${honest.ok ? "PROVEN" : "MISSED"} a smoke that answered the version it overrode to is not refused`
1039
- );
1040
- return { lines: lines2, ok };
1041
- };
1042
- var formatProve = (outcome) => `${outcome.lines.join("\n")}
1043
-
1044
- prove ${outcome.ok ? "PASS" : "FAIL"} \u2014 ${outcome.ok ? "every plant was refused, and the honest runner was not" : "a plant went through"}.
1045
- `;
1046
- var formatVerdict = (verdict) => verdict.ok ? `OK prove: ${verdict.steps.join(" \u2192 ")}
1047
- ` : ` REFUSED after ${verdict.steps.join(" \u2192 ")}: ${verdict.why ?? ""}
1048
- `;
1049
-
1050
- // src/registry.ts
1051
- var DEFAULT_REGISTRY = "https://registry.npmjs.org";
1052
- var TIMEOUT_MS = 15e3;
1053
- var registryPathOf = (name) => name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
1054
- var versionsIn = (body) => {
1055
- const versions = body.versions;
1056
- return typeof versions === "object" && versions !== null ? Object.keys(versions) : [];
1057
- };
1058
- var latestIn = (body) => {
1059
- const tags = body["dist-tags"];
1060
- const latest = typeof tags === "object" && tags !== null ? tags.latest : void 0;
1061
- return typeof latest === "string" ? latest : void 0;
1062
- };
1063
- var askRegistry = async ({
1064
- name,
1065
- registry = DEFAULT_REGISTRY,
1066
- timeoutMs = TIMEOUT_MS
1067
- }) => {
1068
- const url = `${registry.replace(/\/+$/, "")}/${registryPathOf(name)}`;
1069
- let response;
1070
- try {
1071
- response = await fetch(url, {
1072
- headers: { accept: "application/json" },
1073
- signal: AbortSignal.timeout(timeoutMs)
1074
- });
1075
- } catch (error) {
1076
- return { kind: "unreachable", name, why: `GET ${url} \u2014 ${error.message}` };
1077
- }
1078
- if (response.status === 404) return { kind: "absent", name };
1079
- if (!response.ok) {
1080
- return { kind: "unreachable", name, why: `GET ${url} \u2014 HTTP ${response.status}` };
1081
- }
1082
- let body;
1083
- try {
1084
- body = await response.json();
1085
- } catch (error) {
1086
- return {
1087
- kind: "unreachable",
1088
- name,
1089
- why: `GET ${url} answered ${response.status} with something that is not JSON \u2014 ${error.message}`
1090
- };
1091
- }
1092
- return { kind: "present", latest: latestIn(body), name, versions: versionsIn(body) };
1093
- };
1094
-
1095
- // src/published.ts
1096
- import { readdirSync, readFileSync as readFileSync5 } from "fs";
1097
- import { join as join2, relative, sep } from "path";
1098
- var NEVER_WALKED = /* @__PURE__ */ new Set(["build", "coverage", "dist", "node_modules", "storybook-static"]);
1099
- var pathOf = (root, path) => relative(root, path).split(sep).join("/");
1100
- var manifestsUnder = (root) => {
1101
- const found = [];
1102
- const walk = (dir) => {
1103
- let entries;
1104
- try {
1105
- entries = readdirSync(dir, { withFileTypes: true });
1106
- } catch {
1107
- return;
1108
- }
1109
- for (const entry of entries) {
1110
- if (entry.isDirectory()) {
1111
- if (!entry.name.startsWith(".") && !NEVER_WALKED.has(entry.name))
1112
- walk(join2(dir, entry.name));
1113
- continue;
1114
- }
1115
- if (entry.name === "package.json") found.push(join2(dir, entry.name));
1116
- }
1117
- };
1118
- walk(root);
1119
- return found;
1120
- };
1121
- var censusOf = (root) => {
1122
- const excused = [];
1123
- const locals = [];
1124
- for (const path of manifestsUnder(root)) {
1125
- const at = pathOf(root, path);
1126
- let manifest;
1127
- try {
1128
- manifest = JSON.parse(readFileSync5(path, "utf8"));
1129
- } catch (error) {
1130
- excused.push({ path: at, reason: `it does not parse: ${error.message}` });
1131
- continue;
1132
- }
1133
- if (manifest.private === true) {
1134
- excused.push({ path: at, reason: "private: true \u2014 nothing publishes it" });
1135
- continue;
1136
- }
1137
- if (typeof manifest.name !== "string" || manifest.name === "") {
1138
- excused.push({ path: at, reason: "it names no package" });
1139
- continue;
1140
- }
1141
- if (typeof manifest.version !== "string" || manifest.version === "") {
1142
- excused.push({ path: at, reason: "it declares no version" });
1143
- continue;
1144
- }
1145
- locals.push({ at, name: manifest.name, version: manifest.version });
1146
- }
1147
- return { excused, locals: locals.toSorted((a, b) => a.name.localeCompare(b.name)) };
1148
- };
1149
- var EXISTS_BUT = "exists but no version matching";
1150
- var lineFor = async (local, registry) => {
1151
- const answer = await askRegistry({ name: local.name, registry });
1152
- const base = { at: local.at, local: local.version, name: local.name };
1153
- if (answer.kind === "unreachable") {
1154
- return { ...base, latest: void 0, verdict: "UNREACHABLE", why: answer.why };
1155
- }
1156
- if (answer.kind === "absent") {
1157
- return {
1158
- ...base,
1159
- latest: void 0,
1160
- verdict: "ABSENT",
1161
- why: "npm has never heard of this name \u2014 a name published moments ago 404s for a few minutes; --wait <seconds> polls through that window"
1162
- };
1163
- }
1164
- if (answer.latest === local.version) {
1165
- return { ...base, latest: answer.latest, verdict: "MATCH", why: "on npm" };
1166
- }
1167
- return {
1168
- ...base,
1169
- latest: answer.latest,
1170
- verdict: "BEHIND",
1171
- why: answer.versions.includes(local.version) ? `npm has ${local.version}, and its latest tag is ${answer.latest ?? "nothing"} \u2014 the group has not landed on one version` : `the package ${EXISTS_BUT} ${local.version}; npm answers ${answer.latest ?? "nothing"}. Minutes after a publish, suspect the package manager's manifest cache before the registry`
1172
- };
1173
- };
1174
- var sleep = (ms) => new Promise((done) => {
1175
- setTimeout(done, ms);
1176
- });
1177
- var POLL_MS = 5e3;
1178
- var runPublished = async ({
1179
- pollMs = POLL_MS,
1180
- registry = DEFAULT_REGISTRY,
1181
- root,
1182
- waitSeconds = 0
1183
- }) => {
1184
- const { excused, locals } = censusOf(root);
1185
- const startedAt = Date.now();
1186
- const budgetMs = Math.max(0, waitSeconds * 1e3);
1187
- let lines2 = [];
1188
- let sweeps = 0;
1189
- for (; ; ) {
1190
- lines2 = [];
1191
- for (const local of locals) lines2.push(await lineFor(local, registry));
1192
- sweeps += 1;
1193
- const settled = lines2.every((one) => one.verdict === "MATCH");
1194
- const stuck = lines2.some((one) => one.verdict === "UNREACHABLE");
1195
- const left = budgetMs - (Date.now() - startedAt);
1196
- if (settled || stuck || left <= 0) break;
1197
- await sleep(Math.min(pollMs, left));
1737
+ if (promoted["ok"] !== true) return refuse(`the promote of ${versionId} did not pass`);
1738
+ const went = stringAt(promoted, "promotedVersionId");
1739
+ if (went !== void 0 && went !== versionId) {
1740
+ return refuse(`${versionId} was proved and ${went} was promoted \u2014 nothing proved ${went}`);
1741
+ }
1742
+ return { ok: true, steps };
1743
+ } finally {
1744
+ runner.close();
1198
1745
  }
1199
- const unreachable = lines2.filter((one) => one.verdict === "UNREACHABLE").length;
1200
- return {
1201
- considered: lines2.length + excused.length,
1202
- excused: excused.toSorted((a, b) => a.path.localeCompare(b.path)),
1203
- lines: lines2,
1204
- ok: lines2.length > 0 && lines2.every((one) => one.verdict === "MATCH"),
1205
- registry,
1206
- sweeps,
1207
- unreachable,
1208
- waitedMs: Date.now() - startedAt
1209
- };
1210
1746
  };
1211
- var NOTHING_TO_CHECK = "published FAIL \u2014 no workspace in this tree declares a package a publish could be about, and a check that considered nothing has not passed";
1212
- var formatPublished = (report) => {
1213
- const width = Math.max(1, ...report.lines.map((one) => one.name.length));
1214
- const body = report.lines.map((one) => ` ${one.verdict.padEnd(11)} ${one.name.padEnd(width)} ${one.local} ${one.why}`).join("\n");
1215
- const head = report.lines.length === 0 ? NOTHING_TO_CHECK : `published ${report.ok ? "PASS" : "FAIL"} \u2014 ${report.lines.filter((one) => one.verdict === "MATCH").length}/${report.lines.length} of the group on ${report.registry}`;
1216
- const tail = [
1217
- `considered ${report.considered} manifests: ${report.lines.length} read, ${report.excused.length} excused`,
1218
- report.sweeps > 1 ? `swept ${report.sweeps} times over ${report.waitedMs} ms` : ""
1219
- ].filter((one) => one !== "");
1220
- return `${[head, body, ...tail].filter((one) => one !== "").join("\n")}
1221
- `;
1747
+ var PLANTS = [
1748
+ {
1749
+ name: "answered",
1750
+ says: "a smoke that answered a version other than the one it overrode to is refused"
1751
+ },
1752
+ { name: "failed", says: "a smoke that did not pass is refused" },
1753
+ { name: "promoted", says: "a promote of a version nothing proved is refused" }
1754
+ ];
1755
+ var stubPath = () => {
1756
+ const path = fileURLToPath3(new URL("./stub-runner.js", import.meta.url));
1757
+ if (!existsSync6(path)) throw new CannotRun(`${path} is missing \u2014 run pnpm build first`);
1758
+ return path;
1759
+ };
1760
+ var prove = async (cwd, stub = stubPath()) => {
1761
+ const lines2 = [];
1762
+ let ok = true;
1763
+ for (const plant of PLANTS) {
1764
+ const verdict = await proveOver({
1765
+ args: [stub, `--plant=${plant.name}`],
1766
+ command: process.execPath,
1767
+ cwd,
1768
+ timeoutMs: 3e4
1769
+ });
1770
+ if (verdict.ok) ok = false;
1771
+ lines2.push(` ${verdict.ok ? "MISSED" : "PROVEN"} ${plant.says}`);
1772
+ if (plant.name !== "promoted" && verdict.steps.includes("promote")) {
1773
+ ok = false;
1774
+ lines2.push(" MISSED a refused smoke was followed by a promote request");
1775
+ }
1776
+ }
1777
+ const honest = await proveOver({
1778
+ args: [stub, "--plant=good"],
1779
+ command: process.execPath,
1780
+ cwd,
1781
+ timeoutMs: 3e4
1782
+ });
1783
+ if (!honest.ok) ok = false;
1784
+ lines2.push(
1785
+ ` ${honest.ok ? "PROVEN" : "MISSED"} a smoke that answered the version it overrode to is not refused`
1786
+ );
1787
+ return { lines: lines2, ok };
1222
1788
  };
1789
+ var formatProve = (outcome) => `${outcome.lines.join("\n")}
1790
+
1791
+ prove ${outcome.ok ? "PASS" : "FAIL"} \u2014 ${outcome.ok ? "every plant was refused, and the honest runner was not" : "a plant went through"}.
1792
+ `;
1793
+ var formatVerdict = (verdict) => verdict.ok ? `OK prove: ${verdict.steps.join(" \u2192 ")}
1794
+ ` : ` REFUSED after ${verdict.steps.join(" \u2192 ")}: ${verdict.why ?? ""}
1795
+ `;
1223
1796
 
1224
1797
  // src/schema-walls.ts
1225
- import { readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
1226
- import { join as join3, relative as relative2, resolve as resolve6, sep as sep2 } from "path";
1798
+ import { readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
1799
+ import { join as join6, relative as relative3, resolve as resolve8, sep as sep2 } from "path";
1227
1800
  var MIGRATION_FILE = /\.(?:sql|ts)$/;
1228
1801
  var CURRENT_SETTING = /current_setting\s*\(\s*'([^']+)'/gi;
1229
- var filesUnder = (root, dir) => {
1802
+ var filesUnder2 = (root, dir) => {
1230
1803
  const found = [];
1231
1804
  const walk = (at) => {
1232
1805
  let entries;
1233
1806
  try {
1234
- entries = readdirSync2(at, { withFileTypes: true });
1807
+ entries = readdirSync3(at, { withFileTypes: true });
1235
1808
  } catch {
1236
1809
  return;
1237
1810
  }
1238
1811
  for (const entry of entries) {
1239
- const path = join3(at, entry.name);
1812
+ const path = join6(at, entry.name);
1240
1813
  if (entry.isDirectory()) walk(path);
1241
1814
  else if (MIGRATION_FILE.test(entry.name)) found.push(path);
1242
1815
  }
1243
1816
  };
1244
- walk(resolve6(root, dir));
1245
- return found.map((path) => relative2(root, path).split(sep2).join("/")).toSorted();
1817
+ walk(resolve8(root, dir));
1818
+ return found.map((path) => relative3(root, path).split(sep2).join("/")).toSorted();
1246
1819
  };
1247
- var lineOf2 = (body, index) => body.slice(0, index).split("\n").length;
1820
+ var lineOf3 = (body, index) => body.slice(0, index).split("\n").length;
1248
1821
  var wordFor = (table2) => new RegExp(
1249
1822
  String.raw`(?<![\w.])${table2.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)}\b`,
1250
1823
  "i"
@@ -1258,7 +1831,7 @@ var foreignTables = (path, sql, schema) => {
1258
1831
  const found = wordFor(table2).exec(sql);
1259
1832
  return found === null ? [] : [
1260
1833
  {
1261
- line: lineOf2(sql, found.index),
1834
+ line: lineOf3(sql, found.index),
1262
1835
  path,
1263
1836
  why: `"${mine.name}" names "${table2}", which "${other.name}" declares. A domain's migrations may only touch its own tables \u2014 cross-domain talk goes through events, and a foreign key here is a coupling no import graph shows`
1264
1837
  }
@@ -1274,404 +1847,98 @@ var undeclaredSettings = (path, source, schema) => {
1274
1847
  const name = match[1] ?? "";
1275
1848
  if (allowed.has(name)) continue;
1276
1849
  found.push({
1277
- line: lineOf2(source, match.index),
1850
+ line: lineOf3(source, match.index),
1278
1851
  path,
1279
- why: `current_setting('${name}') names a session variable this repo has not declared. release.schema.sessionVariables holds ${schema.sessionVariables.join(", ")} \u2014 add it there, or take the name as an option so the DDL is not written for one consumer's session`
1280
- });
1281
- }
1282
- return found;
1283
- };
1284
- var runSchema = ({ root }) => {
1285
- const schema = readReleaseConfig(root).schema;
1286
- if (schema === void 0) {
1287
- throw new CannotRun(
1288
- "geonosis.json names no release.schema \u2014 nothing here says which tables belong to which domain, or which session variables the DDL may name (#168)"
1289
- );
1290
- }
1291
- if (schema.domains.length === 0 && schema.sessionVariables.length === 0) {
1292
- throw new CannotRun(
1293
- "release.schema is declared and holds neither domains nor sessionVariables \u2014 say which tables belong to which domain and which session variables the DDL may name, or remove the block: a check with nothing to compare against reads exactly like a clean schema"
1294
- );
1295
- }
1296
- const dirs = schema.domains.map((one) => one.dir);
1297
- const files = [...new Set(dirs.flatMap((dir) => filesUnder(root, dir)))].toSorted();
1298
- const findings = [];
1299
- const unreadable = [];
1300
- for (const path of files) {
1301
- const source = readFileSync6(resolve6(root, path), "utf8");
1302
- let sql;
1303
- try {
1304
- sql = sqlOf(path, source);
1305
- } catch (error) {
1306
- if (!(error instanceof Unreadable)) throw error;
1307
- unreadable.push({ path, why: `${path} holds ${error.message} \u2014 this reader cannot read it` });
1308
- continue;
1309
- }
1310
- findings.push(...foreignTables(path, sql, schema), ...undeclaredSettings(path, source, schema));
1311
- }
1312
- return {
1313
- dirs,
1314
- files,
1315
- findings: findings.toSorted((a, b) => a.path.localeCompare(b.path) || a.line - b.line),
1316
- ok: findings.length === 0 && unreadable.length === 0,
1317
- read: files.length - unreadable.length,
1318
- unreadable
1319
- };
1320
- };
1321
- var formatSchema = (report) => {
1322
- if (report.files.length === 0) {
1323
- return `NONE schema: no migration file under ${report.dirs.join(", ")}; nothing was read
1324
- `;
1325
- }
1326
- if (report.ok) {
1327
- return `OK schema: ${report.read} migration(s) read under ${report.dirs.length} declared domain(s), no wall crossed
1328
- `;
1329
- }
1330
- const said = report.findings.map((one) => ` ${one.path}:${one.line}: ${one.why}
1331
- `);
1332
- const cannot = report.unreadable.map((one) => ` ${one.why}
1333
- `);
1334
- return `${said.join("")}${cannot.join("")}schema: ${report.read} of ${report.files.length} files read \u2014 ${report.findings.length} finding(s)${report.unreadable.length === 0 ? "" : `, ${report.unreadable.length} unreadable`}
1335
- `;
1336
- };
1337
-
1338
- // src/smoke.ts
1339
- import { spawnSync as spawnSync3 } from "child_process";
1340
- import {
1341
- cpSync,
1342
- existsSync as existsSync5,
1343
- mkdirSync,
1344
- readdirSync as readdirSync3,
1345
- readFileSync as readFileSync7,
1346
- rmSync as rmSync2,
1347
- statSync,
1348
- writeFileSync as writeFileSync2
1349
- } from "fs";
1350
- import { join as join4, relative as relative3, resolve as resolve7 } from "path";
1351
- var SNAPSHOTS_DIR = ".geonosis/consumer-snapshots";
1352
- var LOCKFILES = [
1353
- { file: "bun.lock", manager: "bun" },
1354
- { file: "bun.lockb", manager: "bun" },
1355
- { file: "pnpm-lock.yaml", manager: "pnpm" }
1356
- ];
1357
- var UNMEASURED = [
1358
- { file: "package-lock.json", manager: "npm" },
1359
- { file: "yarn.lock", manager: "yarn" }
1360
- ];
1361
- var EXCLUDED_DIRS = [
1362
- ".cache",
1363
- ".geonosis",
1364
- ".git",
1365
- ".next",
1366
- ".turbo",
1367
- ".wrangler",
1368
- "coverage",
1369
- "dist",
1370
- "node_modules",
1371
- "storybook-static"
1372
- ];
1373
- var PHASES = ["typecheck", "lint", "doctor"];
1374
- var DOCTOR_PACKAGE = "@geonosis/doctor";
1375
- var DOCTOR_BIN = "geonosis-doctor";
1376
- var DOCTOR_DOORS = ["@geonosis/cli", "geonosis"];
1377
- var DOOR_BIN = "geonosis doctor";
1378
- var isRecord4 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1379
- var readManifest = (path) => {
1380
- try {
1381
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
1382
- return isRecord4(parsed) ? parsed : {};
1383
- } catch (error) {
1384
- throw new CannotRun(`${path} is not readable JSON: ${error.message}`);
1385
- }
1386
- };
1387
- var versionsIn2 = (manifest) => {
1388
- const found = {};
1389
- for (const key of ["dependencies", "devDependencies"]) {
1390
- const block = manifest[key];
1391
- if (!isRecord4(block)) continue;
1392
- for (const [name, range] of Object.entries(block)) {
1393
- if (typeof range === "string") found[name] = range;
1394
- }
1395
- }
1396
- return Object.fromEntries(Object.entries(found).toSorted(([a], [b]) => a.localeCompare(b)));
1397
- };
1398
- var scriptsIn = (manifest) => {
1399
- const scripts = manifest["scripts"];
1400
- if (!isRecord4(scripts)) return {};
1401
- return Object.fromEntries(
1402
- Object.entries(scripts).filter((one) => typeof one[1] === "string")
1403
- );
1404
- };
1405
- var copyInto = (from, to, excluded) => {
1406
- let bytes = 0;
1407
- let files = 0;
1408
- const walk = (dir, into) => {
1409
- mkdirSync(into, { recursive: true });
1410
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
1411
- if (excluded.has(entry.name)) continue;
1412
- const at = join4(dir, entry.name);
1413
- if (entry.isDirectory()) {
1414
- walk(at, join4(into, entry.name));
1415
- continue;
1416
- }
1417
- if (!entry.isFile()) continue;
1418
- cpSync(at, join4(into, entry.name));
1419
- bytes += statSync(at).size;
1420
- files += 1;
1421
- }
1422
- };
1423
- walk(from, to);
1424
- return { bytes, files };
1425
- };
1426
- var manifestsUnder2 = (root) => {
1427
- const found = [];
1428
- const walk = (dir) => {
1429
- for (const entry of readdirSync3(dir, { withFileTypes: true }).toSorted(
1430
- (a, b) => a.name.localeCompare(b.name)
1431
- )) {
1432
- const at = join4(dir, entry.name);
1433
- if (entry.isDirectory()) {
1434
- walk(at);
1435
- continue;
1436
- }
1437
- if (entry.name !== "package.json") continue;
1438
- found.push({ path: relative3(root, at), versions: versionsIn2(readManifest(at)) });
1439
- }
1440
- };
1441
- walk(root);
1442
- return found.toSorted((a, b) => a.path.localeCompare(b.path));
1443
- };
1444
- var managerOf = (from) => {
1445
- const found = LOCKFILES.find((one) => existsSync5(join4(from, one.file)));
1446
- if (found !== void 0) return { lockfile: found.file, manager: found.manager };
1447
- const unmeasured = UNMEASURED.find((one) => existsSync5(join4(from, one.file)));
1448
- if (unmeasured !== void 0) {
1449
- throw new CannotRun(
1450
- `${from} is a ${unmeasured.manager} tree (${unmeasured.file}), and the pack \u2192 rewrite \u2192 install path has never been measured under ${unmeasured.manager} here. It reads ${LOCKFILES.map((one) => one.file).join(", ")}.`
1451
- );
1452
- }
1453
- throw new CannotRun(
1454
- `nothing in ${from} says which runtime installs it \u2014 looked for ${[...LOCKFILES, ...UNMEASURED].map((one) => one.file).join(", ")}. The smoke installs the way the consumer installs, so the manager is read, never assumed.`
1455
- );
1456
- };
1457
- var commandsFor = (manifest, manifests, named) => {
1458
- const scripts = scriptsIn(manifest);
1459
- const installs = (name) => manifests.some((one) => one.versions[name] !== void 0);
1460
- const doctorCommand = installs(DOCTOR_PACKAGE) ? { exec: DOCTOR_BIN } : DOCTOR_DOORS.some(installs) ? { exec: DOOR_BIN } : {
1461
- why: `no manifest in this tree installs ${DOCTOR_PACKAGE} or a door that brings it (${DOCTOR_DOORS.join(", ")}), so nothing here can run ${DOCTOR_BIN}`
1462
- };
1463
- const phase = (name, fallback) => {
1464
- const override = named[name];
1465
- if (override !== void 0) return { exec: override };
1466
- if (scripts[name] !== void 0) return { run: name };
1467
- return fallback;
1468
- };
1469
- return {
1470
- doctor: phase("doctor", doctorCommand),
1471
- lint: phase("lint", { why: 'the tree has no "lint" script and no --lint command was named' }),
1472
- typecheck: phase("typecheck", {
1473
- why: 'the tree has no "typecheck" script and no --typecheck command was named'
1474
- })
1475
- };
1476
- };
1477
- var snapshotDir = (root, name) => join4(root, SNAPSHOTS_DIR, name);
1478
- var readSnapshot = (root, name) => {
1479
- const at = join4(snapshotDir(root, name), "snapshot.json");
1480
- if (!existsSync5(at)) {
1481
- throw new CannotRun(
1482
- `there is no snapshot called "${name}" here \u2014 ${at} does not exist. Record one with: geonosis-release smoke snapshot ${name} --from <their tree>`
1483
- );
1852
+ why: `current_setting('${name}') names a session variable this repo has not declared. release.schema.sessionVariables holds ${schema.sessionVariables.join(", ")} \u2014 add it there, or take the name as an option so the DDL is not written for one consumer's session`
1853
+ });
1484
1854
  }
1485
- return JSON.parse(readFileSync7(at, "utf8"));
1486
- };
1487
- var wouldBeCommitted = (root, path) => {
1488
- const inside = spawnSync3("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root });
1489
- if (inside.error !== void 0 || inside.status !== 0) return false;
1490
- return spawnSync3("git", ["check-ignore", "-q", path], { cwd: root }).status !== 0;
1855
+ return found;
1491
1856
  };
1492
- var NAME_SHAPE = /^[a-z0-9][\w.-]*$/i;
1493
- var runSnapshot = (input) => {
1494
- if (!NAME_SHAPE.test(input.name)) {
1857
+ var runSchema = ({ root }) => {
1858
+ const schema = readReleaseConfig(root).schema;
1859
+ if (schema === void 0) {
1495
1860
  throw new CannotRun(
1496
- `"${input.name}" is not a snapshot name \u2014 a name is letters, digits, dots, dashes and underscores, so that ${SNAPSHOTS_DIR}/<name> is the only place the bytes can land`
1861
+ "geonosis.json names no release.schema \u2014 nothing here says which tables belong to which domain, or which session variables the DDL may name (#168)"
1497
1862
  );
1498
1863
  }
1499
- const from = resolve7(input.from);
1500
- if (!existsSync5(join4(from, "package.json"))) {
1501
- throw new CannotRun(`${from} has no package.json \u2014 that is not a tree a consumer installs`);
1502
- }
1503
- const at = snapshotDir(input.root, input.name);
1504
- if (wouldBeCommitted(input.root, join4(SNAPSHOTS_DIR, input.name))) {
1864
+ if (schema.domains.length === 0 && schema.sessionVariables.length === 0) {
1505
1865
  throw new CannotRun(
1506
- `git here does not ignore ${SNAPSHOTS_DIR}/ \u2014 a consumer's tree is private (D-048) and a copy that reaches a commit cannot be taken back. Add ".geonosis/" to .gitignore and run this again.`
1866
+ "release.schema is declared and holds neither domains nor sessionVariables \u2014 say which tables belong to which domain and which session variables the DDL may name, or remove the block: a check with nothing to compare against reads exactly like a clean schema"
1507
1867
  );
1508
1868
  }
1509
- if (existsSync5(at) && !input.replace) {
1510
- throw new CannotRun(
1511
- `${at} is already a snapshot, and a baseline recorded against other bytes is worse than none \u2014 pass --replace to overwrite it`
1512
- );
1869
+ const dirs = schema.domains.map((one) => one.dir);
1870
+ const files = [...new Set(dirs.flatMap((dir) => filesUnder2(root, dir)))].toSorted();
1871
+ const findings = [];
1872
+ const unreadable = [];
1873
+ for (const path of files) {
1874
+ const source = readFileSync9(resolve8(root, path), "utf8");
1875
+ let sql;
1876
+ try {
1877
+ sql = sqlOf(path, source);
1878
+ } catch (error) {
1879
+ if (!(error instanceof Unreadable)) throw error;
1880
+ unreadable.push({ path, why: `${path} holds ${error.message} \u2014 this reader cannot read it` });
1881
+ continue;
1882
+ }
1883
+ findings.push(...foreignTables(path, sql, schema), ...undeclaredSettings(path, source, schema));
1513
1884
  }
1514
- const { lockfile, manager } = managerOf(from);
1515
- const excluded = [.../* @__PURE__ */ new Set([...EXCLUDED_DIRS, ...input.exclude ?? []])].toSorted();
1516
- rmSync2(at, { force: true, recursive: true });
1517
- const tree = join4(at, "tree");
1518
- const { bytes, files } = copyInto(from, tree, new Set(excluded));
1519
- const manifests = manifestsUnder2(tree);
1520
- const record = {
1521
- at: (/* @__PURE__ */ new Date()).toISOString(),
1522
- bytes,
1523
- commands: commandsFor(readManifest(join4(tree, "package.json")), manifests, input.named),
1524
- excluded,
1885
+ return {
1886
+ dirs,
1525
1887
  files,
1526
- from,
1527
- lockfile,
1528
- manager,
1529
- manifests,
1530
- name: input.name
1888
+ findings: findings.toSorted((a, b) => a.path.localeCompare(b.path) || a.line - b.line),
1889
+ ok: findings.length === 0 && unreadable.length === 0,
1890
+ read: files.length - unreadable.length,
1891
+ unreadable
1531
1892
  };
1532
- writeFileSync2(join4(at, "snapshot.json"), `${JSON.stringify(record, void 0, 2)}
1533
- `);
1534
- return record;
1535
- };
1536
- var describeCommand = (command) => {
1537
- if ("run" in command) return `run ${command.run}`;
1538
- if ("exec" in command) return command.exec;
1539
- return `nothing \u2014 ${command.why}`;
1540
- };
1541
- var formatSnapshot = (record) => [
1542
- `OK snapshot ${record.name}: ${record.files} files, ${record.bytes} bytes, ${record.manifests.length} manifests
1543
- `,
1544
- ` from ${record.from} \u2014 ${record.manager} (${record.lockfile})
1545
- `,
1546
- ...PHASES.map((phase) => ` ${phase}: ${describeCommand(record.commands[phase])}
1547
- `)
1548
- ].join("");
1549
-
1550
- // src/envelope.ts
1551
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
1552
- import { dirname as dirname2, join as join5 } from "path";
1553
- import { fileURLToPath as fileURLToPath3 } from "url";
1554
- var ENVELOPES_DIR = ".geonosis/envelopes";
1555
- var envelopePath = (root, tool) => join5(root, ENVELOPES_DIR, `${tool}.json`);
1556
- var UnbalancedEnvelope = class extends Error {
1557
- constructor(message) {
1558
- super(message);
1559
- this.name = "UnbalancedEnvelope";
1560
- }
1561
1893
  };
1562
- var isCount = (value2) => Number.isSafeInteger(value2) && value2 >= 0;
1563
- var unbalancedMessage = (envelope, next) => `${envelope.tool}: considered ${envelope.considered} but accounts for ${envelope.read + envelope.refused.length + envelope.excused.length} \u2014 ${envelope.read} read + ${envelope.refused.length} refused + ${envelope.excused.length} excused. A run that has lost count of its own inputs cannot say what it measured, so no verdict was rendered and no envelope was written. Next: ${next}`;
1564
- var writeEnvelope = ({
1565
- envelope,
1566
- next,
1567
- root
1568
- }) => {
1569
- if (envelope.tool.trim() === "") {
1570
- throw new UnbalancedEnvelope(
1571
- `an envelope with no tool name cannot be filed or reported against. Next: ${next}`
1572
- );
1573
- }
1574
- if (envelope.version.trim() === "") {
1575
- throw new UnbalancedEnvelope(
1576
- `${envelope.tool}: an envelope that cannot name the build that wrote it dates nothing, and a stale one reads exactly like a fresh one. Next: ${next}`
1577
- );
1578
- }
1579
- if (!isCount(envelope.considered) || !isCount(envelope.read)) {
1580
- throw new UnbalancedEnvelope(
1581
- `${envelope.tool}: considered ${envelope.considered} and read ${envelope.read} \u2014 a census is a whole number of things, and arithmetic over anything else balances by accident. Next: ${next}`
1582
- );
1894
+ var formatSchema = (report) => {
1895
+ if (report.files.length === 0) {
1896
+ return `NONE schema: no migration file under ${report.dirs.join(", ")}; nothing was read
1897
+ `;
1583
1898
  }
1584
- if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
1585
- throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
1899
+ if (report.ok) {
1900
+ return `OK schema: ${report.read} migration(s) read under ${report.dirs.length} declared domain(s), no wall crossed
1901
+ `;
1586
1902
  }
1587
- const at = envelopePath(root, envelope.tool);
1588
- mkdirSync2(dirname2(at), { recursive: true });
1589
- writeFileSync3(at, `${JSON.stringify(envelope, void 0, 2)}
1903
+ const said = report.findings.map((one) => ` ${one.path}:${one.line}: ${one.why}
1590
1904
  `);
1591
- return at;
1592
- };
1593
- var UNKNOWN = "unknown";
1594
- var versionIn = (dir) => {
1595
- try {
1596
- const manifest = JSON.parse(readFileSync8(join5(dir, "package.json"), "utf8"));
1597
- return typeof manifest.version === "string" ? manifest.version : void 0;
1598
- } catch {
1599
- return void 0;
1600
- }
1601
- };
1602
- var versionOf = (moduleUrl) => {
1603
- let dir = dirname2(fileURLToPath3(moduleUrl));
1604
- for (; ; ) {
1605
- const found = versionIn(dir);
1606
- if (found !== void 0) return found;
1607
- const up = dirname2(dir);
1608
- if (up === dir) return UNKNOWN;
1609
- dir = up;
1610
- }
1905
+ const cannot = report.unreadable.map((one) => ` ${one.why}
1906
+ `);
1907
+ return `${said.join("")}${cannot.join("")}schema: ${report.read} of ${report.files.length} files read \u2014 ${report.findings.length} finding(s)${report.unreadable.length === 0 ? "" : `, ${report.unreadable.length} unreadable`}
1908
+ `;
1611
1909
  };
1612
- var MIGRATIONS_TOOL = "release-migrations";
1613
- var MIGRATIONS_NEXT = "geonosis-release migrations --since <ref> --json and compare `files` against `unreadable` \u2014 every added file leaves by exactly one door, and a file in one list and not the other is the accounting bug this refuses over";
1614
- var migrationsEnvelope = (report, durationMs) => ({
1615
- considered: report.files.length,
1616
- durationMs,
1617
- excused: report.files.filter((one) => one.state === "excused").map((one) => ({
1618
- path: one.path,
1619
- reason: "a contract-migration marker this gate could believe"
1620
- })),
1621
- findings: [...report.refusals, ...report.markers, ...report.squawk],
1622
- read: report.files.filter((one) => one.state !== "excused" && one.state !== "unreadable").length,
1623
- refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
1624
- tool: MIGRATIONS_TOOL,
1625
- version: versionOf(import.meta.url)
1626
- });
1627
- var SCHEMA_TOOL = "release-schema";
1628
- var SCHEMA_NEXT = "geonosis-release schema --json and compare `files` against `unreadable` \u2014 the denominator is every migration under every declared domain directory, and a file in neither list is one nothing read (#168)";
1629
- var schemaEnvelope = (report, durationMs) => ({
1630
- considered: report.files.length,
1631
- durationMs,
1632
- excused: [],
1633
- findings: report.findings,
1634
- read: report.read,
1635
- refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
1636
- tool: SCHEMA_TOOL,
1637
- version: versionOf(import.meta.url)
1638
- });
1639
- var PUBLISHED_TOOL = "release-published";
1640
- var PUBLISHED_NEXT = "geonosis-release published --group --json and compare `considered` against `lines` + `excused` \u2014 every manifest in the tree leaves by exactly one door, and a group announced off a sample is the failure this counts against (#147)";
1641
- var publishedEnvelope = (report, durationMs) => ({
1642
- considered: report.considered,
1643
- durationMs,
1644
- excused: report.excused,
1645
- findings: report.lines.filter((one) => one.verdict !== "MATCH"),
1646
- read: report.lines.filter((one) => one.verdict !== "UNREACHABLE").length,
1647
- refused: report.lines.filter((one) => one.verdict === "UNREACHABLE").map((one) => ({ path: one.at, reason: one.why })),
1648
- tool: PUBLISHED_TOOL,
1649
- version: versionOf(import.meta.url)
1650
- });
1651
1910
 
1652
1911
  // src/smoke-run.ts
1653
1912
  import { spawnSync as spawnSync4 } from "child_process";
1654
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
1655
- import { join as join6, relative as relative4, resolve as resolve8 } from "path";
1913
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
1914
+ import { join as join7, relative as relative4, resolve as resolve9 } from "path";
1656
1915
  var BASELINE_FILE = "baseline.json";
1657
1916
  var INSTALL = {
1658
1917
  bun: ["install"],
1659
1918
  pnpm: ["install", "--no-frozen-lockfile"]
1660
1919
  };
1920
+ var MANAGER_CONFIG = [".npmrc", "bunfig.toml", "pnpm-workspace.yaml"];
1921
+ var configBytesOf = (work) => Object.fromEntries(
1922
+ MANAGER_CONFIG.filter((file) => existsSync7(join7(work, file))).map((file) => [
1923
+ file,
1924
+ readFileSync10(join7(work, file), "utf8")
1925
+ ])
1926
+ );
1927
+ var editedByInstall = (before, after) => [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((file) => before[file] !== after[file]).toSorted();
1661
1928
  var isRecord5 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1662
1929
  var readJson = (path) => {
1663
1930
  try {
1664
- return JSON.parse(readFileSync9(path, "utf8"));
1931
+ return JSON.parse(readFileSync10(path, "utf8"));
1665
1932
  } catch (error) {
1666
1933
  throw new CannotRun(`${path} is not readable JSON: ${error.message}`);
1667
1934
  }
1668
1935
  };
1669
1936
  var packRc = (rc, into) => {
1670
- if (!existsSync6(join6(rc, "package.json"))) {
1937
+ if (!existsSync7(join7(rc, "package.json"))) {
1671
1938
  throw new CannotRun(`${rc} has no package.json \u2014 that is not a workspace to pack`);
1672
1939
  }
1673
1940
  mkdirSync3(into, { recursive: true });
1674
- const root = readJson(join6(rc, "package.json"));
1941
+ const root = readJson(join7(rc, "package.json"));
1675
1942
  const isPrivateRoot = isRecord5(root) && root["private"] === true;
1676
1943
  const done = spawnSync4(
1677
1944
  "pnpm",
@@ -1721,11 +1988,11 @@ var sourceManifests = (rc) => {
1721
1988
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
1722
1989
  if (excluded.has(entry.name)) continue;
1723
1990
  if (entry.isDirectory()) {
1724
- walk(join6(dir, entry.name));
1991
+ walk(join7(dir, entry.name));
1725
1992
  continue;
1726
1993
  }
1727
1994
  if (entry.name !== "package.json") continue;
1728
- const manifest = readJson(join6(dir, entry.name));
1995
+ const manifest = readJson(join7(dir, entry.name));
1729
1996
  if (!isRecord5(manifest) || typeof manifest["name"] !== "string") continue;
1730
1997
  found.push({ manifest, name: manifest["name"] });
1731
1998
  }
@@ -1747,9 +2014,9 @@ var OVERRIDES = "overrides:";
1747
2014
  var TOP_LEVEL = /^\S/;
1748
2015
  var KEYED = /^\s+["']?(.+?)["']?\s*:/;
1749
2016
  var pinsInWorkspaceYaml = (work, pins) => {
1750
- const at = join6(work, "pnpm-workspace.yaml");
2017
+ const at = join7(work, "pnpm-workspace.yaml");
1751
2018
  const written = Object.entries(pins).map(([name, spec]) => ` '${name}': '${spec}'`);
1752
- const lines2 = existsSync6(at) ? readFileSync9(at, "utf8").split("\n") : [];
2019
+ const lines2 = existsSync7(at) ? readFileSync10(at, "utf8").split("\n") : [];
1753
2020
  const kept = [];
1754
2021
  let inside = false;
1755
2022
  let found = false;
@@ -1768,8 +2035,8 @@ var pinsInWorkspaceYaml = (work, pins) => {
1768
2035
  writeFileSync4(at, body.join("\n"));
1769
2036
  };
1770
2037
  var pinsInManifest = (work, pins) => {
1771
- const at = join6(work, "package.json");
1772
- if (!existsSync6(at)) return;
2038
+ const at = join7(work, "package.json");
2039
+ if (!existsSync7(at)) return;
1773
2040
  const manifest = readJson(at);
1774
2041
  if (!isRecord5(manifest)) return;
1775
2042
  manifest["overrides"] = {
@@ -1789,8 +2056,8 @@ var rewriteManifests = (tree, manifests, packed) => {
1789
2056
  const byName = new Map(packed.map((one) => [one.name, one.tarball]));
1790
2057
  const swapped = [];
1791
2058
  for (const path of manifests) {
1792
- const at = join6(tree, path);
1793
- if (!existsSync6(at)) continue;
2059
+ const at = join7(tree, path);
2060
+ if (!existsSync7(at)) continue;
1794
2061
  const manifest = readJson(at);
1795
2062
  if (!isRecord5(manifest)) continue;
1796
2063
  let touched = false;
@@ -1826,20 +2093,26 @@ var runPhase = (phase, command, cwd, manager) => {
1826
2093
  const line = commandLine(command, manager);
1827
2094
  const env = {
1828
2095
  ...process.env,
1829
- PATH: `${join6(cwd, "node_modules/.bin")}:${process.env["PATH"] ?? ""}`
2096
+ PATH: `${join7(cwd, "node_modules/.bin")}:${process.env["PATH"] ?? ""}`
1830
2097
  };
1831
2098
  const { code, output } = shellRun(line, cwd, env);
1832
2099
  return { code, command: line, lines: quotable(output), ok: code === 0, phase };
1833
2100
  };
2101
+ var commandsToRun = (record, declared) => Object.fromEntries(
2102
+ PHASES.map((phase) => {
2103
+ const named = declared[phase];
2104
+ return [phase, named === void 0 ? record.commands[phase] : { exec: named }];
2105
+ })
2106
+ );
1834
2107
  var smokeOver = (input) => {
1835
2108
  const record = readSnapshot(input.root, input.name);
1836
2109
  const at = snapshotDir(input.root, input.name);
1837
- const work = join6(at, "run");
2110
+ const work = join7(at, "run");
1838
2111
  rmSync3(work, { force: true, recursive: true });
1839
- copyInto(join6(at, "tree"), work, new Set(EXCLUDED_DIRS));
2112
+ copyInto(join7(at, "tree"), work, new Set(EXCLUDED_DIRS));
1840
2113
  ownRepository(work);
1841
- const packed = input.rc === void 0 ? [] : packRc(resolve8(input.rc), join6(at, "rc"));
1842
- const findings = input.rc === void 0 ? [] : promisedButNotPacked(resolve8(input.rc), packed);
2114
+ const packed = input.rc === void 0 ? [] : packRc(resolve9(input.rc), join7(at, "rc"));
2115
+ const findings = input.rc === void 0 ? [] : promisedButNotPacked(resolve9(input.rc), packed);
1843
2116
  const swapped = rewriteManifests(
1844
2117
  work,
1845
2118
  record.manifests.map((one) => one.path),
@@ -1852,6 +2125,7 @@ var smokeOver = (input) => {
1852
2125
  }
1853
2126
  overrideEveryCopy(work, record.manager, packed);
1854
2127
  const install = input.install ?? `${record.manager} ${INSTALL[record.manager].join(" ")}`;
2128
+ const configBefore = configBytesOf(work);
1855
2129
  const installed = shellRun(install, work, process.env);
1856
2130
  if (installed.code !== 0) {
1857
2131
  throw new CannotRun(
@@ -1859,9 +2133,15 @@ var smokeOver = (input) => {
1859
2133
  ${quotable(installed.output).join("\n")}`
1860
2134
  );
1861
2135
  }
2136
+ for (const file of editedByInstall(configBefore, configBytesOf(work))) {
2137
+ findings.push(
2138
+ `\`${install}\` rewrote ${file} in the copy \u2014 every phase below ran against a tree that is no longer byte-for-byte "${input.name}", and the same install in their own tree would edit theirs`
2139
+ );
2140
+ }
2141
+ const commands = commandsToRun(record, input.commands ?? {});
1862
2142
  return {
1863
2143
  findings,
1864
- outcomes: PHASES.map((phase) => runPhase(phase, record.commands[phase], work, record.manager)),
2144
+ outcomes: PHASES.map((phase) => runPhase(phase, commands[phase], work, record.manager)),
1865
2145
  packed
1866
2146
  };
1867
2147
  };
@@ -1873,14 +2153,14 @@ var ownRepository = (work) => {
1873
2153
  );
1874
2154
  }
1875
2155
  };
1876
- var baselinePath = (root, name) => join6(snapshotDir(root, name), BASELINE_FILE);
2156
+ var baselinePath = (root, name) => join7(snapshotDir(root, name), BASELINE_FILE);
1877
2157
  var recordBaseline = (input) => {
1878
2158
  const { outcomes, packed } = smokeOver(input);
1879
2159
  const baseline = {
1880
2160
  at: (/* @__PURE__ */ new Date()).toISOString(),
1881
2161
  packed: packed.map((one) => ({ name: one.name, version: one.version })),
1882
2162
  phases: outcomes,
1883
- rc: input.rc === void 0 ? "the versions the tree names" : resolve8(input.rc)
2163
+ rc: input.rc === void 0 ? "the versions the tree names" : resolve9(input.rc)
1884
2164
  };
1885
2165
  writeFileSync4(baselinePath(input.root, input.name), `${JSON.stringify(baseline, void 0, 2)}
1886
2166
  `);
@@ -1888,7 +2168,7 @@ var recordBaseline = (input) => {
1888
2168
  };
1889
2169
  var readBaseline = (root, name) => {
1890
2170
  const at = baselinePath(root, name);
1891
- if (!existsSync6(at)) {
2171
+ if (!existsSync7(at)) {
1892
2172
  throw new CannotRun(
1893
2173
  `"${name}" has no baseline \u2014 a run that took its own answer as the standard would certify anything. Record one against the versions this consumer is on: geonosis-release smoke baseline --snapshot ${name}`
1894
2174
  );
@@ -1942,57 +2222,118 @@ var runSmoke = (input) => {
1942
2222
  const { findings, outcomes } = smokeOver(input);
1943
2223
  return compareToBaseline(input.name, baseline, outcomes, findings);
1944
2224
  };
2225
+ var CONSUMER_TREES_FILE = ".geonosis/consumer-trees.local.json";
2226
+ var localTrees = (root) => {
2227
+ const at = join7(root, CONSUMER_TREES_FILE);
2228
+ if (!existsSync7(at)) return {};
2229
+ const parsed = readJson(at);
2230
+ if (!isRecord5(parsed)) {
2231
+ throw new CannotRun(`${at} must be an object of { "<snapshot name>": "<absolute path>" }`);
2232
+ }
2233
+ return Object.fromEntries(
2234
+ Object.entries(parsed).filter((one) => typeof one[1] === "string")
2235
+ );
2236
+ };
2237
+ var recordedAlready = (root, name) => existsSync7(join7(snapshotDir(root, name), "snapshot.json"));
2238
+ var unrecordedButHere = (root, wanted, absent) => {
2239
+ const trees = localTrees(root);
2240
+ return wanted.filter((one) => !absent.includes(one.name) && !recordedAlready(root, one.name)).flatMap((one) => {
2241
+ const path = trees[one.name];
2242
+ if (path === void 0 || !existsSync7(path)) return [];
2243
+ return [
2244
+ `${one.name} is declared and its tree is here (${path}) \u2014 record it: geonosis-release smoke snapshot ${one.name} --from ${path}`
2245
+ ];
2246
+ });
2247
+ };
2248
+ var dating = (root, name) => {
2249
+ const record = readSnapshot(root, name);
2250
+ const commit = record.commit;
2251
+ if (commit === void 0) {
2252
+ return {
2253
+ undated: `UNDATED ${name}: recorded before source commits were kept \u2014 re-record it: geonosis-release smoke snapshot ${name} --from ${record.from}`
2254
+ };
2255
+ }
2256
+ if (!("sha" in commit) || !existsSync7(record.from)) return void 0;
2257
+ const now = headOf(record.from);
2258
+ if (!("sha" in now) || now.sha === commit.sha) return void 0;
2259
+ return { stale: `STALE ${name}: recorded at ${commit.sha}, the tree is at ${now.sha}` };
2260
+ };
1945
2261
  var sweepSmoke = (input) => {
1946
- const comparisons = [];
1947
- const skipped = [];
1948
- for (const one of input.wanted) {
1949
- if (input.skipMissing && !existsSync6(join6(snapshotDir(input.root, one.name), "snapshot.json"))) {
1950
- skipped.push({
2262
+ const absent = input.absent ?? [];
2263
+ if (input.skipMissing) {
2264
+ const owed = unrecordedButHere(input.root, input.wanted, absent);
2265
+ if (owed.length > 0) throw new CannotRun(owed.join("\n"));
2266
+ }
2267
+ const entries = input.wanted.map((one) => {
2268
+ if (absent.includes(one.name)) {
2269
+ return {
2270
+ name: one.name,
2271
+ why: "--absent named it, so this release was not read against it"
2272
+ };
2273
+ }
2274
+ if (input.skipMissing && !recordedAlready(input.root, one.name)) {
2275
+ return {
1951
2276
  name: one.name,
1952
2277
  why: `no snapshot of it here \u2014 record one with: geonosis-release smoke snapshot ${one.name} --from <their tree>`
1953
- });
1954
- continue;
2278
+ };
1955
2279
  }
1956
- comparisons.push(
1957
- runSmoke({
2280
+ const note = dating(input.root, one.name);
2281
+ return {
2282
+ comparison: runSmoke({
2283
+ ...one.commands === void 0 ? {} : { commands: one.commands },
1958
2284
  ...one.install === void 0 ? {} : { install: one.install },
1959
2285
  name: one.name,
1960
2286
  ...input.rc === void 0 ? {} : { rc: input.rc },
1961
2287
  root: input.root
1962
- })
1963
- );
2288
+ }),
2289
+ name: one.name,
2290
+ ...note === void 0 ? {} : { note }
2291
+ };
2292
+ });
2293
+ return { entries };
2294
+ };
2295
+ var comparisonsIn = (sweep) => sweep.entries.flatMap((one) => "comparison" in one ? [one.comparison] : []);
2296
+ var EXCUSED = "EXCUSED";
2297
+ var noteLine = (note) => "stale" in note ? note.stale : note.undated;
2298
+ var entryBlock = (entry) => {
2299
+ if (!("comparison" in entry)) {
2300
+ const head = entry.why.startsWith("--absent") ? EXCUSED : "SKIP ";
2301
+ return `${head} smoke ${entry.name}: ${entry.why}
2302
+ `;
1964
2303
  }
1965
- return { comparisons, skipped };
2304
+ return `${formatSmoke(entry.comparison)}${entry.note === void 0 ? "" : `${noteLine(entry.note)}
2305
+ `}`;
1966
2306
  };
1967
- var formatSweep = (sweep) => [
1968
- ...sweep.comparisons.map(formatSmoke),
1969
- ...sweep.skipped.map((one) => `SKIP smoke ${one.name}: ${one.why}
1970
- `)
1971
- ].join("");
2307
+ var formatSweep = (sweep) => sweep.entries.map(entryBlock).join("");
1972
2308
  var sweepEnvelope = (sweep, durationMs) => {
1973
- const parts = sweep.comparisons.map((one) => smokeEnvelope(one, 0));
2309
+ const excused = [];
2310
+ const findings = [];
2311
+ const refused = [];
2312
+ let considered = 0;
2313
+ let read = 0;
2314
+ for (const entry of sweep.entries) {
2315
+ if (!("comparison" in entry)) {
2316
+ considered += PHASES.length;
2317
+ excused.push(
2318
+ ...PHASES.map((phase) => ({ path: `${entry.name}/${phase}`, reason: entry.why }))
2319
+ );
2320
+ continue;
2321
+ }
2322
+ const part = smokeEnvelope(entry.comparison, 0);
2323
+ considered += part.considered;
2324
+ read += part.read;
2325
+ excused.push(...part.excused.map((one) => ({ ...one, path: `${entry.name}/${one.path}` })));
2326
+ refused.push(...part.refused.map((one) => ({ ...one, path: `${entry.name}/${one.path}` })));
2327
+ findings.push(...part.findings);
2328
+ if (entry.note !== void 0) findings.push(entry.note);
2329
+ }
1974
2330
  return {
1975
- considered: parts.reduce((sum, one) => sum + one.considered, 0) + sweep.skipped.length * 3,
2331
+ considered,
1976
2332
  durationMs,
1977
- excused: [
1978
- ...parts.flatMap(
1979
- (one, index) => one.excused.map((entry) => ({
1980
- path: `${sweep.comparisons[index]?.name ?? ""}/${entry.path}`,
1981
- reason: entry.reason
1982
- }))
1983
- ),
1984
- ...sweep.skipped.flatMap(
1985
- (one) => PHASES.map((phase) => ({ path: `${one.name}/${phase}`, reason: one.why }))
1986
- )
1987
- ],
1988
- findings: parts.flatMap((one) => one.findings),
1989
- read: parts.reduce((sum, one) => sum + one.read, 0),
1990
- refused: parts.flatMap(
1991
- (one, index) => one.refused.map((entry) => ({
1992
- path: `${sweep.comparisons[index]?.name ?? ""}/${entry.path}`,
1993
- reason: entry.reason
1994
- }))
1995
- ),
2333
+ excused,
2334
+ findings,
2335
+ read,
2336
+ refused,
1996
2337
  tool: SMOKE_TOOL,
1997
2338
  version: versionOf(import.meta.url)
1998
2339
  };
@@ -2062,9 +2403,36 @@ var smokeEnvelope = (comparison, durationMs) => {
2062
2403
  };
2063
2404
 
2064
2405
  export {
2406
+ PHASES,
2065
2407
  CannotRun,
2066
2408
  parseReleaseConfig,
2067
2409
  readReleaseConfig,
2410
+ writeEnvelope,
2411
+ MIGRATIONS_NEXT,
2412
+ migrationsEnvelope,
2413
+ SCHEMA_NEXT,
2414
+ schemaEnvelope,
2415
+ PUBLISHED_NEXT,
2416
+ publishedEnvelope,
2417
+ DEFAULT_REGISTRY,
2418
+ registryPathOf,
2419
+ askRegistry,
2420
+ censusOf,
2421
+ EXISTS_BUT,
2422
+ runPublished,
2423
+ formatPublished,
2424
+ SNAPSHOTS_DIR,
2425
+ EXCLUDED_DIRS,
2426
+ headOf,
2427
+ snapshotDir,
2428
+ readSnapshot,
2429
+ runSnapshot,
2430
+ formatSnapshot,
2431
+ runAdoption,
2432
+ formatAdoption,
2433
+ ADOPTION_TOOL,
2434
+ ADOPTION_NEXT,
2435
+ adoptionEnvelope,
2068
2436
  parseJsonc,
2069
2437
  parseToml,
2070
2438
  declaredIn,
@@ -2092,29 +2460,8 @@ export {
2092
2460
  prove,
2093
2461
  formatProve,
2094
2462
  formatVerdict,
2095
- DEFAULT_REGISTRY,
2096
- registryPathOf,
2097
- askRegistry,
2098
- censusOf,
2099
- EXISTS_BUT,
2100
- runPublished,
2101
- formatPublished,
2102
2463
  runSchema,
2103
2464
  formatSchema,
2104
- SNAPSHOTS_DIR,
2105
- EXCLUDED_DIRS,
2106
- PHASES,
2107
- snapshotDir,
2108
- readSnapshot,
2109
- runSnapshot,
2110
- formatSnapshot,
2111
- writeEnvelope,
2112
- MIGRATIONS_NEXT,
2113
- migrationsEnvelope,
2114
- SCHEMA_NEXT,
2115
- schemaEnvelope,
2116
- PUBLISHED_NEXT,
2117
- publishedEnvelope,
2118
2465
  BASELINE_FILE,
2119
2466
  packRc,
2120
2467
  promisedButNotPacked,
@@ -2123,7 +2470,9 @@ export {
2123
2470
  readBaseline,
2124
2471
  compareToBaseline,
2125
2472
  runSmoke,
2473
+ CONSUMER_TREES_FILE,
2126
2474
  sweepSmoke,
2475
+ comparisonsIn,
2127
2476
  formatSweep,
2128
2477
  sweepEnvelope,
2129
2478
  formatSmoke,