/** * Highlights as atomic notes (RCL-04) — the ingest half. * * The extension writes `highlights.json` into the capture envelope beside * `readable.md` (chrome-extension/highlights.js). This module turns each * entry into a NOTE IN ITS OWN RIGHT that points back at the document it * came from — searchable on its own terms, not buried inside a chunk of * the page it was taken from. * * WHY A CHILD NOTE AND NOT A TAG ON THE PARENT. A highlight is a claim * about what mattered, made by the reader, at a moment. Folding it into the * parent's chunks loses all three: it stops being separately findable, it * stops carrying its own comment, and it stops having a date. So each one * is stored as its own entry, keyed under the parent's content hash, and * linked back by `parent:` and `url:` tags. * * THE ANCHOR is the careful part. The extension measured its offsets * against the LIVE PAGE'S TEXT; what got indexed is `readable.md`, a * Readability pass over that same DOM. The two are close and not identical. * So the offsets that arrive are treated as a hint and the QUOTE is treated * as the truth: `locate` re-locates the quote in the extracted text and * cuts a real citation anchor (`#-`, citation.ts's * shape) against the version it actually found. A highlight whose text is * not in the document at all gets NO anchor rather than a plausible wrong * one — the same rule citation.ts applies to a stale anchor, for the same * reason. * * Everything here is as defensive as capture-envelope.ts, and for the same * reason: `highlights.json` was written by a browser extension on a machine * we do not control, and a malformed one must cost the capture nothing. * * @module v1/cli/knowledge/highlights */ import type { CaptureProvenance } from './capture-envelope.js'; export declare const HIGHLIGHTS_FILE = "highlights.json"; /** Tag namespaces, so a highlight is findable as one and traceable to its * parent without parsing anything. */ export declare const HIGHLIGHT_TAG = "highlight"; export declare const PARENT_TAG_PREFIX = "parent:"; export declare const URL_TAG_PREFIX = "url:"; /** The anchor as the extension measured it — offsets against the page's * visible text, plus the quote and its neighbourhood. */ export interface HighlightAnchor { /** A HINT. Measured against the live page, not the extracted text. */ startChar?: number; endChar?: number; /** The truth. What the reader actually selected. */ quote: string; prefix?: string; suffix?: string; /** W3C `:~:text=` fragment, as citation.ts builds one. */ fragment?: string; } export interface Highlight { id: string; text: string; anchor: HighlightAnchor; createdAt?: string; comment?: string; color?: string; /** The page URL pointed at this passage, as the extension wrote it. */ url?: string; } /** One highlight, resolved against the document it belongs to. */ export interface HighlightNote { id: string; /** `:` — the parent document's own id. */ parentDocId: string; parentPath: string; scope: string; /** The parent's identity URL. */ parentUrl?: string; parentTitle?: string; text: string; quote: string; comment?: string; createdAt?: string; color?: string; /** `#-` against the EXTRACTED text — present only * when the quote was actually found there. */ anchor?: string; startChar?: number; endChar?: number; /** The link that lands on the passage in the live page. */ citeUrl?: string; capturedAt?: string; /** What gets stored and embedded: the highlight, its note, its source. */ content: string; tags: string[]; } /** Keep only what we understand, coerced. An entry with no text is not a * highlight and is dropped rather than carried through as an empty note. */ export declare function normalizeHighlight(raw: unknown, index?: number): Highlight | null; /** * readHighlights reads the `highlights.json` sitting in `dir`. Returns an * empty list — never throws — when there is none, it is unreadable, * oversized, not JSON, or the wrong shape entirely. */ export declare function readHighlights(dir: string): Highlight[]; /** readHighlightsFor reads the highlights belonging to an indexed file, * i.e. the ones in its envelope directory. */ export declare function readHighlightsFor(filePath: string): Highlight[]; /** * locate finds a highlight's quote in the extracted text and returns its * span there, or null. * * The order is the order of trustworthiness, and it is the SAME order the * extension's own restore path uses (chrome-extension/highlights.js * `relocate`), so a highlight that can be painted back onto the page can * also be cited out of the archive: * * 1. prefix + quote + suffix — the neighbourhood, which survives the * Readability pass dropping a nav bar above it; * 2. the occurrence nearest the recorded offset, for a quote that appears * more than once; * 3. nothing. A quote that is not in the document gets no anchor. It is * still stored as a note — the reader did read it — but a citation * that cannot be checked is not offered as if it could. */ export declare function locate(text: string, anchor: HighlightAnchor): { startChar: number; endChar: number; } | null; export interface ParentDocument { filePath: string; scope: string; /** Hash of the EXTRACTED text — the same one the anchor binds to. */ contentHash: string; /** The extracted text itself, when the caller has it. Without it, no * anchor can be cut and the notes are stored unanchored. */ text?: string; canonicalUrl?: string; provenance?: CaptureProvenance; } /** * highlightNotes turns an envelope's highlights into notes against a parent * document. Pure: it reads nothing and writes nothing, so it is the whole * of the interesting behaviour and the whole of what is tested. */ export declare function highlightNotes(parent: ParentDocument, highlights: readonly Highlight[]): HighlightNote[]; export interface IngestHighlightsResult { /** How many notes were stored. */ stored: number; /** How many were found in the envelope at all. */ found: number; /** How many resolved to a citable span in the extracted text. */ anchored: number; notes: HighlightNote[]; error?: string; } /** The storage key for one highlight. Keyed under the PARENT'S content * hash, so re-ingesting the same version upserts the same notes rather * than accumulating a copy per ingest — the same discipline as the * pipeline's `doc::` chunk keys. */ export declare function highlightKey(parentContentHash: string, id: string): string; /** * ingestHighlights stores an envelope's highlights as notes linked to the * parent document. * * Reports rather than throws: a capture whose highlights could not be * stored is still a perfectly good capture, and failing the whole ingest * over the annotations would lose the page as well as the notes. */ export declare function ingestHighlights(parent: ParentDocument, opts?: { highlights?: readonly Highlight[]; dbPath?: string; }): Promise; //# sourceMappingURL=highlights.d.ts.map