Files
duocthu/coordination/CLAUDE_CLAIM_2026-08-11.md
T

4.1 KiB

Claude ownership claim — 2026-08-11

Read CLAUDE_HANDOFF_2026-08-10.md first. Two items in it have since moved on (checked against the code and against live production on 2026-08-11):

  1. The structured-claims refactor it describes as in progress is finished and shipped (dfdbf52, then 9c3acd0). pytest -q --ignore=tests/test_api.py --ignore=tests/test_live_datastores.py = 219 passed at the start of this session.
  2. Entailment majority vote (2-of-3) is no longer in the code. df55af4 introduced it; 9c3acd0 replaced it with a single pass (_ENTAILMENT_MAX_ATTEMPTS = 1), with the reasoning in _verify_entailment's docstring: repeating an identical temperature-0 prompt is a correlated retry rather than an independent vote.

What this session is changing, and why

All five items come from driving production (https://realvuxbaro.me) by hand plus direct /api/chat probes — measured, not inferred from docs.

  • rag/answer.py — availability failures during the entailment pass are currently recorded and shown as unsupported_claim, so a timing or outage problem reaches the user as a content failure. The failure taxonomy in docs/current-rag-pipeline-audit.md §4 keeps these separate. Reusing the reason codes that already exist and are already mapped in apps/web/app/api/chat/route.ts (request_budget_exhausted, provider_unavailable, malformed_output) — no new reason code, so the frontend mapping needs no change. Fail-closed behaviour is unchanged; only the label changes.
  • rag/answer.py_verify_entailment's for _ in range(_ENTAILMENT_MAX_ATTEMPTS) loop always returns on its first iteration, so raising that constant silently does nothing, and the unreachable return False after it returns a bool where every caller reads .supported. Making the single-pass intent explicit.
  • rag/agent.py — the pediatric dosing gate asks "Bé bao nhiêu tuổi và cân nặng bao nhiêu kg?" even when the user just gave one of the two. Reproduced 5/5 live ("18 ký", "18 cân", explicit "18 kg", "Trẻ 5 tuổi"). The gate itself is NOT being loosened — both fields stay required, which is clinically right here (the correct answer uses both an age band and a mg/kg rule). Only the question text becomes specific to what is actually missing.
  • apps/web/app/_components/ChatPanel.tsx — a hard 25s client abort against a backend whose own budget is 40s (config.py:60). Measured n=8 sequential: 2/8 exceeded 25s, and one of those was a correct answerable/grounded/2-citation reply at 25.1s that the user never saw. Caddy (reverse_proxy web:3000, no timeout) and the BFF (signal: request.signal, no own timeout) do not cap this, so the client constant is the only binding limit.
  • packages/ui/src/ChatBubble.tsx — chips dedupe by chunkId but the label is only drug+section+page, so several distinct chunks render as identical-looking chips. Not collapsing them by label: the click handler maps to a specific citation index, so collapsing would make real evidence blocks unreachable from the prose, and provenance is a hard guardrail.

Files claimed

apps/ai-service/rag/answer.py, apps/ai-service/rag/agent.py, apps/ai-service/tests/test_grounded_generation.py, apps/ai-service/tests/test_agent.py, apps/web/app/_components/ChatPanel.tsx, packages/ui/src/ChatBubble.tsx, and this file.

Not touching ingestion/, retrieval adapters, rag/service.py, rag/understanding.py, or anything sparse/BM25 related — task #4 (real BM25 via Qdrant native sparse vectors) is still not started.

Test-coverage context for review

These areas start with little automated cover: no tests currently touch missing_pediatric_age_or_weight / missing_population, and the repo has no frontend test setup (no test script in apps/web/package.json, no ChatPanel/ChatBubble tests). A passing pytest run therefore is not on its own sufficient evidence here. Backend tests are being added alongside the changes, and the two frontend changes are verified by driving the real site.