cursedops 0.10.18 → 0.10.19

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
@@ -297,6 +297,12 @@ pins an address** — the deliverable is the distinction, not a way around the n
297
297
  row, because on `music`'s first deploy (2026-09-23, `a54ef71`) one good `/healthz` was followed
298
298
  by thirteen routes answering the pre-secrets version. It returns what answered and never judges.
299
299
 
300
+ `awaitConverged(probes)` (0.10.19) is the same wait over SEVERAL paths: every probe must agree
301
+ three rounds in a row, bounded (120 s), before any leg is judged — a Worker's code and its static
302
+ assets reach the edge separately (roms `f9273ea`, task 2196: `/healthz` at HEAD while the old
303
+ script answered `/host/install.sh` and the new chunk 404'd). A wait that runs out says
304
+ `never converged in Ns — <what lagged>`, and the caller records it as a red row.
305
+
300
306
  Nothing else. The checks are the part `desk` and `flix` wrote differently on purpose,
301
307
  and a shared kit that starts absorbing route lists and health-payload shapes is how the
302
308
  last one reached 277 files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedops",
3
- "version": "0.10.18",
3
+ "version": "0.10.19",
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/smoke.ts CHANGED
@@ -894,6 +894,78 @@ export async function steadyHealth<T = Record<string, unknown>>(
894
894
  }
895
895
  }
896
896
 
897
+ /** One question a deploy must answer the NEW way — `null` when it does, else why not. */
898
+ export interface ConvergenceProbe {
899
+ name: string;
900
+ check: () => Promise<string | null>;
901
+ }
902
+
903
+ export interface ConvergenceOptions {
904
+ /** Rounds in a row in which EVERY probe agreed before the deploy is believed. Default 3. */
905
+ rounds?: number;
906
+ /** How long to keep asking before judging it never converged. Default 120 s. */
907
+ deadlineMs?: number;
908
+ /** Pause after a round that agreed. Default 2 s. */
909
+ pauseMs?: number;
910
+ /** Pause after a round that did not. Default 3 s. */
911
+ retryMs?: number;
912
+ /** Injected for tests. */
913
+ sleep?: (ms: number) => Promise<void>;
914
+ now?: () => number;
915
+ }
916
+
917
+ export interface Convergence {
918
+ converged: boolean;
919
+ /** Rounds asked in all. */
920
+ asked: number;
921
+ /** The probes that disagreed in the LAST round that did, with why — empty when none ever did. */
922
+ lagging: string[];
923
+ /** One line for a smoke row: how it converged, or "never converged in Ns — <what lagged>". */
924
+ detail: string;
925
+ }
926
+
927
+ /**
928
+ * Ask every probe, round after round, until `rounds` rounds IN A ROW all agree — or the deadline.
929
+ *
930
+ * 🔴 **Several different paths, several rounds.** `steadyHealth` asks one path; a Worker's CODE and
931
+ * its static ASSETS are two uploads that reach the edge separately. Measured on roms `f9273ea`
932
+ * (2026-09-26, task 2196): `/healthz` already named the new commit while, in the same run,
933
+ * `/host/install.sh` was answered by the previous script (503, a route the new one 404s) and the
934
+ * new build's hashed chunk 404'd from the old asset set. Sixty seconds later all 17 legs were
935
+ * green. Judging any leg before every probe agrees for several rounds is a false red that stops
936
+ * `deploy` half-way. Like `steadyHealth`, it never decides pass or fail on its own — the caller
937
+ * records `converged` as a row, so a wait that timed out is still a red.
938
+ */
939
+ export async function awaitConverged(probes: readonly ConvergenceProbe[], options: ConvergenceOptions = {}): Promise<Convergence> {
940
+ const rounds = options.rounds ?? 3;
941
+ const deadlineMs = options.deadlineMs ?? 120_000;
942
+ const sleep = options.sleep ?? ((ms: number) => new Promise<void>((done) => setTimeout(done, ms)));
943
+ const now = options.now ?? Date.now;
944
+ const started = now();
945
+ let steady = 0;
946
+ let asked = 0;
947
+ let lagging: string[] = [];
948
+ for (;;) {
949
+ asked++;
950
+ const wrong: string[] = [];
951
+ for (const probe of probes) {
952
+ const why = await probe.check().catch((error: Error) => `threw ${error.message.split("\n")[0]}`);
953
+ if (why !== null) wrong.push(`${probe.name}: ${why}`);
954
+ }
955
+ if (wrong.length > 0) lagging = wrong;
956
+ steady = wrong.length === 0 ? steady + 1 : 0;
957
+ const waited = Math.round((now() - started) / 1000);
958
+ if (probes.length > 0 && steady >= rounds) {
959
+ return { converged: true, asked, lagging, detail: `${probes.length} probe(s) agreed ${rounds} round(s) in a row after ${waited}s (${asked} asked)` };
960
+ }
961
+ if (probes.length === 0 || now() - started >= deadlineMs) {
962
+ const why = probes.length === 0 ? "no probes — nothing was asked" : lagging.join(" · ");
963
+ return { converged: false, asked, lagging, detail: `never converged in ${waited}s — ${why}` };
964
+ }
965
+ await sleep(steady > 0 ? (options.pauseMs ?? 2_000) : (options.retryMs ?? 3_000));
966
+ }
967
+ }
968
+
897
969
  /** One census entry — a private route and the method it is asked with. */
898
970
  export interface CensusRoute {
899
971
  method: string;