/** * proposalModel — pure helpers over the F4 `proposals[]` section (spec §6.3, * INV-13: pending-only). Mirrors threadModel.ts: vscode-free mutations against * an Artifact, used inside CoauthorStore.update() by ProposalController. * The diff body is PRESENTATION ONLY — the truth is fingerprint.text → * replacement (INV-11); a whole-range -/+ rendering, not an LCS diff (§6.7). */ import { newId, type Artifact, type Fingerprint, type Proposal, type Provenance } from "./model"; export function addProposal( artifact: Artifact, fp: Fingerprint, replacement: string, author: Provenance, opts?: { turnId?: string; instruction?: string; granularity?: "block" | "single" }, ): { proposalId: string; anchorId: string } { const anchorId = newId("a"); const proposalId = newId("pr"); artifact.anchors[anchorId] = { fingerprint: fp }; artifact.proposals.push({ id: proposalId, anchorId, replacement, author, createdAt: new Date().toISOString(), ...(opts?.turnId !== undefined ? { turnId: opts.turnId } : {}), ...(opts?.instruction !== undefined ? { instruction: opts.instruction } : {}), ...(opts?.granularity !== undefined ? { granularity: opts.granularity } : {}), }); return { proposalId, anchorId }; } /** Remove a pending proposal (accept and reject both end here — INV-13). */ export function removeProposal(artifact: Artifact, proposalId: string): boolean { const before = artifact.proposals.length; artifact.proposals = artifact.proposals.filter((p) => p.id !== proposalId); return artifact.proposals.length < before; } /** * F12/#64 (INV-48): record a proposal as optimistically applied — store the * pre-apply `original` (for revert + the struck ``) and re-point its anchor * fingerprint to the now-in-buffer applied text so `resolve()` finds it. Idempotent * shape: a second call simply overwrites with the same values. */ export function setProposalApplied( artifact: Artifact, proposalId: string, appliedFp: Fingerprint, original: string, ): boolean { const p = artifact.proposals.find((x) => x.id === proposalId); if (!p) return false; p.original = original; artifact.anchors[p.anchorId] = { fingerprint: appliedFp }; return true; } /** Markdown comment body: instruction header + fenced whole-range diff. */ export function proposalBody(targetText: string, p: Proposal): string { const header = p.instruction ? `**Claude proposes** — _${p.instruction}_` : "**Claude proposes**"; const diffLines = [ ...targetText.split("\n").map((l) => `- ${l}`), ...p.replacement.split("\n").map((l) => `+ ${l}`), ].join("\n"); return `${header}\n\n\`\`\`diff\n${diffLines}\n\`\`\`\n\n✓ Accept applies this replacement · ✗ Reject discards it`; }