Skip to content

Care-plan conflict surfacing, AI Transparency IG, and round-bound review gate (#169) - #173

Open
samschifman wants to merge 17 commits into
mainfrom
issue-169-conflict-provenance
Open

Care-plan conflict surfacing, AI Transparency IG, and round-bound review gate (#169)#173
samschifman wants to merge 17 commits into
mainfrom
issue-169-conflict-provenance

Conversation

@samschifman

@samschifman samschifman commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Implements #169. What began as plan-level conflict surfacing + adoption of the HL7 AI Transparency on FHIR IG has grown, over the life of the branch, into the full care-plan review-gate story: detecting conflicts, feeding them back to the composer under clinician direction, and hardening the human review gate itself. Three bodies of work, plus deploy hardening.


1. Conflict surfacing + AI Transparency IG

acp-writer detects plan-level conflicts across the goals/activities composed from multiple CPGs and surfaces them to the reviewing clinician — without ever mutating the plan. Detection is a generic LLM judgment task (no DMN, no clinical knowledge base).

Categories: overlap · contradiction · divergent_target · divergent_schedule (+ other).

FHIR recording:

  • One AI-Provenance per conflict: targets the affected CarePlan.activity[].detail / Goal via targetPath, lists source recommendations as entities, carries an AI-authored rationale note, and stores conflict-id/-description/-severity/-category/-status/-suggested-resolution + AIconfidence extensions.
  • Exactly one careplan-conflict-detected marker extension on the CarePlan.
  • No auto-resolution — every conflicting item stays; the clinician acts through the review gate. Approval flips conflict-status detected → acknowledged and appends a verifier human agent.
  • Conflicts read back from the Provenances when a stored plan is viewed (not only in the live run view).
  • The plan_composer no longer harmonizes guideline conflicts away, and the conflict_analyst runs on the split/SonataFlow compose path as well as the monolith.

AI Transparency IG adoption: AI-Device (AIKind=LLM), bundle-level AI-Provenance (AIAST reason, fixed AI agent role, occurredDateTime), AIAST labels on all AI-produced resources, AI-InputPrompt DocumentReferences (opt-in), AI-ModelCard (opt-in), and AIconfidence — all under the correct http://hl7.org/fhir/uv/aitransparency canonical.

2. Conflict-resolution feedback loop (request-changes → recompose)

When a clinician requests changes, the composer runs in revision mode: the prior brief is the authoritative base and only clinician-directed changes are applied.

  • A durable "Clinician-directed changes" prompt section (render_clinician_directives) is rendered on every composer iteration from state — the clinician's instruction + accumulated feedback history + the prior brief's unresolved conflicts (with suggestions and source ids) + conflicts already resolved in earlier rounds. It survives the internal brief-review loop (separate from the internal reviewer's channel, which is overwritten each iteration). (F17)
  • Continuity: after the loop converges, the analyst re-runs with the same conflict ids — each prior conflict returns as resolved (recording the clinician's instruction) or still-present.
  • Enforcement, not hope: if a clinician-directed conflict is still detected, the composer is retried once with the unapplied directives listed; if still unapplied, the brief is flagged (review_status: flagged) naming what couldn't be applied — an honest "could not apply" instead of silently re-presenting the same conflicts. (F18)

3. Review Gate v2 — round-bound submissions

A series of live incidents at the human review gate (silent slow submits → multi-click; a GraphQL-replica gate pre-check accepting a submit before the next round's gate armed → engine drop; an in-memory dedupe tracker that then deadlocked the run; a business-key lookup bug that 404'd runs after any BFF restart). Root cause: the review submission had no identity. The fix gives it one — the review round — and validates it in the engine, the only component with authoritative state.

  • Round-binding (P1): the UI stamps each ReviewAction with reviewRound = reviewIteration; a new ValidateReviewRound switch in the workflow discards any submission whose reviewRound no longer matches careplanReviewCount and re-arms the gate (DiscardStaleReview) — so a clinician can never approve a plan version they didn't see. One camelCase spelling end-to-end; reviewRound == null fails open for old clients/manual curl, guarded by an exact-key BFF passthrough test + the cluster verify script.
  • Truthful BFF failure modes (bcf487f): submitted/error UI states; engine-unavailable wrapped as 503; deleted the in-memory dedupe/timing machinery (P3) — duplicates are now handled semantically by the engine.
  • Business-key lookup fix (P4): runs resolve via get_instance_by_business_key, so they still load/accept reviews after a BFF restart.
  • Silent auto-retry (P6): while submitted and still at the same gate, a self-rescheduling loop re-submits the same round-bound action (~10/20/30s, then hands off to a manual retry) — riding out the brief window where the UI shows a new round before the engine arms its gate. Safe by construction (round-binding: consumed-first or engine-discarded, never double-applied). UI-only.
  • Round-scoped UI: the panel is keyed by round so it remounts fresh each gate (no state bleed between rounds); fixed the review-round counter and now renders conflict resolutions.

4. Deploy hardening

d7ef4dd — hardened three latent races that could abort or orphan deploys.


Testing

  • Python: conflict contracts, conflict_analyst, AI-transparency builders, conflict read-back, split-compose conflicts, revision flow, reviewer identity, BFF review-submit passthrough, plan-composer, FHIR builder/server-writer, e2e. Known baseline: the JVM-less test_matches_hypertension_cpg / test_pipeline_integration fail locally (DMN engine unavailable — no JVM), unrelated to these changes.
  • Live-LLM repeatability gate (test_conflict_repeatability.py): passes on gpt-5.6 — overlap, contradiction, divergent_target in 3/3 runs.
  • e2e conflict smoke: ≥1 conflict Provenance + CarePlan marker against a real LLM.
  • UI (vitest + MSW): ReviewPanel 11/11 incl. P6 fake-timer loop tests, ConflictAlert rendering; full UI suite 45 passed / 3 pre-existing unrelated baselines; npm run build clean.
  • Cluster acceptance (working/issue-169-conflict-surfacing/verify-review-gate.sh): posts a stale-round event (must be discarded, gate re-arms, count unchanged — the fail-open detector) and a matching-round event (must advance) — run post-deploy.

Deployed & verified

Live in namespace sschifma-cpg-acp at this branch tip: the ValidateReviewRound/DiscardStaleReview states are present in the running SonataFlow workflow; all acp-writer sandboxes healthy. (Deploy surfaced unrelated infra bugs tracked separately: #179, #180.)

Follow-ups

Docs

New docs/ai-transparency.md (IG conformance inventory, conflict-Provenance pattern + Mermaid, conflict-resolution feedback loop + Mermaid, custom-extension table, reviewer/SMART seam); updated acp-writer/README.md (pipeline diagram, conflict surfacing, round-bound review-gate section + two-round Mermaid sequence, config); AGENTS.md; platform/mlflow/README.md; and API-contract field descriptions (bff-openapi.yaml, incl. reviewRound).

🤖 Generated with Claude Code

samschifman and others added 6 commits August 26, 2026 21:49
WS1 — Contracts (planning_brief.py):
- Replace placeholder ConflictEntry with full model + ConflictSeverity/
  ConflictCategory/ConflictStatus/ConflictSource.
- Add conflict_id(): semantic, index-free id so a conflict's identity
  survives a request-changes regeneration (composer reorders items).
- Add coerce_conflicts(): relocated/upgraded _sanitize_conflicts; handles
  legacy shapes (bare strings, sources:list[str], recommendation_ids).

WS2 — conflict_analyst node (nodes/ + prompts/):
- New LLM node detecting plan-level conflicts (overlap/contradiction/
  divergent_target/divergent_schedule); numbered items, one retry,
  graceful degradation, index clamping, composer-conflict dedupe, and
  carry-forward of clinician status/resolution across re-runs.
- Wire into pipeline between brief-review loop and fhir_bundle_generator;
  FHIR review loop does not pass back through it.
- plan_composer uses coerce_conflicts + stashes rendered prompt; drop the
  conflict-flagging rule and anti-training "conflicts": [] template.
- state.py: add conflict_prompt / plan_composer_prompt.

Tests: 69 pass (test_planning_brief, test_plan_composer, test_pipeline,
new test_conflict_analyst).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WS3: new services/ai_transparency.py with IG-conformant builders
(AI-Device, AI-Provenance, AI-InputPrompt/ModelCard DocRefs, AIconfidence,
AIAST, stock targetPath). Fixes canonical base uv/ai-transparency ->
uv/aitransparency, adds occurredDateTime + AI-agent role. fhir_bundle_builder
refactored onto the module; threads model_id + captured prompts.

WS4: one AI-Provenance per conflict (targetPath into affected activities/goals,
source entities, device rationale note, conflict-id/-description/-severity/
-category/-status + AIconfidence extensions) and a single careplan-conflict-
detected marker on the CarePlan.

Tests: new test_ai_transparency.py, extended test_fhir_builder.py; reconciled
the R4 .detail activity convention. Full suite green except the pre-existing
DMN-model-dependent guideline_resolver test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WS5 — Reviewer identity (SMART-on-FHIR-ready seam):
- New services/reviewer.py: ReviewerContext + default_reviewer (ACP_REVIEWER_*
  env) + reviewer_from_payload (request override); source stays opaque to
  downstream, so a future SMART launch reuses the same path.
- approve_care_plan appends the verifier Humanagent (reference + identifier)
  to every AI-Provenance and flips each conflict Provenance's conflict-status
  to acknowledged; back-compat clinician-string shim retained. Wired through
  api.py and services/fhir_server.py on PUT status=active.

WS6 — BFF mapping + persisted read-back:
- ai_transparency.py: is_conflict_provenance + plan_conflict_from_provenance
  reconstruct a PlanConflict from extensions + entity[] only (never note text);
  _source_display/parse_source_display round-trip source labels.
- artifact_resolver.plan_conflict_from_entry maps ConflictEntry (snake) ->
  PlanConflict (camel); replaces the raw pass-through.
- bff._extract_view_from_bundle reads conflicts from Provenances (was []).

WS7 — UI:
- ConflictAlert.tsx: category-varying titles + de-duplicated "From:" CPG line.
- fixtures one-per-category; extended ConflictAlert tests.

Contract + deploy:
- bff-openapi.yaml: PlanConflict gains category/status/confidence/sources; new
  ConflictSource; ReviewerRef on ReviewAction. Restored SystemStatus
  decisions/cpgs (pre-existing drift from #161 that gen:api surfaced).
  Regenerated ui/src/api/types.ts.
- Env vars added to compose.yml, both Helm charts, and openshell/deploy.sh
  (fhir-gen: ACP_CAPTURE_PROMPTS/LLM_MODEL_CARD_URL; fhir-srv: ACP_REVIEWER_*).

Deferred (tracked for a later milestone): request_changes-per-conflict
resolution (itemId -> resolution note + status resolved + brief carry) — needs
SonataFlow structured-feedback plumbing that does not exist yet.

Tests: 470 py pass (only unrelated JVM-less test_matches_hypertension_cpg
fails); UI 29 pass + 3 pre-existing unrelated failures; npm build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(issue #169)

WS8 — fixtures + verification:
- Add tests/integration/fixtures/htn-cpg.json (SYN-HTN-2026-001) engineered
  to conflict with the diabetes CPG: overlapping diet advice, BP target
  <140/90 vs <130/80, and titrate-lisinopril-up.
- Add dm2-rec-005 (reduce lisinopril dose) to diabetes-cpg.json — the
  contradiction half.
- Add acp-writer/tests/test_conflict_repeatability.py: gated on LLM_BASE_URL
  (falls back to LITELLM_URL); builds the brief deterministically from both
  fixtures, runs the real conflict_analyst 3x, asserts every run flags >=1
  conflict, overlap in >=2/3, contradiction|divergent_target in >=2/3 — on
  categories/index-sets, never wording.
- Extend test_e2e.py with test_comprehensive_patient_surfaces_conflicts:
  ingests both fixture CPGs, runs the comprehensive patient full-pipeline,
  asserts >=1 conflict Provenance + exactly one CarePlan marker; module gate
  now honors LLM_BASE_URL.

WS9 — documentation:
- acp-writer/README.md: Mermaid pipeline with conflict_analyst (node 7),
  expanded AI Transparency section, conflict-surfacing + config-env tables.
- New docs/ai-transparency.md: IG conformance inventory, conflict-Provenance
  Mermaid + pattern, ACP_EXT_BASE extension table, reviewer/SMART seam;
  linked from docs/README.md. Documentation hyperlinks use the browsable
  build URL; the FHIR canonical (http://hl7.org/fhir/uv/aitransparency) is
  kept as resource identity.
- AGENTS.md: IG in Technology Context + acp-writer conflict-Provenance line.
- platform/mlflow/README.md: conflict_analyst in the tracing inventory.
- api/bff-openapi.yaml + api/openapi.yaml: field descriptions for the WS1/WS5
  conflict + reviewer additions; regenerated ui types.ts.

Deferred per-conflict resolution recording is tracked in #172.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
conflict_analyst was wired only into the monolith pipeline.py, so the
pod-split cluster path (BFF -> SonataFlow -> compose/generate-bundle
endpoints) never ran it and deployed care plans surfaced zero conflicts.

Fold conflict_analyst into both the sync /api/v1/compose and async
/api/v1/compose-async handlers, right after the brief-review loop
converges. The detected conflicts are annotated onto the planning brief,
which flows unchanged into generate-bundle -> fhir_bundle_generator where
the conflict Provenances and CarePlan marker are emitted. No SonataFlow
workflow change is needed; the node stays in the llm-reasoning pod where
it already has the recommendations, applicable CPGs, and LLM creds.

Add tests/test_split_compose_conflicts.py to guard the split path, and
note the ComposePlan-embedded ordering in the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Even with conflict_analyst running, no conflicts surfaced because the
plan_composer naturally reconciles disagreeing guidelines into one
coherent plan before the analyst sees the brief (e.g. merging divergent
BP targets into a single goal and dropping the reduce-dose activity in
favour of titrate-up). The analyst then finds a clean brief.

Add a "Preserving conflicts" directive to the composer system prompt:
when recommendations conflict (contradictory directives, divergent
targets/schedules, duplicate activities across CPGs) it must emit BOTH
items as separate goals/activities, each with its own source_cpg, and
must not merge, reconcile, subordinate, or editorialise them away. The
Conflict Analyst then flags them and the clinician resolves at the review
gate -- the intended #169 behaviour ("every conflicting item stays in
the plan").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
samschifman and others added 6 commits August 27, 2026 12:54
…back loop (#169)

Code-review fixes for PR #173, plus the Sam-approved F16 enhancement.

Batch A — pipeline robustness (conflict_analyst / planning_brief / bundle gen):
- F3: _build_entry/_parse_conflicts total — malformed LLM conflict items are
  skipped-and-logged instead of aborting the run
- F14: retry with original messages on transport error (no empty assistant turn)
- F7: coerce_conflicts total — enum synonyms + camelCase sources + drop-bad-source,
  never blanks the brief
- F4: coerce conflicts via PlanningBrief before-validator so every validation site
  is covered; empty bundle from a validation failure now surfaces an error flag
- F6: content key for every conflict category + deterministic id uniquification

Batch B — approval + split-path parity:
- F1: single apply_approval_transition() (CLINAST swap, verifier agent, conflict
  acknowledge) shared by monolith approve and deployed WriteFHIR; reviewer threaded
  through the workflow
- F2: split compose ferries captured prompts to fhir-generation so cluster bundles
  carry AI-InputPrompt DocRefs
- F12: single reviewer_from_payload(); removed duplicate reviewer shims

F16 — "resolve all identified conflicts as you suggested" now works:
- F16a: prior brief conflicts fed back into regeneration (render_conflicts_feedback,
  prior_brief_ref threaded through the workflow + llm_reasoning)
- F16b: analyst emits a conservative suggested_resolution per conflict, surfaced
  end-to-end (prompt -> ConflictEntry -> conflict-suggested-resolution extension +
  both read paths -> BFF contract -> UI)

Deferred (documented): F5 server-side Provenance persistence -> #174; per-conflict
structured feedback -> #172. Batch C (F8/F9/F10/F13/F11-remove/F15) not included.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Conflict-surfacing hardening from the PR #173 review:

- F8: invert the certainty-rating map on read-back so a "medium"
  confidence stored as "moderate" reads back as "medium".
- F9: store conflict sources structurally on the Provenance entity
  (cpg-id / recommendation-id / excerpt extensions) so read-back no
  longer parses the human-readable display string, which corrupted
  cpg ids / excerpts containing the display delimiters. Legacy
  Provenances still fall back to display-string parsing.
- F10: strip DocumentReference attachment payloads from the FHIR
  semantic reviewer prompt; inject the DMN audit trail into the brief
  in code instead of round-tripping it through the plan-composer LLM.
- F11: remove the conflict "detected_by" field and the composer
  carry-forward/merge path. The analyst re-runs on a freshly composed
  brief and is authoritative; per-conflict resolution carry-over is
  deferred to #172 (rebuilt against the conflict Provenances).
- F13: add @mlflow.trace to the conflict normalize / provenance /
  read-back functions and update the MLflow tracing inventory.
- F15: retry `openshell service expose` and fail the deploy with a
  visible error on persistent failure instead of swallowing it
  (shared expose_service helper, used by both component deploys).

Each fix carries a fail-before/pass-after test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C1: extract shared llm_json (strip_code_fence/loads_json), dedup 5 sites
C2: named ReviewerRef schema + $ref in openapi.yaml
C3: dedup conflict test scaffolding into tests/_conflict_fixtures.py
C4: keep ReviewerContext.source with SMART-launch/audit comment
C5: drop unused category param + dead bounds-checks in _content_key
C6: drop redundant is_conflict_provenance() pre-check in bff
C7: fix docs — AIconfidence is conflict-Provenance-only, not per-activity
C8: conflict_prompt captures user_prompt only (matches composer)
C9: run compose pipeline via asyncio.to_thread so /compose doesn't block loop

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ry (#169)

On a care-plan request_changes revision, the composer and conflict analyst
now revise an approved base plan instead of re-authoring from scratch:

- F17a: dedicated REVISION prompt mode for plan_composer (shared HEAD/EXAMPLES
  with a mode-specific AUTHORING|REVISION section). Composer is state-driven —
  revision iff prior_planning_brief has goals/activities — so the monolith path
  is unchanged and authoring output stays byte-identical.
- F17b: accumulated feedback history — every revision sees ALL prior clinician
  comments (oldest-first, newest marked as the round to address), so standing
  constraints don't expire. Workflow accumulates careplanReviewHistory; brief
  records revision_history.
- F17c: analyst resolution continuity — prior conflicts + the clinician comment
  are fed to the analyst on a revision pass. Resolved conflicts re-emit with the
  SAME id + status "resolved" + a resolution note; surviving conflicts keep
  their id. A conflict-resolution Provenance extension carries the note through
  the FHIR round-trip and read-back.
- brief_reviewer: notes when a brief is a minimal revision of an approved base
  so it judges the changes rather than demanding authoring-style rewrites.
- deploy: force `oc rollout restart deployment/acpwriter` after the workflow
  re-apply (the operator does not always roll the pod on a flow-only change);
  verify.sh greps the running pod's mounted flow for a current-version marker;
  workflow version bumped 0.2.2 -> 0.3.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ced (#169)

Fixes the feedback-channel collision that made "resolve all conflicts as
suggested" a no-op (run 1dfa716dedbe): clinician directives were seeded
into brief_review_feedback, which the internal reviewer overwrites every
iteration, and the revision base was frozen at the prior brief, so later
iterations reverted any applied resolutions by construction.

- F18a: durable "Clinician-directed changes" prompt section rendered every
  composer iteration from state (instruction, unresolved conflicts with
  suggestions, resolved-keep-resolved, enforcement note); _seed_feedback
  deleted — brief_review_feedback is reviewer-only again
- F18b: revision base evolves to the latest draft on iterations >= 2
  ("Care Plan Base"), so internal-review fixes build on directed changes
- F18c: analyst reports clinician_directed per carried-forward conflict;
  unapplied directives trigger one composer retry with them spelled out,
  then flag the brief naming what could not be applied
- sync + background compose consolidated into one _compose_pipeline
- new local multi-round harness tests/test_revision_flow.py: 6 structural
  tests through the real pipeline (scripted role LLMs) + LLM-gated live
  two-round driver; live-validated against gpt-5.6-terra via MaaS

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run 32402d542276 proved the F18 pipeline works on the cluster (all
conflicts came back same-id status=resolved with resolutions recorded)
but the display made it look like nothing happened:

- workflow: InitCarePlanReview injected careplanReviewCount: 0 via inject
  `data`, which merges into state BEFORE the null-guard stateDataFilter
  runs — clobbering the counter every request_changes loop, so the UI
  said "round 1" forever. Initialize via the guarded filter only.
- UI: ConflictAlert ignored status/resolution, so resolved records
  rendered identically to open conflicts. Resolved/acknowledged now
  render as compact collapsed green rows (PatternFly expandable Alert) —
  expand to review description, applied resolution, and sources — sorted
  after open conflicts, with the tab label split "N open · M resolved".
  types.ts regenerated for the resolution field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@samschifman
samschifman marked this pull request as ready for review August 28, 2026 00:26
samschifman and others added 4 commits August 28, 2026 12:49
)

All three are timing-dependent failures observed on 2026-08-27/28; the
scripts' set -e + 2>/dev/null pattern made them silent and intermittent:

- lib.sh label_pod: unguarded `oc label` — a transient conflict with the
  sandbox controller's concurrent metadata writes killed the whole deploy
  between "ready" and "labeled" with stderr discarded. Now retries 3x,
  then warns and continues (a missing cosmetic label must not abort).
- openshell/deploy.sh teardown: sandbox deletion was not awaited, so
  wait_for_pod_ready could match the old Terminating pod ("ready after
  1s") and follow-up oc calls raced the deletion. Teardown now waits for
  the old pods to be fully gone before recreation.
- deploy.sh orchestrator: a pod created seconds after the workflow CM
  apply can mount a stale kubelet-cached copy; the delayed projection
  swap later triggers a Quarkus devmode live-reload that wipes in-memory
  workflow instances, orphaning any in-flight run (run 5af4a6e5a58c).
  After rollout, wait for the pod's mounted workflow to match the CM by
  content hash, then trigger the lazy reload while nothing is running.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	acp-writer/deploy/chart-pods/values.yaml
#	acp-writer/deploy/openshell/deploy.sh
)

Clinicians saw Approve/Submit buttons "not work the first few times": every
click succeeded (202) but the UI gave no acknowledgment and re-enabled the
buttons, so users clicked again during the 6-24s window before the devmode
engine moved the run off the review gate. Repeat clicks' events were then
silently dropped, and a changed decision between clicks was silently discarded.

- ReviewPanel: add a terminal `submitted` state (persistent "Review submitted"
  info alert, buttons hidden) and stop swallowing errors (inline danger alert +
  retry). This structurally prevents double-submit and changed-decision loss.
- api.ts: surface the JSON error body's `.message` centrally so alerts show the
  friendly text, not raw JSON.
- bff.submit_review: return 503 (friendly message) when the engine is briefly
  unavailable instead of a bare 500; track in-flight submits per (run_id, gate)
  and return 409 on a duplicate instead of letting the engine drop it silently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bind each care-plan review submission to the round it was made against
(reviewRound == reviewIteration) and validate it in the workflow engine,
the only authoritative component. A submission whose round no longer
matches the armed gate is discarded and the gate re-arms, so a stale
browser tab can't approve a superseded plan or double-apply a change.

This replaces the BFF's timing/dedupe machinery:

- Contract: reviewRound added to ReviewAction (bff-openapi.yaml), types
  regenerated. Single camelCase spelling end to end — no translation
  point to disagree.
- Workflow: ValidateReviewRound switch + DiscardStaleReview inject
  between the gate and the counter. Fails open on reviewRound == null
  (old clients / manual curl events).
- BFF: removed _pending_reviews tracker, _clear_stale_pending_reviews,
  and the duplicate-409 branch. send_review forwards the review verbatim.
  Added _resolve_sf_id so runs are still found after a BFF restart
  (business-key lookup; a raw run_id is never a valid instance id).
- UI: submit payloads stamp reviewRound; RunDetailPage keys the panel by
  round so it remounts fresh each gate; ReviewPanel offers a neutral
  ~30s retry (at-most-once delivery can drop a submit before the gate
  arms — retry is safe by construction).
- Docs: README "Care-plan review gate" subsection with a two-round
  Mermaid sequence diagram.

Tests: BFF review 6/6, ReviewPanel 8/8, UI build clean, types idempotent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The UI can render a new review round a few seconds before the engine arms
its gate (the data-index leads on content, lags on state); a submission in
that window is dropped by at-most-once delivery. Round-binding (P1) makes
retrying a same-round submission provably safe — consumed-first or engine-
discarded, never a double-apply — so the panel now rides out that race
automatically instead of making the clinician wait 30s and retry by hand.

While `submitted` and still at the same gate/round, a self-rescheduling
loop waits REVIEW_RETRY_INTERVAL_MS, silently re-submits the same action,
and schedules the next wait — up to MAX_REVIEW_RETRIES (both constants at
the top of ReviewPanel.tsx). Exactly one timer is ever pending. The loop
ends when the run leaves the gate (panel unmount/remount → cleanup
cancels), a retry errors (fall back to the manual/error path), or the
retries are exhausted (stalled → manual retry affordance). Defaults: retry
at ~10/20/30s, then hand off to the manual retry.

UI-only; no contract/BFF/workflow change. Tests (fake timers): loop
cadence, stop-after-max + manual retry, unmount cancels the loop, an
errored retry stops the loop and surfaces the error. ReviewPanel 11/11;
full UI suite 45 passed / 3 pre-existing baselines; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@samschifman samschifman changed the title Surface plan-level care-plan conflicts + adopt AI Transparency on FHIR IG (#169) Care-plan conflict surfacing, AI Transparency IG, and round-bound review gate (#169) Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant