From 432609808391e5be0a1191cdec18f0d5ec0df836 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Fri, 21 Aug 2026 10:42:32 +0100 Subject: [PATCH 1/2] feat(core): add shard-routable run-ops id format and resolveShard A gen-2 id keeps the 26-char layout but carries a routing shard key at index 24 and version "2" at index 25, so a store can be picked from the id alone with no directory lookup. The version char is one character, so the v1 and gen-2 shape checks are mutually exclusive by construction. resolveShard is total: a gen-2 body returns its shard key, a v1 body returns "new", and everything else returns "legacy" without throwing. classifyResidency keeps its signature and now reports gen-2 ids as part of the dedicated family. Nothing mints gen-2 ids yet. The only behavior change is that a 26-char body ending in "2" now routes by its shard key instead of falling back to legacy. --- .../core/src/v3/isomorphic/friendlyId.test.ts | 162 +++++++++++++++++- packages/core/src/v3/isomorphic/friendlyId.ts | 84 +++++++-- .../src/v3/isomorphic/runOpsResidency.test.ts | 115 ++++++++++++- .../core/src/v3/isomorphic/runOpsResidency.ts | 44 ++++- 4 files changed, 380 insertions(+), 25 deletions(-) diff --git a/packages/core/src/v3/isomorphic/friendlyId.test.ts b/packages/core/src/v3/isomorphic/friendlyId.test.ts index 22b08bce7c0..1ce066883a0 100644 --- a/packages/core/src/v3/isomorphic/friendlyId.test.ts +++ b/packages/core/src/v3/isomorphic/friendlyId.test.ts @@ -7,14 +7,22 @@ import { WebhookDeliveryId, RUN_OPS_ID_LENGTH, RUN_OPS_ID_REGION_INDEX, + RUN_OPS_ID_SHARD_INDEX, RUN_OPS_ID_VERSION, + RUN_OPS_ID_VERSION_2, RUN_OPS_ID_VERSION_INDEX, base32hexDecode, base32hexEncode, generateRunOpsId, + generateRunOpsIdV2, parseRunId, + parseRunOpsIdBody, + parseRunOpsIdV2Body, } from "./friendlyId.js"; +/** Every legal gen-2 shard char: the full DNS-safe lowercase range. */ +const SHARD_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789".split(""); + const CUID_LEN = 25; describe("RunId + WaitpointId mint cuid by default; run-ops v1 via generateRunOpsId", () => { @@ -173,7 +181,8 @@ describe("parseRunId — version-char discrimination (not length)", () => { it("falls back to legacy on a malformed v1 (bad alphabet / wrong version char)", () => { expect(parseRunId(`run_${"A".repeat(25)}1`).format).toBe("legacy"); // uppercase core - expect(parseRunId(`run_${"a".repeat(25)}2`).format).toBe("legacy"); // wrong version + expect(parseRunId(`run_${"a".repeat(25)}3`).format).toBe("legacy"); // unknown version + expect(parseRunId(`run_${"a".repeat(25)}2`).format).toBe("b32hexV2"); // "2" is now gen-2 expect(parseRunId(`run_${"a".repeat(24)}-1`).format).toBe("legacy"); // region char not [a-z0-9] expect(parseRunId(`run_${"a".repeat(27)}`).format).toBe("legacy"); // old 27-char shape }); @@ -250,3 +259,154 @@ describe("WebhookDeliveryId (time-encoded)", () => { expect(WebhookDeliveryId.parseTimestamp(`whd_${"0".repeat(24)}9`)).toBeUndefined(); }); }); + +describe("generateRunOpsIdV2 — gen-2 id spec (shard char at 24, version '2' at 25)", () => { + afterEach(() => vi.useRealTimers()); + + it("emits <24-char base32hex core> — 26 chars total", () => { + const id = generateRunOpsIdV2("a"); + expect(id.length).toBe(RUN_OPS_ID_LENGTH); + expect(id).toMatch(/^[0-9a-v]{24}[a-z0-9]2$/); + expect(id[RUN_OPS_ID_VERSION_INDEX]).toBe(RUN_OPS_ID_VERSION_2); + expect(id[RUN_OPS_ID_SHARD_INDEX]).toBe("a"); + }); + + it("round-trips every legal shard char [a-z0-9] through parseRunOpsIdV2Body", () => { + for (const c of SHARD_CHARS) { + const id = generateRunOpsIdV2(c); + const parsed = parseRunOpsIdV2Body(id); + expect(parsed).toBeDefined(); + expect(parsed?.shard).toBe(c); + expect(parsed?.version).toBe(RUN_OPS_ID_VERSION_2); + // the core survives the round-trip: its bytes re-encode to the id's first 24 chars + expect(base32hexEncode(base32hexDecode(id.slice(0, 24)))).toBe(id.slice(0, 24)); + } + }); + + it("throws on a shard char outside [a-z0-9] (fail loud, never mint an unroutable id)", () => { + for (const bad of ["", "-", "_", "A", "ab", " ", "/"]) { + expect(() => generateRunOpsIdV2(bad)).toThrow(/shard/i); + } + }); + + it("only ever uses lowercase [a-z0-9] and NEVER '-' (DNS-1123 / pod-name invariant)", () => { + for (let i = 0; i < 5_000; i++) { + const id = generateRunOpsIdV2(SHARD_CHARS[i % SHARD_CHARS.length]!); + expect(id).toMatch(/^[a-z0-9]+$/); + expect(id).not.toContain("-"); + } + }); + + it("sorts lexicographically in creation order at ms resolution, like gen-1", () => { + vi.useFakeTimers(); + const t = new Date("2026-07-04T12:00:00.000Z").getTime(); + vi.setSystemTime(t); + const a = generateRunOpsIdV2("a"); + vi.setSystemTime(t + 1000); + const b = generateRunOpsIdV2("a"); + vi.setSystemTime(t + 3); + const c = generateRunOpsIdV2("a"); + expect([b, c, a].sort()).toEqual([a, c, b]); + }); + + it("decode recovers the exact ms timestamp", () => { + vi.useFakeTimers(); + const t = new Date("2026-07-04T12:34:56.789Z"); + vi.setSystemTime(t); + expect(parseRunOpsIdV2Body(generateRunOpsIdV2("e"))?.timestamp.getTime()).toBe(t.getTime()); + }); + + it("is unique across many mints in the same ms (72 bits of CSPRNG)", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-07-04T00:00:00.000Z")); + const n = 2_000; + expect(new Set(Array.from({ length: n }, () => generateRunOpsIdV2("a"))).size).toBe(n); + }); +}); + +describe("parseRunOpsIdV2Body — the mirror of the v1 shape check", () => { + it("rejects a body that is not exactly 26 chars", () => { + const core = "a".repeat(24); + expect(parseRunOpsIdV2Body("")).toBeUndefined(); + expect(parseRunOpsIdV2Body(`${core}2`)).toBeUndefined(); // 25 + expect(parseRunOpsIdV2Body(`${core}ee2`)).toBeUndefined(); // 27 + expect(parseRunOpsIdV2Body("a".repeat(40))).toBeUndefined(); + }); + + it("rejects a body without '2' at index 25", () => { + const core = "a".repeat(24); + for (const version of ["1", "0", "3", "z", "-"]) { + expect(parseRunOpsIdV2Body(`${core}e${version}`)).toBeUndefined(); + } + }); + + it("rejects a body whose 24-char core is not base32hex", () => { + for (const badCore of ["w".repeat(24), "z".repeat(24), "A".repeat(24), `${"a".repeat(23)}-`]) { + expect(parseRunOpsIdV2Body(`${badCore}e2`)).toBeUndefined(); + } + }); + + it("rejects a body whose char at index 24 is outside [a-z0-9]", () => { + const core = "a".repeat(24); + for (const badShard of ["-", "_", "A", ".", " "]) { + expect(parseRunOpsIdV2Body(`${core}${badShard}2`)).toBeUndefined(); + } + }); + + it("never throws, for any input string", () => { + for (const input of ["", "x", "-".repeat(26), " ".repeat(26), "\u{1F642}".repeat(26)]) { + expect(() => parseRunOpsIdV2Body(input)).not.toThrow(); + } + }); +}); + +describe("gen-1 and gen-2 parsers reject each other (the disjointness foundation)", () => { + it("parseRunOpsIdBody rejects every gen-2 id", () => { + for (const c of SHARD_CHARS) { + expect(parseRunOpsIdBody(generateRunOpsIdV2(c))).toBeUndefined(); + } + }); + + it("parseRunOpsIdV2Body rejects every gen-1 v1 id", () => { + for (const region of [undefined, "us-east-1", "us-west-2", "eu-central-1"]) { + expect(parseRunOpsIdV2Body(generateRunOpsId(region))).toBeUndefined(); + } + }); + + it("generateRunOpsId still mints v1 ids — the gen-1 generator is unchanged", () => { + const id = generateRunOpsId("us-east-1"); + expect(id).toMatch(/^[0-9a-v]{24}[a-z0-9]1$/); + expect(id[RUN_OPS_ID_VERSION_INDEX]).toBe(RUN_OPS_ID_VERSION); + expect(parseRunOpsIdBody(id)?.region).toBe("e"); + }); + + it("the shard index and the region index are the same position", () => { + expect(RUN_OPS_ID_SHARD_INDEX).toBe(RUN_OPS_ID_REGION_INDEX); + }); +}); + +describe("parseRunId — v2 arm", () => { + it("parses a gen-2 friendly id as partitioned with its shard + version", () => { + const parsed = parseRunId(`run_${generateRunOpsIdV2("e")}`); + expect(parsed).toMatchObject({ + format: "b32hexV2", + table: "partitioned", + shard: "e", + version: "2", + }); + }); + + it("still parses a gen-1 v1 friendly id as b32hex — the v1 arm is unchanged", () => { + expect(parseRunId(`run_${generateRunOpsId("us-west-2")}`)).toMatchObject({ + format: "b32hex", + table: "partitioned", + region: "w", + version: "1", + }); + }); + + it("classifies a gen-2 body without the run_ prefix, and under a wrong prefix, legacy", () => { + expect(parseRunId(generateRunOpsIdV2("a")).format).toBe("legacy"); + expect(parseRunId(`waitpoint_${generateRunOpsIdV2("a")}`).format).toBe("legacy"); + }); +}); diff --git a/packages/core/src/v3/isomorphic/friendlyId.ts b/packages/core/src/v3/isomorphic/friendlyId.ts index e390e1a4611..c468de65319 100644 --- a/packages/core/src/v3/isomorphic/friendlyId.ts +++ b/packages/core/src/v3/isomorphic/friendlyId.ts @@ -23,6 +23,11 @@ export const RUN_OPS_ID_LENGTH = 26; export const RUN_OPS_ID_REGION_INDEX = 24; export const RUN_OPS_ID_VERSION_INDEX = 25; export const RUN_OPS_ID_VERSION = "1"; +// Gen-2 id: same 26-char layout, but index 24 carries a routing SHARD KEY rather +// than a region char. MUST stay 26 chars: a 27-char shape could collide with the +// pre-cutover base62 format, which must keep classifying legacy. +export const RUN_OPS_ID_VERSION_2 = "2"; +export const RUN_OPS_ID_SHARD_INDEX = RUN_OPS_ID_REGION_INDEX; const RUN_OPS_ID_CORE_BYTES = 15; // 6 timestamp + 9 random → exactly 24 base32hex chars const RUN_OPS_ID_CORE_LENGTH = 24; const RUN_OPS_ID_TIMESTAMP_BYTES = 6; @@ -33,6 +38,8 @@ export const DEFAULT_REGION_CHAR = "0"; // decoding), NOT part of the base32hex core — so it may use the full DNS-safe // lowercase [a-z0-9] range (e.g. "w" for us-west-2, which is outside [0-9a-v]). const REGION_CHAR_PATTERN = /^[a-z0-9]$/; +// Same slot, same range: the gen-2 shard key is a region char's positional twin. +const SHARD_CHAR_PATTERN = REGION_CHAR_PATTERN; /** One lowercase [a-z0-9] char per supported region, at RUN_OPS_ID_REGION_INDEX. */ export const REGION_CODES: Readonly> = { "us-east-1": "e", @@ -110,6 +117,21 @@ export function base32hexDecode(s: string): Uint8Array { return Uint8Array.from(out); } +// Shared by both generations. The buffer MUST be per-call — a hoisted one would +// let concurrent mints overwrite each other's bytes. +function mintRunOpsIdCore(): string { + const core = new Uint8Array(RUN_OPS_ID_CORE_BYTES); + + let ms = Date.now(); + for (let i = RUN_OPS_ID_TIMESTAMP_BYTES - 1; i >= 0; i--) { + core[i] = ms % 256; + ms = Math.floor(ms / 256); + } + getRandomValues(core.subarray(RUN_OPS_ID_TIMESTAMP_BYTES)); + + return base32hexEncode(core); +} + /** * Mint a run-ops v1 id body (26 chars, no prefix): 24-char base32hex core * (6-byte ms timestamp + 9 CSPRNG bytes) + region char + version char "1". @@ -117,20 +139,25 @@ export function base32hexDecode(s: string): Uint8Array { * discriminator — see runOpsResidency.ts. */ export function generateRunOpsId(region?: string): string { - const core = new Uint8Array(RUN_OPS_ID_CORE_BYTES); + return `${mintRunOpsIdCore()}${regionCharForRegion(region)}${RUN_OPS_ID_VERSION}`; +} - let ms = Date.now(); - for (let i = RUN_OPS_ID_TIMESTAMP_BYTES - 1; i >= 0; i--) { - core[i] = ms % 256; - ms = Math.floor(ms / 256); +/** + * Mint a gen-2 id body (26 chars, no prefix): the same core, then the shard key, + * then version char "2". Throws on a shard char outside [a-z0-9] — an id that + * cannot be routed must never be minted. + */ +export function generateRunOpsIdV2(shardChar: string): string { + if (!SHARD_CHAR_PATTERN.test(shardChar)) { + throw new Error(`invalid run-ops shard char: ${JSON.stringify(shardChar)}`); } - getRandomValues(core.subarray(RUN_OPS_ID_TIMESTAMP_BYTES)); - return `${base32hexEncode(core)}${regionCharForRegion(region)}${RUN_OPS_ID_VERSION}`; + return `${mintRunOpsIdCore()}${shardChar}${RUN_OPS_ID_VERSION_2}`; } export type ParsedRunId = | { format: "b32hex"; table: "partitioned"; timestamp: Date; region: string; version: string } + | { format: "b32hexV2"; table: "partitioned"; timestamp: Date; shard: string; version: string } | { format: "legacy"; table: "legacy" }; const LEGACY_RUN_ID: ParsedRunId = { format: "legacy", table: "legacy" }; @@ -149,6 +176,15 @@ export function parseRunOpsIdBody( const region = body[RUN_OPS_ID_REGION_INDEX] ?? ""; if (!REGION_CHAR_PATTERN.test(region)) return undefined; + const timestamp = parseRunOpsIdCoreTimestamp(body); + if (timestamp === undefined) return undefined; + + return { timestamp, region, version: RUN_OPS_ID_VERSION }; +} + +// Decode the leading 24-char core and recover its embedded ms timestamp. +// Returns undefined (never throws) when the core is outside the base32hex alphabet. +function parseRunOpsIdCoreTimestamp(body: string): Date | undefined { let core: Uint8Array; try { core = base32hexDecode(body.slice(0, RUN_OPS_ID_CORE_LENGTH)); @@ -161,7 +197,26 @@ export function parseRunOpsIdBody( ms = ms * 256 + (core[i] ?? 0); } - return { timestamp: new Date(ms), region, version: RUN_OPS_ID_VERSION }; + return new Date(ms); +} + +/** + * Parse a gen-2 id body (no prefix): the mirror of {@link parseRunOpsIdBody}, + * requiring version "2" at index 25 and a shard key in [a-z0-9] at index 24. + * Total: returns undefined for any other string, and never throws. + */ +export function parseRunOpsIdV2Body( + body: string +): { timestamp: Date; shard: string; version: string } | undefined { + if (body.length !== RUN_OPS_ID_LENGTH) return undefined; + if (body[RUN_OPS_ID_VERSION_INDEX] !== RUN_OPS_ID_VERSION_2) return undefined; + const shard = body[RUN_OPS_ID_SHARD_INDEX] ?? ""; + if (!SHARD_CHAR_PATTERN.test(shard)) return undefined; + + const timestamp = parseRunOpsIdCoreTimestamp(body); + if (timestamp === undefined) return undefined; + + return { timestamp, shard, version: RUN_OPS_ID_VERSION_2 }; } /** True if the (prefixless) id body is a well-formed run-ops v1 id. */ @@ -169,11 +224,18 @@ export function isRunOpsIdBody(body: string): boolean { return parseRunOpsIdBody(body) !== undefined; } -/** Parse a `run_`-prefixed friendly id; anything not a well-formed v1 id is legacy. */ +/** Parse a `run_`-prefixed friendly id; anything not a well-formed v1/gen-2 id is legacy. */ export function parseRunId(id: string): ParsedRunId { if (!id.startsWith("run_")) return LEGACY_RUN_ID; - const parsed = parseRunOpsIdBody(id.slice(4)); - return parsed ? { format: "b32hex", table: "partitioned", ...parsed } : LEGACY_RUN_ID; + const body = id.slice(4); + + const v1 = parseRunOpsIdBody(body); + if (v1) return { format: "b32hex", table: "partitioned", ...v1 }; + + const v2 = parseRunOpsIdV2Body(body); + if (v2) return { format: "b32hexV2", table: "partitioned", ...v2 }; + + return LEGACY_RUN_ID; } export function generateInternalId(): string { diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts index ceb6d9b5358..43ff7dee138 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts @@ -1,8 +1,23 @@ import { describe, expect, it } from "vitest"; -import { RunId, WaitpointId, BatchId, SnapshotId, generateRunOpsId } from "./friendlyId.js"; -import { ownerEngine, classifyResidency, classifyKind, isClassifiable } from "./runOpsResidency.js"; +import { + RunId, + WaitpointId, + BatchId, + SnapshotId, + generateRunOpsId, + generateRunOpsIdV2, +} from "./friendlyId.js"; +import { + ownerEngine, + classifyResidency, + classifyKind, + isClassifiable, + resolveShard, +} from "./runOpsResidency.js"; const SAMPLES = 50_000; // property-scale; CI-fast. (Bump locally toward "millions" for deeper coverage.) +/** Every legal gen-2 shard char: the full DNS-safe lowercase range. */ +const SHARD_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789".split(""); describe("ownerEngine — residency classifier (version char at fixed position, not length)", () => { it("cuid ids (default mint) classify LEGACY, friendly + internal", () => { @@ -27,10 +42,11 @@ describe("ownerEngine — residency classifier (version char at fixed position, } }); - it("discriminates on the version char, not length: 26+'1' → NEW, 26+'2' → LEGACY", () => { + it("discriminates on the version char, not length: 26+'1' → NEW, 26+'2' → NEW (gen-2)", () => { const v1 = "a".repeat(24) + "e1"; expect(ownerEngine(v1)).toBe("NEW"); - expect(ownerEngine("a".repeat(24) + "e2")).toBe("LEGACY"); + expect(ownerEngine("a".repeat(24) + "e2")).toBe("NEW"); // gen-2: shard "e" + expect(ownerEngine("a".repeat(24) + "e3")).toBe("LEGACY"); // unknown version char expect(ownerEngine("a".repeat(26))).toBe("LEGACY"); // 26 chars but no version marker }); @@ -60,3 +76,94 @@ describe("ownerEngine — residency classifier (version char at fixed position, expect(ownerEngine(SnapshotId.generate().id)).toBe("LEGACY"); }); }); + +describe("resolveShard — the gen-2 refinement inside the dedicated family", () => { + it("returns the shard char for every legal gen-2 shard key, friendly + internal", () => { + for (const c of SHARD_CHARS) { + const id = generateRunOpsIdV2(c); + expect(resolveShard(id)).toBe(c); + expect(resolveShard(RunId.toFriendlyId(id))).toBe(c); + expect(resolveShard(WaitpointId.toFriendlyId(id))).toBe(c); + expect(resolveShard(BatchId.toFriendlyId(id))).toBe(c); + } + }); + + it("returns 'new' for a gen-1 v1 body, friendly + internal", () => { + for (const region of [undefined, "us-east-1", "us-west-2", "eu-central-1"]) { + const id = generateRunOpsId(region); + expect(resolveShard(id)).toBe("new"); + expect(resolveShard(RunId.toFriendlyId(id))).toBe("new"); + } + }); + + it("returns 'legacy' for a cuid, a nanoid, a pre-cutover base62 id and malformed input", () => { + expect(resolveShard(RunId.generate().id)).toBe("legacy"); // cuid, 25 + expect(resolveShard(RunId.generate().friendlyId)).toBe("legacy"); + expect(resolveShard("123456789abcdefghijkm")).toBe("legacy"); // nanoid, 21 + expect(resolveShard("b".repeat(27))).toBe("legacy"); // pre-cutover base62, 27 + for (const bad of [ + "", + "x".repeat(24) + "01", // 'x' outside base32hex + "x".repeat(24) + "02", // 'x' outside base32hex, gen-2 version + "A".repeat(25) + "1", // uppercase + "A".repeat(25) + "2", // uppercase, gen-2 version + "a".repeat(24) + "-1", // hyphen positional char + "a".repeat(24) + "-2", // hyphen positional char, gen-2 version + "a".repeat(26), // 26 chars, no version marker + "a".repeat(24) + "e3", // unknown version char + "x".repeat(40), + ]) { + expect(resolveShard(bad)).toBe("legacy"); + } + }); + + it("never throws, for any input string", () => { + for (const input of ["", "x", "-".repeat(26), " ".repeat(26), "\u{1F642}".repeat(26)]) { + expect(() => resolveShard(input)).not.toThrow(); + } + }); + + it("the reserved keys are multi-char, so no single-char shard key can collide", () => { + for (const reserved of ["legacy", "new"]) { + expect(reserved.length).toBeGreaterThan(1); + expect(SHARD_CHARS).not.toContain(reserved); + } + }); +}); + +describe("gen-2 ids classify NEW — the dedicated family widens in meaning only", () => { + it("a gen-2 id is NEW / runOpsId across id-shape co-located entities", () => { + for (const util of [RunId, WaitpointId, BatchId]) { + const id = generateRunOpsIdV2("a"); + const friendlyId = util.toFriendlyId(id); + expect(ownerEngine(id)).toBe("NEW"); + expect(ownerEngine(friendlyId)).toBe("NEW"); + expect(classifyResidency(id)).toBe("NEW"); + expect(classifyKind(id)).toBe("runOpsId"); + expect(isClassifiable(id)).toBe(true); + } + }); + + it("26+'2' is now a gen-2 shard route, not LEGACY (the one deliberate flip)", () => { + const genTwo = "a".repeat(24) + "e2"; + expect(ownerEngine(genTwo)).toBe("NEW"); + expect(resolveShard(genTwo)).toBe("e"); + }); + + it("three-way disjointness: cuid, gen-1 v1 and gen-2 never cross-classify", () => { + for (let i = 0; i < SAMPLES; i++) { + const cuid = RunId.generate().id; + const v1 = generateRunOpsId(); + const v2 = generateRunOpsIdV2(SHARD_CHARS[i % SHARD_CHARS.length]!); + + expect(ownerEngine(cuid)).toBe("LEGACY"); + expect(resolveShard(cuid)).toBe("legacy"); + + expect(ownerEngine(v1)).toBe("NEW"); + expect(resolveShard(v1)).toBe("new"); + + expect(ownerEngine(v2)).toBe("NEW"); + expect(resolveShard(v2)).toBe(SHARD_CHARS[i % SHARD_CHARS.length]); + } + }); +}); diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.ts b/packages/core/src/v3/isomorphic/runOpsResidency.ts index f0b1e3db334..c0f98ee5ed9 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.ts @@ -1,8 +1,17 @@ -import { isRunOpsIdBody } from "./friendlyId.js"; +import { isRunOpsIdBody, parseRunOpsIdV2Body } from "./friendlyId.js"; -/** The two run-ops stores a run/waitpoint can reside in. */ +/** + * The two store FAMILIES a run/waitpoint can reside in. "NEW" is the dedicated + * family: one store under gen-1, one per shard under gen-2. Use + * {@link resolveShard} when the specific store matters. + */ export type Residency = "LEGACY" | "NEW"; +// A routing key naming one store: the reserved gen-1 keys, or a gen-2 shard char. +// Reserved keys are multi-char, so a shard char can never collide with them. +// Note: TS reduces this union to `string` — it documents intent, it does not narrow. +export type ShardKey = "legacy" | "new" | string; + /** * Underlying id lineage. "runOpsId" is the label for the NEW-store mint path * — a base32hex run-ops v1 id (see friendlyId.ts). It is the value persisted in @@ -42,15 +51,32 @@ function internalForm(id: string): string { } /** - * Returns the id lineage by the version-char rule: a well-formed run-ops v1 - * body (26 chars, version "1" at index 25, base32hex alphabet) is "runOpsId" - * (NEW store); everything else — including malformed v1 shapes — is "cuid" - * (legacy). Total: never throws. Transition: pre-cutover 27-char base62 ids (the old - * NEW-mint format) now classify LEGACY, so ship this with the base32hex generator only once - * any 27-char NEW-resident runs are drained/disposable — no live run is misrouted mid-cutover. + * Resolve the store that owns an id. A gen-2 body names its own shard; a gen-1 + * v1 body resolves to the single dedicated store; everything else is legacy. + * + * The version char at index 25 is one character, so the v1 and gen-2 shape + * checks are mutually exclusive by construction — a gen-1 id can never resolve + * to a shard, and a gen-2 id can never resolve to "new". Total: never throws. + */ +export function resolveShard(id: string): ShardKey { + const body = internalForm(id); + + const genTwo = parseRunOpsIdV2Body(body); + if (genTwo) return genTwo.shard; + + return isRunOpsIdBody(body) ? "new" : "legacy"; +} + +/** + * Returns the id lineage by the version-char rule: a well-formed run-ops v1 body + * (version "1") or gen-2 body (version "2") is "runOpsId"; everything else — + * including malformed shapes of either — is "cuid" (legacy). Total: never throws. + * Transition: pre-cutover 27-char base62 ids (the old NEW-mint format) classify + * LEGACY, so ship this with the base32hex generator only once any 27-char + * NEW-resident runs are drained/disposable — no live run is misrouted mid-cutover. */ export function classifyKind(id: string): ResidencyKind { - return isRunOpsIdBody(internalForm(id)) ? "runOpsId" : "cuid"; + return resolveShard(id) === "legacy" ? "cuid" : "runOpsId"; } /** Classification is total now; kept for API compatibility. */ From f229ddaf8793d0e35af61ed8ba549f16b9f31906 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Fri, 21 Aug 2026 11:43:58 +0100 Subject: [PATCH 2/2] test(core): pin the unallocated version char to 9 so gen-3 does not break it --- packages/core/src/v3/isomorphic/friendlyId.test.ts | 2 +- packages/core/src/v3/isomorphic/runOpsResidency.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/v3/isomorphic/friendlyId.test.ts b/packages/core/src/v3/isomorphic/friendlyId.test.ts index 1ce066883a0..2e3ba4d83a5 100644 --- a/packages/core/src/v3/isomorphic/friendlyId.test.ts +++ b/packages/core/src/v3/isomorphic/friendlyId.test.ts @@ -181,7 +181,7 @@ describe("parseRunId — version-char discrimination (not length)", () => { it("falls back to legacy on a malformed v1 (bad alphabet / wrong version char)", () => { expect(parseRunId(`run_${"A".repeat(25)}1`).format).toBe("legacy"); // uppercase core - expect(parseRunId(`run_${"a".repeat(25)}3`).format).toBe("legacy"); // unknown version + expect(parseRunId(`run_${"a".repeat(25)}9`).format).toBe("legacy"); // unallocated version expect(parseRunId(`run_${"a".repeat(25)}2`).format).toBe("b32hexV2"); // "2" is now gen-2 expect(parseRunId(`run_${"a".repeat(24)}-1`).format).toBe("legacy"); // region char not [a-z0-9] expect(parseRunId(`run_${"a".repeat(27)}`).format).toBe("legacy"); // old 27-char shape diff --git a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts index 43ff7dee138..d395a16754d 100644 --- a/packages/core/src/v3/isomorphic/runOpsResidency.test.ts +++ b/packages/core/src/v3/isomorphic/runOpsResidency.test.ts @@ -46,7 +46,7 @@ describe("ownerEngine — residency classifier (version char at fixed position, const v1 = "a".repeat(24) + "e1"; expect(ownerEngine(v1)).toBe("NEW"); expect(ownerEngine("a".repeat(24) + "e2")).toBe("NEW"); // gen-2: shard "e" - expect(ownerEngine("a".repeat(24) + "e3")).toBe("LEGACY"); // unknown version char + expect(ownerEngine("a".repeat(24) + "e9")).toBe("LEGACY"); // unallocated version char expect(ownerEngine("a".repeat(26))).toBe("LEGACY"); // 26 chars but no version marker }); @@ -110,7 +110,7 @@ describe("resolveShard — the gen-2 refinement inside the dedicated family", () "a".repeat(24) + "-1", // hyphen positional char "a".repeat(24) + "-2", // hyphen positional char, gen-2 version "a".repeat(26), // 26 chars, no version marker - "a".repeat(24) + "e3", // unknown version char + "a".repeat(24) + "e9", // unallocated version char "x".repeat(40), ]) { expect(resolveShard(bad)).toBe("legacy");