Files
duocthu/docs/26-known-limitations.md
T

9.6 KiB
Raw Blame History

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 991496, 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.py implements 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-ready says 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_text vs text. The retrieval payload embeds text, 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.py likewise.
  • search_lexical is not BM25 — its score is the count of distinct matched tokens, with no term frequency, IDF or length normalisation.
  • text is not in INDEXED_PAYLOAD_FIELDS, yet search_lexical issues MatchText conditions 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_trong only, 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_codes is indexed but never queried.

Conversation and state

  • RagAgent._last_frame and _clarify_streak are in-process dicts. They are lost on restart and not shared across replicas, so multi-turn quality degrades silently if ai-service is scaled horizontally — nothing detects this.
  • conversation_id is an unauthenticated, client-chosen string with no ownership check; anyone who guesses one reads its history into their prompt.
  • rag_conversation_turn grows 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.240.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.
  • CatalogDrugResolver is O(catalog) on a fuzzy miss; the lru_cache fixes repeat lookups but a genuinely new typo still costs ~1 s of CPU.
  • /api/pdf reads 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.ts and route.ts — the 65 s timeout derivation, the abort handling, the Strict-Mode duplicate guard, the REFUSALS map, citation grouping — can regress silently.
  • No test runs in CI. A commit that breaks all 555 tests still deploys.
  • apps/ai-service tests cannot be collected without EMBEDDING_PROVIDER=disabled or 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:latest is 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 TODO placeholders 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.py logs routine timings at WARNING because uvicorn does not wire the root logger.
  • web is 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.