|
| 1 | +// ───────────────────────────────────────────────────────────────────────────── |
| 2 | +// Shared slug helpers — config-free, no side-effect imports. |
| 3 | +// |
| 4 | +// This module exists to break two duplications that previously lived across |
| 5 | +// `pull.ts`, `dep-dedup.ts`, `audit.ts`, and `setup.ts`: |
| 6 | +// - `slugify(name)` — 4 byte-identical copies |
| 7 | +// - `extractBaseSlug(resourceId)` — 2 byte-identical copies |
| 8 | +// |
| 9 | +// It also exposes the strict `isEngineSuffixedSlug` form used by |
| 10 | +// `recanonicalize.ts` to prove a state key was engine-generated (i.e. the |
| 11 | +// captured 8-hex matches the entry's UUID prefix), and the canonical |
| 12 | +// `UUID_SUFFIX_RE` constant. |
| 13 | +// |
| 14 | +// Config-free is load-bearing: `config.ts` asserts `argv[2]` / `VAPI_TOKEN` |
| 15 | +// at module load. Any test that imports a slug helper without going through |
| 16 | +// this module would otherwise have to prime `process.argv` and |
| 17 | +// `process.env.VAPI_TOKEN` (see `tests/recanonicalize.test.ts:7-8`). This |
| 18 | +// module has zero such side effects so it's safely importable from any test. |
| 19 | +// ───────────────────────────────────────────────────────────────────────────── |
| 20 | + |
| 21 | +// `^(.+)-([0-9a-f]{8})$` deliberately requires a non-empty base. An engine- |
| 22 | +// generated state key always carries a real slug before the 8-hex suffix — |
| 23 | +// the synthetic `-deadbeef` shape (empty base) is never produced. |
| 24 | +export const UUID_SUFFIX_RE = /^(.+)-([0-9a-f]{8})$/i; |
| 25 | + |
| 26 | +// Lowercase, replace non-alphanumeric runs with `-`, trim leading/trailing |
| 27 | +// `-`, and collapse repeated `-`. Mirrors the slug shape produced by |
| 28 | +// `generateResourceId` in `src/pull.ts` and downstream `<base>-<uuid8>` |
| 29 | +// patterns. |
| 30 | +export function slugify(name: string): string { |
| 31 | + return name |
| 32 | + .toLowerCase() |
| 33 | + .replace(/[^a-z0-9]+/g, "-") |
| 34 | + .replace(/^-+|-+$/g, "") |
| 35 | + .replace(/-+/g, "-"); |
| 36 | +} |
| 37 | + |
| 38 | +// Loose form: strip a trailing 8-hex segment if the resourceId matches the |
| 39 | +// engine-generated `<base>-<uuid8>` shape; otherwise return the input |
| 40 | +// unchanged. Used by callers that don't have a UUID handy (audit's |
| 41 | +// sibling-base-slug check, the orphan-gate's pairing pass, pull's |
| 42 | +// `findExistingResourceId`, dep-dedup's `extractBaseSlug` consumers). |
| 43 | +// |
| 44 | +// This intentionally does NOT verify that the captured suffix matches any |
| 45 | +// specific UUID — that proof requires `isEngineSuffixedSlug`. Loose callers |
| 46 | +// only need a best-effort canonical form. |
| 47 | +export function extractBaseSlug(resourceId: string): string { |
| 48 | + const match = resourceId.match(UUID_SUFFIX_RE); |
| 49 | + return match?.[1] ?? resourceId; |
| 50 | +} |
| 51 | + |
| 52 | +// Strict form: return the parsed `{ base, suffix }` ONLY when the captured |
| 53 | +// 8 hex chars match the leading 8 hex chars of `uuid` (case-insensitive, |
| 54 | +// dashes stripped defensively). Returns `null` otherwise — including when |
| 55 | +// the resourceId doesn't match the engine shape at all. |
| 56 | +// |
| 57 | +// Use this when you have BOTH a state key AND its entry's UUID and need to |
| 58 | +// prove the key was engine-generated (the precondition-2 check in |
| 59 | +// `recanonicalize.ts`). A user-given name that coincidentally ends in |
| 60 | +// `-abcd1234` will NOT match because its UUID prefix is different. |
| 61 | +// |
| 62 | +// Mirrors `generateResourceId` in `src/pull.ts:265-273` — UUIDs have the |
| 63 | +// form `xxxxxxxx-xxxx-...` so the first 8 hex chars are dash-free, but the |
| 64 | +// dash-strip is kept as defense against malformed input. |
| 65 | +export function isEngineSuffixedSlug( |
| 66 | + stateKey: string, |
| 67 | + uuid: string, |
| 68 | +): { base: string; suffix: string } | null { |
| 69 | + const match = stateKey.match(UUID_SUFFIX_RE); |
| 70 | + if (!match) return null; |
| 71 | + const base = match[1]; |
| 72 | + const capturedSuffix = match[2]; |
| 73 | + if (!base || !capturedSuffix) return null; |
| 74 | + const uuidPrefix = uuid.replace(/-/g, "").slice(0, 8).toLowerCase(); |
| 75 | + if (capturedSuffix.toLowerCase() !== uuidPrefix) return null; |
| 76 | + return { base, suffix: capturedSuffix.toLowerCase() }; |
| 77 | +} |
0 commit comments