--- id: EANCH-WP-0002 type: workplan title: "Anchor resolution hardening: stale/orphan semantics, fuzzy re-anchoring, production PDF adapter" domain: infotech repo: evidence-anchor status: proposed owner: codex topic_slug: citation_evidence_mvp created: "2026-07-08" updated: "2026-07-08" spec_refs: - INTENT.md - SCOPE.md - ../citation-evidence/wiki/SharedContracts.md - ../citation-evidence/wiki/ArchitectureOverview.md --- # EANCH-WP-0002 — Anchor Resolution Hardening ## Goal Close the resolution-quality gaps left open by the MVP extraction (`EANCH-WP-0001`) on the **existing PDF/text path**. The extracted slice resolves exact position/quote/PDF selectors with a fixed confidence ladder and returns `resolved | ambiguous | unresolved`. `INTENT.md` promises more: `stale` detection, orphaned-annotation handling, fuzzy/recovery re-anchoring, and human-confirmation workflows for ambiguity. This workplan makes those real without changing the shared selector *types* (those stay in `citation-engine`). Non-goals: HTML/Markdown selectors (see `EANCH-WP-0003`); new persistence or UI shell behavior. ## Background Current extracted behavior (`src/selectors/resolve.ts`): - confidence ladder 1.00 → 0.70 across TextPosition/TextQuote/PdfPageText/PdfRect - multiple undisambiguated quote matches → `ambiguous` at 0.5 - nothing matches → `unresolved` at 0.0 - there is **no** `stale` path (the `AnnotationResolutionStatus` enum includes `stale`, but resolve.ts never returns it) and **no** fuzzy matching ## Task Breakdown ## T01 — Stale vs. unresolved: distinguish "representation changed" from "never found" ```task id: EANCH-WP-0002-T01 status: todo priority: high ``` Return `stale` (not `unresolved`) when a previously-resolvable anchor no longer matches because the representation's canonical text changed, while the stored quote is still meaningful. Define the signal (e.g. position selector points in-range but slice ≠ quote AND quote no longer found) and preserve the original quote/context on the result. Add unit tests covering stale vs. unresolved. ## T02 — Orphaned-annotation detection surface ```task id: EANCH-WP-0002-T02 status: todo priority: medium depends_on: [T01] ``` Give callers a way to identify annotations whose selectors can no longer be placed at all (orphaned) versus stale-but-recoverable. Decide whether this is a distinct status, a flag on `AnchorResolution`, or a helper. Keep the shared enum unchanged unless a contract change is agreed in the umbrella wiki first. ## T03 — Fuzzy / recovery re-anchoring ```task id: EANCH-WP-0002-T03 status: todo priority: high depends_on: [T01] ``` Add a fuzzy quote-matching fallback (bounded edit-distance / token overlap) below the exact ladder, returning candidates at appropriately low confidence with `ambiguous`/`stale` status. Must never silently emit a confident wrong match (INTENT: "No Silent Misleading Match"). Add tests with lightly-edited source text. Confirm the ladder's exact tiers are untouched above the fuzzy floor. ## T04 — Promote the PDF spike to a production adapter ```task id: EANCH-WP-0002-T04 status: todo priority: medium depends_on: [T03] ``` Turn `PdfSpikeViewer` into a supported `PDFViewerAdapter` that implements the `DocumentViewerAdapter` contract cleanly (remove spike-only debug flags from the public surface, or gate them), keep viewer libs behind `src/pdf/`, and keep the umbrella green through the change. Update `evidence-anchor/pdf` exports and docs. ## T05 — Verification + close-out ```task id: EANCH-WP-0002-T05 status: todo priority: high depends_on: [T02, T04] ``` `pnpm test`/`typecheck`/`lint` green here; umbrella typecheck/test/build green after any contract-visible change; `fix-consistency` clean; progress note.