9.6 KiB
26 — Known limitations
Objective statement of what is incomplete, fragile or unverified. Debt with a suggested remediation is in 27-technical-debt.md; this page is the honest inventory.
Product scope
- Only Part 2 of the book is ingested (printed pages 99–1496, 684
monographs). Part 1 general chapters — special-population guidance, poisoning
management, interaction principles — and Part 3 appendices — BSA table, IV
preparation, ATC index — are excluded by construction
(
segment/detector.py). Questions about them abstain, which is correct but is a real coverage gap for a clinician. - No reverse relations. "Which drugs cause X" and "which drugs are contraindicated in X" are routed to an explicit abstain.
- No dose calculation.
rag/calculators.pyimplements the book's own DuBois BSA formula and is tested, but no runtime code calls it, so a BSA-based dose still depends on a quarantined table the system will not read. - No recommendation or ranking, by design (prompt rule 10) — but this is enforced only by the prompt, not machine-checked.
Unfinished services
apps/api-gateway, apps/auth-service, apps/user-service,
apps/chat-service and apps/mobile contain a README.md and (for four of
them) a four-line package.json. There is no source. Consequences:
- no authentication or authorization anywhere;
- no user accounts, no per-user history, no session ownership;
- rate limiting lives in the frontend because the gateway that should own it does not exist;
infra/k8s/base/{api-gateway,auth-service,chat-service,user-service}/are empty placeholder directories.
Safety and correctness caveats
- The entailment judge is one LLM pass. Deliberate (repeating a temperature-0 prompt is a correlated retry, not an independent vote), but it means a single false acceptance is not caught by redundancy — and its accuracy is not measured by any committed eval run.
- Quarantined content is surfaced, not reconstructed. 151 block descriptors exist; their numbers are unavailable to the system. A dosing table the clinician needs may simply not be answerable.
- Table row/column reconstruction is unverified, and recall for borderless
tables and bar-less formulas is unquantified —
cli chunk-readysays so in its own output. - No whole-document human-reviewed ground truth exists, so content accuracy against the source is not proven by any gate.
prose_textvstext. The retrieval payload embedstext, which may carry repeated context labels. That is deliberate for retrieval, but it means the embedded string is not byte-identical to the book.- Grounding cannot check non-numeric semantics — that is the entailment pass's job, and it is the weaker of the two checks.
Retrieval limitations
- Dense search is used in exactly one place: the indication fallback. A question phrased unlike the book, about a drug's section, relies on the keyword section resolver or on rerank over the whole monograph.
- No hybrid search.
rag/fusion.py(RRF) is implemented and tested but has no runtime caller. - No query expansion / multi-query.
rag/expansion.pylikewise. search_lexicalis not BM25 — its score is the count of distinct matched tokens, with no term frequency, IDF or length normalisation.textis not inINDEXED_PAYLOAD_FIELDS, yetsearch_lexicalissuesMatchTextconditions against it. Qdrant needs an explicit full-text index for that; the effective behaviour of those filters on the deployed collection was not verified in this pass.- Cross-section pooling is enabled for
than_trongonly, on the strength of one measured case. The same class of miss in other sections is not covered. - Parent/child hydration is inert — no chunk in the corpus sets
parent_id. atc_codesis indexed but never queried.
Conversation and state
RagAgent._last_frameand_clarify_streakare in-process dicts. They are lost on restart and not shared across replicas, so multi-turn quality degrades silently ifai-serviceis scaled horizontally — nothing detects this.conversation_idis an unauthenticated, client-chosen string with no ownership check; anyone who guesses one reads its history into their prompt.rag_conversation_turngrows without bound. No retention, no deletion.
Performance
- No streaming. The UI shows a spinner for the whole turn. Measured (n=8, one user, sequential, 2026-08-11): 6.2–40.3 s.
- No caching of any kind at request time — identical questions re-pay for every model call.
- Sequential model calls: 3 on the happy path, up to 8 under the budget.
CatalogDrugResolveris O(catalog) on a fuzzy miss; thelru_cachefixes repeat lookups but a genuinely new typo still costs ~1 s of CPU./api/pdfreads a 37 MB file into memory per request, with no range support, no caching headers, and no rate limit (the middleware has no rule for that prefix).
Testing and evaluation
- Zero frontend tests. Every hard-won fix in
ChatPanel.tsx,middleware.tsandroute.ts— the 65 s timeout derivation, the abort handling, the Strict-Mode duplicate guard, theREFUSALSmap, citation grouping — can regress silently. - No test runs in CI. A commit that breaks all 555 tests still deploys.
apps/ai-servicetests cannot be collected withoutEMBEDDING_PROVIDER=disabledor a reachable Qdrant, and that is documented nowhere in the repository.- No evaluation runner. 209 golden rows and 90 JSONL cases exist; nothing executes them and no metric is tracked over time. No regression gate.
- No load, performance or security testing.
- Migrations are never exercised by a test.
- The Helm chart is never rendered or linted.
Deployment and operations
- Images are built on the production host and untagged, so rollback requires a rebuild and there is no known-good artifact.
- Migrations are forward-only; a rollback across one is uncovered.
- No staging environment is actually deployed.
- No backup automation for PostgreSQL or Qdrant.
qdrant/qdrant:latestis unpinned.- There is no Python lockfile; the Dockerfile installs unpinned ranges
(
"boto3"has no bound at all), so two builds of the same commit can differ. - The Kubernetes/ArgoCD path is written but unapplied, with three
TODOplaceholders per environment and no image registry.
Security
Full detail in 16-security.md. Headline gaps: no authentication, no authorization, no conversation ownership, a committed default PostgreSQL credential, containers running as root, no security context or NetworkPolicy in the chart, no dependency scanning, no security headers, and no retention or redaction for user-supplied patient context.
Observability
- No alerting at all — no Alertmanager, no rule files, no Grafana alerts.
- No log aggregation and no structured logging;
agent.pylogs routine timings at WARNING because uvicorn does not wire the root logger. webis entirely uninstrumented.- Four metric names are registered but never incremented.
- No SLOs or error budgets.
Documentation/code discrepancies
Found by comparing the pre-existing documents against the code. The code wins in every case.
| Claim | Where | Reality |
|---|---|---|
"conversation history is an in-process dict per RagAgent, not yet durable" |
docs/architecture.md service table |
PostgresConversationStore is wired in bootstrap.py and backs recent()/append(). Only _last_frame and _clarify_streak remain in-process |
"web … Calls api-gateway only" |
docs/architecture.md service table |
web calls ai-service directly via AI_SERVICE_URL; no gateway exists |
| "Qwen3 via the Converse API for understanding/generation/entailment" | docs/architecture.md |
The model is configuration. Code default deepseek.v3.2; local .env qwen.qwen3-next-80b-a3b; production value is in an uncommitted .env.prod and cannot be verified from the repository |
| api-gateway / auth-service / user-service / chat-service described with owned responsibilities and data | docs/architecture.md service table |
Not built. The document does flag this elsewhere, but the table reads as current state |
| Redis "session/refresh-token cache, rate-limit counters" | docs/architecture.md |
No Redis client is imported anywhere. Present only in the local-dev Compose file |
"fusion.py/context.py/expand_siblings are dead code" |
docs/current-rag-pipeline-audit.md |
context.py::pack_evidence is now wired into RetrievalService.retrieve_framed. fusion.py and expansion.py remain unwired |
| "trace has no per-stage timing" | docs/current-rag-pipeline-audit.md |
telemetry.stage() now emits duocthu_stage_duration_seconds and per-stage spans |
ADR 0007's Focus/ConversationState and the bounded PLAN/RETRIEVE/ASSESS/REFINE/VERIFY loop |
docs/adr/0007 |
Superseded by ADR 0008; rag/conversation.py and rag/reasoning.py no longer exist. The LOOP_* metric names survive as dead constants |
infra/ci/github-actions/README.md lists five CI workflows |
that README | None exists; the only workflow is deploy.yml |
| ADR 0005 "Contract/schema only — no implementation" | docs/adr/0005 |
The contract is implemented — segment/models.py and chunk/ both follow it |
Dated planning documents (v1-delivery-plan.md, rag-rebuild-plan.md,
answer-experience-implementation-plan.md,
condition-to-drug-audit-and-design.md, full-coverage-parsing-plan.md) record
intent on their date. They were not audited line-by-line here; treat them as
history, not status.