import { anchorIndexAt } from "@/lib/model/locus" import type { DocumentModel, HeadingNode } from "@/lib/schemas/model" /** * The AI request envelope (MVP §9, DATA-ARCHITECTURE "Request envelope"): the * context an AI call carries alongside the normalized quote — its full containing * paragraph and the heading breadcrumbs for its position — plus the nearest * preceding locus anchor (the cache row's `locusAnchor`, for the §12 locus index). * Pure functions of the Tier-1 model; no DOM. */ export interface Envelope { /** The full text of the block containing the offset (paragraph or heading). */ paragraph: string /** Heading labels from the outermost down to the active one (Work › … › §). */ breadcrumbs: string[] /** Nearest preceding anchor id, "" if the text has none. */ locusAnchor: string } function pathToHeading( nodes: readonly HeadingNode[], offset: number, trail: HeadingNode[], ): HeadingNode[] { // The active child at this level is the last heading whose label starts at or // before the offset; descend into it to extend the breadcrumb trail. let active: HeadingNode | null = null for (const n of nodes) { if (n.start <= offset) active = n else break } if (!active) return trail trail.push(active) return pathToHeading(active.children, offset, trail) } export function buildEnvelope(model: DocumentModel, offset: number): Envelope { const block = model.blocks.find((b) => b.start <= offset && offset < b.end) const paragraph = block ? model.text.slice(block.start, block.end) : "" const breadcrumbs = pathToHeading(model.headings, offset, []).map((h) => h.label) const idx = anchorIndexAt(model, offset) const locusAnchor = idx >= 0 ? (model.anchors[idx]?.id ?? "") : "" return { paragraph, breadcrumbs, locusAnchor } }