// Types only. No `node:` import, ever. // // This file exists because of a failure this repo has already had: a value // import that dragged `node:fs` into a client component passed `tsc --noEmit` // and then 500'd every page. The registry is exactly that hazard -- a client // component wants the kind labels for its chips, and the registry also reads // the disk. So the shapes live here, the reading lives in lib/projects/*.mjs, // and a component that only needs a label never touches the latter. /** A registry id. Deliberately `string`: kinds are data, not a closed union. */ export type ProjectKindId = string; /** * The song kind's four deliverables. * * The VALUE lives in lib/projects/song.mjs, because `umtool ls` needs it from a * terminal; the TYPE lives here, because a .mjs export infers as string[] and * every existing caller wants the literal union. One list, two shapes -- which * is the only place in the registry that duplication is unavoidable. */ export type CutName = "wide" | "wide-short" | "vertical" | "vertical-short"; export type ProjectStage = { id: string; label: string; }; /** * The shared state vocabulary, across every kind. * * One vocabulary rather than per-kind words, because the whole point of the * filter is to ask "what is half-done" without first asking "half-done at * what". A kind maps its own situation onto these; it does not invent a sixth. */ export type ProjectState = "draft" | "windows" | "fetched" | "built" | "shipped" | "stale"; export const PROJECT_STATES: ProjectState[] = [ "draft", "windows", "fetched", "built", "shipped", "stale", ]; export const STATE_LABEL: Record = { draft: "draft", windows: "windows", fetched: "fetched", built: "built", shipped: "shipped", stale: "stale", }; /** How a project is reachable, and whether it is reachable at all. */ export type Routing = "ok" | "shadowed" | "unroutable"; export type ProjectRef = { /** * The POSIX path relative to REPORTS_ROOT -- `ferret-rescue`, or * `quartering-uh-song/videos/yoshi`. NOT the basename: bare names collide * across folders (a second `pokemon` is a matter of time) and the path is * what makes a link stable. */ id: string; /** Absolute. */ dir: string; /** The last segment, for display. */ name: string; /** The POSIX path of the containing folder, or "" at the root. */ folder: string; kind: ProjectKindId; template: string; routing: Routing; /** Set when two kinds matched. Never resolved by picking one. */ ambiguousWith?: ProjectKindId[]; }; export type ProjectSummary = ProjectRef & { /** The kind's short badge, copied in so a card needs no registry lookup. */ badge: string; title: string; /** One line under the title. Kind-specific. */ subtitle: string | null; state: ProjectState; /** Newest mtime of anything the summary looked at. Drives `sort=recent`. */ newestMtimeMs: number; /** Small facts for the card, already rendered as text. */ facts: string[]; /** Loud, short, and only when something is wrong. */ flags: string[]; /** Project-relative path of a poster candidate, or null. */ posterRel: string | null; /** Everything a `?q=` substring match should see. */ haystack: string; /** * `data-*` attributes the kind wants on its card. * * The grid renders strings and knows no kinds, so this is how a kind keeps an * assertion surface of its own -- a song's missing cut list, a report's clip * count -- without the grid growing a branch per kind. A key whose value is * absent is OMITTED, so "nothing is missing" is the attribute not being there * rather than an empty string. */ attrs?: Record; }; export type FolderNode = { /** POSIX path relative to REPORTS_ROOT. "" is the root itself. */ path: string; /** * The display label. A pass-through chain (`quartering-uh-song/videos`) is * collapsed to one label; the URL is never collapsed. */ label: string; /** The segments folded into `label`, oldest first. Display only. */ collapsedFrom: string[]; projects: string[]; children: string[]; }; export type ProjectKindMeta = { id: ProjectKindId; template: string; label: string; /** Two to six characters. Goes on the card. */ badge: string; stages: ProjectStage[]; decisionKinds: string[]; /** * What "new project" asks for beyond a slug and a title, or null when the * kind cannot be scaffolded. The menu renders these from data, so it never * has to know what a kind is. */ scaffold: { fields: string[] } | null; };