@geonosis/release 2.0.0 → 2.1.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.
@@ -95,12 +95,41 @@ var parseSchema = (value2) => {
95
95
  };
96
96
  };
97
97
  var SMOKE_KEYS = "expected { exclude?, snapshots? }";
98
+ var SNAPSHOT_KEYS = "expected { commands?, install?, name }";
99
+ var SMOKE_PHASES = ["doctor", "lint", "typecheck"];
100
+ var parseCommands = (value2, at) => {
101
+ if (value2 === void 0) return {};
102
+ if (!isRecord(value2)) {
103
+ throw new CannotRun(
104
+ `${at}.commands must be an object \u2014 expected { doctor?, lint?, typecheck? }`
105
+ );
106
+ }
107
+ const unknown = Object.keys(value2).find(
108
+ (key) => !SMOKE_PHASES.includes(key)
109
+ );
110
+ if (unknown !== void 0) {
111
+ throw new CannotRun(
112
+ `${at}.commands.${unknown} is not a phase the smoke runs \u2014 it runs ${SMOKE_PHASES.join(", ")}`
113
+ );
114
+ }
115
+ const commands = {};
116
+ for (const phase of SMOKE_PHASES) {
117
+ const command = value2[phase];
118
+ if (command === void 0) continue;
119
+ if (typeof command !== "string" || command === "") {
120
+ throw new CannotRun(`${at}.commands.${phase} must be the command that runs their ${phase}`);
121
+ }
122
+ commands[phase] = command;
123
+ }
124
+ return commands;
125
+ };
98
126
  var oneSnapshot = (value2, index) => {
99
127
  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");
128
+ if (!isRecord(value2)) throw new CannotRun(`${at} must be an object \u2014 ${SNAPSHOT_KEYS}`);
129
+ const known = /* @__PURE__ */ new Set(["commands", "install", "name"]);
130
+ const unknown = Object.keys(value2).find((key) => !known.has(key));
102
131
  if (unknown !== void 0) {
103
- throw new CannotRun(`${at}.${unknown} is not a key it takes \u2014 expected { name, install? }`);
132
+ throw new CannotRun(`${at}.${unknown} is not a key it takes \u2014 ${SNAPSHOT_KEYS}`);
104
133
  }
105
134
  const name = value2["name"];
106
135
  if (typeof name !== "string" || name === "") {
@@ -110,7 +139,11 @@ var oneSnapshot = (value2, index) => {
110
139
  if (install !== void 0 && typeof install !== "string") {
111
140
  throw new CannotRun(`${at}.install must be the command that installs that tree`);
112
141
  }
113
- return { ...install === void 0 ? {} : { install }, name };
142
+ return {
143
+ commands: parseCommands(value2["commands"], at),
144
+ ...install === void 0 ? {} : { install },
145
+ name
146
+ };
114
147
  };
115
148
  var parseSmoke = (value2) => {
116
149
  if (value2 === void 0) return { exclude: [], snapshots: [] };
@@ -121,7 +154,7 @@ var parseSmoke = (value2) => {
121
154
  }
122
155
  const snapshots = value2["snapshots"];
123
156
  if (snapshots !== void 0 && !Array.isArray(snapshots)) {
124
- throw new CannotRun("release.smoke.snapshots must be a list of { name, install? }");
157
+ throw new CannotRun(`release.smoke.snapshots must be a list \u2014 ${SNAPSHOT_KEYS}, per entry`);
125
158
  }
126
159
  return {
127
160
  exclude: strings(value2["exclude"], "release.smoke.exclude"),
@@ -175,238 +208,905 @@ var readReleaseConfig = (root) => {
175
208
  }
176
209
  };
177
210
 
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;
210
- }
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
- };
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;
211
+ // src/envelope.ts
212
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
213
+ import { dirname, join } from "path";
214
+ import { fileURLToPath } from "url";
215
+ var ENVELOPES_DIR = ".geonosis/envelopes";
216
+ var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
217
+ var UnbalancedEnvelope = class extends Error {
218
+ constructor(message) {
219
+ super(message);
220
+ this.name = "UnbalancedEnvelope";
241
221
  }
242
- parts.push(current);
243
- return parts.filter((one) => one.trim() !== "");
244
222
  };
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
- })
223
+ var isCount = (value2) => Number.isSafeInteger(value2) && value2 >= 0;
224
+ 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}`;
225
+ var writeEnvelope = ({
226
+ envelope,
227
+ next,
228
+ root
229
+ }) => {
230
+ if (envelope.tool.trim() === "") {
231
+ throw new UnbalancedEnvelope(
232
+ `an envelope with no tool name cannot be filed or reported against. Next: ${next}`
254
233
  );
255
234
  }
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];
235
+ if (envelope.version.trim() === "") {
236
+ throw new UnbalancedEnvelope(
237
+ `${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}`
238
+ );
271
239
  }
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];
240
+ if (!isCount(envelope.considered) || !isCount(envelope.read)) {
241
+ throw new UnbalancedEnvelope(
242
+ `${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}`
243
+ );
279
244
  }
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));
245
+ if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
246
+ throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
312
247
  }
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
- };
248
+ const at = envelopePath(root, envelope.tool);
249
+ mkdirSync(dirname(at), { recursive: true });
250
+ writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
251
+ `);
252
+ return at;
355
253
  };
356
- var readWrangler = (root, relative5, env) => {
357
- const path = resolve2(root, relative5);
358
- let parsed;
254
+ var UNKNOWN = "unknown";
255
+ var versionIn = (dir) => {
359
256
  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}`);
257
+ const manifest = JSON.parse(readFileSync2(join(dir, "package.json"), "utf8"));
258
+ return typeof manifest.version === "string" ? manifest.version : void 0;
259
+ } catch {
260
+ return void 0;
364
261
  }
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
262
  };
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}`);
263
+ var versionOf = (moduleUrl) => {
264
+ let dir = dirname(fileURLToPath(moduleUrl));
265
+ for (; ; ) {
266
+ const found = versionIn(dir);
267
+ if (found !== void 0) return found;
268
+ const up = dirname(dir);
269
+ if (up === dir) return UNKNOWN;
270
+ dir = up;
386
271
  }
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
272
  };
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
- ];
273
+ var MIGRATIONS_TOOL = "release-migrations";
274
+ 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";
275
+ var migrationsEnvelope = (report, durationMs) => ({
276
+ considered: report.files.length,
277
+ durationMs,
278
+ excused: report.files.filter((one) => one.state === "excused").map((one) => ({
279
+ path: one.path,
280
+ reason: "a contract-migration marker this gate could believe"
281
+ })),
282
+ findings: [...report.refusals, ...report.markers, ...report.squawk],
283
+ read: report.files.filter((one) => one.state !== "excused" && one.state !== "unreadable").length,
284
+ refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
285
+ tool: MIGRATIONS_TOOL,
286
+ version: versionOf(import.meta.url)
287
+ });
288
+ var SCHEMA_TOOL = "release-schema";
289
+ 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)";
290
+ var schemaEnvelope = (report, durationMs) => ({
291
+ considered: report.files.length,
292
+ durationMs,
293
+ excused: [],
294
+ findings: report.findings,
295
+ read: report.read,
296
+ refused: report.unreadable.map((one) => ({ path: one.path, reason: one.why })),
297
+ tool: SCHEMA_TOOL,
298
+ version: versionOf(import.meta.url)
299
+ });
300
+ var PUBLISHED_TOOL = "release-published";
301
+ 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)";
302
+ var publishedEnvelope = (report, durationMs) => ({
303
+ considered: report.considered,
304
+ durationMs,
305
+ excused: report.excused,
306
+ findings: report.lines.filter((one) => one.verdict !== "MATCH"),
307
+ read: report.lines.filter((one) => one.verdict !== "UNREACHABLE").length,
308
+ refused: report.lines.filter((one) => one.verdict === "UNREACHABLE").map((one) => ({ path: one.at, reason: one.why })),
309
+ tool: PUBLISHED_TOOL,
310
+ version: versionOf(import.meta.url)
311
+ });
312
+
313
+ // src/registry.ts
314
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
315
+ var TIMEOUT_MS = 15e3;
316
+ var registryPathOf = (name) => name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
317
+ var versionsIn = (body) => {
318
+ const versions = body.versions;
319
+ return typeof versions === "object" && versions !== null ? Object.keys(versions) : [];
320
+ };
321
+ var latestIn = (body) => {
322
+ const tags = body["dist-tags"];
323
+ const latest = typeof tags === "object" && tags !== null ? tags.latest : void 0;
324
+ return typeof latest === "string" ? latest : void 0;
325
+ };
326
+ var askRegistry = async ({
327
+ name,
328
+ registry = DEFAULT_REGISTRY,
329
+ timeoutMs = TIMEOUT_MS
330
+ }) => {
331
+ const url = `${registry.replace(/\/+$/, "")}/${registryPathOf(name)}`;
332
+ let response;
333
+ try {
334
+ response = await fetch(url, {
335
+ headers: { accept: "application/json" },
336
+ signal: AbortSignal.timeout(timeoutMs)
337
+ });
338
+ } catch (error) {
339
+ return { kind: "unreachable", name, why: `GET ${url} \u2014 ${error.message}` };
340
+ }
341
+ if (response.status === 404) return { kind: "absent", name };
342
+ if (!response.ok) {
343
+ return { kind: "unreachable", name, why: `GET ${url} \u2014 HTTP ${response.status}` };
344
+ }
345
+ let body;
346
+ try {
347
+ body = await response.json();
348
+ } catch (error) {
349
+ return {
350
+ kind: "unreachable",
351
+ name,
352
+ why: `GET ${url} answered ${response.status} with something that is not JSON \u2014 ${error.message}`
353
+ };
354
+ }
355
+ return { kind: "present", latest: latestIn(body), name, versions: versionsIn(body) };
356
+ };
357
+
358
+ // src/published.ts
359
+ import { readdirSync, readFileSync as readFileSync3 } from "fs";
360
+ import { join as join2, relative, sep } from "path";
361
+ var NEVER_WALKED = /* @__PURE__ */ new Set(["build", "coverage", "dist", "node_modules", "storybook-static"]);
362
+ var pathOf = (root, path) => relative(root, path).split(sep).join("/");
363
+ var manifestsUnder = (root) => {
364
+ const found = [];
365
+ const walk = (dir) => {
366
+ let entries;
367
+ try {
368
+ entries = readdirSync(dir, { withFileTypes: true });
369
+ } catch {
370
+ return;
371
+ }
372
+ for (const entry of entries) {
373
+ if (entry.isDirectory()) {
374
+ if (!entry.name.startsWith(".") && !NEVER_WALKED.has(entry.name))
375
+ walk(join2(dir, entry.name));
376
+ continue;
377
+ }
378
+ if (entry.name === "package.json") found.push(join2(dir, entry.name));
379
+ }
380
+ };
381
+ walk(root);
382
+ return found;
383
+ };
384
+ var censusOf = (root) => {
385
+ const excused = [];
386
+ const locals = [];
387
+ for (const path of manifestsUnder(root)) {
388
+ const at = pathOf(root, path);
389
+ let manifest;
390
+ try {
391
+ manifest = JSON.parse(readFileSync3(path, "utf8"));
392
+ } catch (error) {
393
+ excused.push({ path: at, reason: `it does not parse: ${error.message}` });
394
+ continue;
395
+ }
396
+ if (manifest.private === true) {
397
+ excused.push({ path: at, reason: "private: true \u2014 nothing publishes it" });
398
+ continue;
399
+ }
400
+ if (typeof manifest.name !== "string" || manifest.name === "") {
401
+ excused.push({ path: at, reason: "it names no package" });
402
+ continue;
403
+ }
404
+ if (typeof manifest.version !== "string" || manifest.version === "") {
405
+ excused.push({ path: at, reason: "it declares no version" });
406
+ continue;
407
+ }
408
+ locals.push({ at, name: manifest.name, version: manifest.version });
409
+ }
410
+ return { excused, locals: locals.toSorted((a, b) => a.name.localeCompare(b.name)) };
411
+ };
412
+ var EXISTS_BUT = "exists but no version matching";
413
+ var lineFor = async (local, registry) => {
414
+ const answer = await askRegistry({ name: local.name, registry });
415
+ const base = { at: local.at, local: local.version, name: local.name };
416
+ if (answer.kind === "unreachable") {
417
+ return { ...base, latest: void 0, verdict: "UNREACHABLE", why: answer.why };
418
+ }
419
+ if (answer.kind === "absent") {
420
+ return {
421
+ ...base,
422
+ latest: void 0,
423
+ verdict: "ABSENT",
424
+ 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"
425
+ };
426
+ }
427
+ if (answer.latest === local.version) {
428
+ return { ...base, latest: answer.latest, verdict: "MATCH", why: "on npm" };
429
+ }
430
+ return {
431
+ ...base,
432
+ latest: answer.latest,
433
+ verdict: "BEHIND",
434
+ 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`
435
+ };
436
+ };
437
+ var sleep = (ms) => new Promise((done) => {
438
+ setTimeout(done, ms);
439
+ });
440
+ var POLL_MS = 5e3;
441
+ var runPublished = async ({
442
+ pollMs = POLL_MS,
443
+ registry = DEFAULT_REGISTRY,
444
+ root,
445
+ waitSeconds = 0
446
+ }) => {
447
+ const { excused, locals } = censusOf(root);
448
+ const startedAt = Date.now();
449
+ const budgetMs = Math.max(0, waitSeconds * 1e3);
450
+ let lines2 = [];
451
+ let sweeps = 0;
452
+ for (; ; ) {
453
+ lines2 = [];
454
+ for (const local of locals) lines2.push(await lineFor(local, registry));
455
+ sweeps += 1;
456
+ const settled = lines2.every((one) => one.verdict === "MATCH");
457
+ const stuck = lines2.some((one) => one.verdict === "UNREACHABLE");
458
+ const left = budgetMs - (Date.now() - startedAt);
459
+ if (settled || stuck || left <= 0) break;
460
+ await sleep(Math.min(pollMs, left));
461
+ }
462
+ const unreachable = lines2.filter((one) => one.verdict === "UNREACHABLE").length;
463
+ return {
464
+ considered: lines2.length + excused.length,
465
+ excused: excused.toSorted((a, b) => a.path.localeCompare(b.path)),
466
+ lines: lines2,
467
+ ok: lines2.length > 0 && lines2.every((one) => one.verdict === "MATCH"),
468
+ registry,
469
+ sweeps,
470
+ unreachable,
471
+ waitedMs: Date.now() - startedAt
472
+ };
473
+ };
474
+ 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";
475
+ var formatPublished = (report) => {
476
+ const width = Math.max(1, ...report.lines.map((one) => one.name.length));
477
+ const body = report.lines.map((one) => ` ${one.verdict.padEnd(11)} ${one.name.padEnd(width)} ${one.local} ${one.why}`).join("\n");
478
+ 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}`;
479
+ const tail = [
480
+ `considered ${report.considered} manifests: ${report.lines.length} read, ${report.excused.length} excused`,
481
+ report.sweeps > 1 ? `swept ${report.sweeps} times over ${report.waitedMs} ms` : ""
482
+ ].filter((one) => one !== "");
483
+ return `${[head, body, ...tail].filter((one) => one !== "").join("\n")}
484
+ `;
485
+ };
486
+
487
+ // src/smoke.ts
488
+ import { spawnSync } from "child_process";
489
+ import {
490
+ cpSync,
491
+ existsSync as existsSync2,
492
+ mkdirSync as mkdirSync2,
493
+ readdirSync as readdirSync2,
494
+ readFileSync as readFileSync4,
495
+ rmSync,
496
+ statSync,
497
+ writeFileSync as writeFileSync2
498
+ } from "fs";
499
+ import { join as join3, relative as relative2, resolve as resolve2 } from "path";
500
+ var SNAPSHOTS_DIR = ".geonosis/consumer-snapshots";
501
+ var LOCKFILES = [
502
+ { file: "bun.lock", manager: "bun" },
503
+ { file: "bun.lockb", manager: "bun" },
504
+ { file: "pnpm-lock.yaml", manager: "pnpm" }
505
+ ];
506
+ var UNMEASURED = [
507
+ { file: "package-lock.json", manager: "npm" },
508
+ { file: "yarn.lock", manager: "yarn" }
509
+ ];
510
+ var EXCLUDED_DIRS = [
511
+ ".cache",
512
+ ".geonosis",
513
+ ".git",
514
+ ".next",
515
+ ".turbo",
516
+ ".wrangler",
517
+ "coverage",
518
+ "dist",
519
+ "node_modules",
520
+ "storybook-static"
521
+ ];
522
+ var PHASES = ["typecheck", "lint", "doctor"];
523
+ var headOf = (from) => {
524
+ const done = spawnSync("git", ["-C", from, "rev-parse", "HEAD"], { encoding: "utf8" });
525
+ if (done.error !== void 0) {
526
+ return { why: `git could not be run here: ${done.error.message}` };
527
+ }
528
+ if (done.status !== 0) {
529
+ return { why: `git says nothing about ${from}: ${done.stderr.trim()}` };
530
+ }
531
+ return { sha: done.stdout.trim() };
532
+ };
533
+ var DOCTOR_PACKAGE = "@geonosis/doctor";
534
+ var DOCTOR_BIN = "geonosis-doctor";
535
+ var DOCTOR_DOORS = ["@geonosis/cli", "geonosis"];
536
+ var DOOR_BIN = "geonosis doctor";
537
+ var isRecord2 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
538
+ var readManifest = (path) => {
539
+ try {
540
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
541
+ return isRecord2(parsed) ? parsed : {};
542
+ } catch (error) {
543
+ throw new CannotRun(`${path} is not readable JSON: ${error.message}`);
544
+ }
545
+ };
546
+ var versionsIn2 = (manifest) => {
547
+ const found = {};
548
+ for (const key of ["dependencies", "devDependencies"]) {
549
+ const block = manifest[key];
550
+ if (!isRecord2(block)) continue;
551
+ for (const [name, range] of Object.entries(block)) {
552
+ if (typeof range === "string") found[name] = range;
553
+ }
554
+ }
555
+ return Object.fromEntries(Object.entries(found).toSorted(([a], [b]) => a.localeCompare(b)));
556
+ };
557
+ var scriptsIn = (manifest) => {
558
+ const scripts = manifest["scripts"];
559
+ if (!isRecord2(scripts)) return {};
560
+ return Object.fromEntries(
561
+ Object.entries(scripts).filter((one) => typeof one[1] === "string")
562
+ );
563
+ };
564
+ var copyInto = (from, to, excluded) => {
565
+ let bytes = 0;
566
+ let files = 0;
567
+ const walk = (dir, into) => {
568
+ mkdirSync2(into, { recursive: true });
569
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
570
+ if (excluded.has(entry.name)) continue;
571
+ const at = join3(dir, entry.name);
572
+ if (entry.isDirectory()) {
573
+ walk(at, join3(into, entry.name));
574
+ continue;
575
+ }
576
+ if (!entry.isFile()) continue;
577
+ cpSync(at, join3(into, entry.name));
578
+ bytes += statSync(at).size;
579
+ files += 1;
580
+ }
581
+ };
582
+ walk(from, to);
583
+ return { bytes, files };
584
+ };
585
+ var manifestsUnder2 = (root) => {
586
+ const found = [];
587
+ const walk = (dir) => {
588
+ for (const entry of readdirSync2(dir, { withFileTypes: true }).toSorted(
589
+ (a, b) => a.name.localeCompare(b.name)
590
+ )) {
591
+ const at = join3(dir, entry.name);
592
+ if (entry.isDirectory()) {
593
+ walk(at);
594
+ continue;
595
+ }
596
+ if (entry.name !== "package.json") continue;
597
+ found.push({ path: relative2(root, at), versions: versionsIn2(readManifest(at)) });
598
+ }
599
+ };
600
+ walk(root);
601
+ return found.toSorted((a, b) => a.path.localeCompare(b.path));
602
+ };
603
+ var managerOf = (from) => {
604
+ const found = LOCKFILES.find((one) => existsSync2(join3(from, one.file)));
605
+ if (found !== void 0) return { lockfile: found.file, manager: found.manager };
606
+ const unmeasured = UNMEASURED.find((one) => existsSync2(join3(from, one.file)));
607
+ if (unmeasured !== void 0) {
608
+ throw new CannotRun(
609
+ `${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(", ")}.`
610
+ );
611
+ }
612
+ throw new CannotRun(
613
+ `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.`
614
+ );
615
+ };
616
+ var commandsFor = (manifest, manifests, named) => {
617
+ const scripts = scriptsIn(manifest);
618
+ const installs = (name) => manifests.some((one) => one.versions[name] !== void 0);
619
+ const doctorCommand = installs(DOCTOR_PACKAGE) ? { exec: DOCTOR_BIN } : DOCTOR_DOORS.some(installs) ? { exec: DOOR_BIN } : {
620
+ 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}`
621
+ };
622
+ const phase = (name, fallback) => {
623
+ const override = named[name];
624
+ if (override !== void 0) return { exec: override };
625
+ if (scripts[name] !== void 0) return { run: name };
626
+ return fallback;
627
+ };
628
+ return {
629
+ doctor: phase("doctor", doctorCommand),
630
+ lint: phase("lint", { why: 'the tree has no "lint" script and no --lint command was named' }),
631
+ typecheck: phase("typecheck", {
632
+ why: 'the tree has no "typecheck" script and no --typecheck command was named'
633
+ })
634
+ };
635
+ };
636
+ var snapshotDir = (root, name) => join3(root, SNAPSHOTS_DIR, name);
637
+ var readSnapshot = (root, name) => {
638
+ const at = join3(snapshotDir(root, name), "snapshot.json");
639
+ if (!existsSync2(at)) {
640
+ throw new CannotRun(
641
+ `there is no snapshot called "${name}" here \u2014 ${at} does not exist. Record one with: geonosis-release smoke snapshot ${name} --from <their tree>`
642
+ );
643
+ }
644
+ return JSON.parse(readFileSync4(at, "utf8"));
645
+ };
646
+ var wouldBeCommitted = (root, path) => {
647
+ const inside = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root });
648
+ if (inside.error !== void 0 || inside.status !== 0) return false;
649
+ return spawnSync("git", ["check-ignore", "-q", path], { cwd: root }).status !== 0;
650
+ };
651
+ var NAME_SHAPE = /^[a-z0-9][\w.-]*$/i;
652
+ var runSnapshot = (input) => {
653
+ if (!NAME_SHAPE.test(input.name)) {
654
+ throw new CannotRun(
655
+ `"${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`
656
+ );
657
+ }
658
+ const from = resolve2(input.from);
659
+ if (!existsSync2(join3(from, "package.json"))) {
660
+ throw new CannotRun(`${from} has no package.json \u2014 that is not a tree a consumer installs`);
661
+ }
662
+ const at = snapshotDir(input.root, input.name);
663
+ if (wouldBeCommitted(input.root, join3(SNAPSHOTS_DIR, input.name))) {
664
+ throw new CannotRun(
665
+ `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.`
666
+ );
667
+ }
668
+ if (existsSync2(at) && !input.replace) {
669
+ throw new CannotRun(
670
+ `${at} is already a snapshot, and a baseline recorded against other bytes is worse than none \u2014 pass --replace to overwrite it`
671
+ );
672
+ }
673
+ const { lockfile, manager } = managerOf(from);
674
+ const excluded = [.../* @__PURE__ */ new Set([...EXCLUDED_DIRS, ...input.exclude ?? []])].toSorted();
675
+ rmSync(at, { force: true, recursive: true });
676
+ const tree = join3(at, "tree");
677
+ const { bytes, files } = copyInto(from, tree, new Set(excluded));
678
+ const manifests = manifestsUnder2(tree);
679
+ const record = {
680
+ at: (/* @__PURE__ */ new Date()).toISOString(),
681
+ bytes,
682
+ commit: headOf(from),
683
+ commands: commandsFor(readManifest(join3(tree, "package.json")), manifests, input.named),
684
+ excluded,
685
+ files,
686
+ from,
687
+ lockfile,
688
+ manager,
689
+ manifests,
690
+ name: input.name
691
+ };
692
+ writeFileSync2(join3(at, "snapshot.json"), `${JSON.stringify(record, void 0, 2)}
693
+ `);
694
+ return record;
695
+ };
696
+ var describeCommand = (command) => {
697
+ if ("run" in command) return `run ${command.run}`;
698
+ if ("exec" in command) return command.exec;
699
+ return `nothing \u2014 ${command.why}`;
700
+ };
701
+ var describeCommit = (commit) => {
702
+ if (commit === void 0) return "nothing recorded its commit";
703
+ if ("sha" in commit) return commit.sha;
704
+ return commit.why;
705
+ };
706
+ var formatSnapshot = (record) => [
707
+ `OK snapshot ${record.name}: ${record.files} files, ${record.bytes} bytes, ${record.manifests.length} manifests
708
+ `,
709
+ ` from ${record.from} \u2014 ${record.manager} (${record.lockfile})
710
+ `,
711
+ ` at ${describeCommit(record.commit)}
712
+ `,
713
+ ...PHASES.map((phase) => ` ${phase}: ${describeCommand(record.commands[phase])}
714
+ `)
715
+ ].join("");
716
+
717
+ // src/adoption.ts
718
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
719
+ import { join as join4 } from "path";
720
+ var groupsOf = (root) => {
721
+ const at = join4(root, ".changeset/config.json");
722
+ if (!existsSync3(at)) return [];
723
+ let parsed;
724
+ try {
725
+ parsed = JSON.parse(readFileSync5(at, "utf8"));
726
+ } catch (error) {
727
+ throw new CannotRun(`${at} is not readable JSON: ${error.message}`);
728
+ }
729
+ const fixed = parsed.fixed;
730
+ return Array.isArray(fixed) ? fixed.filter((one) => Array.isArray(one) && one.every(isString)) : [];
731
+ };
732
+ var isString = (value2) => typeof value2 === "string";
733
+ var RANGE_PREFIX = /^[\^~>=<v\s]+/;
734
+ var RELEASE_NUMBER = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/;
735
+ var numberIn = (spec) => {
736
+ const bare = spec.replace(RANGE_PREFIX, "").trim();
737
+ return RELEASE_NUMBER.test(bare) ? bare : void 0;
738
+ };
739
+ var declarationsIn = (record, kit) => record.manifests.flatMap(
740
+ (manifest) => Object.entries(manifest.versions).filter(([name]) => kit.has(name)).map(([name, spec]) => ({ name, path: manifest.path, spec }))
741
+ );
742
+ var groupName = (index, total) => total > 1 ? `fixed group ${index + 1}` : "the fixed group";
743
+ var incoherence = (consumer, groups, declarations) => groups.flatMap((group, index) => {
744
+ const members = new Set(group);
745
+ const versions = [
746
+ ...new Set(
747
+ declarations.filter((one) => members.has(one.name)).flatMap((one) => {
748
+ const number = numberIn(one.spec);
749
+ return number === void 0 ? [] : [number];
750
+ })
751
+ )
752
+ ].toSorted();
753
+ if (versions.length < 2) return [];
754
+ const name = groupName(index, groups.length);
755
+ return [
756
+ {
757
+ consumer,
758
+ detail: `${name} at ${versions.join(", ")} \u2014 they are published as one number, so this tree is a bump that landed halfway`,
759
+ name,
760
+ verdict: "INCOHERENT"
761
+ }
762
+ ];
763
+ });
764
+ var distance = (consumer, declarations, current) => declarations.flatMap((one) => {
765
+ const now = current.get(one.name);
766
+ if (now === void 0) return [];
767
+ const declared = numberIn(one.spec);
768
+ if (declared === void 0) {
769
+ return [
770
+ {
771
+ consumer,
772
+ detail: `${one.path} names it "${one.spec}", which is not a release number this can compare`,
773
+ name: one.name,
774
+ verdict: "UNJUDGED"
775
+ }
776
+ ];
777
+ }
778
+ if (declared === now) return [];
779
+ return [{ consumer, detail: `${declared} \u2192 ${now}`, name: one.name, verdict: "BEHIND" }];
780
+ });
781
+ var undeclaredFloors = (consumer, floors, declarations) => {
782
+ const declared = new Set(declarations.map((one) => one.name));
783
+ return floors.filter((name) => !declared.has(name)).map((name) => ({
784
+ consumer,
785
+ detail: "nothing in this tree declares it \u2014 informational, a floor is opted into",
786
+ name,
787
+ verdict: "FLOOR UNDECLARED"
788
+ }));
789
+ };
790
+ var currentVersions = async (root, registry) => {
791
+ const locals = censusOf(root).locals;
792
+ const current = new Map(locals.map((one) => [one.name, one.version]));
793
+ if (registry === void 0) return current;
794
+ for (const one of locals) {
795
+ const answer = await askRegistry({ name: one.name, registry });
796
+ if (answer.kind === "present" && answer.latest !== void 0)
797
+ current.set(one.name, answer.latest);
798
+ }
799
+ return current;
800
+ };
801
+ var runAdoption = async ({
802
+ declared,
803
+ registry,
804
+ root
805
+ }) => {
806
+ if (declared.length === 0) {
807
+ throw new CannotRun(
808
+ "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."
809
+ );
810
+ }
811
+ const current = await currentVersions(root, registry);
812
+ const groups = groupsOf(root);
813
+ const grouped = new Set(groups.flat());
814
+ const floors = [...current.keys()].filter((name) => !grouped.has(name)).toSorted();
815
+ const consumers = [];
816
+ const excused = [];
817
+ for (const one of declared) {
818
+ if (!existsSync3(join4(snapshotDir(root, one.name), "snapshot.json"))) {
819
+ excused.push({
820
+ path: one.name,
821
+ reason: `no recording of it here \u2014 record one with: geonosis-release smoke snapshot ${one.name} --from <their tree>`
822
+ });
823
+ continue;
824
+ }
825
+ const declarations = declarationsIn(readSnapshot(root, one.name), new Set(current.keys()));
826
+ consumers.push({
827
+ declared: declarations.length,
828
+ findings: [
829
+ ...distance(one.name, declarations, current),
830
+ ...incoherence(one.name, groups, declarations),
831
+ ...undeclaredFloors(one.name, floors, declarations)
832
+ ],
833
+ name: one.name
834
+ });
835
+ }
836
+ return {
837
+ considered: declared.length,
838
+ consumers,
839
+ excused,
840
+ registry,
841
+ versions: Object.fromEntries([...current.entries()].toSorted(([a], [b]) => a.localeCompare(b)))
842
+ };
843
+ };
844
+ var NOT_A_FAULT = /* @__PURE__ */ new Set(["FLOOR UNDECLARED"]);
845
+ var lineOf = (finding) => {
846
+ if (finding.verdict === "INCOHERENT") return ` INCOHERENT ${finding.consumer}: ${finding.detail}`;
847
+ if (finding.verdict === "FLOOR UNDECLARED") {
848
+ return ` FLOOR UNDECLARED ${finding.consumer} ${finding.name}`;
849
+ }
850
+ return ` ${finding.verdict} ${finding.consumer} ${finding.name} ${finding.detail}`;
851
+ };
852
+ var blockFor = (consumer) => {
853
+ const faults = consumer.findings.filter((one) => !NOT_A_FAULT.has(one.verdict));
854
+ const head = faults.length === 0 ? [
855
+ ` MATCH ${consumer.name} \u2014 ${consumer.declared} spec(s) on the versions this release is cut at`
856
+ ] : [];
857
+ return [...head, ...new Set(consumer.findings.map(lineOf))];
858
+ };
859
+ var formatAdoption = (report) => [
860
+ `adoption \u2014 ${report.considered} declared, ${report.consumers.length} read, ${report.excused.length} excused; against ${report.registry ?? "this workspace"}`,
861
+ ...report.consumers.flatMap(blockFor),
862
+ ...report.excused.map((one) => ` SKIP ${one.path}: ${one.reason}`),
863
+ ""
864
+ ].join("\n");
865
+ var ADOPTION_TOOL = "release-adoption";
866
+ 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)";
867
+ var adoptionEnvelope = (report, durationMs) => ({
868
+ considered: report.considered,
869
+ durationMs,
870
+ excused: report.excused,
871
+ findings: report.consumers.flatMap((one) => one.findings),
872
+ read: report.consumers.length,
873
+ refused: [],
874
+ tool: ADOPTION_TOOL,
875
+ version: versionOf(import.meta.url)
876
+ });
877
+
878
+ // src/wrangler.ts
879
+ import { readFileSync as readFileSync6 } from "fs";
880
+ import { resolve as resolve3 } from "path";
881
+ var isRecord3 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
882
+ var parseJsonc = (source) => {
883
+ let out = "";
884
+ for (let index = 0; index < source.length; index += 1) {
885
+ const char = source[index] ?? "";
886
+ if (char === '"') {
887
+ const start = index;
888
+ index += 1;
889
+ for (; index < source.length; index += 1) {
890
+ if (source[index] === "\\") {
891
+ index += 1;
892
+ continue;
893
+ }
894
+ if (source[index] === '"') break;
895
+ }
896
+ out += source.slice(start, index + 1);
897
+ continue;
898
+ }
899
+ if (char === "/" && source[index + 1] === "/") {
900
+ const end = source.indexOf("\n", index);
901
+ index = end === -1 ? source.length : end - 1;
902
+ continue;
903
+ }
904
+ if (char === "/" && source[index + 1] === "*") {
905
+ const end = source.indexOf("*/", index + 2);
906
+ index = end === -1 ? source.length : end + 1;
907
+ continue;
908
+ }
909
+ out += char;
910
+ }
911
+ return JSON.parse(out.replaceAll(/,(\s*[\]}])/g, "$1"));
912
+ };
913
+ var literal = (text) => {
914
+ const value2 = text.trim();
915
+ if (value2.startsWith('"') || value2.startsWith("'")) return value2.slice(1, -1);
916
+ if (value2 === "true") return true;
917
+ if (value2 === "false") return false;
918
+ const number = Number(value2);
919
+ return Number.isNaN(number) ? value2 : number;
920
+ };
921
+ var split = (text, delimiter) => {
922
+ const parts = [];
923
+ let depth = 0;
924
+ let quote = "";
925
+ let current = "";
926
+ for (const char of text) {
927
+ if (quote !== "") {
928
+ current += char;
929
+ if (char === quote) quote = "";
930
+ continue;
931
+ }
932
+ if (char === '"' || char === "'") quote = char;
933
+ if (char === "[" || char === "{") depth += 1;
934
+ if (char === "]" || char === "}") depth -= 1;
935
+ if (char === delimiter && depth === 0) {
936
+ parts.push(current);
937
+ current = "";
938
+ continue;
939
+ }
940
+ current += char;
941
+ }
942
+ parts.push(current);
943
+ return parts.filter((one) => one.trim() !== "");
944
+ };
945
+ var value = (text) => {
946
+ const trimmed = text.trim();
947
+ if (trimmed.startsWith("[")) return split(trimmed.slice(1, -1), ",").map(value);
948
+ if (trimmed.startsWith("{")) {
949
+ return Object.fromEntries(
950
+ split(trimmed.slice(1, -1), ",").map((pair) => {
951
+ const at = pair.indexOf("=");
952
+ return [pair.slice(0, at).trim(), value(pair.slice(at + 1))];
953
+ })
954
+ );
955
+ }
956
+ return literal(trimmed);
957
+ };
958
+ var put = (into, path, leaf) => {
959
+ let here = into;
960
+ for (const key of path.slice(0, -1)) {
961
+ if (!isRecord3(here[key])) here[key] = {};
962
+ here = here[key];
963
+ }
964
+ here[path.at(-1) ?? ""] = leaf;
965
+ };
966
+ var table = (into, path) => {
967
+ let here = into;
968
+ for (const key of path) {
969
+ if (!isRecord3(here[key])) here[key] = {};
970
+ here = here[key];
971
+ }
972
+ return here;
973
+ };
974
+ var arrayTable = (into, path) => {
975
+ let here = into;
976
+ for (const key of path.slice(0, -1)) {
977
+ if (!isRecord3(here[key])) here[key] = {};
978
+ here = here[key];
979
+ }
980
+ const last = path.at(-1) ?? "";
981
+ if (!Array.isArray(here[last])) here[last] = [];
982
+ const list = here[last];
983
+ const entry = {};
984
+ list.push(entry);
985
+ return entry;
986
+ };
987
+ var parseToml = (source) => {
988
+ const out = {};
989
+ let here = out;
990
+ const lines2 = source.split("\n");
991
+ for (let index = 0; index < lines2.length; index += 1) {
992
+ const line = (lines2[index] ?? "").split("#")[0]?.trim() ?? "";
993
+ if (line === "") continue;
994
+ if (line.startsWith("[[") && line.endsWith("]]")) {
995
+ here = arrayTable(out, line.slice(2, -2).trim().split("."));
996
+ continue;
997
+ }
998
+ if (line.startsWith("[") && line.endsWith("]")) {
999
+ here = table(out, line.slice(1, -1).trim().split("."));
1000
+ continue;
1001
+ }
1002
+ const at = line.indexOf("=");
1003
+ if (at === -1) continue;
1004
+ let text = line.slice(at + 1);
1005
+ while ([...text].filter((one) => one === "[").length > [...text].filter((one) => one === "]").length) {
1006
+ index += 1;
1007
+ if (index >= lines2.length) throw new CannotRun("an unterminated array in the TOML config");
1008
+ text += `
1009
+ ${(lines2[index] ?? "").split("#")[0] ?? ""}`;
1010
+ }
1011
+ put(here, line.slice(0, at).trim().split("."), value(text));
1012
+ }
1013
+ return out;
1014
+ };
1015
+ var BINDING_LISTS = [
1016
+ "ai",
1017
+ "analytics_engine_datasets",
1018
+ "browser",
1019
+ "d1_databases",
1020
+ "dispatch_namespaces",
1021
+ "durable_objects",
1022
+ "hyperdrive",
1023
+ "kv_namespaces",
1024
+ "mtls_certificates",
1025
+ "queues",
1026
+ "r2_buckets",
1027
+ "send_email",
1028
+ "services",
1029
+ "vectorize",
1030
+ "version_metadata",
1031
+ "workflows"
1032
+ ];
1033
+ var bindingsOf = (found) => {
1034
+ if (Array.isArray(found)) return found.flatMap(bindingsOf);
1035
+ if (!isRecord3(found)) return [];
1036
+ const named = found["binding"] ?? found["name"];
1037
+ const here = typeof named === "string" ? [named] : [];
1038
+ const nested = Object.entries(found).filter(([key]) => key === "bindings" || key === "producers" || key === "consumers").flatMap(([, one]) => bindingsOf(one));
1039
+ return [...here, ...nested];
1040
+ };
1041
+ var declaredIn = (config) => {
1042
+ const triggers = config["triggers"];
1043
+ const crons = isRecord3(triggers) && Array.isArray(triggers["crons"]) ? triggers["crons"] : [];
1044
+ const routes = config["routes"];
1045
+ const one = config["route"];
1046
+ const listed = [
1047
+ ...Array.isArray(routes) ? routes : [],
1048
+ ...typeof one === "string" ? [one] : []
1049
+ ];
1050
+ return {
1051
+ bindings: BINDING_LISTS.flatMap((key) => bindingsOf(config[key])).toSorted(),
1052
+ crons: crons.filter((cron) => typeof cron === "string").toSorted(),
1053
+ routes: listed.map((route) => isRecord3(route) ? route["pattern"] : route).filter((pattern) => typeof pattern === "string").toSorted()
1054
+ };
1055
+ };
1056
+ var readWrangler = (root, relative5, env) => {
1057
+ const path = resolve3(root, relative5);
1058
+ let parsed;
1059
+ try {
1060
+ const source = readFileSync6(path, "utf8");
1061
+ parsed = relative5.endsWith(".toml") ? parseToml(source) : parseJsonc(source);
1062
+ } catch (error) {
1063
+ throw new CannotRun(`${relative5} could not be read: ${error.message}`);
1064
+ }
1065
+ if (!isRecord3(parsed)) throw new CannotRun(`${relative5} is not a wrangler configuration`);
1066
+ const environments = parsed["env"];
1067
+ const block = env !== void 0 && isRecord3(environments) && isRecord3(environments[env]) ? environments[env] : parsed;
1068
+ return declaredIn(block);
1069
+ };
1070
+
1071
+ // src/deployed.ts
1072
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
1073
+ import { resolve as resolve4 } from "path";
1074
+ var DEPLOYED_FILE = ".geonosis/deployed.json";
1075
+ 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";
1076
+ var listOf = (found) => Array.isArray(found) ? found.filter((one) => typeof one === "string") : [];
1077
+ var isRecord4 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1078
+ var readDeployed = (root) => {
1079
+ const path = resolve4(root, DEPLOYED_FILE);
1080
+ if (!existsSync4(path)) throw new CannotRun(NOTHING_SAYS);
1081
+ let parsed;
1082
+ try {
1083
+ parsed = JSON.parse(readFileSync7(path, "utf8"));
1084
+ } catch (error) {
1085
+ throw new CannotRun(`${DEPLOYED_FILE} is not readable JSON: ${error.message}`);
1086
+ }
1087
+ if (!isRecord4(parsed)) throw new CannotRun(`${DEPLOYED_FILE} is not an object`);
1088
+ const triggers = isRecord4(parsed["triggers"]) ? parsed["triggers"] : {};
1089
+ return {
1090
+ ...typeof parsed["at"] === "string" ? { at: parsed["at"] } : {},
1091
+ deployed: {
1092
+ bindings: listOf(parsed["bindings"]),
1093
+ crons: listOf(triggers["crons"]),
1094
+ routes: listOf(triggers["routes"]),
1095
+ secrets: listOf(parsed["secrets"])
1096
+ }
1097
+ };
1098
+ };
1099
+ var missingBetween = (kind, declared, deployed) => {
1100
+ const missing = declared.filter((one) => !deployed.includes(one));
1101
+ const extra = deployed.filter((one) => !declared.includes(one));
1102
+ return missing.length === 0 && extra.length === 0 ? [] : [{ extra, kind, missing }];
1103
+ };
1104
+ var driftBetween = (declared, deployed) => [
1105
+ ...missingBetween("crons", declared.crons, deployed.crons),
1106
+ ...missingBetween("routes", declared.routes, deployed.routes),
1107
+ ...missingBetween("bindings", declared.bindings, deployed.bindings),
1108
+ ...missingBetween("secrets", declared.secrets, deployed.secrets)
1109
+ ];
410
1110
  var runDeployed = (input) => {
411
1111
  const config = readReleaseConfig(input.root);
412
1112
  const configs = config.wrangler ?? [];
@@ -441,7 +1141,7 @@ var formatDeployed = (report) => {
441
1141
  };
442
1142
 
443
1143
  // src/added.ts
444
- import { execFileSync, spawnSync } from "child_process";
1144
+ import { execFileSync, spawnSync as spawnSync2 } from "child_process";
445
1145
  var git = (root, args) => {
446
1146
  try {
447
1147
  return execFileSync("git", [...args], { cwd: root, encoding: "utf8", stdio: "pipe" });
@@ -457,7 +1157,7 @@ var addedSince = (root, since) => {
457
1157
  return [.../* @__PURE__ */ new Set([...committed, ...staged, ...untracked])].toSorted();
458
1158
  };
459
1159
  var gitOk = (root, args) => {
460
- const run = spawnSync("git", [...args], { cwd: root, encoding: "utf8" });
1160
+ const run = spawnSync2("git", [...args], { cwd: root, encoding: "utf8" });
461
1161
  return run.error === void 0 && run.status === 0;
462
1162
  };
463
1163
 
@@ -647,32 +1347,32 @@ var NARROWING = [
647
1347
  ["SET NOT NULL", /\bSET\s+NOT\s+NULL\b/i]
648
1348
  ];
649
1349
  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;
1350
+ var lineOf2 = (body, index) => body.slice(0, index).split("\n").length;
651
1351
  var refusalsIn = (path, body, firstLine = 1) => {
652
1352
  const stripped = statementsOf(body);
653
1353
  return NARROWING.flatMap(([verb, pattern]) => {
654
1354
  const found = pattern.exec(stripped);
655
- return found === null ? [] : [{ line: lineOf(stripped, found.index) + firstLine - 1, path, verb }];
1355
+ return found === null ? [] : [{ line: lineOf2(stripped, found.index) + firstLine - 1, path, verb }];
656
1356
  });
657
1357
  };
658
1358
 
659
1359
  // src/migrations.ts
660
- import { mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
1360
+ import { mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
661
1361
  import { tmpdir } from "os";
662
- import { basename, join, resolve as resolve5 } from "path";
1362
+ import { basename, join as join5, resolve as resolve6 } from "path";
663
1363
 
664
1364
  // 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));
1365
+ import { spawnSync as spawnSync3 } from "child_process";
1366
+ import { existsSync as existsSync5 } from "fs";
1367
+ import { dirname as dirname2, resolve as resolve5 } from "path";
1368
+ import { fileURLToPath as fileURLToPath2 } from "url";
1369
+ var HERE = dirname2(fileURLToPath2(import.meta.url));
670
1370
  var PINNED = "2.63.0";
671
1371
  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;
1372
+ for (let dir = from; ; dir = dirname2(dir)) {
1373
+ const candidate = resolve5(dir, "node_modules/.bin/squawk");
1374
+ if (existsSync5(candidate)) return candidate;
1375
+ if (dirname2(dir) === dir) break;
676
1376
  }
677
1377
  throw new CannotRun(
678
1378
  `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 +1385,7 @@ It is an optional peer: a repo whose release.migrations names only the sqlite di
685
1385
  var squawkOn = (input) => {
686
1386
  const { cwd, exclude, files } = input;
687
1387
  if (files.length === 0) return { findings: "", ok: true };
688
- const run = spawnSync2(
1388
+ const run = spawnSync3(
689
1389
  input.bin ?? findSquawk(),
690
1390
  [...exclude.length === 0 ? [] : [`--exclude=${exclude.join(",")}`], ...files],
691
1391
  { cwd, encoding: "utf8" }
@@ -752,7 +1452,7 @@ var runMigrations = (input) => {
752
1452
  const forSquawk = [];
753
1453
  const states = /* @__PURE__ */ new Map();
754
1454
  for (const { entry, file } of matched) {
755
- const source = readFileSync4(resolve5(input.root, file), "utf8");
1455
+ const source = readFileSync8(resolve6(input.root, file), "utf8");
756
1456
  const found = markersIn(source);
757
1457
  const bad = found.flatMap((one) => {
758
1458
  const why = judge(one, input.root, input.since);
@@ -777,11 +1477,11 @@ var runMigrations = (input) => {
777
1477
  }
778
1478
  let clean = true;
779
1479
  if (forSquawk.length > 0) {
780
- const staging = mkdtempSync(join(tmpdir(), "geonosis-release-squawk-"));
1480
+ const staging = mkdtempSync(join5(tmpdir(), "geonosis-release-squawk-"));
781
1481
  try {
782
1482
  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}
1483
+ const path = one.entry.dialect === "mikro-orm-ts" ? join5(staging, `${basename(one.file, ".ts")}.sql`) : resolve6(input.root, one.file);
1484
+ if (one.entry.dialect === "mikro-orm-ts") writeFileSync3(path, `${one.sql}
785
1485
  `);
786
1486
  const found = squawkOn({
787
1487
  cwd: input.root,
@@ -795,7 +1495,7 @@ var runMigrations = (input) => {
795
1495
  }
796
1496
  }
797
1497
  } finally {
798
- rmSync(staging, { force: true, recursive: true });
1498
+ rmSync2(staging, { force: true, recursive: true });
799
1499
  }
800
1500
  }
801
1501
  return {
@@ -959,8 +1659,8 @@ var Runner = class {
959
1659
  };
960
1660
 
961
1661
  // src/prove.ts
962
- import { existsSync as existsSync4 } from "fs";
963
- import { fileURLToPath as fileURLToPath2 } from "url";
1662
+ import { existsSync as existsSync6 } from "fs";
1663
+ import { fileURLToPath as fileURLToPath3 } from "url";
964
1664
  var stringAt = (reply, key) => typeof reply[key] === "string" ? reply[key] : void 0;
965
1665
  var proveOver = async (input) => {
966
1666
  const runner = new Runner(input.command, input.args ?? [], input.cwd);
@@ -988,242 +1688,68 @@ var proveOver = async (input) => {
988
1688
  steps.push("promote");
989
1689
  const promoted = await runner.ask({ step: "promote", versionId }, input.timeoutMs);
990
1690
  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));
1691
+ const went = stringAt(promoted, "promotedVersionId");
1692
+ if (went !== void 0 && went !== versionId) {
1693
+ return refuse(`${versionId} was proved and ${went} was promoted \u2014 nothing proved ${went}`);
1694
+ }
1695
+ return { ok: true, steps };
1696
+ } finally {
1697
+ runner.close();
1198
1698
  }
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
1699
  };
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
- `;
1700
+ var PLANTS = [
1701
+ {
1702
+ name: "answered",
1703
+ says: "a smoke that answered a version other than the one it overrode to is refused"
1704
+ },
1705
+ { name: "failed", says: "a smoke that did not pass is refused" },
1706
+ { name: "promoted", says: "a promote of a version nothing proved is refused" }
1707
+ ];
1708
+ var stubPath = () => {
1709
+ const path = fileURLToPath3(new URL("./stub-runner.js", import.meta.url));
1710
+ if (!existsSync6(path)) throw new CannotRun(`${path} is missing \u2014 run pnpm build first`);
1711
+ return path;
1712
+ };
1713
+ var prove = async (cwd, stub = stubPath()) => {
1714
+ const lines2 = [];
1715
+ let ok = true;
1716
+ for (const plant of PLANTS) {
1717
+ const verdict = await proveOver({
1718
+ args: [stub, `--plant=${plant.name}`],
1719
+ command: process.execPath,
1720
+ cwd,
1721
+ timeoutMs: 3e4
1722
+ });
1723
+ if (verdict.ok) ok = false;
1724
+ lines2.push(` ${verdict.ok ? "MISSED" : "PROVEN"} ${plant.says}`);
1725
+ if (plant.name !== "promoted" && verdict.steps.includes("promote")) {
1726
+ ok = false;
1727
+ lines2.push(" MISSED a refused smoke was followed by a promote request");
1728
+ }
1729
+ }
1730
+ const honest = await proveOver({
1731
+ args: [stub, "--plant=good"],
1732
+ command: process.execPath,
1733
+ cwd,
1734
+ timeoutMs: 3e4
1735
+ });
1736
+ if (!honest.ok) ok = false;
1737
+ lines2.push(
1738
+ ` ${honest.ok ? "PROVEN" : "MISSED"} a smoke that answered the version it overrode to is not refused`
1739
+ );
1740
+ return { lines: lines2, ok };
1222
1741
  };
1742
+ var formatProve = (outcome) => `${outcome.lines.join("\n")}
1743
+
1744
+ prove ${outcome.ok ? "PASS" : "FAIL"} \u2014 ${outcome.ok ? "every plant was refused, and the honest runner was not" : "a plant went through"}.
1745
+ `;
1746
+ var formatVerdict = (verdict) => verdict.ok ? `OK prove: ${verdict.steps.join(" \u2192 ")}
1747
+ ` : ` REFUSED after ${verdict.steps.join(" \u2192 ")}: ${verdict.why ?? ""}
1748
+ `;
1223
1749
 
1224
1750
  // 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";
1751
+ import { readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
1752
+ import { join as join6, relative as relative3, resolve as resolve7, sep as sep2 } from "path";
1227
1753
  var MIGRATION_FILE = /\.(?:sql|ts)$/;
1228
1754
  var CURRENT_SETTING = /current_setting\s*\(\s*'([^']+)'/gi;
1229
1755
  var filesUnder = (root, dir) => {
@@ -1231,20 +1757,20 @@ var filesUnder = (root, dir) => {
1231
1757
  const walk = (at) => {
1232
1758
  let entries;
1233
1759
  try {
1234
- entries = readdirSync2(at, { withFileTypes: true });
1760
+ entries = readdirSync3(at, { withFileTypes: true });
1235
1761
  } catch {
1236
1762
  return;
1237
1763
  }
1238
1764
  for (const entry of entries) {
1239
- const path = join3(at, entry.name);
1765
+ const path = join6(at, entry.name);
1240
1766
  if (entry.isDirectory()) walk(path);
1241
1767
  else if (MIGRATION_FILE.test(entry.name)) found.push(path);
1242
1768
  }
1243
1769
  };
1244
- walk(resolve6(root, dir));
1245
- return found.map((path) => relative2(root, path).split(sep2).join("/")).toSorted();
1770
+ walk(resolve7(root, dir));
1771
+ return found.map((path) => relative3(root, path).split(sep2).join("/")).toSorted();
1246
1772
  };
1247
- var lineOf2 = (body, index) => body.slice(0, index).split("\n").length;
1773
+ var lineOf3 = (body, index) => body.slice(0, index).split("\n").length;
1248
1774
  var wordFor = (table2) => new RegExp(
1249
1775
  String.raw`(?<![\w.])${table2.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)}\b`,
1250
1776
  "i"
@@ -1258,7 +1784,7 @@ var foreignTables = (path, sql, schema) => {
1258
1784
  const found = wordFor(table2).exec(sql);
1259
1785
  return found === null ? [] : [
1260
1786
  {
1261
- line: lineOf2(sql, found.index),
1787
+ line: lineOf3(sql, found.index),
1262
1788
  path,
1263
1789
  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
1790
  }
@@ -1274,404 +1800,98 @@ var undeclaredSettings = (path, source, schema) => {
1274
1800
  const name = match[1] ?? "";
1275
1801
  if (allowed.has(name)) continue;
1276
1802
  found.push({
1277
- line: lineOf2(source, match.index),
1803
+ line: lineOf3(source, match.index),
1278
1804
  path,
1279
1805
  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
- );
1806
+ });
1484
1807
  }
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;
1808
+ return found;
1491
1809
  };
1492
- var NAME_SHAPE = /^[a-z0-9][\w.-]*$/i;
1493
- var runSnapshot = (input) => {
1494
- if (!NAME_SHAPE.test(input.name)) {
1810
+ var runSchema = ({ root }) => {
1811
+ const schema = readReleaseConfig(root).schema;
1812
+ if (schema === void 0) {
1495
1813
  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`
1814
+ "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
1815
  );
1498
1816
  }
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))) {
1817
+ if (schema.domains.length === 0 && schema.sessionVariables.length === 0) {
1505
1818
  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.`
1819
+ "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
1820
  );
1508
1821
  }
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
- );
1822
+ const dirs = schema.domains.map((one) => one.dir);
1823
+ const files = [...new Set(dirs.flatMap((dir) => filesUnder(root, dir)))].toSorted();
1824
+ const findings = [];
1825
+ const unreadable = [];
1826
+ for (const path of files) {
1827
+ const source = readFileSync9(resolve7(root, path), "utf8");
1828
+ let sql;
1829
+ try {
1830
+ sql = sqlOf(path, source);
1831
+ } catch (error) {
1832
+ if (!(error instanceof Unreadable)) throw error;
1833
+ unreadable.push({ path, why: `${path} holds ${error.message} \u2014 this reader cannot read it` });
1834
+ continue;
1835
+ }
1836
+ findings.push(...foreignTables(path, sql, schema), ...undeclaredSettings(path, source, schema));
1513
1837
  }
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,
1838
+ return {
1839
+ dirs,
1525
1840
  files,
1526
- from,
1527
- lockfile,
1528
- manager,
1529
- manifests,
1530
- name: input.name
1841
+ findings: findings.toSorted((a, b) => a.path.localeCompare(b.path) || a.line - b.line),
1842
+ ok: findings.length === 0 && unreadable.length === 0,
1843
+ read: files.length - unreadable.length,
1844
+ unreadable
1531
1845
  };
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
1846
  };
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
- );
1847
+ var formatSchema = (report) => {
1848
+ if (report.files.length === 0) {
1849
+ return `NONE schema: no migration file under ${report.dirs.join(", ")}; nothing was read
1850
+ `;
1583
1851
  }
1584
- if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
1585
- throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
1852
+ if (report.ok) {
1853
+ return `OK schema: ${report.read} migration(s) read under ${report.dirs.length} declared domain(s), no wall crossed
1854
+ `;
1586
1855
  }
1587
- const at = envelopePath(root, envelope.tool);
1588
- mkdirSync2(dirname2(at), { recursive: true });
1589
- writeFileSync3(at, `${JSON.stringify(envelope, void 0, 2)}
1856
+ const said = report.findings.map((one) => ` ${one.path}:${one.line}: ${one.why}
1590
1857
  `);
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
- }
1858
+ const cannot = report.unreadable.map((one) => ` ${one.why}
1859
+ `);
1860
+ 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`}
1861
+ `;
1611
1862
  };
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
1863
 
1652
1864
  // src/smoke-run.ts
1653
1865
  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";
1866
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
1867
+ import { join as join7, relative as relative4, resolve as resolve8 } from "path";
1656
1868
  var BASELINE_FILE = "baseline.json";
1657
1869
  var INSTALL = {
1658
1870
  bun: ["install"],
1659
1871
  pnpm: ["install", "--no-frozen-lockfile"]
1660
1872
  };
1873
+ var MANAGER_CONFIG = [".npmrc", "bunfig.toml", "pnpm-workspace.yaml"];
1874
+ var configBytesOf = (work) => Object.fromEntries(
1875
+ MANAGER_CONFIG.filter((file) => existsSync7(join7(work, file))).map((file) => [
1876
+ file,
1877
+ readFileSync10(join7(work, file), "utf8")
1878
+ ])
1879
+ );
1880
+ var editedByInstall = (before, after) => [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((file) => before[file] !== after[file]).toSorted();
1661
1881
  var isRecord5 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
1662
1882
  var readJson = (path) => {
1663
1883
  try {
1664
- return JSON.parse(readFileSync9(path, "utf8"));
1884
+ return JSON.parse(readFileSync10(path, "utf8"));
1665
1885
  } catch (error) {
1666
1886
  throw new CannotRun(`${path} is not readable JSON: ${error.message}`);
1667
1887
  }
1668
1888
  };
1669
1889
  var packRc = (rc, into) => {
1670
- if (!existsSync6(join6(rc, "package.json"))) {
1890
+ if (!existsSync7(join7(rc, "package.json"))) {
1671
1891
  throw new CannotRun(`${rc} has no package.json \u2014 that is not a workspace to pack`);
1672
1892
  }
1673
1893
  mkdirSync3(into, { recursive: true });
1674
- const root = readJson(join6(rc, "package.json"));
1894
+ const root = readJson(join7(rc, "package.json"));
1675
1895
  const isPrivateRoot = isRecord5(root) && root["private"] === true;
1676
1896
  const done = spawnSync4(
1677
1897
  "pnpm",
@@ -1721,11 +1941,11 @@ var sourceManifests = (rc) => {
1721
1941
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
1722
1942
  if (excluded.has(entry.name)) continue;
1723
1943
  if (entry.isDirectory()) {
1724
- walk(join6(dir, entry.name));
1944
+ walk(join7(dir, entry.name));
1725
1945
  continue;
1726
1946
  }
1727
1947
  if (entry.name !== "package.json") continue;
1728
- const manifest = readJson(join6(dir, entry.name));
1948
+ const manifest = readJson(join7(dir, entry.name));
1729
1949
  if (!isRecord5(manifest) || typeof manifest["name"] !== "string") continue;
1730
1950
  found.push({ manifest, name: manifest["name"] });
1731
1951
  }
@@ -1747,9 +1967,9 @@ var OVERRIDES = "overrides:";
1747
1967
  var TOP_LEVEL = /^\S/;
1748
1968
  var KEYED = /^\s+["']?(.+?)["']?\s*:/;
1749
1969
  var pinsInWorkspaceYaml = (work, pins) => {
1750
- const at = join6(work, "pnpm-workspace.yaml");
1970
+ const at = join7(work, "pnpm-workspace.yaml");
1751
1971
  const written = Object.entries(pins).map(([name, spec]) => ` '${name}': '${spec}'`);
1752
- const lines2 = existsSync6(at) ? readFileSync9(at, "utf8").split("\n") : [];
1972
+ const lines2 = existsSync7(at) ? readFileSync10(at, "utf8").split("\n") : [];
1753
1973
  const kept = [];
1754
1974
  let inside = false;
1755
1975
  let found = false;
@@ -1768,8 +1988,8 @@ var pinsInWorkspaceYaml = (work, pins) => {
1768
1988
  writeFileSync4(at, body.join("\n"));
1769
1989
  };
1770
1990
  var pinsInManifest = (work, pins) => {
1771
- const at = join6(work, "package.json");
1772
- if (!existsSync6(at)) return;
1991
+ const at = join7(work, "package.json");
1992
+ if (!existsSync7(at)) return;
1773
1993
  const manifest = readJson(at);
1774
1994
  if (!isRecord5(manifest)) return;
1775
1995
  manifest["overrides"] = {
@@ -1789,8 +2009,8 @@ var rewriteManifests = (tree, manifests, packed) => {
1789
2009
  const byName = new Map(packed.map((one) => [one.name, one.tarball]));
1790
2010
  const swapped = [];
1791
2011
  for (const path of manifests) {
1792
- const at = join6(tree, path);
1793
- if (!existsSync6(at)) continue;
2012
+ const at = join7(tree, path);
2013
+ if (!existsSync7(at)) continue;
1794
2014
  const manifest = readJson(at);
1795
2015
  if (!isRecord5(manifest)) continue;
1796
2016
  let touched = false;
@@ -1826,19 +2046,25 @@ var runPhase = (phase, command, cwd, manager) => {
1826
2046
  const line = commandLine(command, manager);
1827
2047
  const env = {
1828
2048
  ...process.env,
1829
- PATH: `${join6(cwd, "node_modules/.bin")}:${process.env["PATH"] ?? ""}`
2049
+ PATH: `${join7(cwd, "node_modules/.bin")}:${process.env["PATH"] ?? ""}`
1830
2050
  };
1831
2051
  const { code, output } = shellRun(line, cwd, env);
1832
2052
  return { code, command: line, lines: quotable(output), ok: code === 0, phase };
1833
2053
  };
2054
+ var commandsToRun = (record, declared) => Object.fromEntries(
2055
+ PHASES.map((phase) => {
2056
+ const named = declared[phase];
2057
+ return [phase, named === void 0 ? record.commands[phase] : { exec: named }];
2058
+ })
2059
+ );
1834
2060
  var smokeOver = (input) => {
1835
2061
  const record = readSnapshot(input.root, input.name);
1836
2062
  const at = snapshotDir(input.root, input.name);
1837
- const work = join6(at, "run");
2063
+ const work = join7(at, "run");
1838
2064
  rmSync3(work, { force: true, recursive: true });
1839
- copyInto(join6(at, "tree"), work, new Set(EXCLUDED_DIRS));
2065
+ copyInto(join7(at, "tree"), work, new Set(EXCLUDED_DIRS));
1840
2066
  ownRepository(work);
1841
- const packed = input.rc === void 0 ? [] : packRc(resolve8(input.rc), join6(at, "rc"));
2067
+ const packed = input.rc === void 0 ? [] : packRc(resolve8(input.rc), join7(at, "rc"));
1842
2068
  const findings = input.rc === void 0 ? [] : promisedButNotPacked(resolve8(input.rc), packed);
1843
2069
  const swapped = rewriteManifests(
1844
2070
  work,
@@ -1852,6 +2078,7 @@ var smokeOver = (input) => {
1852
2078
  }
1853
2079
  overrideEveryCopy(work, record.manager, packed);
1854
2080
  const install = input.install ?? `${record.manager} ${INSTALL[record.manager].join(" ")}`;
2081
+ const configBefore = configBytesOf(work);
1855
2082
  const installed = shellRun(install, work, process.env);
1856
2083
  if (installed.code !== 0) {
1857
2084
  throw new CannotRun(
@@ -1859,9 +2086,15 @@ var smokeOver = (input) => {
1859
2086
  ${quotable(installed.output).join("\n")}`
1860
2087
  );
1861
2088
  }
2089
+ for (const file of editedByInstall(configBefore, configBytesOf(work))) {
2090
+ findings.push(
2091
+ `\`${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`
2092
+ );
2093
+ }
2094
+ const commands = commandsToRun(record, input.commands ?? {});
1862
2095
  return {
1863
2096
  findings,
1864
- outcomes: PHASES.map((phase) => runPhase(phase, record.commands[phase], work, record.manager)),
2097
+ outcomes: PHASES.map((phase) => runPhase(phase, commands[phase], work, record.manager)),
1865
2098
  packed
1866
2099
  };
1867
2100
  };
@@ -1873,7 +2106,7 @@ var ownRepository = (work) => {
1873
2106
  );
1874
2107
  }
1875
2108
  };
1876
- var baselinePath = (root, name) => join6(snapshotDir(root, name), BASELINE_FILE);
2109
+ var baselinePath = (root, name) => join7(snapshotDir(root, name), BASELINE_FILE);
1877
2110
  var recordBaseline = (input) => {
1878
2111
  const { outcomes, packed } = smokeOver(input);
1879
2112
  const baseline = {
@@ -1888,7 +2121,7 @@ var recordBaseline = (input) => {
1888
2121
  };
1889
2122
  var readBaseline = (root, name) => {
1890
2123
  const at = baselinePath(root, name);
1891
- if (!existsSync6(at)) {
2124
+ if (!existsSync7(at)) {
1892
2125
  throw new CannotRun(
1893
2126
  `"${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
2127
  );
@@ -1942,57 +2175,118 @@ var runSmoke = (input) => {
1942
2175
  const { findings, outcomes } = smokeOver(input);
1943
2176
  return compareToBaseline(input.name, baseline, outcomes, findings);
1944
2177
  };
2178
+ var CONSUMER_TREES_FILE = ".geonosis/consumer-trees.local.json";
2179
+ var localTrees = (root) => {
2180
+ const at = join7(root, CONSUMER_TREES_FILE);
2181
+ if (!existsSync7(at)) return {};
2182
+ const parsed = readJson(at);
2183
+ if (!isRecord5(parsed)) {
2184
+ throw new CannotRun(`${at} must be an object of { "<snapshot name>": "<absolute path>" }`);
2185
+ }
2186
+ return Object.fromEntries(
2187
+ Object.entries(parsed).filter((one) => typeof one[1] === "string")
2188
+ );
2189
+ };
2190
+ var recordedAlready = (root, name) => existsSync7(join7(snapshotDir(root, name), "snapshot.json"));
2191
+ var unrecordedButHere = (root, wanted, absent) => {
2192
+ const trees = localTrees(root);
2193
+ return wanted.filter((one) => !absent.includes(one.name) && !recordedAlready(root, one.name)).flatMap((one) => {
2194
+ const path = trees[one.name];
2195
+ if (path === void 0 || !existsSync7(path)) return [];
2196
+ return [
2197
+ `${one.name} is declared and its tree is here (${path}) \u2014 record it: geonosis-release smoke snapshot ${one.name} --from ${path}`
2198
+ ];
2199
+ });
2200
+ };
2201
+ var dating = (root, name) => {
2202
+ const record = readSnapshot(root, name);
2203
+ const commit = record.commit;
2204
+ if (commit === void 0) {
2205
+ return {
2206
+ undated: `UNDATED ${name}: recorded before source commits were kept \u2014 re-record it: geonosis-release smoke snapshot ${name} --from ${record.from}`
2207
+ };
2208
+ }
2209
+ if (!("sha" in commit) || !existsSync7(record.from)) return void 0;
2210
+ const now = headOf(record.from);
2211
+ if (!("sha" in now) || now.sha === commit.sha) return void 0;
2212
+ return { stale: `STALE ${name}: recorded at ${commit.sha}, the tree is at ${now.sha}` };
2213
+ };
1945
2214
  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({
2215
+ const absent = input.absent ?? [];
2216
+ if (input.skipMissing) {
2217
+ const owed = unrecordedButHere(input.root, input.wanted, absent);
2218
+ if (owed.length > 0) throw new CannotRun(owed.join("\n"));
2219
+ }
2220
+ const entries = input.wanted.map((one) => {
2221
+ if (absent.includes(one.name)) {
2222
+ return {
2223
+ name: one.name,
2224
+ why: "--absent named it, so this release was not read against it"
2225
+ };
2226
+ }
2227
+ if (input.skipMissing && !recordedAlready(input.root, one.name)) {
2228
+ return {
1951
2229
  name: one.name,
1952
2230
  why: `no snapshot of it here \u2014 record one with: geonosis-release smoke snapshot ${one.name} --from <their tree>`
1953
- });
1954
- continue;
2231
+ };
1955
2232
  }
1956
- comparisons.push(
1957
- runSmoke({
2233
+ const note = dating(input.root, one.name);
2234
+ return {
2235
+ comparison: runSmoke({
2236
+ ...one.commands === void 0 ? {} : { commands: one.commands },
1958
2237
  ...one.install === void 0 ? {} : { install: one.install },
1959
2238
  name: one.name,
1960
2239
  ...input.rc === void 0 ? {} : { rc: input.rc },
1961
2240
  root: input.root
1962
- })
1963
- );
2241
+ }),
2242
+ name: one.name,
2243
+ ...note === void 0 ? {} : { note }
2244
+ };
2245
+ });
2246
+ return { entries };
2247
+ };
2248
+ var comparisonsIn = (sweep) => sweep.entries.flatMap((one) => "comparison" in one ? [one.comparison] : []);
2249
+ var EXCUSED = "EXCUSED";
2250
+ var noteLine = (note) => "stale" in note ? note.stale : note.undated;
2251
+ var entryBlock = (entry) => {
2252
+ if (!("comparison" in entry)) {
2253
+ const head = entry.why.startsWith("--absent") ? EXCUSED : "SKIP ";
2254
+ return `${head} smoke ${entry.name}: ${entry.why}
2255
+ `;
1964
2256
  }
1965
- return { comparisons, skipped };
2257
+ return `${formatSmoke(entry.comparison)}${entry.note === void 0 ? "" : `${noteLine(entry.note)}
2258
+ `}`;
1966
2259
  };
1967
- var formatSweep = (sweep) => [
1968
- ...sweep.comparisons.map(formatSmoke),
1969
- ...sweep.skipped.map((one) => `SKIP smoke ${one.name}: ${one.why}
1970
- `)
1971
- ].join("");
2260
+ var formatSweep = (sweep) => sweep.entries.map(entryBlock).join("");
1972
2261
  var sweepEnvelope = (sweep, durationMs) => {
1973
- const parts = sweep.comparisons.map((one) => smokeEnvelope(one, 0));
2262
+ const excused = [];
2263
+ const findings = [];
2264
+ const refused = [];
2265
+ let considered = 0;
2266
+ let read = 0;
2267
+ for (const entry of sweep.entries) {
2268
+ if (!("comparison" in entry)) {
2269
+ considered += PHASES.length;
2270
+ excused.push(
2271
+ ...PHASES.map((phase) => ({ path: `${entry.name}/${phase}`, reason: entry.why }))
2272
+ );
2273
+ continue;
2274
+ }
2275
+ const part = smokeEnvelope(entry.comparison, 0);
2276
+ considered += part.considered;
2277
+ read += part.read;
2278
+ excused.push(...part.excused.map((one) => ({ ...one, path: `${entry.name}/${one.path}` })));
2279
+ refused.push(...part.refused.map((one) => ({ ...one, path: `${entry.name}/${one.path}` })));
2280
+ findings.push(...part.findings);
2281
+ if (entry.note !== void 0) findings.push(entry.note);
2282
+ }
1974
2283
  return {
1975
- considered: parts.reduce((sum, one) => sum + one.considered, 0) + sweep.skipped.length * 3,
2284
+ considered,
1976
2285
  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
- ),
2286
+ excused,
2287
+ findings,
2288
+ read,
2289
+ refused,
1996
2290
  tool: SMOKE_TOOL,
1997
2291
  version: versionOf(import.meta.url)
1998
2292
  };
@@ -2065,6 +2359,33 @@ export {
2065
2359
  CannotRun,
2066
2360
  parseReleaseConfig,
2067
2361
  readReleaseConfig,
2362
+ writeEnvelope,
2363
+ MIGRATIONS_NEXT,
2364
+ migrationsEnvelope,
2365
+ SCHEMA_NEXT,
2366
+ schemaEnvelope,
2367
+ PUBLISHED_NEXT,
2368
+ publishedEnvelope,
2369
+ DEFAULT_REGISTRY,
2370
+ registryPathOf,
2371
+ askRegistry,
2372
+ censusOf,
2373
+ EXISTS_BUT,
2374
+ runPublished,
2375
+ formatPublished,
2376
+ SNAPSHOTS_DIR,
2377
+ EXCLUDED_DIRS,
2378
+ PHASES,
2379
+ headOf,
2380
+ snapshotDir,
2381
+ readSnapshot,
2382
+ runSnapshot,
2383
+ formatSnapshot,
2384
+ runAdoption,
2385
+ formatAdoption,
2386
+ ADOPTION_TOOL,
2387
+ ADOPTION_NEXT,
2388
+ adoptionEnvelope,
2068
2389
  parseJsonc,
2069
2390
  parseToml,
2070
2391
  declaredIn,
@@ -2092,29 +2413,8 @@ export {
2092
2413
  prove,
2093
2414
  formatProve,
2094
2415
  formatVerdict,
2095
- DEFAULT_REGISTRY,
2096
- registryPathOf,
2097
- askRegistry,
2098
- censusOf,
2099
- EXISTS_BUT,
2100
- runPublished,
2101
- formatPublished,
2102
2416
  runSchema,
2103
2417
  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
2418
  BASELINE_FILE,
2119
2419
  packRc,
2120
2420
  promisedButNotPacked,
@@ -2123,7 +2423,9 @@ export {
2123
2423
  readBaseline,
2124
2424
  compareToBaseline,
2125
2425
  runSmoke,
2426
+ CONSUMER_TREES_FILE,
2126
2427
  sweepSmoke,
2428
+ comparisonsIn,
2127
2429
  formatSweep,
2128
2430
  sweepEnvelope,
2129
2431
  formatSmoke,