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

165 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 26 — Known limitations
Objective statement of what is incomplete, fragile or unverified. Debt with a
suggested remediation is in [27-technical-debt.md](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](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.