{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table-schema",
  "title": "Data Table Schema System",
  "description": "Declarative table definitions with col.* factories, presets, and generators for columns, filters, and sheet fields.",
  "dependencies": [
    "@tanstack/react-table@^9.2.3",
    "@tanstack/react-query@^5.101.4"
  ],
  "registryDependencies": [
    "https://data-table.openstatus.dev/r/data-table.json",
    "https://data-table.openstatus.dev/r/data-table-cell.json",
    "https://data-table.openstatus.dev/r/data-table-sheet.json",
    "https://data-table.openstatus.dev/r/data-table-filter-command.json"
  ],
  "files": [
    {
      "path": "src/lib/table-schema/index.ts",
      "content": "import { col as _col, resolveColumns } from \"./col\";\nimport { presets } from \"./presets\";\nimport {\n  deserializeSchema,\n  migrateSchemaJSON,\n  serializeSchema,\n} from \"./serialize\";\nimport { validateSchema } from \"./validate\";\n\n/**\n * Column builder factories and presets for defining table schemas.\n *\n * **Primitive factories** — choose based on the data type of the column:\n * - `col.string()` — text data (`string`)\n * - `col.number()` — numeric data (`number`)\n * - `col.boolean()` — boolean data (`boolean`)\n * - `col.timestamp()` — date/time data (`Date`)\n * - `col.enum(values)` — string union (`T[number]`)\n * - `col.array(item)` — array of values (`U[]`)\n * - `col.record()` — key-value map (`Record<string, string>`)\n *\n * **Presets** — pre-configured builders for common log table patterns:\n * - `col.presets.logLevel(values)` — severity levels\n * - `col.presets.httpMethod(values)` — HTTP verbs\n * - `col.presets.httpStatus(codes?)` — HTTP status codes\n * - `col.presets.duration(unit?, slider?)` — timing / latency\n * - `col.presets.timestamp()` — sortable timestamp with timerange filter\n * - `col.presets.traceId()` — trace / request ID (code display, not filterable)\n * - `col.presets.pathname()` — URL path with text search\n *\n * @example\n * ```ts\n * import { col, createTableSchema } from \"@/lib/table-schema\";\n *\n * export const tableSchema = createTableSchema({\n *   level:   col.presets.logLevel(LEVELS).description(\"Log severity\"),\n *   date:    col.presets.timestamp().label(\"Date\").size(200).sheet(),\n *   latency: col.presets.duration(\"ms\").label(\"Latency\").sortable().size(110).sheet(),\n *   status:  col.presets.httpStatus().label(\"Status\").size(60),\n *   method:  col.presets.httpMethod(METHODS).size(69),\n *   host:    col.string().label(\"Host\").size(125).sheet(),\n *   headers: col.record().label(\"Headers\").hidden().sheet(),\n * });\n * ```\n */\nexport const col = { ..._col, presets };\nexport type {\n  ColBuilder,\n  ColKind,\n  ColRenderers,\n  ColumnDescriptor,\n  ColumnDescriptorCommon,\n  DatePresetDescriptor,\n  DisplayConfig,\n  DisplayDescriptor,\n  FilterDescriptor,\n  FilterType,\n  InferTableType,\n  JsonValue,\n  OptionDescriptor,\n  Provenance,\n  ResolvedColumn,\n  ResolvedColumnEntry,\n  SchemaJSON,\n  SchemaJSONVersion,\n  SerializableDisplayConfig,\n  SheetConfig,\n  SheetDescriptor,\n  TableSchemaDefinition,\n} from \"./types\";\nexport { resolveColumn, resolveColumns } from \"./col\";\nexport {\n  NAMED_DISPLAY_TYPES,\n  applyRenderers,\n  isNamedDisplayType,\n  type ApplyRenderersOptions,\n  type NamedDisplayType,\n  type RendererOverrides,\n} from \"./renderers\";\nexport { generateColumns } from \"./generators/columns\";\nexport { generateFilterFields } from \"./generators/filter-fields\";\nexport { generateFilterSchema } from \"./generators/filter-schema\";\nexport { generateSheetFields } from \"./generators/sheet-fields\";\nexport {\n  SCHEMA_JSON_VERSION,\n  deserializeSchema,\n  migrateSchemaJSON,\n  serializeSchema,\n} from \"./serialize\";\nexport {\n  DEFAULT_CAPABILITIES,\n  MANIFEST_LIMITS,\n  TABLE_MANIFEST_VERSION,\n  TableManifestError,\n  createRowAccessors,\n  createTableManifest,\n  createTableManifestHandler,\n  fetchTableManifest,\n  isSafeColumnKey,\n  manifestETag,\n  parseTableManifest,\n  type FetchManifestOptions,\n  type ManifestHandlerOptions,\n  type ParseManifestOptions,\n  type RowAccessors,\n} from \"./manifest\";\nexport {\n  type TableCapabilities,\n  type TableChartConfig,\n  type TableManifest,\n  type TableManifestDefaults,\n} from \"./manifest\";\nexport {\n  manifestToModule,\n  pullManifestModule,\n  type SnapshotOptions,\n} from \"./snapshot\";\n\n/**\n * Derive defaultColumnVisibility from the schema.\n * Returns { [key]: false } for every column marked with .hidden().\n */\nexport function getDefaultColumnVisibility(\n  schema: import(\"./types\").TableSchemaDefinition,\n): Record<string, boolean> {\n  const visibility: Record<string, boolean> = {};\n  for (const { key, hidden } of resolveColumns(schema)) {\n    if (hidden) visibility[key] = false;\n  }\n  return visibility;\n}\n\n/**\n * Create a table schema from a map of col.* builders.\n *\n * The returned object holds the definition and exposes:\n * - `toJSON()` — serializes the schema to a function-free JSON descriptor,\n *   suitable for AI agents, MCP tools, and `JSON.stringify`.\n *\n * Use `createTableSchema.fromJSON(json)` to reconstruct a schema from a\n * JSON descriptor (e.g. one generated by an AI agent). Custom renderers\n * (display.cell, filter.component, sheet.component) are not serialized and\n * must be applied manually on top of the reconstructed builders.\n *\n * @example\n * ```ts\n * export const tableSchema = createTableSchema({\n *   level: col.enum(LEVELS).label(\"Level\").defaultOpen().sheet(),\n *   date: col.timestamp().label(\"Date\").sortable().size(200).sheet(),\n * });\n *\n * export type ColumnSchema = InferTableType<typeof tableSchema.definition>;\n *\n * // Serialize for an AI agent or MCP tool\n * const json = tableSchema.toJSON();\n * // JSON.stringify(tableSchema) also works — toJSON() is called automatically\n *\n * // Reconstruct from AI-generated JSON\n * const schema = createTableSchema.fromJSON(json);\n * ```\n */\nexport function createTableSchema<\n  T extends import(\"./types\").TableSchemaDefinition,\n>(definition: T): { definition: T; toJSON(): import(\"./types\").SchemaJSON } {\n  validateSchema(definition);\n  return {\n    definition,\n    toJSON() {\n      return serializeSchema(definition);\n    },\n  };\n}\n\n/**\n * Reconstruct a schema from a JSON descriptor.\n *\n * Takes `unknown` on purpose: the builder pastes user-typed text in here, and\n * the cache replays JSON written by an older build. `migrateSchemaJSON` brings\n * older versions forward and throws on anything unrecognisable, so the cast\n * that used to sit on untrusted input is gone.\n */\ncreateTableSchema.fromJSON = (\n  json: unknown,\n): {\n  definition: import(\"./types\").TableSchemaDefinition;\n  toJSON(): import(\"./types\").SchemaJSON;\n} => {\n  return createTableSchema(deserializeSchema(migrateSchemaJSON(json)));\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/col.ts",
      "content": "import type {\n  DatePreset,\n  Option,\n} from \"@/components/data-table/types\";\nimport type {\n  ColBuilder,\n  ColKind,\n  ColRenderers,\n  ColumnDescriptor,\n  ColumnDescriptorCommon,\n  DatePresetDescriptor,\n  DisplayDescriptor,\n  FilterDescriptor,\n  FilterType,\n  OptionDescriptor,\n  Provenance,\n  ResolvedColumn,\n  ResolvedColumnEntry,\n  SheetConfig,\n  TableSchemaDefinition,\n} from \"./types\";\n\n// ── Per-kind defaults — the ONE definition ──────────────────────────────────\n\n/**\n * The default cell display for a column kind.\n *\n * This is the single definition. `infer.ts`, `generators/sheet-fields.ts`, and\n * `serialize.ts` each used to carry their own copy, and the third disagreed\n * with the other two.\n */\nexport function defaultDisplayForKind(kind: ColKind): DisplayDescriptor {\n  switch (kind) {\n    case \"number\":\n      return { type: \"number\" };\n    case \"boolean\":\n    case \"select\":\n      return { type: \"boolean\" };\n    case \"timestamp\":\n      return { type: \"timestamp\" };\n    case \"enum\":\n    case \"array\":\n      return { type: \"badge\" };\n    case \"string\":\n    case \"record\":\n      return { type: \"text\" };\n  }\n}\n\nconst MANUAL: Provenance = { source: \"manual\" };\n\n/**\n * Drop keys whose value is `undefined`.\n *\n * Descriptors have to be canonical: `JSON.stringify` erases an explicit\n * `undefined`, so a descriptor carrying one compares unequal to its own round\n * trip under `toStrictEqual` and under any key-based comparison, while looking\n * identical through `JSON`. `display(\"number\", { unit: undefined })` — which\n * `col.presets.duration()` produces whenever no unit is given — is the case\n * that actually occurs.\n *\n * Exported for `infer.ts`, which builds descriptors outside the builder and so\n * has to hold the same invariant.\n */\nexport function compact<T extends Record<string, unknown>>(object: T): T {\n  const result = {} as Record<string, unknown>;\n  for (const [key, value] of Object.entries(object)) {\n    if (value !== undefined) result[key] = value;\n  }\n  return result as T;\n}\n\n/**\n * Strip caller-supplied `Option`s down to their serializable fields.\n *\n * `Option.value` is `string | boolean | number | undefined`, but an option with\n * no value cannot be selected, cannot be encoded into filter state, and would\n * vanish from the descriptor on `JSON.stringify` — so it is dropped here,\n * at construction, rather than disappearing later at serialization time.\n */\nfunction toOptionDescriptors(options: readonly Option[]): OptionDescriptor[] {\n  const result: OptionDescriptor[] = [];\n  for (const option of options) {\n    if (option.value === undefined) continue;\n    result.push({ label: option.label, value: option.value });\n  }\n  return result;\n}\n\n/** Store preset bounds as ISO instants so they survive JSON. */\nfunction toPresetDescriptor(preset: DatePreset): DatePresetDescriptor {\n  return {\n    label: preset.label,\n    shortcut: preset.shortcut,\n    from: preset.from.toISOString(),\n    to: preset.to.toISOString(),\n  };\n}\n\n/** Revive a serialized preset back into the `DatePreset` the UI consumes. */\nexport function fromPresetDescriptor(\n  descriptor: DatePresetDescriptor,\n): DatePreset {\n  return {\n    label: descriptor.label,\n    shortcut: descriptor.shortcut,\n    from: new Date(descriptor.from),\n    to: new Date(descriptor.to),\n  };\n}\n\n// ── The builder ─────────────────────────────────────────────────────────────\n\n/**\n * Build a `ColBuilder` over an explicit descriptor + renderers pair.\n *\n * Exported because `deserializeSchema` reconstructs builders by handing a\n * descriptor straight back — no per-field replay of the fluent chain.\n */\nexport function createColBuilder<T, F extends FilterType = FilterType>(\n  descriptor: ColumnDescriptor,\n  renderers: ColRenderers = {},\n): ColBuilder<T, F> {\n  // The implementation uses loose parameter types to satisfy all overload\n  // signatures at once. TypeScript enforces the constraints at call sites\n  // via the ColBuilder<T, F> interface overloads, not here.\n  const next = <U, G extends FilterType>(\n    patch: Partial<ColumnDescriptor>,\n    rendererPatch?: ColRenderers,\n  ): ColBuilder<U, G> =>\n    createColBuilder<U, G>(\n      { ...descriptor, ...patch } as ColumnDescriptor,\n      rendererPatch ? compact({ ...renderers, ...rendererPatch }) : renderers,\n    );\n\n  /** Return `renderers` without the named closures. */\n  const withoutRenderers = (...keys: (keyof ColRenderers)[]): ColRenderers => {\n    const result = { ...renderers };\n    for (const key of keys) delete result[key];\n    return result;\n  };\n\n  const builder = {\n    get _descriptor() {\n      return descriptor;\n    },\n\n    get _renderers() {\n      return renderers;\n    },\n\n    label(text: string): ColBuilder<T, F> {\n      return next<T, F>({ label: text });\n    },\n\n    description(text: string): ColBuilder<T, F> {\n      return next<T, F>({ description: text });\n    },\n\n    display(type: string, options?: Record<string, unknown>): ColBuilder<T, F> {\n      // \"custom\" is not a descriptor state — the closure goes to the renderers\n      // half and the descriptor keeps whatever serializable display it had, so\n      // it is already the correct fallback for the sheet and for toJSON().\n      if (type === \"custom\") {\n        const { cell, colorMap } = options as {\n          cell: ColRenderers[\"cell\"];\n          colorMap?: Record<string, string>;\n        };\n        return next<T, F>(\n          colorMap\n            ? {\n                display: {\n                  ...descriptor.display,\n                  colorMap,\n                } as DisplayDescriptor,\n              }\n            : {},\n          { cell },\n        );\n      }\n      const display = compact(\n        options ? { type, ...options } : { type },\n      ) as DisplayDescriptor;\n      return next<T, F>({ display });\n    },\n\n    filterable(\n      type?: string,\n      options?: Record<string, unknown>,\n    ): ColBuilder<T, F> {\n      const filterType = (type ||\n        descriptor.filter?.type ||\n        \"input\") as FilterType;\n      const existing = descriptor.filter;\n      const sameType = filterType === existing?.type;\n      const {\n        component,\n        options: rawOptions,\n        presets: rawPresets,\n        ...rest\n      } = (options ?? {}) as {\n        component?: ColRenderers[\"filterComponent\"];\n        options?: Option[];\n        presets?: DatePreset[];\n        min?: number;\n        max?: number;\n        unit?: string;\n      };\n\n      const filter: FilterDescriptor = {\n        type: filterType,\n        defaultOpen: existing?.defaultOpen ?? false,\n        commandDisabled: existing?.commandDisabled ?? false,\n        // Preserve auto-derived options when the filter type stays the same\n        // and no explicit options are provided by the caller.\n        ...(sameType && existing?.options ? { options: existing.options } : {}),\n        ...compact(rest),\n        ...(rawOptions ? { options: toOptionDescriptors(rawOptions) } : {}),\n        ...(rawPresets\n          ? { presets: rawPresets.map(toPresetDescriptor) }\n          : sameType && existing?.presets\n            ? { presets: existing.presets }\n            : {}),\n      };\n      return next<T, F>(\n        { filter },\n        component ? { filterComponent: component } : undefined,\n      );\n    },\n\n    notFilterable(): ColBuilder<T, never> {\n      return createColBuilder<T, never>(\n        { ...descriptor, filter: null },\n        // The filter component is meaningless without a filter.\n        withoutRenderers(\"filterComponent\"),\n      );\n    },\n\n    defaultOpen(): ColBuilder<T, F> {\n      if (!descriptor.filter) return next<T, F>({});\n      return next<T, F>({\n        filter: { ...descriptor.filter, defaultOpen: true },\n      });\n    },\n\n    commandDisabled(): ColBuilder<T, F> {\n      if (!descriptor.filter) return next<T, F>({});\n      return next<T, F>({\n        filter: { ...descriptor.filter, commandDisabled: true },\n      });\n    },\n\n    hidden(): ColBuilder<T, F> {\n      return next<T, F>({ hidden: true });\n    },\n\n    hideHeader(): ColBuilder<T, F> {\n      return next<T, F>({ hideHeader: true });\n    },\n\n    resizable(): ColBuilder<T, F> {\n      return next<T, F>({ resizable: true });\n    },\n\n    size(px: number): ColBuilder<T, F> {\n      return next<T, F>({ size: px });\n    },\n\n    minSize(px: number): ColBuilder<T, F> {\n      return next<T, F>({ minSize: px });\n    },\n\n    sortable(): ColBuilder<T, F> {\n      return next<T, F>({ sortable: true });\n    },\n\n    optional(): ColBuilder<T | undefined, F> {\n      return next<T | undefined, F>({ optional: true });\n    },\n\n    sheet(sheetConfig?: SheetConfig): ColBuilder<T, F> {\n      const { component, condition, ...sheetDescriptor } = sheetConfig ?? {};\n      return next<T, F>(\n        { sheet: compact(sheetDescriptor) },\n        compact({ sheetComponent: component, sheetCondition: condition }),\n      );\n    },\n\n    sheetOnly(): ColBuilder<T, never> {\n      return createColBuilder<T, never>(\n        {\n          ...descriptor,\n          hidden: true,\n          filter: null,\n          enableHiding: false,\n        },\n        withoutRenderers(\"filterComponent\"),\n      );\n    },\n  } as ColBuilder<T, F>;\n\n  return builder;\n}\n\n/**\n * Attach provenance to a builder.\n *\n * Internal to this package — `col.presets.*` records which preset produced the\n * column and with what arguments, and `infer.ts` records which heuristic fired.\n * `schemaToTypeScript` reads this instead of pattern-matching the descriptor.\n */\nexport function withProvenance<T, F extends FilterType>(\n  builder: ColBuilder<T, F>,\n  provenance: Provenance,\n): ColBuilder<T, F> {\n  return createColBuilder<T, F>(\n    { ...builder._descriptor, provenance },\n    builder._renderers,\n  );\n}\n\n// ── The public read model ───────────────────────────────────────────────────\n\n/**\n * Read a column's complete state — both halves — through one public accessor.\n *\n * Prefer this over reaching into `_descriptor` / `_renderers`, which are\n * `@internal` and may change shape.\n */\n// `any` on the filter-type parameter: callers hold builders of every filter\n// type at once, and the read model does not depend on which one it is.\nexport function resolveColumn(\n  builder: ColBuilder<unknown, any>,\n): ResolvedColumn {\n  return { ...builder._descriptor, renderers: builder._renderers };\n}\n\n/** `resolveColumn` over a whole definition, preserving key insertion order. */\nexport function resolveColumns(\n  definition: TableSchemaDefinition,\n): ResolvedColumnEntry[] {\n  return Object.entries(definition).map(([key, builder]) => ({\n    key,\n    ...resolveColumn(builder),\n  }));\n}\n\n// ── Factories ───────────────────────────────────────────────────────────────\n\n/** The descriptor every `col.*` factory starts from, before kind-specific bits. */\nfunction base(kind: ColKind): ColumnDescriptorCommon {\n  return {\n    optional: false,\n    label: \"\",\n    display: defaultDisplayForKind(kind),\n    hidden: false,\n    enableHiding: true,\n    hideHeader: false,\n    resizable: false,\n    sortable: false,\n    filter: null,\n    sheet: null,\n    provenance: MANUAL,\n  };\n}\n\n/**\n * A string column.\n *\n * - Data type: `string`\n * - Default display: `\"text\"` (plain text with overflow tooltip)\n * - Default filter: `\"input\"` (text search)\n * - Allowed filters: `\"input\"`\n *\n * @example\n * col.string().label(\"Host\").size(125).sheet()\n * col.string().label(\"Message\").notFilterable().optional().hidden()\n */\nfunction string(): ColBuilder<string, \"input\"> {\n  return createColBuilder<string, \"input\">({\n    ...base(\"string\"),\n    kind: \"string\",\n    filter: { type: \"input\", defaultOpen: false, commandDisabled: false },\n  });\n}\n\n/**\n * A numeric column.\n *\n * - Data type: `number`\n * - Default display: `\"number\"` (formatted, with optional unit)\n * - Default filter: `\"input\"` (exact match)\n * - Allowed filters: `\"input\"` | `\"slider\"` | `\"checkbox\"`\n *   - Use `\"slider\"` for continuous values (latency, file size)\n *   - Use `\"checkbox\"` for discrete values (HTTP status codes, port numbers)\n *\n * @example\n * col.number().label(\"Latency\").display(\"number\", { unit: \"ms\" }).filterable(\"slider\", { min: 0, max: 5000 }).sortable()\n * col.number().label(\"Status\").filterable(\"checkbox\", { options: [{ label: \"200\", value: 200 }] })\n */\nfunction number(): ColBuilder<number, \"input\" | \"slider\" | \"checkbox\"> {\n  return createColBuilder<number, \"input\" | \"slider\" | \"checkbox\">({\n    ...base(\"number\"),\n    kind: \"number\",\n    filter: { type: \"input\", defaultOpen: false, commandDisabled: false },\n  });\n}\n\n/**\n * A boolean column.\n *\n * - Data type: `boolean`\n * - Default display: `\"boolean\"` (checkmark / dash icon)\n * - Default filter: `\"checkbox\"` with `true` / `false` options pre-wired\n * - Allowed filters: `\"checkbox\"`\n *\n * @example\n * col.boolean().label(\"Cache Hit\").defaultOpen()\n */\nfunction boolean(): ColBuilder<boolean, \"checkbox\"> {\n  return createColBuilder<boolean, \"checkbox\">({\n    ...base(\"boolean\"),\n    kind: \"boolean\",\n    filter: {\n      type: \"checkbox\",\n      defaultOpen: false,\n      commandDisabled: false,\n      options: [\n        { label: \"true\", value: true },\n        { label: \"false\", value: false },\n      ],\n    },\n  });\n}\n\n/**\n * A timestamp column.\n *\n * - Data type: `Date`\n * - Default display: `\"timestamp\"` (relative time, absolute datetime on hover)\n * - Default filter: `\"timerange\"` (date range picker)\n * - Allowed filters: `\"timerange\"`\n *\n * @example\n * col.timestamp().label(\"Date\").sortable().commandDisabled().size(200).sheet()\n */\nfunction timestamp(): ColBuilder<Date, \"timerange\"> {\n  return createColBuilder<Date, \"timerange\">({\n    ...base(\"timestamp\"),\n    kind: \"timestamp\",\n    filter: { type: \"timerange\", defaultOpen: false, commandDisabled: true },\n  });\n}\n\n/**\n * An enum column from a `readonly string[]` union.\n *\n * - Data type: `T[number]` (union of the provided string literals)\n * - Default display: `\"badge\"` (colored chip)\n * - Default filter: `\"checkbox\"` with options auto-derived from `values`\n * - Allowed filters: `\"checkbox\"`\n *\n * Checkbox options are auto-derived from `values` by default. Override them via\n * `.filterable(\"checkbox\", { options: [...] })` when you need custom labels,\n * icons, or a subset of values.\n *\n * @param values - `as const` array of allowed string values\n *\n * @example\n * col.enum(LEVELS).label(\"Level\").defaultOpen()\n * col.enum(LEVELS).label(\"Level\").filterable(\"checkbox\", {\n *   options: LEVELS.map(v => ({ label: v, value: v })), // override labels\n * })\n */\nfunction colEnum<T extends readonly string[]>(\n  values: T,\n): ColBuilder<T[number], \"checkbox\"> {\n  return createColBuilder<T[number], \"checkbox\">({\n    ...base(\"enum\"),\n    kind: \"enum\",\n    enumValues: values,\n    filter: {\n      type: \"checkbox\",\n      defaultOpen: false,\n      commandDisabled: false,\n      options: Array.from(values).map((v) => ({ label: v, value: v })),\n    },\n  });\n}\n\n/**\n * An array column, typically used for multi-value enum fields.\n *\n * - Data type: `U[]` where `U` is the item builder's type\n * - Default display: `\"badge\"` (colored chip per value)\n * - Default filter: `\"checkbox\"`\n * - Allowed filters: `\"checkbox\"`\n *\n * Most commonly used as `col.array(col.enum(values))` for tags / regions / labels.\n * `col.array(col.string())` is equally valid — the item type is recorded, so a\n * `string[]` column stays a `string[]` column across a JSON round trip.\n *\n * @param itemBuilder - A `ColBuilder` describing the array item type\n *\n * @example\n * col.array(col.enum(REGIONS)).label(\"Regions\").filterable(\"checkbox\", {\n *   options: REGIONS.map(r => ({ label: r, value: r })),\n * })\n */\nfunction array<U>(\n  itemBuilder: ColBuilder<U, any>,\n): ColBuilder<U[], \"checkbox\"> {\n  return createColBuilder<U[], \"checkbox\">({\n    ...base(\"array\"),\n    kind: \"array\",\n    arrayItem: itemBuilder._descriptor,\n    filter: { type: \"checkbox\", defaultOpen: false, commandDisabled: false },\n  });\n}\n\n/**\n * A key-value record column.\n *\n * - Data type: `Record<string, string>`\n * - Default display: `\"text\"`\n * - Not filterable (`F = never`)\n *\n * Use for metadata maps, HTTP headers, environment variables, etc.\n * Typically rendered with a custom sheet component (key-value table / tabs).\n *\n * @example\n * col.record().label(\"Headers\").hidden().sheet({\n *   component: (row) => <KVTabs data={row.headers} />,\n *   className: \"flex-col items-start w-full gap-1\",\n * })\n */\nfunction record(): ColBuilder<Record<string, string>, never> {\n  return createColBuilder<Record<string, string>, never>({\n    ...base(\"record\"),\n    kind: \"record\",\n  });\n}\n\n/**\n * A row-selection checkbox column.\n *\n * - Data type: `boolean` (selected state)\n * - Not filterable (`F = never`)\n * - Not shown in sheet or filters\n * - Renders a checkbox in both the header (select all) and each row\n *\n * @example\n * col.select().label(\"Select\")\n */\nfunction select(): ColBuilder<boolean, never> {\n  return createColBuilder<boolean, never>({\n    ...base(\"select\"),\n    kind: \"select\",\n    label: \"Select\",\n    enableHiding: false,\n    size: 40,\n  });\n}\n\nexport const col = {\n  string,\n  number,\n  boolean,\n  timestamp,\n  enum: colEnum,\n  array,\n  record,\n  select,\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/presets.ts",
      "content": "import { col, withProvenance } from \"./col\";\nimport type { ColBuilder, FilterType, JsonValue } from \"./types\";\n\n/**\n * Tag a preset-built column so `schemaToTypeScript` can re-emit the exact call\n * instead of reverse-engineering it from the descriptor's shape.\n */\nfunction tag<T, F extends FilterType>(\n  preset: string,\n  args: readonly (JsonValue | undefined)[],\n  builder: ColBuilder<T, F>,\n): ColBuilder<T, F> {\n  // Trailing omitted args are dropped so the re-emitted call matches what the\n  // author wrote: `duration(\"ms\")`, not `duration(\"ms\", undefined)`. An omitted\n  // arg in the *middle* becomes `null`, which the emitter prints as `undefined`\n  // — `undefined` inside a JSON array does not survive `JSON.stringify`.\n  const trimmed = [...args];\n  while (trimmed.length > 0 && trimmed[trimmed.length - 1] === undefined) {\n    trimmed.pop();\n  }\n  return withProvenance(builder, {\n    source: \"preset\",\n    preset,\n    args: trimmed.map((a) => (a === undefined ? null : a)),\n  });\n}\n\nconst DEFAULT_HTTP_STATUS_CODES = [\n  200, 201, 204, 301, 302, 400, 401, 403, 404, 422, 429, 500, 502, 503, 504,\n];\n\n/**\n * Pre-configured column builders for patterns common in log and observability tables.\n *\n * Every preset returns a `ColBuilder` with sensible defaults already applied.\n * All builders remain fully customizable — chain additional methods to override\n * any default (label, size, display, sheet, etc.).\n *\n * @example\n * ```ts\n * const tableSchema = createTableSchema({\n *   level:   col.presets.logLevel(LEVELS).description(\"Log severity\"),\n *   date:    col.presets.timestamp().label(\"Date\").size(200).sheet(),\n *   latency: col.presets.duration(\"ms\").label(\"Latency\").sortable().size(110).sheet(),\n *   status:  col.presets.httpStatus().label(\"Status\").size(60),\n *   method:  col.presets.httpMethod(METHODS).size(69),\n *   path:    col.presets.pathname().label(\"Path\").size(130).sheet(),\n *   traceId: col.presets.traceId().label(\"Request ID\").hidden().sheet(),\n * });\n * ```\n */\nexport const presets = {\n  /**\n   * A log severity level column.\n   *\n   * Defaults: enum + badge display + checkbox filter + `defaultOpen`.\n   * Checkbox options are auto-derived from `values` — no need to map them manually.\n   *\n   * @param values - The allowed severity levels, e.g. `[\"error\", \"warn\", \"info\", \"debug\"] as const`\n   *\n   * @example\n   * ```ts\n   * col.presets.logLevel(LEVELS)\n   *   .label(\"Level\")\n   *   .description(\"Log severity: error > warn > info > debug\")\n   *   .size(27)\n   * ```\n   */\n  logLevel<T extends readonly string[]>(\n    values: T,\n  ): ColBuilder<T[number], \"checkbox\"> {\n    return tag(\n      \"logLevel\",\n      [values as readonly string[]],\n      col\n        .enum(values)\n        .label(\"Level\")\n        .filterable(\"checkbox\", {\n          options: values.map((v) => ({ label: v, value: v })),\n        })\n        .defaultOpen(),\n    );\n  },\n\n  /**\n   * An HTTP method column.\n   *\n   * Defaults: enum + plain text display + checkbox filter.\n   * Options are auto-derived from `values`.\n   *\n   * @param values - The allowed HTTP methods, e.g. `[\"GET\", \"POST\", \"PUT\", \"DELETE\"] as const`\n   *\n   * @example\n   * ```ts\n   * col.presets.httpMethod(METHODS).size(69)\n   * ```\n   */\n  httpMethod<T extends readonly string[]>(\n    values: T,\n  ): ColBuilder<T[number], \"checkbox\"> {\n    return tag(\n      \"httpMethod\",\n      [values as readonly string[]],\n      col\n        .enum(values)\n        .label(\"Method\")\n        .display(\"text\")\n        .filterable(\"checkbox\", {\n          options: values.map((v) => ({ label: v, value: v })),\n        }),\n    );\n  },\n\n  /**\n   * An HTTP status code column.\n   *\n   * Defaults: number + checkbox filter with a standard set of common status codes.\n   * Pass a custom `codes` array to override the defaults.\n   *\n   * Default codes: 200, 201, 204, 301, 302, 400, 401, 403, 404, 422, 429, 500, 502, 503, 504\n   *\n   * @param codes - Override the default status code options\n   *\n   * @example\n   * ```ts\n   * col.presets.httpStatus().label(\"Status\").size(60)\n   * col.presets.httpStatus([200, 400, 500]).label(\"Status\") // custom codes\n   * ```\n   */\n  httpStatus(\n    codes?: number[],\n  ): ColBuilder<number, \"input\" | \"slider\" | \"checkbox\"> {\n    return tag(\n      \"httpStatus\",\n      [codes as readonly number[] | undefined],\n      col\n        .number()\n        .label(\"Status\")\n        .filterable(\"checkbox\", {\n          options: (codes ?? DEFAULT_HTTP_STATUS_CODES).map((code) => ({\n            label: String(code),\n            value: code,\n          })),\n        }),\n    );\n  },\n\n  /**\n   * A duration / latency / timing column.\n   *\n   * Defaults: number + formatted number display with unit + slider filter\n   * with bounds `{ min: 0, max: 5000 }`.\n   *\n   * @param unit   - Unit label shown after the value, e.g. `\"ms\"`, `\"s\"`, `\"µs\"`\n   * @param slider - Override the slider bounds (default: `{ min: 0, max: 5000 }`)\n   *\n   * @example\n   * ```ts\n   * col.presets.duration(\"ms\").label(\"Latency\").sortable().size(110).sheet()\n   * col.presets.duration(\"s\", { min: 0, max: 60 }).label(\"Response time\")\n   * ```\n   */\n  duration(\n    unit?: string,\n    slider?: { min: number; max: number },\n  ): ColBuilder<number, \"input\" | \"slider\" | \"checkbox\"> {\n    return tag(\n      \"duration\",\n      [unit, slider],\n      col\n        .number()\n        .label(\"Duration\")\n        .display(\"number\", { unit })\n        .filterable(\"slider\", slider ?? { min: 0, max: 5000 }),\n    );\n  },\n\n  /**\n   * A timestamp column.\n   *\n   * Defaults: Date + relative timestamp display (absolute on hover) +\n   * timerange filter + sortable.\n   *\n   * @example\n   * ```ts\n   * col.presets.timestamp().label(\"Date\").commandDisabled().size(200).sheet()\n   * ```\n   */\n  timestamp(): ColBuilder<Date, \"timerange\"> {\n    return tag(\n      \"timestamp\",\n      [],\n      col.timestamp().label(\"Timestamp\").display(\"timestamp\").sortable(),\n    );\n  },\n\n  /**\n   * A trace / span / request ID column.\n   *\n   * Defaults: string + monospace code display + not filterable.\n   * Typically hidden in the table and shown only in the row detail drawer.\n   *\n   * @example\n   * ```ts\n   * col.presets.traceId().label(\"Request ID\").hidden().sheet({ skeletonClassName: \"w-64\" })\n   * ```\n   */\n  traceId(): ColBuilder<string, never> {\n    return tag(\n      \"traceId\",\n      [],\n      col.string().label(\"Trace ID\").display(\"code\").notFilterable(),\n    );\n  },\n\n  /**\n   * A URL pathname column.\n   *\n   * Defaults: string + plain text display + input (text search) filter.\n   *\n   * @example\n   * ```ts\n   * col.presets.pathname().size(130).sheet()\n   * ```\n   */\n  pathname(): ColBuilder<string, \"input\"> {\n    return tag(\n      \"pathname\",\n      [],\n      col.string().label(\"Pathname\").filterable(\"input\"),\n    );\n  },\n\n  /**\n   * A latency column with heatmap visualization.\n   *\n   * Defaults: number + heatmap display with unit + slider filter\n   * with bounds `{ min: 0, max: 5000 }` + sortable.\n   *\n   * @param unit   - Unit label shown after the value, e.g. `\"ms\"`, `\"s\"`, `\"µs\"`\n   * @param slider - Override the slider bounds (default: `{ min: 0, max: 5000 }`)\n   *\n   * @example\n   * ```ts\n   * col.presets.latency(\"ms\").label(\"Latency\").size(110).sheet()\n   * col.presets.latency(\"s\", { min: 0, max: 60 }).label(\"Response time\")\n   * ```\n   */\n  latency(\n    unit?: string,\n    slider?: { min: number; max: number },\n  ): ColBuilder<number, \"input\" | \"slider\" | \"checkbox\"> {\n    const bounds = slider ?? { min: 0, max: 5000 };\n    return tag(\n      \"latency\",\n      [unit, slider],\n      col\n        .number()\n        .label(\"Latency\")\n        .display(\"heatmap\", { unit, min: bounds.min, max: bounds.max })\n        .filterable(\"slider\", bounds)\n        .sortable(),\n    );\n  },\n\n  /**\n   * A health/score column with gauge visualization.\n   *\n   * Defaults: number + gauge display (0–100) + sortable.\n   *\n   * @param range - Override the min/max range (default: `{ min: 0, max: 100 }`)\n   *\n   * @example\n   * ```ts\n   * col.presets.health().label(\"Health\")\n   * col.presets.health({ min: 0, max: 1000 }).label(\"Score\")\n   * ```\n   */\n  health(range?: {\n    min: number;\n    max: number;\n  }): ColBuilder<number, \"input\" | \"slider\" | \"checkbox\"> {\n    const { min = 0, max = 100 } = range ?? {};\n    return tag(\n      \"health\",\n      [range],\n      col.number().label(\"Health\").display(\"gauge\", { min, max }).sortable(),\n    );\n  },\n\n  /**\n   * A progress column with bar visualization.\n   *\n   * Defaults: number + bar display (0–100) + sortable.\n   *\n   * @param range - Override the min/max range (default: `{ min: 0, max: 100 }`)\n   *\n   * @example\n   * ```ts\n   * col.presets.progress().label(\"Progress\")\n   * col.presets.progress({ min: 0, max: 1000 }).label(\"Completion\")\n   * ```\n   */\n  progress(range?: {\n    min: number;\n    max: number;\n  }): ColBuilder<number, \"input\" | \"slider\" | \"checkbox\"> {\n    const { min = 0, max = 100 } = range ?? {};\n    return tag(\n      \"progress\",\n      [range],\n      col.number().label(\"Progress\").display(\"bar\", { min, max }).sortable(),\n    );\n  },\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/validate.ts",
      "content": "import { resolveColumns } from \"./col\";\nimport type { TableSchemaDefinition } from \"./types\";\n\n/**\n * Validates a table schema definition and throws a descriptive error on the\n * first violation found.\n *\n * Called automatically by `createTableSchema()` — no need to call manually.\n *\n * Catches errors that the TypeScript type system cannot prevent:\n * - Missing label (`.label()` was never called)\n * - Slider `min` greater than `max`\n *\n * These checks run for both the TypeScript-authored path (`createTableSchema({...})`)\n * and the AI-generated path (`createTableSchema.fromJSON(json)`).\n */\nexport function validateSchema(definition: TableSchemaDefinition): void {\n  for (const c of resolveColumns(definition)) {\n    const { key } = c;\n\n    // 1. Label is required — col.* factories default to label: \"\"\n    if (!c.label) {\n      throw new Error(\n        `[createTableSchema] Column \"${key}\" is missing a label.\\n` +\n          `  Fix: .label(\"${key[0]!.toUpperCase()}${key.slice(1)}\")`,\n      );\n    }\n\n    // 2. Number checkbox filter requires explicit options — the number factory\n    //    has no value list to auto-derive from, so an empty options list would\n    //    render a filter with no checkboxes (a silent no-op in the UI).\n    if (\n      c.kind === \"number\" &&\n      c.filter?.type === \"checkbox\" &&\n      (!c.filter.options || c.filter.options.length === 0)\n    ) {\n      throw new Error(\n        `[createTableSchema] Column \"${key}\": checkbox filter on a number column requires explicit options.\\n` +\n          `  Fix: .filterable(\"checkbox\", { options: [{ label: \"200\", value: 200 }, ...] })`,\n      );\n    }\n\n    // 3. Slider bounds must be valid — type system requires { min, max } to be\n    //    passed but cannot enforce min < max\n    if (c.filter?.type === \"slider\") {\n      const { min, max } = c.filter;\n      if (min === undefined || max === undefined) {\n        throw new Error(\n          `[createTableSchema] Column \"${key}\": slider filter is missing min/max bounds.\\n` +\n            `  Fix: .filterable(\"slider\", { min: 0, max: 100 })`,\n        );\n      }\n      if (min > max) {\n        throw new Error(\n          `[createTableSchema] Column \"${key}\": slider min (${min}) must be less than max (${max}).\\n` +\n            `  Fix: swap the values — .filterable(\"slider\", { min: ${max}, max: ${min} })`,\n        );\n      }\n    }\n\n    // 4. A sheet-only column cannot carry a filter. `.sheetOnly()` is the only\n    //    chain step that sets `enableHiding: false` on a hideable column, and\n    //    it clears the filter, so no builder can produce this combination —\n    //    only hand-written or AI-generated JSON can. It has no chain form,\n    //    which means `schemaToTypeScript` would have to drop one half of it.\n    if (c.enableHiding === false && c.hidden && c.filter !== null) {\n      throw new Error(\n        `[createTableSchema] Column \"${key}\": a sheet-only column cannot have a filter.\\n` +\n          `  Fix: drop the filter (.sheetOnly() implies it), or make the column filterable and hidden instead.`,\n      );\n    }\n  }\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/serialize.ts",
      "content": "import { createColBuilder, defaultDisplayForKind } from \"./col\";\nimport type {\n  ColKind,\n  ColumnDescriptor,\n  DisplayDescriptor,\n  FilterDescriptor,\n  Provenance,\n  SchemaJSON,\n  SchemaJSONVersion,\n  SheetDescriptor,\n  TableSchemaDefinition,\n} from \"./types\";\n\n/** The current `SchemaJSON` version. Bump alongside a `migrateSchemaJSON` step. */\nexport const SCHEMA_JSON_VERSION: SchemaJSONVersion = 1;\n\n// ── Serialization ───────────────────────────────────────────────────────────\n//\n// The descriptor IS the serializable state, so serialization is a rename of the\n// key and nothing else. There is no projection to keep in sync, which is why\n// `unit`, `presets`, and `resizable` can no longer be silently dropped.\n\nexport function serializeSchema(definition: TableSchemaDefinition): SchemaJSON {\n  return {\n    version: SCHEMA_JSON_VERSION,\n    columns: Object.entries(definition).map(([key, builder]) => ({\n      key,\n      ...builder._descriptor,\n    })),\n  };\n}\n\n// ── Deserialization ─────────────────────────────────────────────────────────\n//\n// Descriptors are stored verbatim, so reconstruction hands the descriptor\n// straight back to the builder. The old ~40-branch replay of the fluent chain\n// — including the `enableHiding === false && hidden ⇒ .sheetOnly()` inference\n// archaeology — is gone.\n//\n// Limitation, unchanged: renderers (display cell, filter component, sheet\n// component/condition) are closures and cannot be serialized. A deserialized\n// column falls back to its descriptor's display, which is why the descriptor\n// keeps a real display type even when a custom renderer was supplied.\n\nexport function deserializeSchema(json: SchemaJSON): TableSchemaDefinition {\n  const definition: TableSchemaDefinition = {};\n  for (const { key, ...descriptor } of json.columns) {\n    definition[key] = createColBuilder(descriptor as ColumnDescriptor, {});\n  }\n  return definition;\n}\n\n// ── Migration ───────────────────────────────────────────────────────────────\n\nconst COL_KINDS: readonly ColKind[] = [\n  \"string\",\n  \"number\",\n  \"boolean\",\n  \"timestamp\",\n  \"enum\",\n  \"array\",\n  \"record\",\n  \"select\",\n];\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction asKind(value: unknown, key: unknown): ColKind {\n  if (COL_KINDS.includes(value as ColKind)) return value as ColKind;\n  // Falling back is better than throwing — one bad column should not make a\n  // whole saved schema unloadable — but it is a real loss, so say so.\n  if (value !== undefined) {\n    console.warn(\n      `[migrateSchemaJSON] Column ${JSON.stringify(key)} has unknown kind ` +\n        `${JSON.stringify(value)}. Falling back to \"string\".`,\n    );\n  }\n  return \"string\";\n}\n\n/**\n * Normalize an array column's item descriptor.\n *\n * v1 nests a full `ColumnDescriptor`; v0 carried only `{ dataType, enumValues }`\n * under `arrayItemType` — and, for non-enum items, nothing at all, which is\n * exactly how `string[]` columns used to collapse into `string` columns.\n */\nfunction normalizeArrayItem(raw: unknown): ColumnDescriptor {\n  const { key: _key, ...item } = normalizeColumn(raw);\n  return item as ColumnDescriptor;\n}\n\nfunction isProvenance(value: unknown): value is Provenance {\n  if (!isRecord(value)) return false;\n  return (\n    value.source === \"manual\" ||\n    (value.source === \"preset\" &&\n      typeof value.preset === \"string\" &&\n      Array.isArray(value.args)) ||\n    (value.source === \"inferred\" && typeof value.rule === \"string\")\n  );\n}\n\n/**\n * Normalize one column from either version into a complete `ColumnDescriptor`.\n *\n * v1 payloads go through this too rather than being trusted: `fromJSON` accepts\n * user-typed text, so \"it says version 1\" is not evidence that its columns are\n * well-formed. Every required field is filled or defaulted here, which is what\n * makes the unchecked `as SchemaJSON` cast on untrusted input unnecessary.\n */\nfunction normalizeColumn(raw: unknown): ColumnDescriptor & { key: string } {\n  const c = isRecord(raw) ? raw : {};\n  // v1 writes `kind`; v0 wrote `dataType`.\n  const kind = asKind(c.kind ?? c.dataType, c.key);\n\n  // v0 wrote `{ type: \"text\" }` for custom displays and omitted `unit`,\n  // `presets`, and `resizable` entirely. Nothing can recover those — the\n  // migration fills the type-required defaults and stops there.\n  const display = isRecord(c.display)\n    ? (c.display as DisplayDescriptor)\n    : defaultDisplayForKind(kind);\n\n  // Carried through wholesale, like `display` and `sheet`. Whitelisting the\n  // optional fields here would reintroduce exactly the hand-maintained\n  // projection this split removed from `serializeSchema` — a new\n  // `FilterDescriptor` field would be dropped on migration and nothing would\n  // say so. Only the three required fields are coerced, because they must exist.\n  const filter: FilterDescriptor | null = isRecord(c.filter)\n    ? {\n        ...(c.filter as Partial<FilterDescriptor>),\n        type: (c.filter.type ?? \"input\") as FilterDescriptor[\"type\"],\n        defaultOpen: c.filter.defaultOpen === true,\n        commandDisabled: c.filter.commandDisabled === true,\n      }\n    : null;\n\n  const sheet: SheetDescriptor | null = isRecord(c.sheet)\n    ? (c.sheet as SheetDescriptor)\n    : null;\n\n  const common = {\n    label: typeof c.label === \"string\" ? c.label : \"\",\n    ...(typeof c.description === \"string\"\n      ? { description: c.description }\n      : {}),\n    optional: c.optional === true,\n    display,\n    ...(typeof c.size === \"number\" ? { size: c.size } : {}),\n    ...(typeof c.minSize === \"number\" ? { minSize: c.minSize } : {}),\n    hidden: c.hidden === true,\n    enableHiding: c.enableHiding !== false,\n    hideHeader: c.hideHeader === true,\n    // v0 had no `resizable` at all, so it defaults to false there; v1 writes it.\n    resizable: c.resizable === true,\n    sortable: c.sortable === true,\n    filter,\n    sheet,\n    provenance: (isProvenance(c.provenance)\n      ? c.provenance\n      : { source: \"manual\" }) as Provenance,\n  };\n\n  const key = typeof c.key === \"string\" ? c.key : \"\";\n\n  if (kind === \"enum\") {\n    return {\n      key,\n      ...common,\n      kind: \"enum\",\n      enumValues: Array.isArray(c.enumValues) ? (c.enumValues as string[]) : [],\n    };\n  }\n  if (kind === \"array\") {\n    return {\n      key,\n      ...common,\n      kind: \"array\",\n      // v1 nests a full descriptor under `arrayItem`; v0 carried only\n      // `{ dataType, enumValues }` under `arrayItemType`.\n      arrayItem: normalizeArrayItem(c.arrayItem ?? c.arrayItemType),\n    };\n  }\n  return { key, ...common, kind } as ColumnDescriptor & { key: string };\n}\n\n/**\n * Bring untrusted schema JSON up to the current version.\n *\n * `SchemaJSON` is persisted (the builder cache) and pasted by hand into the\n * schema editor, so every entry point that accepts it must run through here\n * first. This is the one hand-written conversion the descriptor split\n * deliberately keeps.\n *\n * v0 (no `version` field) → v1: `dataType` → `kind`, `arrayItemType` →\n * recursive `arrayItem`, and the newly-required `resizable`, `enableHiding`,\n * `hideHeader`, and `provenance` get their defaults.\n *\n * @throws if `json` is not an object with a `columns` array.\n */\nexport function migrateSchemaJSON(json: unknown): SchemaJSON {\n  if (!isRecord(json) || !Array.isArray(json.columns)) {\n    throw new Error(\n      \"[migrateSchemaJSON] Expected an object with a `columns` array.\",\n    );\n  }\n\n  if (json.version !== undefined && json.version !== SCHEMA_JSON_VERSION) {\n    throw new Error(\n      `[migrateSchemaJSON] Unknown schema version ${JSON.stringify(json.version)}. ` +\n        `This build understands version ${SCHEMA_JSON_VERSION}.`,\n    );\n  }\n\n  return {\n    version: SCHEMA_JSON_VERSION,\n    columns: json.columns.map(normalizeColumn),\n  };\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/manifest.ts",
      "content": "import type { ActionDescriptor } from \"@/lib/actions/types\";\nimport { sanitizeActionDescriptors } from \"@/lib/actions/validate\";\nimport type { ActionValidationOptions } from \"@/lib/actions/validate\";\nimport { migrateSchemaJSON } from \"./serialize\";\nimport type { SchemaJSON } from \"./types\";\n\n/**\n * The table manifest — what an endpoint says about itself.\n *\n * This is the missing half of the headless story. The schema was already\n * serializable, but every consumer built it locally from TypeScript, so\n * \"point the table at a URL\" was never actually possible. A manifest is that\n * URL's answer: the schema, which column identifies a row, what the server can\n * and cannot compute, and what may be done to a row.\n *\n * It is fetched over the network from a server the app may not own, so\n * `parseTableManifest` treats every field as untrusted and bounded.\n */\n\n/** Bump alongside a migration step in `parseTableManifest`. */\nexport const TABLE_MANIFEST_VERSION = 1;\n\n/**\n * What the endpoint can compute.\n *\n * The client used to assume all of it. An endpoint that cannot group facets or\n * count matching rows had no way to say so, and the UI had no way to degrade —\n * it simply rendered empty filters and a blank count. Each flag here is a thing\n * the table will stop asking for, or compute on the client instead.\n */\nexport type TableCapabilities = {\n  /** `meta.facets` is populated. When false, facet counts come from loaded rows. */\n  facets: boolean;\n  /**\n   * The columns the server can facet, when it can only do some. Omitted means\n   * \"every filterable column\".\n   */\n  facetedColumns?: readonly string[];\n  /** `meta.totalRowCount` is meaningful. */\n  totalRowCount: boolean;\n  /** `meta.filterRowCount` is meaningful. */\n  filterRowCount: boolean;\n  /** `meta.chartData` is populated. When false, the chart slot is not rendered. */\n  chart: boolean;\n  /** `prevCursor` is honoured — the precondition for live mode. */\n  backwardPagination: boolean;\n  /** `meta.actions` and the per-row `_actions` stamp are populated. */\n  actions: boolean;\n};\n\n/**\n * The conservative reading of an endpoint that said nothing.\n *\n * Everything optional is off: a table that renders no chart against a server\n * that has one is a missing feature, while a table that renders a chart against\n * a server that has none is a broken screen.\n */\nexport const DEFAULT_CAPABILITIES: TableCapabilities = {\n  facets: false,\n  totalRowCount: false,\n  filterRowCount: false,\n  chart: false,\n  backwardPagination: false,\n  actions: false,\n};\n\n/**\n * How the timeline chart is drawn.\n *\n * `meta.chartData` is `{ timestamp, [series]: number }[]`, and until now which\n * column it bucketed, which series it carried, and what colour each series took\n * were all decided in app code. A table pointed at an endpoint has none of that,\n * so the endpoint states it.\n */\nexport type TableChartConfig = {\n  /** The timestamp column the buckets are over. Must be a column in the schema. */\n  columnKey: string;\n  /** The numeric series in each point, in stacking order. */\n  series: { key: string; label?: string; color?: string }[];\n  /** Bucket width in milliseconds, when the endpoint has a fixed one. */\n  intervalMs?: number;\n};\n\nexport type TableManifestDefaults = {\n  sort?: { id: string; desc: boolean };\n  size?: number;\n  columnVisibility?: Record<string, boolean>;\n};\n\nexport type TableManifest = {\n  version: number;\n  schema: SchemaJSON;\n  /**\n   * The column that identifies a row on the wire.\n   *\n   * Actions key rows by it and the sheet resolves the open row through it. It\n   * used to be a closure written by hand at each call site\n   * (`getRowId={(row) => row.uuid}`), which is exactly the kind of thing a\n   * pointed-at table cannot supply.\n   */\n  primaryKey: string;\n  /**\n   * A human name for a row, as a template over column keys:\n   * `\"{method} {pathname}\"`. Used for accessible labels, never for identity.\n   */\n  rowLabel?: string;\n  capabilities: TableCapabilities;\n  /** Present when `capabilities.chart` is true. */\n  chart?: TableChartConfig;\n  actions?: ActionDescriptor[];\n  defaults?: TableManifestDefaults;\n};\n\n/**\n * Build a manifest from a table schema.\n *\n * The server side of the contract. `createTableSchema(...)` already produces\n * the `SchemaJSON`; this names the row identity and states what the endpoint\n * can actually compute, which are the two things the schema alone never said.\n */\nexport function createTableManifest(config: {\n  schema: { toJSON(): SchemaJSON } | SchemaJSON;\n  primaryKey: string;\n  rowLabel?: string;\n  capabilities?: Partial<TableCapabilities>;\n  chart?: TableChartConfig;\n  actions?: ActionDescriptor[];\n  defaults?: TableManifestDefaults;\n}): TableManifest {\n  const schema =\n    \"toJSON\" in config.schema ? config.schema.toJSON() : config.schema;\n\n  if (!schema.columns.some((column) => column.key === config.primaryKey)) {\n    // Caught here rather than at the client, which would only be able to blank\n    // the table and say the endpoint was wrong.\n    throw new TableManifestError(\n      `primaryKey ${JSON.stringify(config.primaryKey)} is not a column in the schema`,\n    );\n  }\n\n  return {\n    version: TABLE_MANIFEST_VERSION,\n    schema,\n    primaryKey: config.primaryKey,\n    ...(config.rowLabel ? { rowLabel: config.rowLabel } : {}),\n    capabilities: { ...DEFAULT_CAPABILITIES, ...config.capabilities },\n    ...(config.chart ? { chart: config.chart } : {}),\n    ...(config.actions?.length ? { actions: config.actions } : {}),\n    ...(config.defaults ? { defaults: config.defaults } : {}),\n  };\n}\n\n// ── Bounds ──────────────────────────────────────────────────────────────────\n\n/**\n * Caps on an incoming manifest.\n *\n * A remote schema drives column generation, filter rendering and URL state, so\n * an unbounded one is a way to hang the tab. These are deliberately far above\n * any real table.\n */\nexport const MANIFEST_LIMITS = {\n  maxColumns: 200,\n  maxKeyLength: 200,\n  maxLabelLength: 500,\n  maxActions: 50,\n} as const;\n\n/**\n * Path segments that reach an object's prototype rather than its own data.\n *\n * Column keys are server-authored. `definition[key] = builder` with a\n * `\"__proto__\"` key does not add an own property — it reassigns the object's\n * prototype, so the column silently vanishes from every `Object.keys` consumer\n * with none of the warnings every other rejected input here produces. As a\n * `primaryKey` it is worse: `getRowId` returns `\"[object Object]\"` for every\n * row, collapsing selection and bulk actions onto a single id.\n */\nconst RESERVED_SEGMENTS: ReadonlySet<string> = new Set([\n  \"__proto__\",\n  \"constructor\",\n  \"prototype\",\n]);\n\n/** True when every segment of a dotted key addresses own data. */\nexport function isSafeColumnKey(key: string): boolean {\n  return key.split(\".\").every((segment) => !RESERVED_SEGMENTS.has(segment));\n}\n\nexport class TableManifestError extends Error {\n  constructor(message: string, options?: ErrorOptions) {\n    super(message, options);\n    this.name = \"TableManifestError\";\n  }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction parseCapabilities(value: unknown): TableCapabilities {\n  if (!isRecord(value)) return { ...DEFAULT_CAPABILITIES };\n  const facetedColumns = Array.isArray(value.facetedColumns)\n    ? value.facetedColumns.filter(\n        (key): key is string =>\n          typeof key === \"string\" && key.length <= MANIFEST_LIMITS.maxKeyLength,\n      )\n    : undefined;\n  return {\n    facets: value.facets === true,\n    ...(facetedColumns ? { facetedColumns } : {}),\n    totalRowCount: value.totalRowCount === true,\n    filterRowCount: value.filterRowCount === true,\n    chart: value.chart === true,\n    backwardPagination: value.backwardPagination === true,\n    actions: value.actions === true,\n  };\n}\n\nfunction parseDefaults(\n  value: unknown,\n  keys: ReadonlySet<string>,\n): TableManifestDefaults | undefined {\n  if (!isRecord(value)) return undefined;\n  const defaults: TableManifestDefaults = {};\n\n  if (isRecord(value.sort) && typeof value.sort.id === \"string\") {\n    // A default sort on a column that does not exist would be written straight\n    // into the URL and then fail to resolve against the table.\n    if (keys.has(value.sort.id)) {\n      defaults.sort = { id: value.sort.id, desc: value.sort.desc === true };\n    }\n  }\n\n  if (\n    typeof value.size === \"number\" &&\n    Number.isInteger(value.size) &&\n    value.size > 0\n  ) {\n    defaults.size = value.size;\n  }\n\n  if (isRecord(value.columnVisibility)) {\n    const visibility: Record<string, boolean> = {};\n    for (const [key, visible] of Object.entries(value.columnVisibility)) {\n      if (keys.has(key) && typeof visible === \"boolean\") {\n        visibility[key] = visible;\n      }\n    }\n    if (Object.keys(visibility).length > 0) {\n      defaults.columnVisibility = visibility;\n    }\n  }\n\n  return Object.keys(defaults).length > 0 ? defaults : undefined;\n}\n\nfunction parseChart(\n  value: unknown,\n  keys: ReadonlySet<string>,\n  warn: (message: string) => void,\n): TableChartConfig | undefined {\n  if (!isRecord(value)) return undefined;\n\n  if (typeof value.columnKey !== \"string\" || !keys.has(value.columnKey)) {\n    // Without a column to bucket over there is nothing to draw, and a chart\n    // pinned to a column that no longer exists is worse than no chart.\n    warn(\"Dropped the chart config: `columnKey` names no column in the schema\");\n    return undefined;\n  }\n\n  const series = (Array.isArray(value.series) ? value.series : [])\n    .filter(\n      (item): item is Record<string, unknown> =>\n        isRecord(item) &&\n        typeof item.key === \"string\" &&\n        item.key.length > 0 &&\n        item.key.length <= MANIFEST_LIMITS.maxKeyLength,\n    )\n    .slice(0, MANIFEST_LIMITS.maxColumns)\n    .map((item) => ({\n      key: item.key as string,\n      ...(typeof item.label === \"string\" ? { label: item.label } : {}),\n      ...(typeof item.color === \"string\" ? { color: item.color } : {}),\n    }));\n\n  if (series.length === 0) {\n    warn(\"Dropped the chart config: it declares no series\");\n    return undefined;\n  }\n\n  return {\n    columnKey: value.columnKey,\n    series,\n    ...(typeof value.intervalMs === \"number\" && value.intervalMs > 0\n      ? { intervalMs: value.intervalMs }\n      : {}),\n  };\n}\n\nexport type ParseManifestOptions = {\n  /** Passed to the action validator — see `sanitizeActionDescriptors`. */\n  actionValidation?: ActionValidationOptions;\n  /** Called with anything dropped, so a host can log it. */\n  onWarning?: (message: string) => void;\n};\n\n/**\n * Validate and normalize an untrusted manifest.\n *\n * Throws only for the failures that leave nothing renderable — a missing\n * schema, no columns, no usable primary key. Everything else is repaired or\n * dropped with a warning, because one bad column should not blank the table.\n */\nexport function parseTableManifest(\n  value: unknown,\n  options?: ParseManifestOptions,\n): TableManifest {\n  const warn = options?.onWarning ?? (() => {});\n\n  if (!isRecord(value)) {\n    throw new TableManifestError(\"Manifest is not an object\");\n  }\n\n  // `migrateSchemaJSON` already normalizes every descriptor field and brings\n  // older versions forward, so this only has to add the bounds it does not. Its\n  // own rejection is re-thrown as a `TableManifestError` so that a caller has\n  // one error type to catch for \"this endpoint did not answer with a manifest\".\n  let schema: SchemaJSON;\n  try {\n    schema = migrateSchemaJSON(value.schema);\n  } catch (cause) {\n    throw new TableManifestError(\n      `Manifest schema is not usable: ${cause instanceof Error ? cause.message : String(cause)}`,\n      { cause },\n    );\n  }\n\n  if (schema.columns.length === 0) {\n    throw new TableManifestError(\"Manifest schema has no columns\");\n  }\n  if (schema.columns.length > MANIFEST_LIMITS.maxColumns) {\n    throw new TableManifestError(\n      `Manifest schema has ${schema.columns.length} columns, over the limit of ${MANIFEST_LIMITS.maxColumns}`,\n    );\n  }\n\n  const seenKeys = new Set<string>();\n  const columns = schema.columns.filter((column) => {\n    if (column.key.length === 0) {\n      warn(\"Dropped a column with an empty key\");\n      return false;\n    }\n    if (column.key.length > MANIFEST_LIMITS.maxKeyLength) {\n      warn(`Dropped column ${JSON.stringify(column.key)}: key too long`);\n      return false;\n    }\n    if (!isSafeColumnKey(column.key)) {\n      warn(`Dropped column ${JSON.stringify(column.key)}: reserved key`);\n      return false;\n    }\n    // An empty label is not merely ugly: `createTableSchema.fromJSON` rejects\n    // it, which throws inside the component and blanks the whole table.\n    if (column.label.length === 0) {\n      warn(`Dropped column ${JSON.stringify(column.key)}: empty label`);\n      return false;\n    }\n    if (column.label.length > MANIFEST_LIMITS.maxLabelLength) {\n      warn(`Dropped column ${JSON.stringify(column.key)}: label too long`);\n      return false;\n    }\n    // The definition is keyed by column key, so a duplicate would silently\n    // overwrite the first with no diagnostic.\n    if (seenKeys.has(column.key)) {\n      warn(`Dropped column ${JSON.stringify(column.key)}: duplicate key`);\n      return false;\n    }\n    seenKeys.add(column.key);\n    return true;\n  });\n\n  if (columns.length === 0) {\n    throw new TableManifestError(\"Manifest schema has no usable columns\");\n  }\n\n  const keys = new Set(columns.map((column) => column.key));\n\n  const primaryKey =\n    typeof value.primaryKey === \"string\" && keys.has(value.primaryKey)\n      ? value.primaryKey\n      : null;\n  if (!primaryKey) {\n    throw new TableManifestError(\n      \"Manifest primaryKey is missing or names a column that does not exist\",\n    );\n  }\n\n  const capabilities = parseCapabilities(value.capabilities);\n\n  const { actions, rejected } = sanitizeActionDescriptors(\n    Array.isArray(value.actions)\n      ? value.actions.slice(0, MANIFEST_LIMITS.maxActions)\n      : undefined,\n    options?.actionValidation,\n  );\n  for (const rejection of rejected) {\n    warn(`Dropped action ${rejection.id}: ${rejection.reason}`);\n  }\n\n  return {\n    version: TABLE_MANIFEST_VERSION,\n    schema: { version: schema.version, columns },\n    primaryKey,\n    ...(typeof value.rowLabel === \"string\" &&\n    value.rowLabel.length <= MANIFEST_LIMITS.maxLabelLength\n      ? { rowLabel: value.rowLabel }\n      : {}),\n    capabilities,\n    ...(() => {\n      const chart = parseChart(value.chart, keys, warn);\n      return chart ? { chart } : {};\n    })(),\n    ...(actions.length > 0 ? { actions } : {}),\n    ...(() => {\n      const defaults = parseDefaults(value.defaults, keys);\n      return defaults ? { defaults } : {};\n    })(),\n  };\n}\n\n// ── ETag ────────────────────────────────────────────────────────────────────\n\n/**\n * FNV-1a over the serialized manifest.\n *\n * A schema changes on deploy, not per request, so the point of the ETag is to\n * turn the client's revalidation into a 304 rather than a re-download. A\n * non-cryptographic hash is the right tool: this guards cache freshness, not\n * integrity.\n */\nexport function manifestETag(manifest: TableManifest): string {\n  const json = JSON.stringify(manifest);\n  let hash = 0x811c9dc5;\n  for (let i = 0; i < json.length; i++) {\n    hash ^= json.charCodeAt(i);\n    hash = Math.imul(hash, 0x01000193) >>> 0;\n  }\n  return `\"${hash.toString(16)}-${json.length.toString(16)}\"`;\n}\n\n// ── Server ──────────────────────────────────────────────────────────────────\n\nexport type ManifestHandlerOptions = {\n  /**\n   * `Cache-Control` for the response. The default revalidates every time and\n   * relies on the ETag for the cheap path, which is the safe default for\n   * something that changes on deploy without a version in its URL.\n   */\n  cacheControl?: string;\n  /** Extra response headers — CORS, most likely. */\n  headers?: Record<string, string>;\n};\n\n/**\n * A `Request → Response` handler serving the manifest, with ETag revalidation.\n *\n * Pass a function rather than a value when the manifest depends on the request\n * — per-tenant columns, or actions that depend on the caller's permissions.\n */\nexport function createTableManifestHandler(\n  manifest: TableManifest | ((request: Request) => TableManifest),\n  options?: ManifestHandlerOptions,\n) {\n  return async function handler(request: Request): Promise<Response> {\n    const resolved =\n      typeof manifest === \"function\" ? manifest(request) : manifest;\n    const etag = manifestETag(resolved);\n\n    const headers: Record<string, string> = {\n      \"content-type\": \"application/json\",\n      etag,\n      \"cache-control\": options?.cacheControl ?? \"no-cache\",\n      ...options?.headers,\n    };\n\n    // `If-None-Match` may carry a list, and a proxy may have weakened the tag.\n    const ifNoneMatch = request.headers.get(\"if-none-match\");\n    if (ifNoneMatch) {\n      const candidates = ifNoneMatch\n        .split(\",\")\n        .map((value) => value.trim().replace(/^W\\//, \"\"));\n      if (candidates.includes(etag) || candidates.includes(\"*\")) {\n        return new Response(null, { status: 304, headers });\n      }\n    }\n\n    return new Response(JSON.stringify(resolved), { status: 200, headers });\n  };\n}\n\n// ── Client ──────────────────────────────────────────────────────────────────\n\nexport type FetchManifestOptions = ParseManifestOptions & {\n  headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n  credentials?: RequestCredentials;\n  fetch?: typeof fetch;\n  signal?: AbortSignal;\n};\n\n/**\n * Fetch and validate a manifest.\n *\n * Deliberately not wired to React Query here — the manifest is needed *before*\n * the query layer exists, because the URL-state adapter is built from it.\n */\nexport async function fetchTableManifest(\n  url: string,\n  options?: FetchManifestOptions,\n): Promise<TableManifest> {\n  const doFetch = options?.fetch ?? fetch;\n  const headers =\n    typeof options?.headers === \"function\"\n      ? await options.headers()\n      : options?.headers;\n\n  const response = await doFetch(url, {\n    headers,\n    credentials: options?.credentials,\n    signal: options?.signal,\n  });\n\n  if (!response.ok) {\n    throw new TableManifestError(\n      `Could not load the table manifest from ${url}: ${response.status} ${response.statusText}`.trim(),\n    );\n  }\n\n  let payload: unknown;\n  try {\n    payload = await response.json();\n  } catch (cause) {\n    throw new TableManifestError(\n      `The table manifest at ${url} is not valid JSON`,\n      { cause },\n    );\n  }\n\n  return parseTableManifest(payload, options);\n}\n\n// ── Row accessors ───────────────────────────────────────────────────────────\n\n/** Reads a possibly-dotted key path off a row. */\nfunction readPath(row: unknown, path: string): unknown {\n  if (row === null || typeof row !== \"object\") return undefined;\n  if (!isSafeColumnKey(path)) return undefined;\n  const record = row as Record<string, unknown>;\n  // Rows may carry the dotted key literally — `createDrizzleHandler` projects\n  // `\"timing.dns\"` as a flat property — so the exact key wins before walking.\n  if (Object.hasOwn(record, path)) return record[path];\n  if (!path.includes(\".\")) return undefined;\n  let current: unknown = row;\n  for (const segment of path.split(\".\")) {\n    if (current === null || typeof current !== \"object\") return undefined;\n    // Own data only. `isSafeColumnKey` is a name list and cannot catch\n    // `\"meta.toString\"`, which otherwise resolves an inherited method — the\n    // same value on every row, collapsing every row id onto one.\n    if (!Object.hasOwn(current, segment)) return undefined;\n    current = (current as Record<string, unknown>)[segment];\n  }\n  return current;\n}\n\nexport type RowAccessors<TRow> = {\n  /** The row's wire identity, from `manifest.primaryKey`. */\n  getRowId: (row: TRow) => string;\n  /**\n   * A human name for the row, from `manifest.rowLabel`. `undefined` when the\n   * manifest declared none — callers then fall back to static copy rather than\n   * reading an internal id out loud.\n   */\n  getRowLabel?: (row: TRow) => string;\n};\n\n/**\n * Derive `getRowId` / `getRowLabel` from a manifest.\n *\n * These were hand-written closures at every call site\n * (`getRowId={(row) => row.uuid}`), which a table pointed at an endpoint cannot\n * supply. `rowLabel` is a template over column keys — `\"{method} {pathname}\"` —\n * with unknown keys left as written, so a typo is visible rather than silent.\n */\nexport function createRowAccessors<TRow = unknown>(\n  manifest: Pick<TableManifest, \"primaryKey\" | \"rowLabel\">,\n): RowAccessors<TRow> {\n  const getRowId = (row: TRow): string => {\n    const value = readPath(row, manifest.primaryKey);\n    // Coerced rather than asserted: the id addresses a row in the DOM and on\n    // the wire, and a numeric primary key is perfectly ordinary.\n    return value === null || value === undefined ? \"\" : String(value);\n  };\n\n  const template = manifest.rowLabel;\n  if (!template) return { getRowId };\n\n  return {\n    getRowId,\n    getRowLabel: (row: TRow): string =>\n      template.replace(/\\{([^{}]+)\\}/g, (match, key: string) => {\n        const value = readPath(row, key.trim());\n        return value === null || value === undefined ? match : String(value);\n      }),\n  };\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/renderers.ts",
      "content": "import { createColBuilder } from \"./col\";\nimport { isSafeColumnKey } from \"./manifest\";\nimport type {\n  ColRenderers,\n  ColumnDescriptor,\n  TableSchemaDefinition,\n} from \"./types\";\n\n/**\n * Attaching renderers to a schema that came over the wire.\n *\n * `serialize.ts` is explicit that the four renderers — the display cell, the\n * filter component, the sheet component and its condition — are closures and\n * cannot be serialized. A deserialized column therefore falls back to whatever\n * named display its descriptor carries, which is why the descriptor keeps a\n * real display type even when a custom renderer was originally supplied.\n *\n * The named displays (`badge`, `bar`, `status-code`, `level-indicator`, …)\n * already cover most of what a table needs, and they travel as data. This\n * module is the escape hatch for the rest: an app pointed at a remote schema\n * supplies the handful of columns it wants to draw itself, by key, and keeps\n * every other column's declarative rendering.\n *\n * @example\n * ```ts\n * const { definition } = createTableSchema.fromJSON(manifest.schema);\n * const withRenderers = applyRenderers(definition, {\n *   pathname: { cell: (value) => <PathnameCell value={String(value)} /> },\n *   timing: { sheetComponent: (row) => <TimingPhases row={row} /> },\n * });\n * ```\n */\n\n/** Renderer closures to attach, keyed by column key. */\nexport type RendererOverrides = Record<string, ColRenderers>;\n\nexport type ApplyRenderersOptions = {\n  /**\n   * Called for an override whose key is not in the schema.\n   *\n   * Worth listening to: a remote schema can drop a column between deploys, and\n   * an override left behind for it is silently dead. The default warns to the\n   * console rather than throwing, because one stale override should not blank\n   * a table.\n   */\n  onUnknownKey?: (key: string) => void;\n  /**\n   * Called for a schema column whose key cannot be used.\n   *\n   * A separate channel from {@link onUnknownKey} because the two say opposite\n   * things: one means \"your override names nothing\", the other means \"the\n   * schema's own column is unusable\". Reporting a dropped column through the\n   * override warning sends the reader looking for a stale override that does\n   * not exist.\n   */\n  onUnusableColumn?: (key: string) => void;\n};\n\nfunction defaultOnUnknownKey(key: string): void {\n  console.warn(\n    `[applyRenderers] no column ${JSON.stringify(key)} in the schema — ` +\n      `the override will not be used. Did the endpoint's schema change?`,\n  );\n}\n\nfunction defaultOnUnusableColumn(key: string): void {\n  console.warn(\n    `[applyRenderers] dropped schema column ${JSON.stringify(key)}: the key ` +\n      `reaches an object prototype, so it cannot address a column.`,\n  );\n}\n\n/**\n * Return a copy of `definition` with the given renderers attached.\n *\n * Non-mutating: builders are immutable, so each overridden column is rebuilt\n * over the same descriptor with the merged renderers. Columns with no override\n * are passed through by reference.\n *\n * Only the renderer keys present in an override are set; passing\n * `{ cell }` leaves an existing `sheetComponent` alone.\n */\nexport function applyRenderers(\n  definition: TableSchemaDefinition,\n  overrides: RendererOverrides,\n  options?: ApplyRenderersOptions,\n): TableSchemaDefinition {\n  const onUnknownKey = options?.onUnknownKey ?? defaultOnUnknownKey;\n  const onUnusableColumn = options?.onUnusableColumn ?? defaultOnUnusableColumn;\n  const keys = Object.keys(overrides);\n  if (keys.length === 0) return definition;\n\n  for (const key of keys) {\n    // `in` walks the prototype chain, so `toString` would read as a column.\n    if (!Object.hasOwn(definition, key)) onUnknownKey(key);\n  }\n\n  const result: TableSchemaDefinition = {};\n  for (const [key, builder] of Object.entries(definition)) {\n    // Skipped before any assignment, not merely excluded from overrides:\n    // `result[\"__proto__\"] = builder` invokes the setter and reassigns the\n    // result's prototype rather than adding a column, so the column vanishes\n    // from every `Object.keys` consumer with no diagnostic. Guarding only the\n    // override lookup still ran that assignment on the pass-through path.\n    if (!isSafeColumnKey(key)) {\n      onUnusableColumn(key);\n      continue;\n    }\n    const override = Object.hasOwn(overrides, key) ? overrides[key] : undefined;\n    if (!override) {\n      result[key] = builder;\n      continue;\n    }\n    result[key] = createColBuilder(\n      builder._descriptor as ColumnDescriptor,\n      // The descriptor is untouched: an override changes how a column draws,\n      // never what it is. `toJSON()` on the result still round-trips.\n      { ...builder._renderers, ...override },\n    );\n  }\n  return result;\n}\n\n/**\n * The display types a schema can name without shipping code.\n *\n * Exported so a tool — a schema builder UI, an agent writing a schema, a\n * validator — can enumerate what is available declaratively before reaching\n * for {@link applyRenderers}.\n */\nexport const NAMED_DISPLAY_TYPES = [\n  \"text\",\n  \"code\",\n  \"boolean\",\n  \"star\",\n  \"badge\",\n  \"timestamp\",\n  \"number\",\n  \"bar\",\n  \"heatmap\",\n  \"gauge\",\n  \"status-code\",\n  \"level-indicator\",\n] as const;\n\nexport type NamedDisplayType = (typeof NAMED_DISPLAY_TYPES)[number];\n\n/** Is this display type one the renderer can draw from the descriptor alone? */\nexport function isNamedDisplayType(type: string): type is NamedDisplayType {\n  return (NAMED_DISPLAY_TYPES as readonly string[]).includes(type);\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/snapshot.ts",
      "content": "import { parseTableManifest, type TableManifest } from \"./manifest\";\n\n/**\n * Freezing a manifest into the app at build time.\n *\n * Fetching the manifest at runtime is one round trip before the table can draw\n * anything. A snapshot removes it: check the endpoint's answer into the repo,\n * pass it as `initialManifest`, and the first paint has a schema while the\n * query revalidates behind it.\n *\n * The trade is explicit rather than hidden. A snapshot is a copy, so it goes\n * stale when the endpoint changes — which is exactly why `useTableManifest`\n * still revalidates instead of trusting it forever, and why the generated file\n * records where and when it came from.\n */\n\nexport type SnapshotOptions = {\n  /** The URL it came from, recorded in the header comment. */\n  source?: string;\n  /** Name of the exported const. Defaults to `manifest`. */\n  exportName?: string;\n  /**\n   * Import specifier for the `TableManifest` type. Defaults to the registry\n   * path; set it to wherever the schema block landed in the consuming app\n   * (`@/lib/table-schema`, most likely).\n   */\n  typeImport?: string;\n  /**\n   * Timestamp for the header comment. Injectable so the output is\n   * deterministic in tests, and so a build can stamp its own commit time.\n   */\n  generatedAt?: Date | string;\n};\n\nconst DEFAULT_TYPE_IMPORT = \"@/lib/table-schema\";\n\n/** A JS identifier, so the generated module actually parses. */\nfunction assertIdentifier(name: string): void {\n  if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) {\n    throw new Error(\n      `exportName ${JSON.stringify(name)} is not a valid identifier`,\n    );\n  }\n}\n\n/**\n * Render a manifest as a TypeScript module.\n *\n * The manifest is re-parsed on the way in: a snapshot is generated from a live\n * endpoint's response, so it goes through the same validation as one consumed\n * at runtime. Freezing an invalid manifest into the repo would only move the\n * failure to a place where nobody is looking for it.\n */\nexport function manifestToModule(\n  manifest: unknown,\n  options?: SnapshotOptions,\n): string {\n  const exportName = options?.exportName ?? \"manifest\";\n  assertIdentifier(exportName);\n\n  const parsed: TableManifest = parseTableManifest(manifest);\n  const typeImport = options?.typeImport ?? DEFAULT_TYPE_IMPORT;\n  const generatedAt =\n    options?.generatedAt instanceof Date\n      ? options.generatedAt.toISOString()\n      : options?.generatedAt;\n\n  const provenance = [\n    \" * Generated by `manifestToModule`. Do not edit by hand.\",\n    options?.source ? ` * Source: ${options.source}` : null,\n    generatedAt ? ` * Generated: ${generatedAt}` : null,\n    \" *\",\n    \" * This is a snapshot. The table still revalidates it at runtime, so an\",\n    \" * endpoint whose schema has moved on will correct itself — but regenerate\",\n    \" * this file when that happens, or every first paint starts from stale\",\n    \" * columns.\",\n  ].filter((line): line is string => line !== null);\n\n  return [\n    \"/**\",\n    ...provenance,\n    \" */\",\n    \"\",\n    // Built with `JSON.stringify` rather than an interpolated string literal:\n    // it escapes the specifier properly, and it keeps this line from reading as\n    // a real import to any tool that scans this file's own imports.\n    `import type { TableManifest } from ${JSON.stringify(typeImport)};`,\n    \"\",\n    `export const ${exportName}: TableManifest = ${JSON.stringify(parsed, null, 2)};`,\n    \"\",\n  ].join(\"\\n\");\n}\n\n/**\n * Fetch a manifest and render it as a module, ready to write to disk.\n *\n * The whole `dtf pull` step, minus the file write — which is left to the\n * caller so this stays runtime-agnostic and needs no filesystem access.\n *\n * @example\n * ```ts\n * // scripts/pull-manifest.ts\n * import { writeFileSync } from \"node:fs\";\n * import { pullManifestModule } from \"@/lib/table-schema\";\n *\n * const url = \"https://api.example.com/logs/schema\";\n * writeFileSync(\"src/app/logs/manifest.ts\", await pullManifestModule(url));\n * ```\n */\nexport async function pullManifestModule(\n  url: string,\n  options?: SnapshotOptions & {\n    headers?: HeadersInit;\n    fetch?: typeof fetch;\n  },\n): Promise<string> {\n  const doFetch = options?.fetch ?? fetch;\n  const response = await doFetch(url, { headers: options?.headers });\n  if (!response.ok) {\n    throw new Error(\n      `Could not pull the manifest from ${url}: ${response.status} ${response.statusText}`.trim(),\n    );\n  }\n  return manifestToModule(await response.json(), {\n    source: url,\n    generatedAt: new Date(),\n    ...options,\n  });\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/infer.ts",
      "content": "import { compact, defaultDisplayForKind } from \"./col\";\nimport { SCHEMA_JSON_VERSION } from \"./serialize\";\nimport type {\n  ColKind,\n  ColumnDescriptor,\n  DisplayDescriptor,\n  FilterDescriptor,\n  SchemaJSON,\n} from \"./types\";\n\n/** A descriptor plus the key it will be registered under. */\ntype InferredColumn = ColumnDescriptor & { key: string };\n\n// Unix ms timestamps are 13-digit numbers (> Sep 2001, < Nov 2286)\nconst UNIX_MS_MIN = 1_000_000_000_000;\nconst UNIX_MS_MAX = 9_999_999_999_999;\n\nfunction isIso8601(value: string): boolean {\n  return (\n    /^\\d{4}-\\d{2}-\\d{2}(T[\\d:.Z+\\-]+)?$/.test(value) &&\n    !isNaN(Date.parse(value))\n  );\n}\n\nfunction isUnixMs(value: number): boolean {\n  return (\n    Number.isInteger(value) && value >= UNIX_MS_MIN && value <= UNIX_MS_MAX\n  );\n}\n\n/** Convert camelCase, snake_case, or kebab-case key to a human-readable label. */\nfunction keyToLabel(key: string): string {\n  let label = key.replace(/[-_]/g, \" \");\n  label = label.replace(/([a-z])([A-Z])/g, \"$1 $2\");\n  return label.replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/**\n * The fields every inferred column shares. `display` comes from the one\n * `defaultDisplayForKind` in `col.ts` — this module used to carry its own copy.\n */\nfunction inferredCommon(\n  label: string,\n  kind: ColKind,\n  filter: FilterDescriptor | null,\n  rule: string,\n) {\n  return {\n    label,\n    optional: false,\n    display: defaultDisplayForKind(kind) as DisplayDescriptor,\n    hidden: false,\n    enableHiding: true,\n    hideHeader: false,\n    resizable: false,\n    sortable: false,\n    filter,\n    sheet: {} as { label?: string },\n    provenance: { source: \"inferred\" as const, rule },\n  };\n}\n\nfunction makeDescriptor(\n  key: string,\n  label: string,\n  kind: Exclude<ColKind, \"array\" | \"enum\">,\n  filter: FilterDescriptor | null,\n  rule: string,\n): InferredColumn {\n  return { key, ...inferredCommon(label, kind, filter, rule), kind };\n}\n\n/** Split a key into lowercase words, handling camelCase, snake_case, and kebab-case. */\nfunction keyToWords(key: string): string[] {\n  return key\n    .replace(/([a-z])([A-Z])/g, \"$1_$2\")\n    .split(/[-_]/)\n    .map((w) => w.toLowerCase())\n    .filter(Boolean);\n}\n\nconst ID_WORDS = new Set([\"id\", \"uuid\", \"hash\", \"token\", \"key\"]);\nconst CODE_WORDS = new Set([\n  \"path\",\n  \"url\",\n  \"uri\",\n  \"endpoint\",\n  \"route\",\n  \"host\",\n  \"link\",\n  \"href\",\n  \"website\",\n]);\nconst LATENCY_WORDS = new Set([\n  \"latency\",\n  \"duration\",\n  \"elapsed\",\n  \"delay\",\n  \"wait\",\n  \"ttfb\",\n  \"rtt\",\n  \"ping\",\n]);\nconst SIZE_WORDS = new Set([\"size\", \"bytes\", \"length\"]);\nconst LEVEL_WORDS = new Set([\"level\", \"severity\"]);\nconst TRACE_ID_WORDS = new Set([\"trace\", \"span\", \"request\"]);\nconst FAVORITE_WORDS = new Set([\"favorite\", \"starred\", \"bookmarked\", \"pinned\"]);\nconst EMAIL_WORDS = new Set([\"email\", \"mail\"]);\nconst STATUS_WORDS = new Set([\"status\", \"state\"]);\nconst HEALTH_WORDS = new Set([\n  \"health\",\n  \"score\",\n  \"rating\",\n  \"accuracy\",\n  \"uptime\",\n]);\nconst PROGRESS_WORDS = new Set([\"progress\", \"completion\", \"percentage\"]);\nconst HP_WORDS = new Set([\"hp\", \"hitpoints\"]);\n\n/** Semantic color mapping for status-like enum values. */\nconst STATUS_COLORS: Record<string, string> = {\n  active: \"#22c55e\",\n  completed: \"#22c55e\",\n  success: \"#22c55e\",\n  published: \"#22c55e\",\n  approved: \"#22c55e\",\n  pending: \"#f59e0b\",\n  draft: \"#f59e0b\",\n  inactive: \"#f59e0b\",\n  paused: \"#f59e0b\",\n  error: \"#ef4444\",\n  failed: \"#ef4444\",\n  rejected: \"#ef4444\",\n  cancelled: \"#ef4444\",\n  archived: \"#6b7280\",\n  deleted: \"#6b7280\",\n  disabled: \"#6b7280\",\n};\n\n/** Neutral palette for enum values without semantic meaning. */\nconst NEUTRAL_PALETTE = [\n  \"#6366f1\",\n  \"#8b5cf6\",\n  \"#ec4899\",\n  \"#14b8a6\",\n  \"#f97316\",\n  \"#06b6d4\",\n  \"#84cc16\",\n  \"#eab308\",\n  \"#ef4444\",\n  \"#64748b\",\n];\n\n/** Generate a colorMap for enum values, using semantic colors where possible. */\nfunction generateColorMap(values: readonly string[]): Record<string, string> {\n  const colorMap: Record<string, string> = {};\n  let neutralIdx = 0;\n  for (const value of values) {\n    const lower = value.toLowerCase();\n    if (STATUS_COLORS[lower]) {\n      colorMap[value] = STATUS_COLORS[lower];\n    } else {\n      colorMap[value] = NEUTRAL_PALETTE[neutralIdx % NEUTRAL_PALETTE.length]!;\n      neutralIdx++;\n    }\n  }\n  return colorMap;\n}\n\n/** Post-process an inferred descriptor with smart display/config heuristics. */\nfunction enhanceDescriptor(descriptor: InferredColumn): InferredColumn {\n  const words = keyToWords(descriptor.key);\n  const joined = words.join(\"\");\n  const d = { ...descriptor };\n\n  const hasIdWord = words.some((w) => ID_WORDS.has(w));\n  const hasCodeWord = words.some((w) => CODE_WORDS.has(w));\n  const hasLatencyWord =\n    words.some((w) => LATENCY_WORDS.has(w)) || joined.includes(\"responsetime\");\n  const hasSizeWord = words.some((w) => SIZE_WORDS.has(w));\n  const hasLevelWord = words.some((w) => LEVEL_WORDS.has(w));\n  const isTraceId = hasIdWord && words.some((w) => TRACE_ID_WORDS.has(w));\n  const hasFavoriteWord = words.some((w) => FAVORITE_WORDS.has(w));\n  const hasEmailWord = words.some((w) => EMAIL_WORDS.has(w));\n  const hasStatusWord = words.some((w) => STATUS_WORDS.has(w));\n  const hasHealthWord = words.some((w) => HEALTH_WORDS.has(w));\n  const hasProgressWord = words.some((w) => PROGRESS_WORDS.has(w));\n  const hasHpWord = words.some((w) => HP_WORDS.has(w));\n\n  // ID-like columns → code display, not sortable\n  if (hasIdWord) {\n    d.display = { type: \"code\" };\n    d.sortable = false;\n    // Trace/span/request IDs → hidden, not filterable (matches col.presets.traceId())\n    if (isTraceId) {\n      d.hidden = true;\n      d.filter = null;\n    }\n  }\n  // Favorite/starred booleans → star display, hide column header\n  else if (hasFavoriteWord && d.kind === \"boolean\") {\n    d.display = { type: \"star\" };\n    d.hideHeader = true;\n  }\n  // Email columns → code display\n  else if (hasEmailWord && d.kind === \"string\") {\n    d.display = { type: \"code\" };\n  }\n  // Path/URL-like columns → code display\n  else if (hasCodeWord) {\n    d.display = { type: \"code\" };\n  }\n  // Latency-like number columns → heatmap with ms unit, sortable\n  else if (hasLatencyWord && d.kind === \"number\") {\n    // `compact` for the same reason the builder uses it: a zero-variance\n    // sample gives the column an `input` filter with no bounds, and a display\n    // carrying `min: undefined` is not the canonical form of one that omits it.\n    d.display = compact({\n      type: \"heatmap\",\n      unit: \"ms\",\n      min: d.filter?.min,\n      max: d.filter?.max,\n    }) as DisplayDescriptor;\n    d.sortable = true;\n  }\n  // Size-like number columns → number with B unit, sortable\n  else if (hasSizeWord && d.kind === \"number\") {\n    d.display = { type: \"number\", unit: \"B\" };\n    d.sortable = true;\n  }\n  // Health/score/rating → gauge display (min always 0 for visual baseline)\n  else if (hasHealthWord && d.kind === \"number\") {\n    d.display = compact({\n      type: \"gauge\",\n      min: 0,\n      max: d.filter?.max,\n    }) as DisplayDescriptor;\n    d.sortable = true;\n  }\n  // HP/hitpoints → bar display (min always 0 for visual baseline)\n  else if (hasHpWord && d.kind === \"number\") {\n    d.display = compact({\n      type: \"bar\",\n      min: 0,\n      max: d.filter?.max,\n    }) as DisplayDescriptor;\n    d.sortable = true;\n  }\n  // Progress/completion → bar display (min always 0 for visual baseline)\n  else if (hasProgressWord && d.kind === \"number\") {\n    d.display = compact({\n      type: \"bar\",\n      min: 0,\n      max: d.filter?.max,\n    }) as DisplayDescriptor;\n    d.sortable = true;\n  }\n\n  // Sortable defaults by type (unless ID-like)\n  if (!hasIdWord) {\n    if (d.kind === \"timestamp\" || d.kind === \"number\") {\n      d.sortable = true;\n    }\n  }\n\n  // Log level / severity enums: expand filter by default (matches col.presets.logLevel())\n  if (hasLevelWord && d.kind === \"enum\" && d.filter) {\n    d.filter = { ...d.filter, defaultOpen: true };\n  }\n\n  // Status/state enums → semantic colorMap on badge display\n  if (hasStatusWord && d.kind === \"enum\") {\n    d.display = { type: \"badge\", colorMap: generateColorMap(d.enumValues) };\n  }\n\n  // Column sizing defaults\n  const sizeDefaults: Record<string, number> = {\n    boolean: 100,\n    timestamp: 220,\n    number: 120,\n    enum: 130,\n  };\n  if (sizeDefaults[d.kind] !== undefined) {\n    d.size = sizeDefaults[d.kind];\n  }\n\n  return d;\n}\n\n/** Infer the item descriptor of an array column from its flattened items. */\nfunction inferArrayItem(items: unknown[]): ColumnDescriptor {\n  const common = (kind: ColKind, rule: string) =>\n    inferredCommon(\"\", kind, null, rule);\n\n  if (items.length === 0) {\n    return { ...common(\"string\", \"array-item:empty\"), kind: \"string\" };\n  }\n  if (items.every((v) => typeof v === \"string\")) {\n    const distinct = new Set(items as string[]);\n    if (distinct.size <= 10) {\n      return {\n        ...common(\"enum\", \"array-item:enum\"),\n        kind: \"enum\",\n        enumValues: Array.from(distinct),\n      };\n    }\n    return { ...common(\"string\", \"array-item:string\"), kind: \"string\" };\n  }\n  if (items.every((v) => typeof v === \"number\")) {\n    return { ...common(\"number\", \"array-item:number\"), kind: \"number\" };\n  }\n  if (items.every((v) => typeof v === \"boolean\")) {\n    return { ...common(\"boolean\", \"array-item:boolean\"), kind: \"boolean\" };\n  }\n  return { ...common(\"string\", \"array-item:mixed\"), kind: \"string\" };\n}\n\nfunction inferColDescriptor(key: string, values: unknown[]): InferredColumn {\n  const label = keyToLabel(key);\n  const nonNull = values.filter((v) => v !== null && v !== undefined);\n\n  if (nonNull.length === 0) {\n    return makeDescriptor(\n      key,\n      label,\n      \"string\",\n      { type: \"input\", defaultOpen: false, commandDisabled: false },\n      \"empty-column\",\n    );\n  }\n\n  // Timestamp: ISO 8601 strings\n  if (nonNull.every((v) => typeof v === \"string\" && isIso8601(v as string))) {\n    return makeDescriptor(\n      key,\n      label,\n      \"timestamp\",\n      { type: \"timerange\", defaultOpen: false, commandDisabled: true },\n      \"iso8601-string\",\n    );\n  }\n\n  // Timestamp: Unix ms numbers\n  if (nonNull.every((v) => typeof v === \"number\" && isUnixMs(v as number))) {\n    return makeDescriptor(\n      key,\n      label,\n      \"timestamp\",\n      { type: \"timerange\", defaultOpen: false, commandDisabled: true },\n      \"unix-ms-number\",\n    );\n  }\n\n  // Boolean — options match `col.boolean()` exactly. Inference used to omit\n  // them and let `generateFilterFields` derive \"Yes\"/\"No\", while the factory\n  // baked in \"true\"/\"false\": two construction paths producing different filter\n  // options for the same column kind. The codegen round trip cannot hold while\n  // they disagree, because no chain step can clear the factory's options.\n  if (nonNull.every((v) => v === true || v === false)) {\n    return makeDescriptor(\n      key,\n      label,\n      \"boolean\",\n      {\n        type: \"checkbox\",\n        defaultOpen: false,\n        commandDisabled: false,\n        options: [\n          { label: \"true\", value: true },\n          { label: \"false\", value: false },\n        ],\n      },\n      \"boolean\",\n    );\n  }\n\n  // Number\n  if (nonNull.every((v) => typeof v === \"number\")) {\n    const nums = nonNull as number[];\n    const min = Math.min(...nums);\n    const max = Math.max(...nums);\n    const filter: FilterDescriptor =\n      min !== max\n        ? {\n            type: \"slider\",\n            defaultOpen: false,\n            commandDisabled: false,\n            min,\n            max,\n          }\n        : { type: \"input\", defaultOpen: false, commandDisabled: false };\n    return makeDescriptor(key, label, \"number\", filter, \"number\");\n  }\n\n  // Array — the item type is always described, so a `string[]` column with more\n  // distinct values than the enum threshold stays a `string[]` column.\n  if (nonNull.every((v) => Array.isArray(v))) {\n    const allItems = (nonNull as unknown[][])\n      .flat()\n      .filter((v) => v !== null && v !== undefined);\n    const arrayItem = inferArrayItem(allItems);\n    const filter: FilterDescriptor | null =\n      arrayItem.kind === \"enum\"\n        ? {\n            type: \"checkbox\",\n            defaultOpen: false,\n            commandDisabled: false,\n            options: arrayItem.enumValues.map((v) => ({ label: v, value: v })),\n          }\n        : null;\n    return {\n      key,\n      ...inferredCommon(label, \"array\", filter, \"array\"),\n      kind: \"array\",\n      arrayItem,\n    };\n  }\n\n  // Record (plain object, non-array)\n  if (nonNull.every((v) => typeof v === \"object\" && !Array.isArray(v))) {\n    return makeDescriptor(key, label, \"record\", null, \"record\");\n  }\n\n  // String: check if enum (≤ 10 distinct values)\n  if (nonNull.every((v) => typeof v === \"string\")) {\n    const distinct = new Set(nonNull as string[]);\n    if (distinct.size <= 10) {\n      const enumValues = Array.from(distinct);\n      return {\n        key,\n        ...inferredCommon(\n          label,\n          \"enum\",\n          {\n            type: \"checkbox\",\n            defaultOpen: false,\n            commandDisabled: false,\n            options: enumValues.map((v) => ({ label: v, value: v })),\n          },\n          \"string-enum\",\n        ),\n        kind: \"enum\",\n        enumValues,\n      };\n    }\n    return makeDescriptor(\n      key,\n      label,\n      \"string\",\n      { type: \"input\", defaultOpen: false, commandDisabled: false },\n      \"string\",\n    );\n  }\n\n  // Fallback: mixed or unrecognised types — warn and treat as string\n  const types = [\n    ...new Set(nonNull.map((v) => (Array.isArray(v) ? \"array\" : typeof v))),\n  ];\n  console.warn(\n    `[inferSchemaFromJSON] Column \"${key}\" has mixed or ambiguous types (${types.join(\", \")}). ` +\n      `Falling back to string input filter.`,\n  );\n  return makeDescriptor(\n    key,\n    label,\n    \"string\",\n    { type: \"input\", defaultOpen: false, commandDisabled: false },\n    \"mixed-fallback\",\n  );\n}\n\n/**\n * Infer a SchemaJSON from an array of plain data objects.\n *\n * Walks all rows, collects per-key values, and infers the best ColKind and\n * FilterType for each column using these heuristics:\n * - `timestamp`: ISO 8601 strings or Unix-ms numbers\n * - `boolean`: all values strictly true/false\n * - `number`: all non-null values are typeof \"number\"\n * - `enum`: strings with ≤ 10 distinct values across the sample\n * - `array`: values are arrays (item type inferred recursively)\n * - `record`: values are plain objects (non-array)\n * - `string`: fallback\n *\n * Number columns with min ≠ max get a \"slider\" filter; otherwise \"input\".\n */\nexport function inferSchemaFromJSON(data: unknown[]): SchemaJSON {\n  if (!Array.isArray(data) || data.length === 0) {\n    return { version: SCHEMA_JSON_VERSION, columns: [] };\n  }\n\n  // Collect all keys and their values across rows (preserving insertion order)\n  const keyValues = new Map<string, unknown[]>();\n\n  for (const row of data) {\n    if (typeof row !== \"object\" || row === null || Array.isArray(row)) continue;\n    for (const [key, value] of Object.entries(row as Record<string, unknown>)) {\n      if (!keyValues.has(key)) keyValues.set(key, []);\n      keyValues.get(key)!.push(value);\n    }\n  }\n\n  const columns = Array.from(keyValues.entries()).map(([key, values]) =>\n    enhanceDescriptor(inferColDescriptor(key, values)),\n  );\n\n  return { version: SCHEMA_JSON_VERSION, columns };\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/to-typescript.ts",
      "content": "import { col } from \"./col\";\nimport { presets } from \"./presets\";\nimport type {\n  ColBuilder,\n  ColumnDescriptor,\n  ColumnDescriptorCommon,\n  DatePresetDescriptor,\n  JsonValue,\n  SchemaJSON,\n} from \"./types\";\n\n/**\n * Emit the `col.*` / `col.presets.*` factory call a descriptor came from, and\n * produce the descriptor that call yields on its own.\n *\n * Provenance is recorded at construction, so this reads it instead of\n * pattern-matching the descriptor's shape back into a guess at the preset.\n */\nfunction factoryFor(c: ColumnDescriptor): {\n  source: string;\n  baseline: ColumnDescriptor;\n} {\n  if (c.provenance.source === \"preset\") {\n    const factory = presets[c.provenance.preset as keyof typeof presets];\n    if (typeof factory === \"function\") {\n      const args = c.provenance.args;\n      // Re-running the preset with its recorded arguments gives the exact\n      // baseline the author started from, so the chain below emits only what\n      // they actually changed.\n      const builder = (\n        factory as (...a: unknown[]) => ColBuilder<unknown, any>\n      )(...args.map((a) => (a === null ? undefined : a)));\n      return {\n        source: `col.presets.${c.provenance.preset}(${args.map(emitValue).join(\", \")})`,\n        baseline: builder._descriptor,\n      };\n    }\n    // Unknown preset name (schema written by a newer build) — fall through to\n    // the primitive factory rather than emitting a call that will not compile.\n  }\n\n  if (c.kind === \"enum\") {\n    return {\n      source: `col.enum([${c.enumValues.map(emitValue).join(\", \")}])`,\n      baseline: col.enum(c.enumValues)._descriptor,\n    };\n  }\n\n  if (c.kind === \"array\") {\n    const item = factoryFor(c.arrayItem);\n    const itemChain = chainFor(c.arrayItem, item.baseline);\n    // `col.array()` requires an item builder — emitting a bare `col.array()`\n    // is what used to produce code that did not compile.\n    const itemSource = item.source + itemChain.join(\"\");\n    return {\n      source: `col.array(${itemSource})`,\n      baseline: col.array(rebuild(c.arrayItem))._descriptor,\n    };\n  }\n\n  const factory = col[c.kind] as () => ColBuilder<unknown, any>;\n  return { source: `col.${c.kind}()`, baseline: factory()._descriptor };\n}\n\n/** Rebuild a builder from a descriptor — used to seed `col.array`'s baseline. */\nfunction rebuild(c: ColumnDescriptor): ColBuilder<unknown, any> {\n  if (c.kind === \"enum\") return col.enum(c.enumValues);\n  if (c.kind === \"array\") return col.array(rebuild(c.arrayItem));\n  return (col[c.kind] as () => ColBuilder<unknown, any>)();\n}\n\nfunction emitValue(value: JsonValue | readonly string[] | undefined): string {\n  if (value === undefined || value === null) return \"undefined\";\n  // Arrays are spaced by hand so preset args (`logLevel([\"a\", \"b\"])`) match the\n  // per-value assembly used elsewhere (`col.enum([\"a\", \"b\"])`) — bare\n  // `JSON.stringify` would emit one of them without spaces.\n  if (Array.isArray(value)) {\n    return `[${value.map((v) => emitValue(v as JsonValue)).join(\", \")}]`;\n  }\n  return JSON.stringify(value);\n}\n\n/**\n * Emit a timerange preset.\n *\n * The descriptor stores `from`/`to` as ISO strings so they survive JSON, but\n * `.filterable(\"timerange\", { presets })` takes `DatePreset`s holding real\n * `Date`s — printing the descriptor verbatim produced code that threw\n * `preset.from.toISOString is not a function` on the first run.\n */\nfunction emitDatePreset(preset: DatePresetDescriptor): string {\n  return (\n    `{ label: ${JSON.stringify(preset.label)}, ` +\n    `shortcut: ${JSON.stringify(preset.shortcut)}, ` +\n    `from: new Date(${JSON.stringify(preset.from)}), ` +\n    `to: new Date(${JSON.stringify(preset.to)}) }`\n  );\n}\n\n/** Emit an object literal with unquoted keys, matching the docs' house style. */\nfunction emitObject(entries: Record<string, unknown>): string {\n  const parts = Object.entries(entries)\n    .filter(([, v]) => v !== undefined)\n    .map(([k, v]) => `${k}: ${JSON.stringify(v)}`);\n  return `{ ${parts.join(\", \")} }`;\n}\n\nfunction sameJSON(a: unknown, b: unknown): boolean {\n  return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);\n}\n\n/**\n * One emitter per serializable descriptor field.\n *\n * The `-?` plus full key coverage means adding a field to\n * `ColumnDescriptorCommon` without adding an emitter here is a **compile\n * error**, not a silently dropped chain step.\n */\ntype Emitter<K extends keyof ColumnDescriptorCommon> = (\n  value: ColumnDescriptorCommon[K],\n  baseline: ColumnDescriptor,\n  descriptor: ColumnDescriptor,\n) => string[];\n\ntype EmitterMap = { [K in keyof ColumnDescriptorCommon]-?: Emitter<K> };\n\nconst EMITTERS: EmitterMap = {\n  label: (value) => [`.label(${JSON.stringify(value)})`],\n\n  description: (value) =>\n    value === undefined ? [] : [`.description(${JSON.stringify(value)})`],\n\n  display: (value) => {\n    const { type, ...options } = value as { type: string } & Record<\n      string,\n      unknown\n    >;\n    const hasOptions = Object.values(options).some((v) => v !== undefined);\n    return [\n      hasOptions\n        ? `.display(${JSON.stringify(type)}, ${emitObject(options)})`\n        : `.display(${JSON.stringify(type)})`,\n    ];\n  },\n\n  filter: (value) => {\n    if (value === null) return [\".notFilterable()\"];\n    const parts: string[] = [];\n    const { type, defaultOpen, commandDisabled, options, presets, ...rest } =\n      value;\n\n    if (type === \"checkbox\") {\n      parts.push(\n        options\n          ? `.filterable(\"checkbox\", { options: [${options\n              .map((o) => emitObject({ label: o.label, value: o.value }))\n              .join(\", \")}] })`\n          : `.filterable(\"checkbox\")`,\n      );\n    } else if (type === \"slider\") {\n      parts.push(`.filterable(\"slider\", ${emitObject(rest)})`);\n    } else if (type === \"timerange\") {\n      parts.push(\n        presets\n          ? `.filterable(\"timerange\", { presets: [${presets\n              .map(emitDatePreset)\n              .join(\", \")}] })`\n          : `.filterable(\"timerange\")`,\n      );\n    } else {\n      parts.push(`.filterable(\"input\")`);\n    }\n\n    if (defaultOpen) parts.push(\".defaultOpen()\");\n    if (commandDisabled) parts.push(\".commandDisabled()\");\n    return parts;\n  },\n\n  sheet: (value) => {\n    if (value === null) return [];\n    const args = emitObject(value as Record<string, unknown>);\n    return [args === \"{  }\" ? \".sheet()\" : `.sheet(${args})`];\n  },\n\n  size: (value) => (value === undefined ? [] : [`.size(${value})`]),\n  minSize: (value) => (value === undefined ? [] : [`.minSize(${value})`]),\n  hidden: (value) => (value ? [\".hidden()\"] : []),\n  hideHeader: (value) => (value ? [\".hideHeader()\"] : []),\n  resizable: (value) => (value ? [\".resizable()\"] : []),\n  sortable: (value) => (value ? [\".sortable()\"] : []),\n  optional: (value) => (value ? [\".optional()\"] : []),\n\n  // `enableHiding: false` together with `hidden` is exactly `.sheetOnly()`;\n  // on its own it is only reachable via `col.select()`, whose baseline already\n  // has it, so nothing needs emitting.\n  enableHiding: (value, _baseline, descriptor) =>\n    value === false && descriptor.hidden ? [\".sheetOnly()\"] : [],\n\n  // Not a chain step — provenance is how the factory call was chosen.\n  provenance: () => [],\n};\n\n/**\n * The order chain steps are emitted in. Listed explicitly because `.sheetOnly()`\n * clears the filter and must come after `.filterable()` would have set it.\n */\nexport const EMIT_ORDER: (keyof ColumnDescriptorCommon)[] = [\n  \"label\",\n  \"description\",\n  \"display\",\n  \"filter\",\n  \"enableHiding\",\n  \"sortable\",\n  \"hidden\",\n  \"hideHeader\",\n  \"resizable\",\n  \"optional\",\n  \"size\",\n  \"minSize\",\n  \"sheet\",\n];\n\n/** Emit only the steps where the descriptor differs from the factory baseline. */\nfunction chainFor(c: ColumnDescriptor, baseline: ColumnDescriptor): string[] {\n  const parts: string[] = [];\n  for (const key of EMIT_ORDER) {\n    const value = c[key];\n    if (sameJSON(value, baseline[key])) continue;\n    // `.sheetOnly()` already implies `.hidden()` and `.notFilterable()`.\n    //\n    // The `filter === null` check is the whole reason this branch is safe:\n    // without it, any descriptor shaped like a sheet-only column had its\n    // filter dropped from the emitted source, silently. `validateSchema`\n    // rejects that combination, so it can only arrive here through a raw\n    // `SchemaJSON` that never went through a schema — and then the filter is\n    // emitted rather than swallowed.\n    if (\n      c.enableHiding === false &&\n      c.hidden &&\n      c.filter === null &&\n      baseline.enableHiding !== false &&\n      (key === \"hidden\" || key === \"filter\")\n    ) {\n      continue;\n    }\n    const emit = EMITTERS[key] as Emitter<typeof key>;\n    parts.push(...emit(value as never, baseline, c));\n  }\n  return parts;\n}\n\n/**\n * Convert a `SchemaJSON` descriptor to a `createTableSchema(...)` TypeScript\n * source code string.\n *\n * The output is ready to copy-paste into a project that imports from\n * `@/lib/table-schema`. Custom cell renderers are not serialized, so the\n * emitted chain uses the descriptor's fallback display for those columns.\n *\n * @example\n * ```ts\n * const ts = schemaToTypeScript(tableSchema.toJSON());\n * // → 'import { createTableSchema, col } from \"@/lib/table-schema\"; ...'\n * ```\n */\nexport function schemaToTypeScript(json: SchemaJSON): string {\n  const lines: string[] = [\n    'import { createTableSchema, col } from \"@/lib/table-schema\";',\n    \"\",\n    \"export const schema = createTableSchema({\",\n  ];\n\n  for (const { key, ...descriptor } of json.columns) {\n    const c = descriptor as ColumnDescriptor;\n    const { source, baseline } = factoryFor(c);\n    const chain = [source, ...chainFor(c, baseline)].join(\"\\n    \");\n    lines.push(`  ${key}: ${chain},`);\n  }\n\n  lines.push(\"});\");\n  return lines.join(\"\\n\");\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/filters/types.ts",
      "content": "import type {\n  ColBuilder,\n  ColKind,\n  FilterType,\n  TableSchemaDefinition,\n} from \"../table-schema/types\";\n\nexport type { ColKind, FilterType };\n\n/** The member type of a checkbox filter: the item type for an array column. */\ntype FilterItem<T> =\n  NonNullable<T> extends readonly (infer U)[] ? U : NonNullable<T>;\n\n/**\n * The value one column's filter accepts, from its `ColBuilder<T, F>`.\n *\n * Mirrors `normalize`: dispatch is on the declared filter type, and the\n * member type comes from the column's data type — so a checkbox on\n * `col.enum(LEVELS)` only accepts members of `LEVELS`. Distributes over `F`,\n * so a column whose filter type was never narrowed (`col.number()` allows\n * three) accepts any of them.\n *\n * A checkbox on a non-scalar member type is `never`: `col.array()` takes any\n * item builder, so `col.array(col.record())` type-checks, but `normalize` has\n * nothing to compare a record against. Advertising the member type there\n * would promise a filter the engine cannot plan.\n */\nexport type FilterValueFor<T, F extends FilterType> = F extends \"input\"\n  ? NonNullable<T> extends number\n    ? number\n    : string\n  : F extends \"checkbox\"\n    ? [FilterItem<T>] extends [Scalar]\n      ? FilterItem<T> | readonly FilterItem<T>[]\n      : never\n    : F extends \"slider\"\n      ? number | readonly [number, number]\n      : F extends \"timerange\"\n        ? Date | readonly [Date, Date]\n        : never;\n\n/**\n * The filter values a table schema accepts, keyed by filterable column.\n *\n * Columns that are `.notFilterable()` (or `col.record()` / `col.select()`,\n * which never are) are absent, so a key typo or a guard on an unfilterable\n * column is a compile error — the same rule `defineActions` enforces at\n * runtime for a `when` clause.\n */\nexport type FilterValues<TSchema extends TableSchemaDefinition> = {\n  [K in keyof TSchema as TSchema[K] extends ColBuilder<unknown, infer F>\n    ? [F] extends [never]\n      ? never\n      : K\n    : never]: TSchema[K] extends ColBuilder<infer T, infer F>\n    ? FilterValueFor<T, F>\n    : never;\n};\n\n/** A value a filter can compare against. */\nexport type Scalar = string | number | boolean;\n\n/**\n * A column's declared filter semantics, flattened to plain data.\n *\n * This is the whole input to the normalization table. Backends never see\n * anything else about a column, which is what stops them re-deriving semantics\n * from the runtime shape of a value.\n */\nexport type FilterSpec = {\n  /** Dot-notation — the ONE identity space, matching the table schema key. */\n  key: string;\n  type: FilterType;\n  kind: ColKind;\n  /** Present when `kind === \"array\"`. */\n  itemKind?: ColKind;\n  options?: readonly Scalar[];\n  min?: number;\n  max?: number;\n};\n\n/**\n * The canonical, backend-neutral operation set.\n *\n * CLOSED union — exactly six members. Every backend implements all six with an\n * exhaustive switch and no default branch, so adding a seventh is a compile\n * error at every backend rather than a silent fallthrough. That is the point of\n * the design, not a defect: a new op is a breaking change to every engine.\n */\nexport type FilterOp =\n  /** Case-insensitive substring match. */\n  | { op: \"substring\"; key: string; value: string }\n  | { op: \"equals\"; key: string; value: Scalar }\n  /** Scalar column, value ∈ set. */\n  | { op: \"oneOf\"; key: string; values: Scalar[] }\n  /** Array column, column ∩ set ≠ ∅. */\n  | { op: \"overlaps\"; key: string; values: Scalar[] }\n  /** Inclusive on both ends. */\n  | { op: \"numberRange\"; key: string; min: number; max: number }\n  /** Inclusive on both ends. */\n  | { op: \"dateRange\"; key: string; from: Date; to: Date };\n\n/** Restrict which keys `plan` / `matches` consider. */\nexport type FilterSelection = {\n  only?: readonly string[];\n  exclude?: readonly string[];\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/filters/normalize.ts",
      "content": "import type { ColKind, FilterOp, FilterSpec, Scalar } from \"./types\";\n\n/** Unix ms timestamps are 13-digit numbers (> Sep 2001, < Nov 2286). */\nconst UNIX_MS_MIN = 1_000_000_000_000;\n\n/**\n * Is this filter value active?\n *\n * Inactive means \"the user has not filtered on this column\": absent, cleared,\n * an empty multi-select, or a value that cannot be compared at all. This was\n * written four slightly different ways across four engines.\n */\nexport function isActive(value: unknown): boolean {\n  if (value === null || value === undefined) return false;\n  if (typeof value === \"string\") return value.length > 0;\n  if (typeof value === \"number\") return !Number.isNaN(value);\n  if (value instanceof Date) return !Number.isNaN(value.getTime());\n  if (Array.isArray(value)) {\n    // ANY usable member makes the filter active. `normalize` drops the members\n    // it cannot use, so requiring all of them to be usable would make one blank\n    // entry silently delete the whole filter — and inconsistently, since\n    // `[\"abc\", 500]` on a slider degenerates to 500 while `[\"\", 500]` vanished.\n    return value.some(isActive);\n  }\n  return true;\n}\n\n/** Coerce to a `Date`, or `null` if it cannot be one. */\nfunction asDate(value: unknown): Date | null {\n  if (value instanceof Date) {\n    return Number.isNaN(value.getTime()) ? null : value;\n  }\n  // superjson revives Dates, but MCP args and LLM output arrive as ISO strings\n  // or epoch millis.\n  if (typeof value === \"string\" || typeof value === \"number\") {\n    // Epoch millis that arrived as a string: `new Date(\"1700000000000\")` is an\n    // Invalid Date while `new Date(1700000000000)` is not. The command palette\n    // serializes timeranges with `getTime()`, and JSON has no Date type, so\n    // this shape is common. The magnitude test disambiguates it from a bare\n    // year like \"2024\", which `Date` should keep parsing as a year.\n    if (typeof value === \"string\" && /^\\d+$/.test(value)) {\n      const millis = Number(value);\n      if (millis >= UNIX_MS_MIN) {\n        const date = new Date(millis);\n        return Number.isNaN(date.getTime()) ? null : date;\n      }\n    }\n    const date = new Date(value);\n    return Number.isNaN(date.getTime()) ? null : date;\n  }\n  return null;\n}\n\nfunction asNumber(value: unknown): number | null {\n  if (typeof value === \"number\") return Number.isNaN(value) ? null : value;\n  if (typeof value === \"string\" && value.trim() !== \"\") {\n    const parsed = Number(value);\n    return Number.isNaN(parsed) ? null : parsed;\n  }\n  return null;\n}\n\nfunction asScalar(value: unknown): Scalar | null {\n  if (\n    typeof value === \"string\" ||\n    typeof value === \"number\" ||\n    typeof value === \"boolean\"\n  ) {\n    return value;\n  }\n  return null;\n}\n\nfunction asArray(value: unknown): unknown[] {\n  return Array.isArray(value) ? value : [value];\n}\n\nfunction asBoolean(value: unknown): boolean | null {\n  if (typeof value === \"boolean\") return value;\n  if (value === \"true\") return true;\n  if (value === \"false\") return false;\n  return null;\n}\n\n/**\n * Coerce one checkbox member to the column's declared type.\n *\n * The URL layer, MCP args, and LLM output all deliver `[\"200\", \"500\"]` for a\n * numeric column. Without this, the SQL backend received\n * `inArray(integerColumn, [\"200\", \"500\"])` and depended on Postgres casting the\n * literals, and `coerce()` — the entry point documented as producing validated\n * values — handed back strings for a number column.\n */\nfunction asDeclaredScalar(kind: ColKind, value: unknown): Scalar | null {\n  switch (kind) {\n    case \"number\":\n      return asNumber(value);\n    case \"boolean\":\n      return asBoolean(value);\n    default: {\n      const scalar = asScalar(value);\n      return scalar === null ? null : String(scalar);\n    }\n  }\n}\n\nfunction startOfDay(date: Date): Date {\n  const result = new Date(date);\n  result.setHours(0, 0, 0, 0);\n  return result;\n}\n\nfunction endOfDay(date: Date): Date {\n  const result = new Date(date);\n  result.setHours(23, 59, 59, 999);\n  return result;\n}\n\n/**\n * Turn one declared filter and its value into a canonical op.\n *\n * This is the whole design. Dispatch is on the **declared** `(type, kind)`\n * pair — never on the runtime shape of `value`. A numeric checkbox is\n * `{ type: \"checkbox\", kind: \"number\" }`, so `[200, 500]` can only become\n * `oneOf`; `numberRange` is unreachable from a checkbox and\n * `BETWEEN 200 AND 500` is therefore unrepresentable. Backends never see array\n * length, so length-based dispatch has nowhere to live.\n *\n * Returns `null` when the filter is inactive or the value cannot be coerced\n * into the declared shape.\n */\nexport function normalize(spec: FilterSpec, value: unknown): FilterOp | null {\n  if (!isActive(value)) return null;\n  const { key } = spec;\n\n  switch (spec.type) {\n    case \"input\": {\n      // A number column's text box is an exact match, not a substring search —\n      // substring matching on a stringified number makes \"5\" match 1500.\n      if (spec.kind === \"number\") {\n        const parsed = asNumber(Array.isArray(value) ? value[0] : value);\n        return parsed === null ? null : { op: \"equals\", key, value: parsed };\n      }\n      const raw = Array.isArray(value) ? value[0] : value;\n      const text = asScalar(raw);\n      return text === null\n        ? null\n        : { op: \"substring\", key, value: String(text) };\n    }\n\n    case \"checkbox\": {\n      // For an array column the members are compared against the *item* type.\n      const memberKind =\n        spec.kind === \"array\" ? (spec.itemKind ?? \"string\") : spec.kind;\n      const values = asArray(value)\n        .map((member) => asDeclaredScalar(memberKind, member))\n        .filter((v): v is Scalar => v !== null);\n      if (values.length === 0) return null;\n      // An array column is a set on both sides — overlap, not membership.\n      return spec.kind === \"array\"\n        ? { op: \"overlaps\", key, values }\n        : { op: \"oneOf\", key, values };\n    }\n\n    case \"slider\": {\n      const numbers = asArray(value)\n        .map(asNumber)\n        .filter((v): v is number => v !== null);\n      if (numbers.length === 0) return null;\n      // A single-handle slider is a degenerate range, not an equality — the\n      // two disagree once the column holds non-integers.\n      const [first, second] = numbers;\n      const min = first!;\n      const max = second ?? first!;\n      return {\n        op: \"numberRange\",\n        key,\n        min: Math.min(min, max),\n        max: Math.max(min, max),\n      };\n    }\n\n    case \"timerange\": {\n      const dates = asArray(value)\n        .map(asDate)\n        .filter((v): v is Date => v !== null);\n      if (dates.length === 0) return null;\n      // One date means \"that whole day\".\n      if (dates.length === 1) {\n        return {\n          op: \"dateRange\",\n          key,\n          from: startOfDay(dates[0]!),\n          to: endOfDay(dates[0]!),\n        };\n      }\n      const [from, to] = dates;\n      return from!.getTime() <= to!.getTime()\n        ? { op: \"dateRange\", key, from: from!, to: to! }\n        : { op: \"dateRange\", key, from: to!, to: from! };\n    }\n  }\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/filters/evaluate.ts",
      "content": "import type { FilterOp, Scalar } from \"./types\";\n\n/**\n * Read a possibly dotted key off a row.\n *\n * Table schema keys are dot-notation (`\"timing.dns\"`). Rows may store that\n * either as a literal flat key or as a nested object, so both are tried —\n * a flat hit wins, since that is the wire shape.\n */\nexport function getValueAtKey(row: unknown, key: string): unknown {\n  if (row === null || typeof row !== \"object\") return undefined;\n  const flat = (row as Record<string, unknown>)[key];\n  if (flat !== undefined) return flat;\n  if (!key.includes(\".\")) return undefined;\n  let current: unknown = row;\n  for (const part of key.split(\".\")) {\n    if (current === null || typeof current !== \"object\") return undefined;\n    current = (current as Record<string, unknown>)[part];\n  }\n  return current;\n}\n\nfunction sameScalar(cell: unknown, value: Scalar): boolean {\n  if (cell === value) return true;\n  // The URL layer can only produce strings, so a number column filtered from a\n  // link arrives as one. Compare by string as a last resort rather than by\n  // coercing, which would make `0 == false` true.\n  if (\n    (typeof cell === \"number\" || typeof cell === \"boolean\") &&\n    typeof value === \"string\"\n  ) {\n    return String(cell) === value;\n  }\n  if (typeof cell === \"string\" && typeof value !== \"string\") {\n    return cell === String(value);\n  }\n  return false;\n}\n\nfunction asComparableNumber(cell: unknown): number | null {\n  if (typeof cell === \"number\") return Number.isNaN(cell) ? null : cell;\n  if (typeof cell === \"string\" && cell.trim() !== \"\") {\n    const parsed = Number(cell);\n    return Number.isNaN(parsed) ? null : parsed;\n  }\n  return null;\n}\n\nfunction asComparableTime(cell: unknown): number | null {\n  if (cell instanceof Date) {\n    return Number.isNaN(cell.getTime()) ? null : cell.getTime();\n  }\n  if (typeof cell === \"string\" || typeof cell === \"number\") {\n    const time = new Date(cell).getTime();\n    return Number.isNaN(time) ? null : time;\n  }\n  return null;\n}\n\n/**\n * Evaluate one canonical op against one cell value.\n *\n * Exhaustive over `FilterOp` with no default branch — a seventh op fails to\n * compile here rather than silently passing every row.\n */\nexport function evaluateOp(op: FilterOp, cell: unknown): boolean {\n  switch (op.op) {\n    case \"substring\": {\n      if (cell === null || cell === undefined) return false;\n      // Case-insensitive, matching the SQL side's `ilike`.\n      return String(cell).toLowerCase().includes(op.value.toLowerCase());\n    }\n\n    case \"equals\":\n      return sameScalar(cell, op.value);\n\n    case \"oneOf\":\n      return op.values.some((value) => sameScalar(cell, value));\n\n    case \"overlaps\": {\n      // The column is a set. A scalar cell is treated as a one-element set so\n      // a single-value column still matches — this is where the infinite route\n      // used to look only at `row[key][0]`.\n      const cells = Array.isArray(cell) ? cell : [cell];\n      return cells.some((item) =>\n        op.values.some((value) => sameScalar(item, value)),\n      );\n    }\n\n    case \"numberRange\": {\n      const value = asComparableNumber(cell);\n      return value !== null && value >= op.min && value <= op.max;\n    }\n\n    case \"dateRange\": {\n      const time = asComparableTime(cell);\n      return (\n        time !== null && time >= op.from.getTime() && time <= op.to.getTime()\n      );\n    }\n  }\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/filters/index.ts",
      "content": "import { resolveColumns } from \"../table-schema/col\";\nimport type {\n  ColumnDescriptor,\n  SchemaJSON,\n  TableSchemaDefinition,\n} from \"../table-schema/types\";\nimport { evaluateOp, getValueAtKey } from \"./evaluate\";\nimport { isActive, normalize } from \"./normalize\";\nimport type {\n  FilterOp,\n  FilterSelection,\n  FilterSpec,\n  FilterValues,\n  Scalar,\n} from \"./types\";\n\nexport { evaluateOp, getValueAtKey } from \"./evaluate\";\nexport { isActive, normalize } from \"./normalize\";\nexport type {\n  ColKind,\n  FilterOp,\n  FilterSelection,\n  FilterSpec,\n  FilterType,\n  FilterValueFor,\n  FilterValues,\n  Scalar,\n} from \"./types\";\n\n/**\n * Where a `Filters` gets its declarations.\n *\n * `SchemaJSON` is here so the declaration can cross the `\"use client\"` boundary\n * as data: `table-schema.tsx` cannot be imported server-side, but its\n * `toJSON()` can.\n */\nexport type FilterSource =\n  | TableSchemaDefinition\n  | SchemaJSON\n  | readonly FilterSpec[];\n\n/**\n * `TValues` is the shape of the values these semantics accept, keyed by\n * filterable column. It is `FilterValues<typeof definition>` when built from\n * a table schema definition and the loose `Record<string, unknown>` from\n * `SchemaJSON` or specs, where nothing about the columns is known at compile\n * time. Search params stay untyped either way — `plan` / `matches` take what\n * the wire delivers — but declarations written by hand (an action's `when`\n * guard) and the output of `coerce` are checked against it.\n */\nexport interface Filters<TValues = Record<string, unknown>> {\n  readonly specs: readonly FilterSpec[];\n\n  /** Look up one column's declaration. */\n  spec(key: string): FilterSpec | undefined;\n\n  /**\n   * Values → canonical ops. Never guesses from the shape of a value.\n   * Inactive and unknown keys are dropped.\n   */\n  plan(\n    values: Record<string, unknown>,\n    selection?: FilterSelection,\n  ): FilterOp[];\n\n  /** In-memory predicate. Replaces `filterData` and `filterGenericData`. */\n  matches(\n    values: Record<string, unknown>,\n    row: unknown,\n    selection?: FilterSelection,\n  ): boolean;\n\n  /** Filter a whole collection. */\n  apply<TRow>(\n    rows: readonly TRow[],\n    values: Record<string, unknown>,\n    selection?: FilterSelection,\n  ): TRow[];\n\n  /**\n   * A TanStack `ColumnDef.filterFn`. Returns a *function*, so nothing has to be\n   * registered on the table via `filterFns: { inDateRange, arrSome }`.\n   */\n  filterFn(\n    key: string,\n  ):\n    | ((\n        row: { getValue: (id: string) => unknown },\n        columnId: string,\n        value: unknown,\n      ) => boolean)\n    | undefined;\n\n  /** Validate and coerce untrusted input (AI structured output, MCP args). */\n  coerce(raw: Record<string, unknown>): Partial<TValues>;\n}\n\n// ── Deriving specs ──────────────────────────────────────────────────────────\n\nfunction specFromDescriptor(\n  key: string,\n  column: Pick<ColumnDescriptor, \"kind\" | \"filter\"> &\n    Partial<Pick<Extract<ColumnDescriptor, { kind: \"array\" }>, \"arrayItem\">>,\n): FilterSpec | null {\n  const { filter } = column;\n  if (!filter) return null;\n\n  const spec: FilterSpec = {\n    key,\n    type: filter.type,\n    kind: column.kind,\n  };\n\n  if (column.kind === \"array\" && column.arrayItem) {\n    spec.itemKind = column.arrayItem.kind;\n  }\n  if (filter.options) {\n    spec.options = filter.options.map((option) => option.value);\n  }\n  if (filter.min !== undefined) spec.min = filter.min;\n  if (filter.max !== undefined) spec.max = filter.max;\n\n  return spec;\n}\n\nfunction isSchemaJSON(source: FilterSource): source is SchemaJSON {\n  return (\n    !Array.isArray(source) &&\n    typeof source === \"object\" &&\n    source !== null &&\n    Array.isArray((source as SchemaJSON).columns)\n  );\n}\n\nfunction toSpecs(source: FilterSource): FilterSpec[] {\n  if (Array.isArray(source)) {\n    return [...(source as readonly FilterSpec[])];\n  }\n\n  if (isSchemaJSON(source)) {\n    const specs: FilterSpec[] = [];\n    for (const column of source.columns) {\n      const spec = specFromDescriptor(column.key, column);\n      if (spec) specs.push(spec);\n    }\n    return specs;\n  }\n\n  const specs: FilterSpec[] = [];\n  for (const column of resolveColumns(source as TableSchemaDefinition)) {\n    const spec = specFromDescriptor(column.key, column);\n    if (spec) specs.push(spec);\n  }\n  return specs;\n}\n\nfunction isSelected(key: string, selection?: FilterSelection): boolean {\n  if (selection?.exclude?.includes(key)) return false;\n  if (selection?.only && !selection.only.includes(key)) return false;\n  return true;\n}\n\n// ── Coercion ────────────────────────────────────────────────────────────────\n\nfunction coerceValue(spec: FilterSpec, raw: unknown): unknown {\n  // Round-tripping through `normalize` is the coercion: if the declared\n  // semantics cannot make an op out of it, it is not a usable filter value.\n  const op = normalize(spec, raw);\n  if (!op) return undefined;\n\n  switch (op.op) {\n    case \"substring\":\n      return op.value;\n    case \"equals\":\n      return op.value;\n    case \"oneOf\":\n    case \"overlaps\": {\n      // Drop values outside the declared option set — an LLM will happily\n      // invent an enum member that does not exist.\n      if (!spec.options) return op.values;\n      const allowed = op.values.filter((value) =>\n        spec.options!.some((option) => String(option) === String(value)),\n      );\n      return allowed.length > 0 ? allowed : undefined;\n    }\n    case \"numberRange\": {\n      // Clamp to the declared bounds. `coerce` is the untrusted-input entry\n      // point, and an LLM asked for \"slow requests\" will happily produce\n      // `[0, 99999]` against a slider declared `{ min: 0, max: 5000 }`.\n      const lower = spec.min ?? -Infinity;\n      const upper = spec.max ?? Infinity;\n      const min = Math.min(Math.max(op.min, lower), upper);\n      const max = Math.min(Math.max(op.max, lower), upper);\n      return [min, max];\n    }\n    case \"dateRange\":\n      return [op.from, op.to];\n  }\n}\n\n// ── Entry point ─────────────────────────────────────────────────────────────\n\n/**\n * Build the one interpretation of a table's filter semantics.\n *\n * Every engine — SQL, in-memory, TanStack — goes through this, so a column's\n * declared `(FilterType, ColKind)` pair is honoured identically everywhere\n * instead of being re-derived from whatever the value happened to look like at\n * runtime.\n *\n * Built from a table schema definition, the result knows which columns are\n * filterable and what each accepts (`FilterValues`); from `SchemaJSON` or\n * specs it is untyped.\n */\nexport function defineFilters<TSchema extends TableSchemaDefinition>(\n  source: TSchema,\n): Filters<FilterValues<TSchema>>;\nexport function defineFilters(source: FilterSource): Filters;\nexport function defineFilters(source: FilterSource): Filters {\n  const specs = toSpecs(source);\n  const byKey = new Map(specs.map((spec) => [spec.key, spec]));\n\n  const plan = (\n    values: Record<string, unknown>,\n    selection?: FilterSelection,\n  ): FilterOp[] => {\n    const ops: FilterOp[] = [];\n    for (const [key, value] of Object.entries(values)) {\n      if (!isSelected(key, selection)) continue;\n      const spec = byKey.get(key);\n      if (!spec) continue;\n      const op = normalize(spec, value);\n      if (op) ops.push(op);\n    }\n    return ops;\n  };\n\n  const matches = (\n    values: Record<string, unknown>,\n    row: unknown,\n    selection?: FilterSelection,\n  ): boolean =>\n    plan(values, selection).every((op) =>\n      evaluateOp(op, getValueAtKey(row, op.key)),\n    );\n\n  return {\n    specs,\n\n    spec: (key) => byKey.get(key),\n\n    plan,\n\n    matches,\n\n    apply: (rows, values, selection) => {\n      const ops = plan(values, selection);\n      if (ops.length === 0) return [...rows];\n      return rows.filter((row) =>\n        ops.every((op) => evaluateOp(op, getValueAtKey(row, op.key))),\n      );\n    },\n\n    filterFn: (key) => {\n      const spec = byKey.get(key);\n      if (!spec) return undefined;\n      return (row, columnId, value) => {\n        const op = normalize(spec, value);\n        // An inactive filter matches everything, which is what TanStack expects\n        // when a column filter is present but empty.\n        if (!op) return true;\n        return evaluateOp(op, row.getValue(columnId));\n      };\n    },\n\n    coerce: (raw) => {\n      const result: Record<string, unknown> = {};\n      for (const spec of specs) {\n        const value = raw[spec.key];\n        if (!isActive(value)) continue;\n        const coerced = coerceValue(spec, value);\n        if (coerced !== undefined) result[spec.key] = coerced;\n      }\n      return result;\n    },\n  };\n}\n\nexport type { Scalar as FilterScalar };\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/generators/columns.tsx",
      "content": "\"use client\";\n\nimport {\n  DataTableCellBadge,\n  DataTableCellBar,\n  DataTableCellBoolean,\n  DataTableCellCode,\n  DataTableCellGauge,\n  DataTableCellHeatmap,\n  DataTableCellLevelIndicator,\n  DataTableCellNumber,\n  DataTableCellStar,\n  DataTableCellStatusCode,\n  DataTableCellText,\n  DataTableCellTimestamp,\n} from \"@/components/data-table/data-table-cell\";\nimport { DataTableColumnHeader } from \"@/components/data-table/data-table-column-header\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { defineFilters } from \"@/lib/filters\";\nimport type { DataTableFeatures } from \"@/lib/table/features\";\nimport type { ColumnDef, RowData } from \"@tanstack/react-table\";\nimport type { JSX } from \"react\";\nimport { resolveColumns } from \"../col\";\nimport type { DisplayDescriptor, TableSchemaDefinition } from \"../types\";\n\n/**\n * Render the cell based on the display config.\n */\nfunction renderCell(\n  display: DisplayDescriptor,\n  value: unknown,\n  context?: { min: number; max: number },\n): JSX.Element | null {\n  const fallback = <DataTableCellText value={String(value ?? \"\")} />;\n  const colorMap = \"colorMap\" in display ? display.colorMap : undefined;\n  switch (display.type) {\n    case \"text\": {\n      const hex = colorMap?.[String(value)];\n      return typeof value === \"string\" || typeof value === \"number\" ? (\n        <DataTableCellText value={value} color={hex} />\n      ) : (\n        fallback\n      );\n    }\n    case \"code\": {\n      const hex = colorMap?.[String(value)];\n      return typeof value === \"string\" || typeof value === \"number\" ? (\n        <DataTableCellCode value={value} color={hex} />\n      ) : (\n        fallback\n      );\n    }\n    case \"number\": {\n      const hex = colorMap?.[String(value)];\n      return typeof value === \"number\" ? (\n        <DataTableCellNumber value={value} unit={display.unit} color={hex} />\n      ) : (\n        fallback\n      );\n    }\n    case \"timestamp\": {\n      const hex = colorMap?.[String(value)];\n      return value instanceof Date ||\n        typeof value === \"string\" ||\n        typeof value === \"number\" ? (\n        <DataTableCellTimestamp date={value} color={hex} />\n      ) : (\n        fallback\n      );\n    }\n    case \"badge\": {\n      if (Array.isArray(value)) {\n        return (\n          <div className=\"flex-no-wrap flex gap-1\">\n            {value.map((item, i) => (\n              <DataTableCellBadge\n                key={i}\n                value={item}\n                color={colorMap?.[String(item)]}\n              />\n            ))}\n          </div>\n        );\n      }\n      const hex = colorMap?.[String(value)];\n      return typeof value === \"string\" || typeof value === \"number\" ? (\n        <DataTableCellBadge value={value} color={hex} />\n      ) : (\n        fallback\n      );\n    }\n    case \"boolean\": {\n      const hex = colorMap?.[String(value)];\n      return typeof value === \"boolean\" ? (\n        <DataTableCellBoolean value={value} color={hex} />\n      ) : (\n        fallback\n      );\n    }\n    case \"star\": {\n      return typeof value === \"boolean\" ? (\n        <DataTableCellStar value={value} />\n      ) : (\n        fallback\n      );\n    }\n    case \"status-code\": {\n      const hex = colorMap?.[String(value)];\n      return typeof value === \"number\" ? (\n        <DataTableCellStatusCode value={value} color={hex} />\n      ) : (\n        fallback\n      );\n    }\n    case \"level-indicator\": {\n      const hex = colorMap?.[String(value)];\n      return typeof value === \"string\" ? (\n        <DataTableCellLevelIndicator value={value} color={hex} />\n      ) : (\n        fallback\n      );\n    }\n    case \"heatmap\": {\n      const { min = 0, max = 100 } = context ?? {};\n      return typeof value === \"number\" ? (\n        <DataTableCellHeatmap\n          value={value}\n          min={min}\n          max={max}\n          unit={display.unit}\n          color={display.color}\n        />\n      ) : (\n        fallback\n      );\n    }\n    case \"bar\": {\n      const { min = 0, max = 100 } = context ?? {};\n      return typeof value === \"number\" ? (\n        <DataTableCellBar\n          value={value}\n          min={min}\n          max={max}\n          unit={display.unit}\n          color={display.color}\n        />\n      ) : (\n        fallback\n      );\n    }\n    case \"gauge\": {\n      const { min = 0, max = 100 } = context ?? {};\n      return typeof value === \"number\" ? (\n        <DataTableCellGauge\n          value={value}\n          min={min}\n          max={max}\n          unit={display.unit}\n          color={display.color}\n        />\n      ) : (\n        fallback\n      );\n    }\n  }\n}\n\n/**\n * The one interpretation of `.size()` / `.minSize()` / `.resizable()`:\n *\n * - `.minSize(px)` — a flexing column with a floor: it absorbs the table's\n *   leftover width but never compresses below `px`.\n * - `.size(px)` without `.resizable()` — locked: min/max pin the rendered\n *   width, so only an unsized column can flex.\n * - `.size(px)` with `.resizable()` — `px` is the initial width only.\n */\nfunction sizingFor(config: {\n  size?: number;\n  minSize?: number;\n  resizable: boolean;\n}): { size?: number; minSize?: number; maxSize?: number } {\n  if (config.minSize !== undefined) {\n    return {\n      minSize: config.minSize,\n      ...(config.size !== undefined ? { size: config.size } : {}),\n    };\n  }\n  if (config.size === undefined) return {};\n  if (config.resizable) return { size: config.size };\n  return { size: config.size, minSize: config.size, maxSize: config.size };\n}\n\n/**\n * Generate ColumnDef[] from a table schema definition.\n *\n * Rules:\n * - Dotted keys (e.g. \"timing.dns\") → id + accessorFn\n * - Non-dotted keys → accessorKey\n * - Sortable columns get DataTableColumnHeader; others get a plain string header\n * - filterFn comes from the shared filter-semantics module\n * - Cell renders via built-in display components or the \"custom\" cell function\n * - meta.label is always set; meta.hidden reflects .hidden() calls\n *\n * Composite/virtual columns that span multiple fields must be appended manually:\n * @example\n * ```ts\n * const columns = [\n *   ...generateColumns(tableSchema),\n *   { id: \"timing\", header: ..., cell: ..., size: 130 },\n * ];\n * ```\n */\nexport function generateColumns<TData extends RowData>(\n  schema: TableSchemaDefinition,\n): ColumnDef<DataTableFeatures, TData>[] {\n  // One interpretation of filter semantics, shared with the SQL and in-memory\n  // engines. `filterFn` returns a *function*, so the consuming table no longer\n  // has to register `filterFns: { inDateRange, arrSome }` — a contract that was\n  // undocumented outside a comment and silently broke filtering when missed.\n  const filters = defineFilters(schema);\n\n  return resolveColumns(schema).map((config) => {\n    const { key } = config;\n\n    // Select column — checkbox header + cell\n    if (config.kind === \"select\") {\n      return {\n        id: key,\n        header: ({ table }) => (\n          <div className=\"flex items-center justify-center\">\n            <Checkbox\n              checked={\n                table.getIsAllPageRowsSelected() ||\n                (table.getIsSomePageRowsSelected() && \"indeterminate\")\n              }\n              onCheckedChange={(value) =>\n                table.toggleAllPageRowsSelected(!!value)\n              }\n              aria-label=\"Select all\"\n              className=\"shadow-none\"\n            />\n          </div>\n        ),\n        cell: ({ row }) => (\n          <div\n            className=\"flex items-center justify-center\"\n            onClick={(e) => e.stopPropagation()}\n          >\n            <Checkbox\n              checked={row.getIsSelected()}\n              onCheckedChange={(value) => row.toggleSelected(!!value)}\n              aria-label=\"Select row\"\n              className=\"shadow-none\"\n            />\n          </div>\n        ),\n        enableSorting: false,\n        enableHiding: false,\n        enableResizing: false,\n        ...sizingFor(config),\n        meta: { label: config.label, kind: \"select\", hidden: config.hidden },\n      } as ColumnDef<DataTableFeatures, TData>;\n    }\n\n    const isDotted = key.includes(\".\");\n    const filterFn = filters.filterFn(key);\n\n    const header = config.hideHeader\n      ? () => <span className=\"sr-only\">{config.label}</span>\n      : config.sortable\n        ? ({\n            column,\n          }: {\n            column: Parameters<typeof DataTableColumnHeader>[0][\"column\"];\n          }) => <DataTableColumnHeader column={column} title={config.label} />\n        : config.label;\n\n    const needsMinMax =\n      config.display.type === \"heatmap\" ||\n      config.display.type === \"bar\" ||\n      config.display.type === \"gauge\";\n\n    const customCell = config.renderers.cell;\n\n    const cell = ({\n      getValue,\n      row,\n      column,\n    }: {\n      getValue: () => unknown;\n      row: { original: TData };\n      column: { getFacetedMinMaxValues?: () => [number, number] | undefined };\n    }) => {\n      // A custom renderer overrides the descriptor's display. The descriptor\n      // still carries a real display type, which is what the sheet and\n      // `toJSON()` fall back to.\n      if (customCell) return customCell(getValue(), row.original);\n      if (needsMinMax) {\n        const display = config.display as {\n          min?: number;\n          max?: number;\n        };\n        const faceted = column.getFacetedMinMaxValues?.();\n        const min = faceted?.[0] ?? display.min ?? 0;\n        const max = faceted?.[1] ?? display.max ?? 100;\n        return renderCell(config.display, getValue(), { min, max });\n      }\n      return renderCell(config.display, getValue());\n    };\n\n    const meta = {\n      label: config.label,\n      hidden: config.hidden,\n      kind: config.kind,\n    };\n\n    const base = {\n      header,\n      cell,\n      enableResizing: config.resizable,\n      ...(config.enableHiding === false ? { enableHiding: false } : {}),\n      ...(filterFn ? { filterFn } : {}),\n      ...sizingFor(config),\n      meta,\n    };\n\n    if (isDotted) {\n      return {\n        ...base,\n        id: key,\n        accessorFn: (row: TData) => (row as Record<string, unknown>)[key],\n      } as ColumnDef<DataTableFeatures, TData>;\n    }\n\n    return {\n      ...base,\n      accessorKey: key,\n    } as ColumnDef<DataTableFeatures, TData>;\n  });\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/generators/filter-fields.ts",
      "content": "import type { DataTableFilterField } from \"@/components/data-table/types\";\nimport { fromPresetDescriptor, resolveColumns } from \"../col\";\nimport type { TableSchemaDefinition } from \"../types\";\n\n/**\n * Generate DataTableFilterField[] from a table schema definition.\n *\n * Only includes fields where filter !== null.\n * Order follows schema definition order (JS object key insertion order).\n *\n * Options for checkbox fields are auto-derived from col.enum(values) or\n * col.boolean() if not explicitly provided via filterable(\"checkbox\", { options }).\n */\nexport function generateFilterFields<TData>(\n  schema: TableSchemaDefinition,\n): DataTableFilterField<TData>[] {\n  const result: DataTableFilterField<TData>[] = [];\n\n  for (const config of resolveColumns(schema)) {\n    const { key, filter, label, kind } = config;\n    if (!filter) continue;\n\n    const base = {\n      label,\n      value: key as keyof TData,\n      defaultOpen: filter.defaultOpen || undefined,\n      commandDisabled: filter.commandDisabled || undefined,\n    };\n\n    switch (filter.type) {\n      case \"input\": {\n        result.push({ ...base, type: \"input\" });\n        break;\n      }\n      case \"timerange\": {\n        result.push({\n          ...base,\n          type: \"timerange\",\n          presets: filter.presets?.map(fromPresetDescriptor),\n        });\n        break;\n      }\n      case \"checkbox\": {\n        // Derive options if not explicitly provided\n        let options = filter.options;\n        if (!options) {\n          if (config.kind === \"enum\") {\n            options = config.enumValues.map((v) => ({ label: v, value: v }));\n          } else if (kind === \"boolean\") {\n            options = [\n              { label: \"Yes\", value: true },\n              { label: \"No\", value: false },\n            ];\n          } else if (\n            config.kind === \"array\" &&\n            config.arrayItem.kind === \"enum\"\n          ) {\n            options = config.arrayItem.enumValues.map((v) => ({\n              label: v,\n              value: v,\n            }));\n          }\n        }\n        result.push({\n          ...base,\n          type: \"checkbox\",\n          options,\n          component: config.renderers.filterComponent,\n        });\n        break;\n      }\n      case \"slider\": {\n        const displayUnit =\n          \"unit\" in config.display ? config.display.unit : undefined;\n        result.push({\n          ...base,\n          type: \"slider\",\n          min: filter.min ?? 0,\n          max: filter.max ?? 100,\n          unit: filter.unit ?? displayUnit,\n        });\n        break;\n      }\n    }\n  }\n\n  return result;\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/generators/filter-schema.ts",
      "content": "import {\n  ARRAY_DELIMITER,\n  RANGE_DELIMITER,\n  SLIDER_DELIMITER,\n} from \"@/lib/delimiters\";\nimport { createSchema, field } from \"@/lib/store/schema\";\nimport type {\n  FieldBuilder,\n  Schema,\n  SchemaDefinition,\n} from \"@/lib/store/schema\";\nimport { resolveColumns } from \"../col\";\nimport type { ColBuilder, TableSchemaDefinition } from \"../types\";\n\n/**\n * Extract keys from a TableSchemaDefinition where the column is filterable.\n * A column is filterable when its filter type `F` is not `never`.\n * Uses `[F] extends [never]` to avoid distribution issues with `any`.\n */\ntype FilterableKeys<T extends TableSchemaDefinition> = {\n  [K in keyof T]: T[K] extends ColBuilder<infer _T, infer F>\n    ? [F] extends [never]\n      ? never\n      : K\n    : never;\n}[keyof T];\n\n// Extract T and F separately — TS struggles to infer both at once from ColBuilder\ntype GetColValue<B> = B extends ColBuilder<infer T, any> ? T : never;\ntype GetColFilter<B> = B extends ColBuilder<any, infer F> ? F : never;\n\n/**\n * Map a single ColBuilder to the correct FieldBuilder type based on its\n * value type `T` and filter type `F`.\n */\ntype InferFilterFieldType<B> = [GetColFilter<B>] extends [never]\n  ? never\n  : GetColFilter<B> extends \"input\"\n    ? GetColValue<B> extends string\n      ? FieldBuilder<string | null>\n      : GetColValue<B> extends number\n        ? FieldBuilder<number | null>\n        : FieldBuilder<unknown>\n    : GetColFilter<B> extends \"slider\"\n      ? FieldBuilder<(number | null)[]>\n      : GetColFilter<B> extends \"timerange\"\n        ? FieldBuilder<(Date | null)[]>\n        : GetColFilter<B> extends \"checkbox\"\n          ? GetColValue<B> extends (infer U)[]\n            ? FieldBuilder<(U | null)[]>\n            : FieldBuilder<(GetColValue<B> | null)[]>\n          : FieldBuilder<unknown>;\n\n/** The generated filter definition — preserves the filterable keys from `T`. */\ntype GeneratedFilterDef<T extends TableSchemaDefinition> = {\n  [K in FilterableKeys<T>]: InferFilterFieldType<T[K]>;\n};\n\nfunction buildFilterDefinition(\n  schema: TableSchemaDefinition,\n): SchemaDefinition {\n  const definition: SchemaDefinition = {};\n\n  for (const config of resolveColumns(schema)) {\n    const { key, kind, filter } = config;\n    if (!filter) continue;\n\n    switch (filter.type) {\n      case \"input\": {\n        if (kind === \"string\") {\n          definition[key] = field.string();\n        } else if (kind === \"number\") {\n          definition[key] = field.number();\n        }\n        break;\n      }\n      case \"checkbox\": {\n        if (config.kind === \"enum\") {\n          definition[key] = field.array(field.stringLiteral(config.enumValues));\n        } else if (kind === \"number\") {\n          definition[key] = field\n            .array(field.number())\n            .delimiter(ARRAY_DELIMITER);\n        } else if (kind === \"boolean\") {\n          definition[key] = field\n            .array(field.boolean())\n            .delimiter(ARRAY_DELIMITER);\n        } else if (\n          config.kind === \"array\" &&\n          config.arrayItem.kind === \"enum\"\n        ) {\n          definition[key] = field.array(\n            field.stringLiteral(config.arrayItem.enumValues),\n          );\n        } else if (config.kind === \"array\") {\n          // Non-enum array item — filter on the item's own scalar type.\n          definition[key] =\n            config.arrayItem.kind === \"number\"\n              ? field.array(field.number()).delimiter(ARRAY_DELIMITER)\n              : field.array(field.string()).delimiter(ARRAY_DELIMITER);\n        }\n        break;\n      }\n      case \"slider\": {\n        definition[key] = field\n          .array(field.number())\n          .delimiter(SLIDER_DELIMITER);\n        break;\n      }\n      case \"timerange\": {\n        definition[key] = field\n          .array(field.timestamp())\n          .delimiter(RANGE_DELIMITER);\n        break;\n      }\n    }\n  }\n\n  return definition;\n}\n\n/**\n * Generate a BYOS filter schema from a table schema definition.\n *\n * Each filterable column maps to the appropriate field.* builder:\n * - col.string()  + input    → field.string()\n * - col.number()  + input    → field.number()\n * - col.number()  + slider   → field.array(field.number()).delimiter(SLIDER_DELIMITER)\n * - col.number()  + checkbox → field.array(field.number()).delimiter(ARRAY_DELIMITER)\n * - col.boolean() + checkbox → field.array(field.boolean()).delimiter(ARRAY_DELIMITER)\n * - col.timestamp()+ timerange→ field.array(field.timestamp()).delimiter(RANGE_DELIMITER)\n * - col.enum(v)   + checkbox → field.array(field.stringLiteral(v))\n * - col.array(col.enum(v)) + checkbox → field.array(field.stringLiteral(v))\n *\n * Non-filterable fields are excluded. Pass `extraFields` for pagination,\n * sorting, and other non-filter state.\n *\n * @example\n * ```ts\n * // Without extra fields (filter fields only)\n * const filterSchema = generateFilterSchema(tableSchema.definition);\n *\n * // With extra fields (recommended)\n * const filterSchema = generateFilterSchema(tableSchema.definition, {\n *   sort: field.sort(),\n *   uuid: field.string(),\n *   live: field.boolean().default(false),\n *   size: field.number().default(40),\n * });\n * ```\n */\nexport function generateFilterSchema<T extends TableSchemaDefinition>(\n  schema: T,\n): Schema<GeneratedFilterDef<T>>;\nexport function generateFilterSchema<\n  T extends TableSchemaDefinition,\n  E extends SchemaDefinition,\n>(schema: T, extraFields: E): Schema<GeneratedFilterDef<T> & E>;\nexport function generateFilterSchema<\n  T extends TableSchemaDefinition,\n  E extends SchemaDefinition,\n>(schema: T, extraFields?: E) {\n  const generated = buildFilterDefinition(schema);\n  const merged = extraFields ? { ...generated, ...extraFields } : generated;\n  return createSchema(merged);\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/table-schema/generators/sheet-fields.ts",
      "content": "import type { SheetField } from \"@/components/data-table/types\";\nimport { resolveColumns } from \"../col\";\nimport type { TableSchemaDefinition } from \"../types\";\n\n/**\n * Generate SheetField[] from a table schema definition.\n *\n * Only includes fields where sheet !== null (.sheet() was called).\n * Sheet type is derived from the filter type, or \"readonly\" if not filterable.\n * Sheet label falls back to the column label if not overridden in .sheet({ label }).\n */\nexport function generateSheetFields<TData>(\n  schema: TableSchemaDefinition,\n): SheetField<TData>[] {\n  const result: SheetField<TData>[] = [];\n\n  for (const config of resolveColumns(schema)) {\n    const sheetConfig = config.sheet;\n    if (sheetConfig === null) continue;\n\n    // Derive sheet type from filter type, or \"readonly\" if not filterable\n    const sheetType: SheetField<TData>[\"type\"] =\n      config.filter?.type ?? \"readonly\";\n\n    result.push({\n      id: config.key as keyof TData,\n      label: sheetConfig.label ?? config.label,\n      type: sheetType,\n      // The descriptor's display is always a real serializable display, even\n      // when a custom cell renderer was supplied — no per-kind fallback needed.\n      display: config.display,\n      component: config.renderers\n        .sheetComponent as SheetField<TData>[\"component\"],\n      condition: config.renderers\n        .sheetCondition as SheetField<TData>[\"condition\"],\n      className: sheetConfig.className,\n      skeletonClassName: sheetConfig.skeletonClassName,\n    });\n  }\n\n  return result;\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/components/data-table/data-table-auto.tsx",
      "content": "\"use client\";\n\nimport { DataTableFilterCommand } from \"@/components/data-table/data-table-filter-command\";\nimport { DataTableInfinite } from \"@/components/data-table/data-table-infinite\";\nimport { useDataTable } from \"@/components/data-table/data-table-provider\";\nimport { MemoizedDataTableSheetContent } from \"@/components/data-table/data-table-sheet/data-table-sheet-content\";\nimport { DataTableSheetDetails } from \"@/components/data-table/data-table-sheet/data-table-sheet-details\";\nimport type { SheetField } from \"@/components/data-table/types\";\nimport { useMemoryAdapter } from \"@/lib/store/adapters/memory\";\nimport { DataTableStoreProvider } from \"@/lib/store/provider/DataTableStoreProvider\";\nimport { field } from \"@/lib/store/schema\";\nimport type { SchemaDefinition } from \"@/lib/store/schema/types\";\nimport {\n  createTableSchema,\n  generateColumns,\n  generateFilterFields,\n  generateFilterSchema,\n  generateSheetFields,\n  getDefaultColumnVisibility,\n} from \"@/lib/table-schema\";\nimport { inferSchemaFromJSON } from \"@/lib/table-schema/infer\";\nimport * as React from \"react\";\n\ntype AutoRow = Record<string, unknown>;\n\nexport interface DataTableAutoProps {\n  data: AutoRow[];\n}\n\nexport function DataTableAuto({ data }: DataTableAutoProps) {\n  const schemaJson = React.useMemo(() => inferSchemaFromJSON(data), [data]);\n\n  const { definition } = React.useMemo(\n    () => createTableSchema.fromJSON(schemaJson),\n    [schemaJson],\n  );\n\n  const columns = React.useMemo(\n    () => generateColumns<AutoRow>(definition),\n    [definition],\n  );\n\n  const filterFields = React.useMemo(\n    () => generateFilterFields<AutoRow>(definition),\n    [definition],\n  );\n\n  const sheetFields = React.useMemo(\n    () => generateSheetFields<AutoRow>(definition),\n    [definition],\n  );\n\n  const defaultColumnVisibility = React.useMemo(\n    () => getDefaultColumnVisibility(definition),\n    [definition],\n  );\n\n  // Merge `sort` BEFORE createSchema so `defaults` cannot disagree with\n  // `definition` — splicing it in afterwards left `definition.sort` present\n  // while `defaults.sort` was missing.\n  const filterSchema = React.useMemo(\n    () => generateFilterSchema(definition, { sort: field.sort() }),\n    [definition],\n  );\n\n  const adapter = useMemoryAdapter(filterSchema.definition, { id: \"auto\" });\n\n  return (\n    <DataTableStoreProvider adapter={adapter}>\n      <DataTableAutoInner\n        data={data}\n        columns={columns}\n        filterFields={filterFields}\n        sheetFields={sheetFields}\n        defaultColumnVisibility={defaultColumnVisibility}\n        schema={filterSchema.definition}\n      />\n    </DataTableStoreProvider>\n  );\n}\n\nconst noop = () => Promise.resolve();\nconst noopRefetch = () => {};\n\nfunction DataTableAutoInner({\n  data,\n  columns,\n  filterFields,\n  sheetFields,\n  defaultColumnVisibility,\n  schema,\n}: {\n  data: AutoRow[];\n  columns: React.ComponentProps<typeof DataTableInfinite<AutoRow>>[\"columns\"];\n  filterFields: React.ComponentProps<\n    typeof DataTableInfinite<AutoRow>\n  >[\"filterFields\"];\n  sheetFields: SheetField<AutoRow>[];\n  defaultColumnVisibility: Record<string, boolean>;\n  schema: SchemaDefinition;\n}) {\n  return (\n    <DataTableInfinite\n      columns={columns}\n      data={data}\n      filterFields={filterFields}\n      defaultColumnVisibility={defaultColumnVisibility}\n      totalRowsFetched={data.length}\n      hasNextPage={false}\n      fetchNextPage={noop}\n      refetch={noopRefetch}\n      isFetching={false}\n      isLoading={false}\n      tableId=\"auto\"\n      commandSlot={<DataTableFilterCommand schema={schema} tableId=\"auto\" />}\n      sheetSlot={\n        <AutoSheetSlot sheetFields={sheetFields} totalRows={data.length} />\n      }\n      footerSlot={\n        <div className=\"text-muted-foreground text-sm\">\n          powered by{\" \"}\n          <a\n            href=\"https://openstatus.dev\"\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"text-foreground hover:text-primary\"\n          >\n            openstatus.dev\n          </a>\n        </div>\n      }\n    />\n  );\n}\n\nfunction AutoSheetSlot({\n  sheetFields: fields,\n  totalRows,\n}: {\n  sheetFields: SheetField<AutoRow>[];\n  totalRows: number;\n}) {\n  const { table, rowSelection, isLoading, filterFields } = useDataTable<\n    AutoRow,\n    unknown\n  >();\n  const selectedRowKey = Object.keys(rowSelection)?.[0];\n  const selectedRow = React.useMemo(() => {\n    if (isLoading && !selectedRowKey) return undefined;\n    return table\n      .getCoreRowModel()\n      .flatRows.find((row) => row.id === selectedRowKey);\n  }, [selectedRowKey, isLoading, table]);\n\n  return (\n    <DataTableSheetDetails\n      title={\n        selectedRow ? String(Object.values(selectedRow.original)[0] ?? \"\") : \"\"\n      }\n      titleClassName=\"font-mono\"\n    >\n      <MemoizedDataTableSheetContent\n        table={table}\n        data={selectedRow?.original}\n        filterFields={filterFields}\n        fields={fields}\n        metadata={{\n          totalRows,\n          filterRows: totalRows,\n          totalRowsFetched: totalRows,\n        }}\n      />\n    </DataTableSheetDetails>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "src/hooks/use-table-manifest.ts",
      "content": "\"use client\";\n\nimport {\n  fetchTableManifest,\n  type FetchManifestOptions,\n  type TableManifest,\n} from \"@/lib/table-schema\";\nimport { useQuery, type UseQueryResult } from \"@tanstack/react-query\";\n\n/**\n * Load a table's manifest.\n *\n * ## Why this is separate from the data query\n *\n * The manifest cannot be fetched alongside the rows, because everything that\n * fetches rows is *built from* it: the columns, the filter fields, and — the\n * binding constraint — the URL-state adapter, whose parsers are derived from\n * the schema. `useNuqsAdapter(schema, …)` needs a schema at first render, and a\n * hook cannot be called conditionally, so the manifest has to resolve one level\n * up from anything that consumes it.\n *\n * That is why the headless table is two components: an outer one that resolves\n * the manifest, and an inner one that is only mounted once it has. This hook is\n * the outer half.\n *\n * ## Avoiding the waterfall\n *\n * A round trip for the manifest before the first row request is a real cost.\n * Two ways out, both supported by `initialManifest`:\n *\n * - **Snapshot at build time.** Check the manifest into the app and pass it.\n *   The query still revalidates in the background, so a schema change on the\n *   server is picked up without a redeploy, but nothing blocks on it.\n * - **Prefetch on the server.** `fetchTableManifest` on the server component and\n *   hand the result down, or seed the React Query cache under `tableManifestKey`.\n *\n * With `initialManifest` the hook never suspends and the table renders on the\n * first paint.\n */\n\n/** The query key a manifest is cached under. Exported so a server can seed it. */\nexport function tableManifestKey(endpoint: string) {\n  return [\"data-table-manifest\", endpoint] as const;\n}\n\nexport type UseTableManifestOptions = FetchManifestOptions & {\n  /**\n   * A manifest that is already known — a build-time snapshot, or one prefetched\n   * on the server. Used as the initial value; the query still revalidates.\n   */\n  initialManifest?: TableManifest;\n  /**\n   * How long a manifest stays fresh. Schemas change on deploy, not per request,\n   * so this is long by default.\n   */\n  staleTime?: number;\n  /** Set false to render only from `initialManifest` and never fetch. */\n  enabled?: boolean;\n  /**\n   * Retries before the query gives up. Defaults to 1: a malformed manifest is\n   * not transient — retrying cannot repair the endpoint's answer — but a single\n   * retry still covers a dropped connection.\n   */\n  retry?: number | boolean;\n};\n\nconst FIVE_MINUTES = 1000 * 60 * 5;\n\nexport function useTableManifest(\n  endpoint: string,\n  options?: UseTableManifestOptions,\n): UseQueryResult<TableManifest, Error> {\n  const {\n    initialManifest,\n    staleTime = FIVE_MINUTES,\n    enabled = true,\n    retry = 1,\n    ...fetchOptions\n  } = options ?? {};\n\n  return useQuery<TableManifest, Error>({\n    queryKey: tableManifestKey(endpoint),\n    queryFn: ({ signal }) =>\n      fetchTableManifest(endpoint, { ...fetchOptions, signal }),\n    ...(initialManifest ? { initialData: initialManifest } : {}),\n    enabled,\n    staleTime,\n    retry,\n    refetchOnWindowFocus: false,\n  });\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:block"
}
