{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "segments",
  "title": "Segments model",
  "description": "Shared model + helpers for page-segmented results (partition/split): Segment type, Tableau palette, color map, page-range and confidence helpers.",
  "files": [
    {
      "path": "registry/new-york-v4/lib/segments.ts",
      "content": "/**\n * Shared model for page-segmented document results — Retab \"partition\" (keyed\n * chunks) and \"split\" (named subdocuments). Both reduce to the same `Segment[]`:\n * a label, the pages it owns, a deterministic color, and optional confidence.\n *\n * The legend, sidebar, and timeline primitives all consume `Segment[]`, so a\n * partition and a split render through the exact same components.\n */\n\n// Tableau 20 — the palette Retab's dashboard uses, so colors match.\nexport const SEGMENT_PALETTE = [\n  \"#4E79A7\",\n  \"#A0CBE8\",\n  \"#F28E2B\",\n  \"#FFBE7D\",\n  \"#59A14F\",\n  \"#8CD17D\",\n  \"#B6992D\",\n  \"#F1CE63\",\n  \"#499894\",\n  \"#86BCB6\",\n  \"#E15759\",\n  \"#FF9D9A\",\n  \"#79706E\",\n  \"#BAB0AC\",\n  \"#D37295\",\n  \"#FABFD2\",\n  \"#B07AA1\",\n  \"#D4A6C8\",\n  \"#9D7660\",\n  \"#D7B5A6\",\n] as const;\n\nexport interface Segment {\n  /** Stable id (label + occurrence, since a split name can repeat). */\n  id: string;\n  /** Display label — the partition key or the subdocument name. */\n  label: string;\n  /** Sorted, de-duplicated 1-based pages owned by this segment. */\n  pages: number[];\n  /** Deterministic color (by label, so the same label is always one color). */\n  color: string;\n  /** Render order. */\n  index: number;\n  /** Optional consensus confidence in [0, 1]. */\n  confidence?: number | null;\n}\n\nexport interface SegmentChunk {\n  key?: string;\n  name?: string;\n  pages: number[];\n}\n\nexport function segmentDisplayLabel(label: string): string {\n  const trimmed = typeof label === \"string\" ? label.trim() : \"\";\n  return trimmed || \"unnamed\";\n}\n\nexport function normalizePageCount(value: number): number {\n  return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;\n}\n\n/** Assign one palette color per distinct display label, ordered by sorted label. */\nexport function buildColorMap(labels: string[]): Map<string, string> {\n  const distinct = Array.from(new Set(labels.map(segmentDisplayLabel))).sort(\n    (a, b) => a.localeCompare(b),\n  );\n  const map = new Map<string, string>();\n  distinct.forEach((label, i) => {\n    map.set(label, SEGMENT_PALETTE[i % SEGMENT_PALETTE.length]);\n  });\n  return map;\n}\n\nfunction normalizePages(pages: number[]): number[] {\n  const rawPages = Array.isArray(pages) ? pages : [];\n  return Array.from(\n    new Set(rawPages.filter((p) => Number.isInteger(p) && p > 0)),\n  ).sort((a, b) => a - b);\n}\n\nexport function segmentPageCount(pages: number[]): number {\n  return normalizePages(pages).length;\n}\n\nexport function firstSegmentPage(pages: number[]): number | null {\n  return normalizePages(pages)[0] ?? null;\n}\n\n/** Normalize partition/split output into the shared `Segment[]` model. */\nexport function toSegments(\n  output: SegmentChunk[] | null | undefined,\n  confidences?: (number | null | undefined)[],\n  /** Reuse a shared color map (e.g. so partition votes match the consensus). */\n  colorOverride?: Map<string, string>,\n): Segment[] {\n  if (!output) return [];\n  const labels = output.map((c) => c.key ?? c.name ?? \"\");\n  const colors = colorOverride ?? buildColorMap(labels);\n  return output.map((chunk, index) => {\n    const label = labels[index];\n    return {\n      id: `${label}#${index}`,\n      label,\n      pages: normalizePages(chunk.pages),\n      color:\n        colors.get(label) ??\n        colors.get(segmentDisplayLabel(label)) ??\n        \"#888888\",\n      index,\n      confidence: normalizeConfidence(confidences?.[index]),\n    };\n  });\n}\n\nfunction normalizeConfidence(value: number | null | undefined): number | null {\n  return value != null && Number.isFinite(value)\n    ? Math.max(0, Math.min(1, value))\n    : null;\n}\n\n/** Total page count implied by a set of segments (max page seen). */\nexport function segmentsPageCount(segments: Segment[]): number {\n  let max = 0;\n  for (const s of segments) {\n    for (const p of s.pages) {\n      if (Number.isInteger(p) && p > 0) max = Math.max(max, p);\n    }\n  }\n  return max;\n}\n\n/** Map every 1-based page to the segment indexes that own it (handles overlap). */\nexport function pageOwners(segments: Segment[]): Map<number, number[]> {\n  const owners = new Map<number, number[]>();\n  segments.forEach((segment) => {\n    segment.pages.forEach((page) => {\n      if (!Number.isInteger(page) || page <= 0) return;\n      const list = owners.get(page) ?? [];\n      list.push(segment.index);\n      owners.set(page, list);\n    });\n  });\n  return owners;\n}\n\n/** Collapse a page list into contiguous runs, e.g. [1,2,3,5] -> [[1,3],[5,5]]. */\nexport function buildPageRuns(pages: number[]): Array<[number, number]> {\n  const sorted = normalizePages(pages);\n  if (sorted.length === 0) return [];\n  const runs: Array<[number, number]> = [];\n  let start = sorted[0];\n  let end = sorted[0];\n  for (let i = 1; i < sorted.length; i++) {\n    if (sorted[i] === end + 1) {\n      end = sorted[i];\n    } else {\n      runs.push([start, end]);\n      start = sorted[i];\n      end = sorted[i];\n    }\n  }\n  runs.push([start, end]);\n  return runs;\n}\n\n/** Format a page list as compact ranges, e.g. [1,2,3,5] -> \"1–3, 5\". */\nexport function formatPageRanges(pages: number[]): string {\n  const runs = buildPageRuns(pages);\n  if (runs.length === 0) return \"—\";\n  return runs.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(\", \");\n}\n\nexport type ConfidenceLevel = \"high\" | \"medium\" | \"low\";\n\nexport function confidenceLevel(\n  value: number | null | undefined,\n): ConfidenceLevel | null {\n  if (value == null || !Number.isFinite(value)) return null;\n  if (value >= 0.9) return \"high\";\n  if (value >= 0.7) return \"medium\";\n  return \"low\";\n}\n\n/** Average a per-page likelihood array into a single segment confidence. */\nexport function meanConfidence(values: number[] | undefined): number | null {\n  const finiteValues = values?.filter((value) => Number.isFinite(value)) ?? [];\n  if (finiteValues.length === 0) return null;\n  return finiteValues.reduce((a, b) => a + b, 0) / finiteValues.length;\n}\n",
      "type": "registry:lib",
      "target": "@lib/segments.ts"
    }
  ],
  "type": "registry:lib"
}