/** * Binding service — links EvidenceItems to structured targets. * * Implements `wiki/ArchitectureOverview.md` §4.6 + SharedContracts §2.4 * (status enum), §2.5 (relation enum). Emits §4 events: * `EvidenceLinkCreated`, `EvidenceLinkUpdated`, `EvidenceItemActivated`. * * MVP semantics: * - `linkEvidenceToTarget` defaults `relation="supports"`, `status="candidate"`. * - `unlinkEvidence` is hard-delete; the rejected-status path is left to * a later ADR. * - `setActiveEvidence` emits an `EvidenceItemActivated` event with * `source="form-field"` so the viewer/sidebar can react. */ import type { EvidenceLink, EvidenceLinkStoredStatus, EvidenceRelation, EvidenceTarget, } from "@citation-evidence/engine/shared"; import type { EvidenceItemId, EvidenceLinkId } from "@citation-evidence/engine/shared"; import { newId } from "@citation-evidence/engine/shared"; import type { EventBus } from "@citation-evidence/engine"; import type { EvidenceLinkRepository } from "../repos/in-memory-links"; export interface LinkEvidenceToTargetInput { readonly evidenceItemId: EvidenceItemId; readonly target: EvidenceTarget; readonly relation?: EvidenceRelation; readonly status?: EvidenceLinkStoredStatus; readonly confidence?: number; readonly createdBy?: string; } export interface UpdateLinkStatusInput { readonly status?: EvidenceLinkStoredStatus; readonly relation?: EvidenceRelation; readonly confidence?: number; } export interface BindingService { linkEvidenceToTarget(input: LinkEvidenceToTargetInput): EvidenceLink; unlinkEvidence(id: EvidenceLinkId): boolean; updateLink(id: EvidenceLinkId, input: UpdateLinkStatusInput): EvidenceLink; getLink(id: EvidenceLinkId): EvidenceLink | null; listEvidenceForTarget(target: EvidenceTarget): readonly EvidenceLink[]; listTargetsForEvidence(evidenceItemId: EvidenceItemId): readonly EvidenceLink[]; setActiveEvidence(evidenceItemId: EvidenceItemId): void; } export function createBindingService( links: EvidenceLinkRepository, bus: EventBus, now: () => string = () => new Date().toISOString(), ): BindingService { return { linkEvidenceToTarget(input) { const ts = now(); const link: EvidenceLink = { id: newId("evidence-link"), evidenceItemId: input.evidenceItemId, targetType: input.target.targetType, targetId: input.target.targetId, relation: input.relation ?? "supports", status: input.status ?? "candidate", ...(input.confidence !== undefined ? { confidence: input.confidence } : {}), ...(input.createdBy !== undefined ? { createdBy: input.createdBy } : {}), createdAt: ts, updatedAt: ts, }; const stored = links.create(link); bus.emit({ type: "EvidenceLinkCreated", linkId: stored.id, link: stored }); return stored; }, unlinkEvidence(id) { const removed = links.delete(id); if (removed) { bus.emit({ type: "EvidenceLinkRemoved", linkId: id }); } return removed; }, updateLink(id, input) { const existing = links.get(id); if (!existing) { throw new Error(`BindingService.updateLink: unknown id ${id}`); } const next: EvidenceLink = { ...existing, ...(input.status !== undefined ? { status: input.status } : {}), ...(input.relation !== undefined ? { relation: input.relation } : {}), ...(input.confidence !== undefined ? { confidence: input.confidence } : {}), updatedAt: now(), }; const stored = links.update(next); bus.emit({ type: "EvidenceLinkUpdated", linkId: stored.id, link: stored }); return stored; }, getLink(id) { return links.get(id); }, listEvidenceForTarget(target) { return links.listForTarget(target); }, listTargetsForEvidence(evidenceItemId) { return links.listForEvidenceItem(evidenceItemId); }, setActiveEvidence(evidenceItemId) { bus.emit({ type: "EvidenceItemActivated", evidenceItemId, source: "form-field", }); }, }; }