cursedops 0.19.0 → 0.19.2

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.
package/README.md CHANGED
@@ -25,7 +25,7 @@ import shape, worked example, and the incident each one is pinned to.
25
25
  | `cursedops/backups` | `BACKUP_KEEP`, the fleet's one retention for dated snapshots; `BACKUP_STAMP_PATTERN`, `backupStamp` and `stampTime` |
26
26
  | `cursedops/serve` | the static tier's four helpers — the path-traversal guard, the MIME table, the hashed-asset test, the crash handlers |
27
27
  | `cursedops/worker-safety` | the two static checks every Worker app's suite runs over its own source — the import-graph walk an app's own test does from `worker/index.ts` (a value import of `bun:sqlite` or a Bun-only subpath is a deploy that goes green and a Worker that never starts), and the await port's silent shapes (`rows.filter(async …)` keeps every row) |
28
- | `cursedops/api-floor` | the rule that an unmatched `/api/...` is a phrase and never the app shell — the namespace predicates, the trailing-slash normaliser, the default 404 body |
28
+ | `cursedops/api-floor` | the rule that an unmatched `/api/...` is a phrase and never the app shell — the namespace predicates, the trailing-slash normalizer, the default 404 body |
29
29
  | `cursedops/build-info` | which commit a checkout-served process is running and whether its tree was dirty, read once at load. `node:child_process`, so never in a Worker |
30
30
  | `cursedops/bound-lists` | the static check that no SQL builds an `IN (…)` list one `?` per value — the shape D1 500s on past 100 ids |
31
31
  | `cursedops/staged-client` | the IMMUTABLE client a checkout-served app serves — a build staged into `<APP_DATA_DIR>/client/<commit>/` behind `CURRENT` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedops",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "The build-and-ops answers this generation's apps wrote independently and identically: finding a generation's roots — and printing a command that runs when pasted — without knowing a path, the generation's whole-tree laws run over one repo from a checkout or a worktree, macOS launchd agent install/replace/remove and the live port a job serves, the scaffolding and verdicts of a deployed smoke (origin probe, the smoke's own environment, a network that lies about DNS, a settled version), the static-serving helpers eight apps copied — the path-traversal guard among them — the API floor that keeps an unmatched /api/... from ever being answered with the app shell, the commit and dirty flag a checkout-served process reports, the Cloudflare Worker deploy toolkit four apps copied (the deploy sequence, exact-set secrets over a pipe, origin-first rollback, the curl edge fetch, the row-for-row D1 import proof, the billed-CPU tail check around a deploy's walk and smoke, and each app's worker:secrets and worker:smoke main as one function of its data), the relay a Worker fronts a Mac-bound app with (the Durable Object, the frames, the Mac's dialer and key rotation — lifted from station for roms — and the signed-in stage walk's skeleton and the relay app's whole worker:deploy), the Worker import-graph and await-port checks every Worker app's suite runs over its own source, and the public-surface ratchet three published libraries each carried a forked copy of. Mechanism only — no app knows its name from here. Bun, zero runtime dependencies (typescript is an optional peer, for public-surface only), ships source.",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/apiFloor.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  * > this commit and reload"*, which sent the owner at a deploy that could not have
20
20
  * > helped.
21
21
  *
22
- * Both halves of that are here, because a floor without the normaliser fixes the symptom
22
+ * Both halves of that are here, because a floor without the normalizer fixes the symptom
23
23
  * the owner did not have: {@link canonicalApiPath} makes the trailing slash a non-event,
24
24
  * and {@link isApiPath} + {@link apiNotFoundBody} make every remaining miss a JSON 404
25
25
  * that names the route, the app and the commit answering. A wrong path says "wrong path";
@@ -73,7 +73,7 @@ export const isApiPath = (urlPath: string): boolean =>
73
73
  * 🔴 One rule, and it is the one that broke the Openclaw page: a trailing slash on an API
74
74
  * path is never meaningful. Hono mounts a sub-app's `get("/")` at the mount point WITHOUT
75
75
  * a trailing slash and then matches strictly, so `/api/openclaw/` is a different path
76
- * from `/api/openclaw` and matched nothing at all. Normalising once, in front of the
76
+ * from `/api/openclaw` and matched nothing at all. Normalizing once, in front of the
77
77
  * router, means no caller anywhere can spell it the losing way again — which a fix in the
78
78
  * one client module that happened to do it would not have bought.
79
79
  *
@@ -86,8 +86,8 @@ export function canonicalApiPath(urlPath: string): string | null {
86
86
  }
87
87
 
88
88
  /**
89
- * The same normalisation, applied to a whole request. Returns the request unchanged when
90
- * there is nothing to normalise, so a caller that guards on {@link canonicalApiPath}
89
+ * The same normalization, applied to a whole request. Returns the request unchanged when
90
+ * there is nothing to normalize, so a caller that guards on {@link canonicalApiPath}
91
91
  * allocates nothing.
92
92
  *
93
93
  * 🔴 **Call it from INSIDE the floor handler and re-dispatch, not from the host's
package/src/d1Import.ts CHANGED
@@ -14,17 +14,17 @@
14
14
  * ## 🔴 Row for row, never a count
15
15
  *
16
16
  * Two tables can hold the same NUMBER of different rows, and a count is exactly the check that
17
- * cannot tell — on `collections`' catalogue that would have been a caption, a rotation or a
17
+ * cannot tell — on `collections`' catalog that would have been a caption, a rotation or a
18
18
  * checksum silently replaced by another file's. `collections`, `vault`, `patterns` and `music`
19
19
  * each wrote this comparison (task 2097); the `rowDigest` bodies had drifted:
20
20
  *
21
- * · `collections` normalised numbers — `bun:sqlite` hands back `0` where D1's JSON carries
21
+ * · `collections` normalized numbers — `bun:sqlite` hands back `0` where D1's JSON carries
22
22
  * `0.0`, and float noise past twelve significant digits — because `duration_seconds` is REAL
23
- * on 39 of its rows and a comparison that called those different reported the catalogue
24
- * corrupt on every run. `vault` and `music` did not normalise, and would have.
23
+ * on 39 of its rows and a comparison that called those different reported the catalog
24
+ * corrupt on every run. `vault` and `music` did not normalize, and would have.
25
25
  * · `vault` and `music` spelled a BLOB as `X'…'` for the insert; `patterns` could not carry one.
26
26
  *
27
- * The body here is the union: numbers normalised, blobs compared as hex whichever side they came
27
+ * The body here is the union: numbers normalized, blobs compared as hex whichever side they came
28
28
  * from (a `Uint8Array` from `bun:sqlite`, an array of bytes from D1's JSON), booleans as 0/1.
29
29
  */
30
30
 
@@ -54,7 +54,7 @@ function cell(value: unknown): string | number | null {
54
54
  return String(value);
55
55
  }
56
56
 
57
- /** One row flattened to a comparable string over `columns` — see the header for the normalisation. */
57
+ /** One row flattened to a comparable string over `columns` — see the header for the normalization. */
58
58
  export function rowDigest(row: Readonly<Record<string, unknown>>, columns: readonly string[]): string {
59
59
  return JSON.stringify(columns.map((column) => cell(row[column])));
60
60
  }
package/src/edgeFetch.ts CHANGED
@@ -271,7 +271,7 @@ export const NO_ANSWER_STATUS = 599;
271
271
  * {@link NO_ANSWER_STATUS} `Response` whose body and `x-edge-error` header carry the reason, and
272
272
  * `onThrow` hears it first so the smoke's ledger can name it.
273
273
  *
274
- * 🔴 Why (2026-09-25): roms' `worker:smoke` asked a relayed path while the Mac's link redialled,
274
+ * 🔴 Why (2026-09-25): roms' `worker:smoke` asked a relayed path while the Mac's link redialed,
275
275
  * `edgeFetch` threw on curl exit 28, and the process died with a stack trace and no ledger — so a
276
276
  * healthy release exited 1. Every leg of a smoke is supposed to be RECORDED: a throw is a failed
277
277
  * leg naming its path, never the end of the run.
package/src/launchd.ts CHANGED
@@ -66,7 +66,7 @@ export interface CommandResult {
66
66
  /**
67
67
  * How this module reaches launchd and the LaunchAgents directory.
68
68
  *
69
- * Injectable as one object because the interesting behaviour — clearing a label
69
+ * Injectable as one object because the interesting behavior — clearing a label
70
70
  * and *waiting* for it to go — cannot otherwise be reached from a test that does
71
71
  * not install a real login agent first, and a test nobody runs is not a test.
72
72
  */
package/src/relay.ts CHANGED
@@ -38,7 +38,7 @@ export const DEFAULT_MAX_BODY_BYTES = 4 * 1024 * 1024;
38
38
  /** How long the Worker waits for the Mac to answer one request before it says so. */
39
39
  export const DEFAULT_RELAY_TIMEOUT_MS = 30_000;
40
40
 
41
- /** A request travelling DOWN the link. `path` includes the query string. */
41
+ /** A request traveling DOWN the link. `path` includes the query string. */
42
42
  export interface RequestFrame {
43
43
  t: "req";
44
44
  id: string;
@@ -48,7 +48,7 @@ export interface RequestFrame {
48
48
  body: string | null;
49
49
  }
50
50
 
51
- /** The Mac's answer, travelling UP. Multiple `set-cookie` lines stay separate pairs. */
51
+ /** The Mac's answer, traveling UP. Multiple `set-cookie` lines stay separate pairs. */
52
52
  export interface ResponseFrame {
53
53
  t: "res";
54
54
  id: string;
@@ -459,7 +459,7 @@ export class RelayLink {
459
459
  if (body.length <= (cache.maxKeptBody ?? 512 * 1024)) {
460
460
  const at = this.now();
461
461
  await this.state.storage.put(`c:${owner}:${keyed}`, { body, contentType: response.headers.get("content-type") ?? "application/json", at } satisfies Kept);
462
- // The newest cookie the Mac honoured is the one a `warm` frame refreshes under.
462
+ // The newest cookie the Mac honored is the one a `warm` frame refreshes under.
463
463
  await this.state.storage.put("warm", { cookie, at });
464
464
  }
465
465
  }
@@ -474,7 +474,7 @@ export class RelayLink {
474
474
  if (keys.length > 0) await this.state.storage.delete(keys);
475
475
  }
476
476
 
477
- /** Refresh every read the Mac named, under the newest cookie it honoured — if that is still fresh. */
477
+ /** Refresh every read the Mac named, under the newest cookie it honored — if that is still fresh. */
478
478
  private async warm(paths: readonly string[]): Promise<void> {
479
479
  const cache = this.options.offline;
480
480
  if (!cache) return;
package/src/relayStage.ts CHANGED
@@ -163,7 +163,7 @@ export const realRun: Run = (argv, options) => {
163
163
  /**
164
164
  * 🔴 How many rounds in a row every `/healthz` must name a just-put link digest before a walk uses it.
165
165
  * A digest put is a new Worker version, and the edge takes it up instance by instance: the link
166
- * staying up for 15 s proves only that the instance the link dialled has it. The Worker signs the
166
+ * staying up for 15 s proves only that the instance the link dialed has it. The Worker signs the
167
167
  * Mac's identity with that digest, so a request answered by an instance still on the old one is the
168
168
  * Mac's own 401 (and station's kept read a 503). Measured 2026-10-15 (forge task 3235): station's
169
169
  * fast-lane walk at baf8495, the link steady for 15 s, then four writes 401 at the Mac; on the real
package/src/roots.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * written independently against the same problem; measured 2026-09-16 the
9
9
  * `findForgeRoot` bodies were character-identical and the surrounding halves were
10
10
  * not, which is the definition of an extraction that is measurable rather than
11
- * speculative. Two implementations, one behaviour.
11
+ * speculative. Two implementations, one behavior.
12
12
  *
13
13
  * ## The rule this serves
14
14
  *
@@ -332,7 +332,7 @@ function shellDoubleQuoted(value: string): string {
332
332
  * {@link findForgeRoot}).
333
333
  *
334
334
  * 🔴 **Verified, never trusted.** A `$FORGE` pointing at a directory with no `forge.env` is a
335
- * stale export from a shell that outlived a move, and honouring it re-creates the exact defect
335
+ * stale export from a shell that outlived a move, and honoring it re-creates the exact defect
336
336
  * {@link repoCd} exists to end: a command naming a directory that is not there.
337
337
  * {@link forgeCodeRoot} deliberately keeps its older contract (the variable wins unchecked); this
338
338
  * is the one for text a person will paste.
package/src/serve.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * `station` and `vault` each have a `src/server/serve.ts`. The FILES differ — 247
10
10
  * to 387 lines, eight distinct versions, which is why no `diff` ever flagged them —
11
11
  * but these four bodies are character-identical in **eight of eight**, with the
12
- * whitespace and comments normalised away.
12
+ * whitespace and comments normalized away.
13
13
  *
14
14
  * 🔴 **`fileWithin` is why this was urgent rather than tidy.** It is the check that
15
15
  * stops `%2e%2e%2f` reaching the filesystem. Eight copies of a security guarantee
@@ -28,7 +28,7 @@
28
28
  * `8 <symbol> src/server/serve.ts` and is the census that will notice if a copy
29
29
  * comes back.
30
30
  *
31
- * ## 🔴 The one behaviour that is NOT a byte-for-byte carry
31
+ * ## 🔴 The one behavior that is NOT a byte-for-byte carry
32
32
  *
33
33
  * The eight copies resolved containment LEXICALLY — `resolve(join(root, …))` and a
34
34
  * `startsWith` — which is correct for `..` and for `%2e%2e%2f`, and blind to a
@@ -42,7 +42,7 @@
42
42
  * the same way — the failure mode a naive `realpath` on one side only would
43
43
  * have introduced, and the reason this is not one line;
44
44
  * - every caller today passes a Vite `dist/`, and all eight were checked for
45
- * symlinks on 2026-09-17: there are none. So no app's behaviour moves, and the
45
+ * symlinks on 2026-09-17: there are none. So no app's behavior moves, and the
46
46
  * day one of them serves a directory it did not build, the guard already holds.
47
47
  *
48
48
  * `serve.test.ts` drives both halves, including the symlink escape, because a guard
package/src/ship.ts CHANGED
@@ -39,12 +39,12 @@
39
39
  * flipped yesterday — the precedence the retired `fastMode.ts` settled on 2026-08-13). A stage
40
40
  * deploy is never deferred: it IS the fast way to see a change on a real edge.
41
41
  *
42
- * 🔴 **Under a batch (`$AUTOPILOT_BATCH` set) it is honoured only for the ship task or an incident**
42
+ * 🔴 **Under a batch (`$AUTOPILOT_BATCH` set) it is honored only for the ship task or an incident**
43
43
  * ({@link shipNowVerdict}), and only with a reason in `FORGE_SHIP_NOW_WHY`. Until task 2384 any agent
44
44
  * could prefix one variable and turn the owner's switch off for itself — the thing
45
45
  * `~/.claude/CLAUDE.md` §2 says an agent may never do — and no record said it had. Now every ask,
46
- * honoured or refused, is a `kind: "ship-now"` ledger row, and `autopilot doctor` lists the ones
47
- * with no reason. In the owner's own shell (no batch) it is honoured exactly as before, and recorded.
46
+ * honored or refused, is a `kind: "ship-now"` ledger row, and `autopilot doctor` lists the ones
47
+ * with no reason. In the owner's own shell (no batch) it is honored exactly as before, and recorded.
48
48
  */
49
49
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
50
50
  import { dirname, join } from "node:path";
@@ -92,7 +92,7 @@ export function readShipSetting(state: string | null): ShipSetting {
92
92
  }
93
93
  }
94
94
 
95
- /** Is `FORGE_SHIP_NOW` set to something that means yes? Asked is not honoured — see {@link shipNowVerdict}. */
95
+ /** Is `FORGE_SHIP_NOW` set to something that means yes? Asked is not honored — see {@link shipNowVerdict}. */
96
96
  export function shipNowAsked(env: NodeJS.ProcessEnv = process.env): boolean {
97
97
  const v = env[SHIP_NOW_VAR]?.trim().toLowerCase();
98
98
  return v === "1" || v === "true" || v === "yes";
@@ -116,7 +116,7 @@ export interface ShipNowVerdict {
116
116
  asked: boolean;
117
117
  /** Ship now: asked, and (outside a batch, or on a claim that earns it). */
118
118
  honoured: boolean;
119
- /** One sentence: what made it honoured or refused. */
119
+ /** One sentence: what made it honored or refused. */
120
120
  why: string;
121
121
  /** `FORGE_SHIP_NOW_WHY`, trimmed — `""` when not given. */
122
122
  reason: string;
@@ -159,7 +159,7 @@ export interface ShipNowWhere {
159
159
  }
160
160
 
161
161
  /**
162
- * Is `FORGE_SHIP_NOW` honoured here? Outside a batch, whenever it is asked: that is the owner's own
162
+ * Is `FORGE_SHIP_NOW` honored here? Outside a batch, whenever it is asked: that is the owner's own
163
163
  * shell. Under `$AUTOPILOT_BATCH`, only when that batch holds a live `kind: "task"` board claim
164
164
  * (`$FORGE_STATE/state/board/*.json`, the pid alive and not expired) on a task in
165
165
  * `$FORGE/tasks/In_Progress/` whose `escalation-key:` is one of {@link SHIP_NOW_KEYS}, AND
@@ -206,11 +206,11 @@ export function shipNowVerdict(where: ShipNowWhere, env: NodeJS.ProcessEnv = pro
206
206
  if (key && SHIP_NOW_KEYS.some((k) => k.test(key))) return { ...base, honoured: true, why: `batch ${batch} holds ${c.task} (escalation-key: ${key})`, task: c.task };
207
207
  }
208
208
  return refuse(
209
- `batch ${batch} holds ${held.length ? held.join(", ") : "no live task claim"} — ${SHIP_NOW_VAR} is honoured under a batch only for the ship task or a prod incident (escalation-key: ship | prod-down:<host>)`,
209
+ `batch ${batch} holds ${held.length ? held.join(", ") : "no live task claim"} — ${SHIP_NOW_VAR} is honored under a batch only for the ship task or a prod incident (escalation-key: ship | prod-down:<host>)`,
210
210
  );
211
211
  }
212
212
 
213
- /** The ledger line every ship-now ask leaves — honoured or refused. */
213
+ /** The ledger line every ship-now ask leaves — honored or refused. */
214
214
  export interface ShipNowEventLine {
215
215
  at: string;
216
216
  kind: "ship-now";
package/src/smoke.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * its logs was wrong.
14
14
  * 3. A deploy *succeeded* while every byte of media was unreachable, because the
15
15
  * failing layer answered before the thing being health-checked ran.
16
- * 4. An app was up, green, and serving a catalogue a MONTH out of date.
16
+ * 4. An app was up, green, and serving a catalog a MONTH out of date.
17
17
  * 5. Two apps went live answering `no built client at …/public` at `/` while the
18
18
  * edge returned 200 out of its cache of the RETIRED deployment. Gate green,
19
19
  * deploy green, smoke green, app down; the owner found it, not a check.
@@ -62,7 +62,7 @@
62
62
  * built, `patterns.cursedalchemy.com` served one built on 2026-09-12 out of a
63
63
  * Worker nobody knew about, and **every automated surface was green for six days.**
64
64
  * The app-local implementation that found it — `apps/patterns/scripts/worker-smoke.ts`,
65
- * `CROSS_CHECKS` — is what this generalises, because five more apps are moving
65
+ * `CROSS_CHECKS` — is what this generalizes, because five more apps are moving
66
66
  * behind an edge Worker and a check told ONE NAME is a check about that name.
67
67
  *
68
68
  * ## What this module will never grow
@@ -501,7 +501,7 @@ export async function compareDeployedShells(addresses: string[], options: ShellO
501
501
  url.searchParams.set("cb", cacheBust());
502
502
  // 🔴 Re-thrown WITH the address and with the original message intact: which
503
503
  // of the addresses could not be reached is the whole diagnosis, and
504
- // {@link Smoke.dnsHint} still has to recognise an ENOTFOUND inside it.
504
+ // {@link Smoke.dnsHint} still has to recognize an ENOTFOUND inside it.
505
505
  const response = await send(url.toString(), {
506
506
  redirect: "manual",
507
507
  signal: AbortSignal.timeout(timeoutMs),
@@ -929,7 +929,7 @@ export async function steadyHealth<T = Record<string, unknown>>(
929
929
  */
930
930
  export const HEALTH_READ_MAX_TIME_SEC = 10;
931
931
 
932
- /** The init a smoke's `/healthz` read carries — `cursedops/edge-fetch` honours it; a plain `fetch` ignores it. */
932
+ /** The init a smoke's `/healthz` read carries — `cursedops/edge-fetch` honors it; a plain `fetch` ignores it. */
933
933
  const healthRead: EdgeRequestInit = { maxTimeSec: HEALTH_READ_MAX_TIME_SEC };
934
934
 
935
935
  /** One question a deploy must answer the NEW way — `null` when it does, else why not. */
@@ -82,7 +82,7 @@ export function parseWorkerEnv(argv: readonly string[]): WorkerEnv {
82
82
  const value = (joined !== undefined ? joined.slice("--env=".length) : argv[index + 1])?.trim();
83
83
  if (value === "production" || value === "stage") return value;
84
84
  throw new Error(
85
- `unrecognised --env ${JSON.stringify(value ?? "")} — production (no flag) or stage. ` +
85
+ `unrecognized --env ${JSON.stringify(value ?? "")} — production (no flag) or stage. ` +
86
86
  "This refuses rather than defaulting: a typo that meant production would report success against the owner's data.",
87
87
  );
88
88
  }
@@ -802,6 +802,23 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
802
802
  return finish({ code: 1, step, detail, commit, dirty });
803
803
  };
804
804
  const step = (message: string): void => log(`\n▸ ${message}`);
805
+ /**
806
+ * 🔴 The clean-tree refusal, asked again where production READS the tree (forge task 3601). It
807
+ * was asked once, at the start — and the stage leg is minutes long. On 2026-10-23 the ship's
808
+ * station lane passed it clean at 9ab4e676, deployed the stage clean, then built production from
809
+ * a session's half-written edit landed in between: `build.json` stamped `9ab4e676 (dirty)`, and
810
+ * only the asset budget stopped a production that would have reported one commit and run
811
+ * another. So before the production build, and again before anything is uploaded, HEAD must
812
+ * still be the commit stamped and the tree must still be clean.
813
+ */
814
+ const moved = (during: string): DeployResult | null => {
815
+ if (spec.env !== "production") return null;
816
+ const head = deps.git(["rev-parse", "HEAD"]);
817
+ const now = deps.git(["status", "--porcelain"]);
818
+ if (head === commit && now.length === 0) return null;
819
+ const what = head !== commit ? `HEAD is now ${head.slice(0, 8) || "unreadable"}, not ${commit.slice(0, 8)}` : `it has uncommitted changes:\n${now}`;
820
+ return stop("tree-moved", `the tree moved ${during} — ${what}\nProduction would report ${commit.slice(0, 8)} and run something else, so nothing was deployed to it. Deploy again once the tree is still.`);
821
+ };
805
822
  /** What passed under a CPU tail that never attached — decision 6 of the CPU check. */
806
823
  const unmeasured: string[] = [];
807
824
 
@@ -879,6 +896,8 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
879
896
  else if (walked !== 0) return stop("stage", `\`${command.join(" ")}\` failed — production was NOT touched.`);
880
897
  }
881
898
  }
899
+ const beforeBuild = moved(spec.stageFirst?.length || spec.stageWalk?.length ? "while the stage deployed" : "before the build");
900
+ if (beforeBuild) return beforeBuild;
882
901
  if (spec.build) {
883
902
  step("build");
884
903
  enter("build");
@@ -889,6 +908,8 @@ export function runWorkerDeploy(spec: WorkerDeploySpec, deps: DeployDeps): Deplo
889
908
  const why = refusal();
890
909
  if (why) return stop("refusal", `${why} — nothing was deployed.`);
891
910
  }
911
+ const beforeUpload = moved("while it built");
912
+ if (beforeUpload) return beforeUpload;
892
913
  const envArgs = wranglerEnvArgs(spec.env);
893
914
  if (spec.databaseName) {
894
915
  const schema = spec.schemaFile ?? "db/schema.sql";
@@ -1264,7 +1285,7 @@ export interface JudgeOpts {
1264
1285
  scriptName: string;
1265
1286
  /** Paths that are never the static tier. Default {@link DEFAULT_API_PREFIXES}. */
1266
1287
  apiPrefixes?: readonly string[];
1267
- /** Replace the labeller entirely. Default {@link tierRoute}. */
1288
+ /** Replace the labeler entirely. Default {@link tierRoute}. */
1268
1289
  route?: (method: string, path: string) => string | null;
1269
1290
  }
1270
1291
 
@@ -16,7 +16,7 @@
16
16
  * ## Why this order, and why it is a library entry
17
17
  *
18
18
  * `patterns` wrote it first (2026-09-19) because the owner required the rollback to be ONE
19
- * command — a launchd invocation nobody runs monthly and a route id nobody has memorised are not
19
+ * command — a launchd invocation nobody runs monthly and a route id nobody has memorized are not
20
20
  * cheap at the moment somebody needs them — and `collections`, `vault` and `music` copied it.
21
21
  * Deleting the route while nothing listens on the Mac points the household's hostname at a dead
22
22
  * box; so this starts the origin, WAITS for it to answer, and refuses to touch the route if it
@@ -184,7 +184,7 @@ export async function runWorkerRollback(spec: WorkerRollbackSpec, argv: readonly
184
184
  const found = await findWorkerRoute(spec.api, host);
185
185
  log(`route ${found.pattern} → ${found.route ? `${found.script} (${found.route})` : "(none — the hostname already falls through to the Mac)"}`);
186
186
 
187
- log("\n▸ what D1 holds against the Mac's frozen catalogue (the writes a rollback loses):");
187
+ log("\n▸ what D1 holds against the Mac's frozen catalog (the writes a rollback loses):");
188
188
  bun(["run", "scripts/worker-import.ts", "--verify-only"], spec.root);
189
189
 
190
190
  if (argv.includes("--check")) {
@@ -30,7 +30,7 @@
30
30
  * never noticed an extra — the drift this module ends.
31
31
  *
32
32
  * 🔴 **A value that is already single-quoted is written as is.** {@link envFileLines}:
33
- * `collections`' seed serialiser returns `'{"kdf":…}'`, quoting it again wrote an escaped pair
33
+ * `collections`' seed serializer returns `'{"kdf":…}'`, quoting it again wrote an escaped pair
34
34
  * around it, `readEnvFile` stripped one layer, and the stage's master lock came up asking to
35
35
  * ENROLL a new password (found 2026-09-22 by the stage walk, one step past sign-in).
36
36
  */