Implement CE-WP-0003 T01-T08: form binding + visual guide overlay
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>
2026-05-25 15:53:17 +02:00
|
|
|
/**
|
|
|
|
|
* Active state machine + React context for the form-binding flow.
|
|
|
|
|
*
|
|
|
|
|
* Tracks the `(activeTarget, activeEvidenceItemId, activeAnnotationId)`
|
|
|
|
|
* triple that the SVG visual guide and the viewer adapter both depend on.
|
|
|
|
|
*
|
|
|
|
|
* Transitions:
|
|
|
|
|
* - `focusTarget(target)` — clears the active evidence, emits
|
|
|
|
|
* `FormFieldActivated`.
|
|
|
|
|
* - `setActiveEvidence(evidenceItemId, annotationId?)` — sets active
|
|
|
|
|
* evidence (and optionally the active annotation derived from it),
|
|
|
|
|
* emits `EvidenceItemActivated` with `source="form-field"`. The
|
|
|
|
|
* binding-service helper does the same; the state machine owns the
|
|
|
|
|
* React-facing source of truth.
|
|
|
|
|
* - `clear()` — drops everything back to undefined.
|
|
|
|
|
*
|
|
|
|
|
* The state itself is a small immutable record (so React equality checks
|
|
|
|
|
* stay simple). All mutations go through a single reducer.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
createContext,
|
|
|
|
|
createElement,
|
|
|
|
|
useCallback,
|
|
|
|
|
useContext,
|
|
|
|
|
useEffect,
|
|
|
|
|
useMemo,
|
|
|
|
|
useReducer,
|
|
|
|
|
useRef,
|
|
|
|
|
type ReactNode,
|
|
|
|
|
} from "react";
|
|
|
|
|
|
|
|
|
|
import type { EvidenceTarget } from "@shared/evidence-link";
|
|
|
|
|
import type { AnnotationId, EvidenceItemId } from "@shared/ids";
|
|
|
|
|
|
|
|
|
|
import type { EventBus } from "@engine/events";
|
|
|
|
|
|
|
|
|
|
export interface ActiveState {
|
|
|
|
|
readonly activeTarget: EvidenceTarget | null;
|
|
|
|
|
readonly activeEvidenceItemId: EvidenceItemId | null;
|
|
|
|
|
readonly activeAnnotationId: AnnotationId | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const EMPTY_ACTIVE_STATE: ActiveState = {
|
|
|
|
|
activeTarget: null,
|
|
|
|
|
activeEvidenceItemId: null,
|
|
|
|
|
activeAnnotationId: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type Action =
|
|
|
|
|
| { type: "focus-target"; target: EvidenceTarget }
|
|
|
|
|
| {
|
|
|
|
|
type: "set-active-evidence";
|
|
|
|
|
evidenceItemId: EvidenceItemId;
|
|
|
|
|
annotationId: AnnotationId | null;
|
|
|
|
|
}
|
2026-06-08 00:37:34 +02:00
|
|
|
| { type: "clear-active-evidence" }
|
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay
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>
2026-05-25 15:53:17 +02:00
|
|
|
| { type: "clear" };
|
|
|
|
|
|
|
|
|
|
function reducer(state: ActiveState, action: Action): ActiveState {
|
|
|
|
|
switch (action.type) {
|
|
|
|
|
case "focus-target":
|
|
|
|
|
// Focusing a target resets the active evidence — a different field
|
|
|
|
|
// means a different evidence set.
|
|
|
|
|
if (
|
|
|
|
|
state.activeTarget?.targetType === action.target.targetType &&
|
|
|
|
|
state.activeTarget?.targetId === action.target.targetId
|
|
|
|
|
) {
|
|
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
activeTarget: action.target,
|
|
|
|
|
activeEvidenceItemId: null,
|
|
|
|
|
activeAnnotationId: null,
|
|
|
|
|
};
|
|
|
|
|
case "set-active-evidence":
|
|
|
|
|
return {
|
|
|
|
|
activeTarget: state.activeTarget,
|
|
|
|
|
activeEvidenceItemId: action.evidenceItemId,
|
|
|
|
|
activeAnnotationId: action.annotationId,
|
|
|
|
|
};
|
2026-06-08 00:37:34 +02:00
|
|
|
case "clear-active-evidence":
|
|
|
|
|
return {
|
|
|
|
|
activeTarget: state.activeTarget,
|
|
|
|
|
activeEvidenceItemId: null,
|
|
|
|
|
activeAnnotationId: null,
|
|
|
|
|
};
|
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay
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>
2026-05-25 15:53:17 +02:00
|
|
|
case "clear":
|
|
|
|
|
return EMPTY_ACTIVE_STATE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ActiveStateApi {
|
|
|
|
|
readonly state: ActiveState;
|
|
|
|
|
focusTarget(target: EvidenceTarget): void;
|
|
|
|
|
setActiveEvidence(
|
|
|
|
|
evidenceItemId: EvidenceItemId,
|
|
|
|
|
annotationId?: AnnotationId | null,
|
|
|
|
|
): void;
|
2026-06-08 00:37:34 +02:00
|
|
|
clearActiveEvidence(): void;
|
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay
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>
2026-05-25 15:53:17 +02:00
|
|
|
clear(): void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ActiveStateContext = createContext<ActiveStateApi | null>(null);
|
|
|
|
|
|
|
|
|
|
export interface ActiveStateProviderProps {
|
|
|
|
|
readonly bus: EventBus;
|
|
|
|
|
readonly children: ReactNode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* React provider for the binder's active-state machine. Mounts inside the
|
|
|
|
|
* EngineProvider so it can wire `bus` from the engine.
|
|
|
|
|
*/
|
|
|
|
|
export function ActiveStateProvider(props: ActiveStateProviderProps) {
|
|
|
|
|
const [state, dispatch] = useReducer(reducer, EMPTY_ACTIVE_STATE);
|
|
|
|
|
const stateRef = useRef(state);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
stateRef.current = state;
|
|
|
|
|
}, [state]);
|
|
|
|
|
|
|
|
|
|
const focusTarget = useCallback(
|
|
|
|
|
(target: EvidenceTarget) => {
|
|
|
|
|
const previousTarget = stateRef.current.activeTarget;
|
|
|
|
|
const samePrevious =
|
|
|
|
|
previousTarget?.targetType === target.targetType &&
|
|
|
|
|
previousTarget?.targetId === target.targetId;
|
|
|
|
|
if (samePrevious) return;
|
|
|
|
|
props.bus.emit({
|
|
|
|
|
type: "FormFieldActivated",
|
|
|
|
|
target,
|
|
|
|
|
...(previousTarget !== null ? { previousTarget } : {}),
|
|
|
|
|
});
|
|
|
|
|
dispatch({ type: "focus-target", target });
|
|
|
|
|
},
|
|
|
|
|
[props.bus],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const setActiveEvidence = useCallback(
|
|
|
|
|
(evidenceItemId: EvidenceItemId, annotationId?: AnnotationId | null) => {
|
|
|
|
|
props.bus.emit({
|
|
|
|
|
type: "EvidenceItemActivated",
|
|
|
|
|
evidenceItemId,
|
|
|
|
|
source: "form-field",
|
|
|
|
|
});
|
|
|
|
|
dispatch({
|
|
|
|
|
type: "set-active-evidence",
|
|
|
|
|
evidenceItemId,
|
|
|
|
|
annotationId: annotationId ?? null,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[props.bus],
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-08 00:37:34 +02:00
|
|
|
const clearActiveEvidence = useCallback(() => {
|
|
|
|
|
dispatch({ type: "clear-active-evidence" });
|
|
|
|
|
}, []);
|
|
|
|
|
|
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay
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>
2026-05-25 15:53:17 +02:00
|
|
|
const clear = useCallback(() => {
|
|
|
|
|
dispatch({ type: "clear" });
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const value = useMemo<ActiveStateApi>(
|
2026-06-08 00:37:34 +02:00
|
|
|
() => ({ state, focusTarget, setActiveEvidence, clearActiveEvidence, clear }),
|
|
|
|
|
[state, focusTarget, setActiveEvidence, clearActiveEvidence, clear],
|
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay
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>
2026-05-25 15:53:17 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return createElement(ActiveStateContext.Provider, { value }, props.children);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useActiveState(): ActiveStateApi {
|
|
|
|
|
const ctx = useContext(ActiveStateContext);
|
|
|
|
|
if (!ctx) {
|
|
|
|
|
throw new Error("useActiveState must be used inside <ActiveStateProvider />");
|
|
|
|
|
}
|
|
|
|
|
return ctx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Pure reducer + initial state, exported so the headless tests can verify
|
|
|
|
|
* transitions without spinning up React.
|
|
|
|
|
*/
|
|
|
|
|
export const __test = { reducer, EMPTY_ACTIVE_STATE };
|