// Redirect map builder (pure, testable). // // The producer's warehouse makes the GROUP the addressable model (its // `model_id` / `model_route_id`). Every variant, dated snapshot, or // older-cased spelling the producer folds into a group lives only in that group // row's `raw_model_ids` list and has no page of its own. This builds the // old-route -> canonical-group-route 301 map straight from `models_view`, so a // link to any folded spelling lands on the real group page. Every redirect // target is an addressable `model_route_id` by construction. // // Kept separate from the generated data file (`lib/model-url-redirects.ts`) so // the generator script AND unit tests can exercise the build logic without // importing the (large) generated map. // // Route id form: RFC 3986 percent-encoded whole id (`/` -> `%2F`), matching the // producer's `route_id` column and middleware's `encodeURIComponent` lookup key. export interface ModelsViewRow { /** canonical encoded route for this group — the redirect TARGET. */ model_route_id: string model_id?: string | null route_id?: string | null model_key?: string | null model_group_id?: string | null /** raw spellings folded into this group (HF-cased, may contain `/`). */ raw_model_ids?: string[] | null } /** Encode a plain model id into the producer `route_id` form. */ export function routeId(id: string): string { return encodeURIComponent(id.trim()) } export interface BuildResult { /** old encoded route id -> canonical (group) encoded route id. */ redirects: Map /** * Raw spellings that fan out to MORE THAN ONE group (would be ambiguous to * redirect). Excluded from the map and surfaced for review rather than * picking one arbitrarily. Expected to be empty for a well-formed warehouse. */ ambiguous: Map> } /** * Build the redirect map from models_view rows. Pure and deterministic. * * A folded spelling is redirected to its owning group's `model_route_id` unless * (a) it already equals that route, or (b) it is itself an addressable page * (some row's own id) — we never hijack a working URL. A spelling that maps to * multiple distinct groups is recorded as ambiguous and excluded. */ export function buildRedirectsFromModelsView(rows: ModelsViewRow[]): BuildResult { // Pass 1: every route form that resolves to a real page (mirrors the // getModelSummaryById lookup columns), so we never redirect a live URL. const addressable = new Set() for (const r of rows) { if (r.model_route_id) addressable.add(r.model_route_id) if (r.route_id) addressable.add(r.route_id) if (r.model_id) addressable.add(routeId(r.model_id)) if (r.model_group_id) addressable.add(routeId(r.model_group_id)) if (r.model_key) addressable.add(routeId(r.model_key)) } // Pass 2: map each folded raw spelling to its owning group route. const candidates = new Map>() for (const r of rows) { const target = r.model_route_id if (!target) continue for (const raw of r.raw_model_ids ?? []) { if (!raw) continue const old = routeId(raw) if (old === target) continue // already the canonical route if (addressable.has(old)) continue // has its own page; don't hijack it const set = candidates.get(old) ?? new Set() set.add(target) candidates.set(old, set) } } const redirects = new Map() const ambiguous = new Map>() for (const [old, targets] of candidates) { if (targets.size === 1) redirects.set(old, [...targets][0]) else ambiguous.set(old, targets) } return { redirects, ambiguous } } /** Serialise a redirect map to the generated TS module source. */ export function serializeRedirectModule( redirects: Map, meta: { source: string }, ): string { const sorted = [...redirects.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) const entries = sorted.map(([o, n]) => ` [${JSON.stringify(o)}, ${JSON.stringify(n)}],`).join("\n") return `// GENERATED by scripts/generate-model-redirects.ts — do not edit by hand. // // Derived from the warehouse models_view (${meta.source}). Each entry maps a // folded model spelling (a variant / dated snapshot / older-cased id the // producer collapses into a group) to that group's canonical route, so a link // to any folded spelling 301s to the real group page. Every target is an // addressable model_route_id by construction. // // Entries: ${redirects.size}. // // Old encoded route id -> canonical encoded route id (RFC 3986, \`/\` -> %2F). // middleware.ts looks up the incoming route here and issues a 301 (query params // preserved). /** old encoded route id -> canonical (group) encoded route id. */ export const MODEL_URL_REDIRECTS = new Map([ ${entries} ]) export function resolveModelRedirect(routeId: string): string | undefined { return MODEL_URL_REDIRECTS.get(routeId) } ` }