generated from coulomb/repo-seed
T01 EvidenceLink/EvidenceSet types
- src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target
- src/shared/evidence-set.ts: ordered group + activeEvidenceItemId
- enum-conformance test parses SharedContracts.md and asserts the
runtime lists match exactly
T02 Binding service + in-memory link repo + active-state machine
- src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository
- src/binder/services/bindings.ts: link/unlink/list/update/setActive
emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated /
EvidenceItemActivated
- src/binder/state/active.ts: (target, evidence, annotation) reducer
+ ActiveStateProvider + useActiveState hook
- extended engine/events/types.ts with EvidenceLinkCreated,
EvidenceLinkUpdated, FormFieldActivated payloads
T03 Rect registry (SharedContracts §7 — contract FROZEN)
- src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe
+ invalidate + getVersion for useSyncExternalStore
- events.ts: scroll/resize/focus pumps via window + ResizeObserver +
IntersectionObserver, rAF-throttled
- react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref),
useRectRegistryVersion
T04 Form schema + renderer
- src/app/forms/demo-schema.ts: text/textarea/date minimal schema
- src/binder/FormRenderer.tsx: renders schema, each field registers
as rect kind="field"; active field gets aria-current="true"
- placed in binder/ (not work/) because work cannot import binder per
DependencyMap.md §2 and the renderer needs the rect-registry hook;
workplan T04 was amended in-place to document this
T05 Side-by-side Forms layout + click-to-link
- src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with
hash route #/forms/demo
- BinderProvider mounted at app root so links survive tab switching
- stage-evidence-then-click-field linking interaction with banner
+ per-field link-count chip
T06 Active-evidence cycling
- src/app/forms/ActiveEvidenceChips.tsx: chips per active target,
Tab cycles natively, first chip auto-activates on field focus,
each chip registers as rect kind="evidence-card"
- ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll
- EvidenceSidebar + EvidenceStrip highlight the active item via the
new useLastActivatedEvidence hook in work/EngineContext
T07 SVG visual-guide overlay
- src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG,
draws field→card and card→highlight bezier curves for the active
triple, rAF-throttled via the registry
- src/anchor exposes getHighlightClientRects(annotationId); the
spike viewer wraps highlights in [data-highlight-id] so the helper
can locate them
- src/app/forms/HighlightRectBridge.tsx: registers the active
annotation's rect via that helper
T08 End-to-end test (PRD scenario steps 5-9)
- tests/integration/forms-overlay-e2e.dom.test.tsx: full path from
Review-mode capture through Forms-mode link to active triple +
aria-current assertions + 2 SVG paths in the overlay
- additional integration coverage: forms-link-flow + forms-active-cycling
Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
134 lines
4.2 KiB
TypeScript
134 lines
4.2 KiB
TypeScript
/**
|
||
* Rect registry — the contract from `wiki/SharedContracts.md` §7.
|
||
*
|
||
* Three subsystems publish DOMRects into a single registry so the SVG
|
||
* visual-guide overlay can draw curves between them:
|
||
*
|
||
* - "field" — a form field (registered by FormRenderer)
|
||
* - "evidence-card" — a sidebar entry (registered by EvidenceSidebar / similar)
|
||
* - "highlight" — a viewer highlight (registered by the PDF adapter)
|
||
*
|
||
* The overlay subscribes once and queries `getRect` on demand each frame.
|
||
* **The contract MUST NOT change after CE-WP-0003-T03 lands** — that's the
|
||
* whole point of pinning it now in §7. Future kinds (HTML-doc-section,
|
||
* comment-anchor, …) are added by extending the `RectKind` union here AND
|
||
* the doc together.
|
||
*/
|
||
|
||
export type RectKind = "field" | "evidence-card" | "highlight";
|
||
|
||
export interface RectRegistryRegistration {
|
||
readonly kind: RectKind;
|
||
readonly id: string;
|
||
readonly getRect: () => DOMRect | null;
|
||
}
|
||
|
||
/**
|
||
* A registry-change signal. Listeners typically don't care which
|
||
* specific rect changed — they re-query everything they're tracking —
|
||
* but the discriminator is kept so optimised consumers can filter.
|
||
*
|
||
* `rect-changed` with an undefined `kind`+`id` means "the registry is
|
||
* advising you that *something* may have moved" (scroll, resize). That
|
||
* is the dominant event in practice.
|
||
*/
|
||
export type RectRegistryEvent =
|
||
| { readonly type: "registered"; readonly kind: RectKind; readonly id: string }
|
||
| { readonly type: "unregistered"; readonly kind: RectKind; readonly id: string }
|
||
| {
|
||
readonly type: "rect-changed";
|
||
readonly kind?: RectKind;
|
||
readonly id?: string;
|
||
};
|
||
|
||
export interface RectRegistry {
|
||
register(
|
||
kind: RectKind,
|
||
id: string,
|
||
getRect: () => DOMRect | null,
|
||
): () => void;
|
||
getRect(kind: RectKind, id: string): DOMRect | null;
|
||
subscribe(listener: (event: RectRegistryEvent) => void): () => void;
|
||
/**
|
||
* Imperative invalidation. Useful for the overlay's own
|
||
* active-evidence-change trigger (which isn't observable via DOM
|
||
* observers). Emits a `rect-changed` with no kind/id.
|
||
*/
|
||
invalidate(): void;
|
||
/**
|
||
* Monotonically-increasing counter that bumps once per emit. Lets React
|
||
* consumers use `useSyncExternalStore` without per-snapshot subscriber
|
||
* leaks — the hook only adds one subscriber and reads `getVersion()`
|
||
* for the snapshot.
|
||
*/
|
||
getVersion(): number;
|
||
/**
|
||
* Debug-only — enumerate everything currently registered. Used in tests
|
||
* and not part of the §7 contract; safe to use from `app/`.
|
||
*/
|
||
list(): readonly { kind: RectKind; id: string }[];
|
||
}
|
||
|
||
function keyOf(kind: RectKind, id: string): string {
|
||
return `${kind} |