/** * PendingEditRegistry — the seam's bookkeeping (spec §6.2, INV-9). Before * applyAgentEdit applies a WorkspaceEdit, it registers the exact expected * change; the controller's change handler consumes a matching registration to * attribute that change to the agent. Anything unmatched is human typing. * vscode-free and unit-testable. */ import type { Provenance } from "./model"; export interface PendingEdit { docPath: string; /** half-open [start, end) offsets of the replaced range. */ start: number; end: number; newText: string; provenance: Provenance; turnId?: string; } export class PendingEditRegistry { private pending: PendingEdit[] = []; register(edit: PendingEdit): void { this.pending.push(edit); } /** * Remove a registration that failed to apply. Identity-based (`===`), so * callers must keep the exact registered object; it is a no-op when `match` * already consumed the registration. */ unregister(edit: PendingEdit): void { this.pending = this.pending.filter((p) => p !== edit); } /** * Find-and-consume the registration exactly matching a change event * (same doc, same replaced range, same inserted text). Null → human edit. */ match(docPath: string, change: { start: number; end: number; text: string }): PendingEdit | null { const i = this.pending.findIndex( (p) => p.docPath === docPath && p.start === change.start && p.end === change.end && p.newText === change.text, ); if (i === -1) return null; const [hit] = this.pending.splice(i, 1); return hit; } }