Uh oh!
There was an error while loading. Please reload this page.
feat(S4): agent-graph canvas + analysis cache/replay - #5
Merged
Conversation
Adds patient_id-keyed analysis_cache table to the shared migrate() function and a new db/analysisCache.ts module (writeAnalysisCache / readAnalysisCache) so a validated analysis result (post-citation-gate) can be persisted and replayed without depending on any Task still existing. One row per patient, overwritten on re-run (no history table, per S4 ponytail note). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task A2: POST /:id/analysis now defaults to replaying a patient's analysis_cache row (zero orchestrator calls, zero HAPI reads/writes) if one exists, falls back to a live run + cache write on a cold cache, and honors ?live=1 to always force a fresh orchestrator run that overwrites the row. Replay re-emits the same finding/task/complete SSE events, in the same phased per-agent order, that a live run produces, so the S4 canvas can't tell the two paths apart. Threads `db` through createAnalysisRouter (and apps/api/src/index.ts's wiring) with readAnalysisCache/writeAnalysisCache as injectable, defaulted params mirroring the existing runAnalysis pattern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The cache-replay branch of POST /:id/analysis skipped getPatientBundle entirely (by design, to avoid a HAPI call) but that also skipped the only place role->scope enforcement and audit logging happened for this data (FhirReadService.guard, private). A role denied 'clinical' scope (e.g. Social Worker) could read another user's cached clinical analysis with no audit trail, once anyone had triggered one live run for that patient. Replay now checks hasScope(actor.role, 'clinical') itself (a local comparison, no HAPI call) before replaying, writes the same denial audit entry FhirReadService.guard would write, and returns the same 403 shape the live path already returns for this role. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…path Code-quality review of the A2 cache-replay work (c256fe0, 2b68194) flagged two Important issues: 1. The replay-path scope check hand-rolled hasScope/writeAudit/ ScopeDeniedError inline instead of reusing FhirReadService's existing guard, risking future drift between the live and replay enforcement of the same invariant. Added a public `assertScope` on FhirReadService (a thin delegate to the existing private `guard`, so every other call site is untouched) and had the route call it, wrapped in the same try/catch-ScopeDeniedError pattern the live path already uses around getPatientBundle. 2. The replay branch had no error boundary: an unchecked cast (`cached.resultJson as AnalysisResultJson`) meant a malformed/legacy cached row would throw inside replayCachedAnalysis after headers were already sent, hanging the connection with no `error`/`done` event — inconsistent with the live path's established try/catch convention. Wrapped the replay call in the same pattern, emitting the same `error` SSE event on failure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
B1 — pure analysisGraphReducer maps the real per-agent SSE vocabulary (token/finding/complete/task/done, each tagged with agentId) to graph-level state (idle→init→dispatch→analyzing→synthesizing→complete) and per-node status (risk/careGap/sdoh/actionPlanner: pending→analyzing→complete), plus a thin useAnalysisGraph() hook wrapper for future wiring into PatientDetail.tsx. Covered by a fixture mirroring replayCachedAnalysis's phased risk→careGap→sdoh→actionPlanner→done order. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ize test indices Code review on 80b2b95 flagged filename/export-vocabulary mismatch (file said StateMachine, exports said Graph) and brittle numeric snapshot indices in the test (silently point at the wrong snapshot if FIXTURE is edited). Renamed the module/test to analysisGraph(.test).ts to match the Graph-* export names, and restructured the fixture into labeled {label, action} steps with runFixture returning snapshots keyed by label instead of array position. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports reference-materials/caresync-ai.html's #agentGraph canvas (5-node radial layout, bezier edges, particle flow, per-agent color identity) into a React/Canvas2D component driven by B1's real AnalysisGraphState instead of the mockup's fake elapsed-time clock. Pure geometry/timing math lives in agentGraphGeometry.ts (unit tested); AgentGraph.tsx owns the rAF loop, SSR/ null-ctx guards, prefers-reduced-motion static fallback, and per-node visuals driven by individual agent status (supports real phased completion, unlike the mockup's single fake timeline). PatientDetail now dispatches to useAnalysisGraph() from the same streamAnalysis handlers that already update feed state, and renders AgentGraph above the feeds grid, closing W03's last recorded mockup-fidelity deviation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ced-motion Self-review catch: with prefers-reduced-motion active, the component skipped the rAF loop entirely and only ever painted once at mount (freezing on the initial idle state) — a repaint on state change was missing, so it never reflected real progress. Adds a `repaintOnceRef` the mount effect assigns once the canvas ctx is ready, invoked by a second effect keyed on `state`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s exercise real draw path Addresses the B2 code-quality review (Changes Requested): - IMPORTANT 1: the reduced-motion static frame now paints the SETTLED resting state (completion rings shown, "✓ Analysis complete" text shown, orchestrator settle-glow faded off) instead of the elapsed≈0 mid-settle frame the [state] repaint effect used to produce on the `done` transition. A `settledTiming()` feeds SETTLED_ELAPSED_SEC to every since-transition animation so the plan's "render final state statically" actually holds. - MINOR 3: the reduced-motion resize handler now repaints, so a window resize no longer blanks the canvas until the next state change. - MINOR 4: extracted the ~125-line draw closure to a module-level `paintFrame(ctx, W, H, state, timing)` seam — shrinks the mount effect and makes the draw logic directly unit-testable. - IMPORTANT 2: canvas tests now stub a no-op 2D context (jsdom's getContext returns null, so the old tests early-returned before painting and guarded nothing). The reduced-motion regression test now spies clearRect and asserts a state change fires an ADDITIONAL paint, plus fillText asserts the settled checkmark text — both verified to FAIL when their respective fixes are reverted. Added direct paintFrame unit tests too. Web suite 56 -> 61 (all green); build + lint clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add an optional { live } flag to streamAnalysis that appends ?live=1,
and a secondary "Run live" button in PatientDetail alongside the default
cache-first "Run Analysis". Both drive the identical graph + feeds path;
a minimal mode note (derived from the pressed button) surfaces
cached replay vs live run for the demo narrative.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>…e; add aria-live The client only knows which button was pressed, not the backend outcome — a default "Run Analysis" press on a cold cache is served by a live run + cache-write, so "cached replay" would assert an unconfirmable outcome. Reword to "requested: cached" / "requested: live" and add aria-live="polite" so the status change is announced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers C2: AgentGraph <canvas> renders above the feeds grid (idle), the default Run Analysis button requests without ?live=1 while Run live requests with ?live=1, both modes render graph->feeds->tasks identically (GD2 same UI treatment), and the analysis-mode indicator reflects the pressed button. Reduced motion disabled so the animated canvas path runs. SSE intercepted (no OPENAI_API_KEY); backend cache guarantees are covered by A2 Supertest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the S4 subagent-driven execution: per-task commits, the review-caught fixes folded in (clinical-scope enforcement on cache replay, reduced-motion settled-final-frame + real canvas tests, honest intent-not-outcome mode label), and the C1/C2 verification results (API 88/88, web 68/68, E2E 7/7). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Whole-slice review found a Critical bug: analysisGraphReducer's `start`
action was a one-way idle→init transition (no-op from any other state).
After a completed run (graphState 'complete', all nodes 'complete'), a
second run's `start` returned state unchanged, so every subsequent event's
first-event / pending-node checks failed and the AgentGraph canvas stayed
frozen on "Analysis complete" for the entire second run while the feeds
re-animated — a visible "the graph is lying" bug in exactly the B3
run-then-run-live compare flow.
`start` now unconditionally resets to a fresh {graphState:'init', all nodes
'pending'} state (fresh nodes object, not the shared initial constant's
reference), so any run — first or Nth — animates from scratch. Existing
idle→init behavior is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>…ort cache write
Whole-slice review found two integration issues in the cache-aware route:
1. Narration parity gap: the live path streamed per-agent `token` narration
but AnalysisResultJson never captured it, so replayCachedAnalysis emitted
none — a cached replay showed blank reasoning prose where a live run showed
streamed narration, breaking the "same UI treatment" / "cache is real prior
output" guarantee (C2/GD2). Now each agent's accumulated SAFE (GD11-redacted,
as-emitted) narration is stored and re-emitted as a byte-identical
`{ agentId, text }` token event, in phased order before that agent's
findings — so a replay is indistinguishable from the live run that made it.
2. A cache-write failure sank an otherwise-successful run: writeCache sat in
the main try, so a persistence throw after the stream + HAPI Task writes
already succeeded flipped the run to the `error`/no-`done` path (hanging the
client graph in `synthesizing`). Made it best-effort: log and continue to
`done`; worst case the next non-live view re-runs live instead of replaying.
Tests: (f) live→replay narration is byte-identical per agent + persisted;
(g) a throwing writeCache still emits `done`, no `error`, Task still created.
Full API suite 90/90 (serial; +2), tsc clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>…ity) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
assertScope only writes an audit row on denial; the cache-replay path called it directly but never wrote a success row afterward, unlike every other clinical read in FhirReadService. A default "Run Analysis" against a cached patient served full clinical findings with zero audit_log trail. Add the missing writeAudit call, mirroring getPatientBundle's guard-then-audit pattern, and a regression assertion in test (a) mirroring test (d)'s existing denial check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
verification.md maps every S4 acceptance criterion to its proving artifact with evidence-strength labels and re-confirms the whole-slice review's earlier fixes hold. review.md runs Standards + Spec axes on e8a9309...HEAD and records the audit-gap fix. issues.md and implementation-plan.md's S4 checkboxes were stale despite the work being done (same drift pattern S3's verification caught) — corrected to [x]. tasks/todo.md records the post-review fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Jul 5, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
IDLE→INIT→DISPATCH→ANALYZING→SYNTHESIZING→COMPLETE, with per-agent color identity consistent from graph node → feed box → task citation.analysis_cache): default "Run Analysis" replays the last successful run deterministically with zero model calls; an explicit "Run live" trigger forces a fresh orchestrator run and re-caches (GD2). Cached and live runs emit byte-identical SSE, so the client has one render path for both.Test plan
cd apps/api && npx jest --runInBand— 90/90 (parallel workers flake on shared-HAPI contention, a pre-existing env issue, not new in this PR)cd apps/web && npm test— 69/69cd apps/web && npx playwright test— 7/7 (3 new S4 specs + 4 existing, no regressions)npm run build+npm run lintfor both apps — cleanverification-before-completion→docs/plans/caresync-ai/verification.md(every AC mapped to its proving artifact with evidence-strength labels)code-review(Standards + Spec axes) →docs/plans/caresync-ai/review.md; the one confirmed defect (replay success-audit gap) fixed and re-verifiedFull detail:
docs/superpowers/specs/feature-caresync-s4-agent-graph-cache/2026-07-05-changelog.md,docs/plans/caresync-ai/verification.md,docs/plans/caresync-ai/review.md.🤖 Generated with Claude Code