Extract binder package from citation-evidence (EBIND-WP-0001)
Extracts citation-evidence/src/binder/ into this repo as the standalone @citation-evidence/evidence-binder package: headless binding service + in-memory link repo, active-state machine, the SharedContracts §7 rect-registry contract (registry, change pumps, hooks, SVG overlay), and the target-neutral reference FormRenderer. - toolchain mirrors sibling extracted repos (pnpm/tsc/vitest/eslint); imports rewritten from @shared/@engine aliases to the engine's @citation-evidence/engine package specifiers - dependency boundary (engine + anchor only; no source/work/umbrella) enforced via eslint no-restricted-imports - docs: extraction inventory + contract deltas, ADR-0001 (reference UI kept as supported exports), refreshed README/SCOPE/INTENT, populated capabilities index - typecheck + lint green, 37 tests passing Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
87d4eb3006
commit
a10f080f12
36 changed files with 6643 additions and 170 deletions
118
src/services/bindings.ts
Normal file
118
src/services/bindings.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* 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",
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue