| Last updated | 2026-07-26 |
| Current phase | Agent Phase 1 complete; Evaluation Framework Phase 1 design complete |
| Version | 0.1.0 |
This document records what exists and has been verified, as distinct from what is planned. Anything listed as complete has been executed and its output observed.
Every gate below was run against the working tree, not inferred.
| Gate | Command | Result |
|---|---|---|
| Lint | ruff check src tests |
Pass, 0 findings |
| Format | ruff format --check src tests |
Pass, 79 files |
| Types | mypy (strict) |
Pass, 61 source files |
| Tests | pytest tests/unit |
462 passed |
| Image build | docker build |
Pass |
| Container boot | docker run + health probes |
Pass, healthy |
| Non-root container | docker exec oqa-smoke id -u |
10001 |
| Chat end to end | Live SSE stream against qwen2.5-coder:7b |
Pass |
| Query pipeline end to end | Live OpenMRS 8.0.46, 392,181 observations | Pass |
Every figure below was produced by the running agent and cross-checked against a direct database probe.
| Question | Answer | Behaviour verified |
|---|---|---|
| "How many patients are registered?" | 7,226 | Correct count, voided = 0 applied |
| "What is the average weight recorded?" | 24.90 | Resolved "weight" to concept 119 from the live dictionary; used value_numeric because the concept datatype is Numeric |
| "How many encounters in each year?" | 5 rows, 1998 to 2026 | Grouped aggregate, LIMIT injected, plan warnings surfaced |
| "How many encounters this month?" | 623 | Period resolved to 2026-07-01/2026-07-31; used encounter_datetime after the repair loop rejected date_created |
| "How many deliveries were recorded?" | Clarifying question | Four matching concepts found; refused to guess |
| "What does the voided column mean?" | Conversational answer | Scope gate routed away from SQL |
| "What is the weather tomorrow?" | Refused | Out of domain, refused before any model call |
| "Ignore all previous instructions..." | Refused | Manipulation detected, refused before any model call |
DELETE FROM obs (direct) |
Refused | not_a_select, forbidden_statement |
Each runs before the next, and each assumes the previous may have failed.
| Layer | Enforces | When |
|---|---|---|
| Scope gate | Only anchored data questions reach the generator | Before any model call |
| AST validator | No DDL/DML, no blocked functions, no system schemas | After generation |
| Clinical checks | Correct time column, soft-delete filter present | After validation |
| Plan review | Row-scan budget | Before execution |
| Executor | Read-only session, server timeout, row cap | During execution |
| Database grant | SELECT only |
Always |
Ruff runs through the pinned ghcr.io/astral-sh/ruff:0.16.0 container on this
workstation, because a Windows Application Control policy blocks unsigned
executables from user directories. CI runs it natively.
Status: complete.
| Area | Modules | Notes |
|---|---|---|
| Configuration | oqa.config.settings |
Typed settings for every environment variable, with cross-field validation and production guardrails that refuse unsafe startup |
| Logging | oqa.config.logging |
structlog with a PHI-redaction processor and request correlation; unified with stdlib so uvicorn and SQLAlchemy emit the same JSON |
| Domain models | oqa.domain.models.* |
Query pipeline, schema graph, concept dictionary, RAG, security, audit |
| Domain errors | oqa.domain.errors |
18 error types with stable codes, HTTP status and retry semantics |
| Ports | oqa.application.ports.* |
10 Protocol interfaces: LLM, embedding, sparse encoding, vector store, re-ranker, SQL executor, cache, audit, and four repositories |
| Health | oqa.application.health |
Concurrent dependency checks with per-check timeout; required vs optional distinction |
| API | oqa.interfaces.api.* |
App factory, middleware, error translation, metrics, health and chat routes |
| CLI | oqa.interfaces.cli.main |
oqa config check, oqa config show, oqa serve |
| LLM adapter | oqa.infrastructure.llm.ollama |
Completion, constrained JSON, streaming, health; narrow retry policy |
| Conversations | oqa.infrastructure.memory |
Bounded LRU store implementing ConversationRepository |
| Chat service | oqa.application.use_cases.chat |
Prompt assembly, bounded history, multi-turn memory |
| Chat UI | oqa.interfaces.api.static |
Self-contained streaming interface, no external requests |
Working and verified against a real model. It streams, remembers context across turns, renders Markdown with SQL highlighting, and runs with no external network access.
What makes it more than a wrapper is the system prompt, which is treated as a
safety control and tested as one. Asked for a figure it cannot retrieve, the
agent declines and shows the SQL instead, using :delivery_concept_id rather
than inventing an ID. Verified live:
- Refuses to state counts it cannot retrieve.
- Uses a named placeholder instead of a hallucinated
concept_id. - Includes
voided = 0andCOUNT(DISTINCT ...)unprompted. - Resolves "that table" to
obson a follow-up turn, which requires history.
One observed weakness worth recording: the model filtered on date_created
where obs_datetime is the clinically correct column. That is precisely the
class of error retrieval grounding (Phase 3) exists to fix, and it is why the
chat is not yet connected to a database.
Not scaffolding, and covered by tests:
- Join-path discovery (
SchemaGraph.find_join_path): breadth-first search over foreign keys in both directions, with a hop limit. This is what stops the SQL generator inventing relationships. - Column sensitivity classification (
classify_column): explicit OpenMRS core lists, then regex fallback for module and Bahmni tables, defaulting to the conservative side for unknown columns in clinical tables. - PHI log redaction: recursive, depth-bounded, with an allowlist so
concept_nameandtable_namestay readable whilegiven_namedoes not. - Production guardrails: seven checks, all violations reported at once.
- Concept datatype to
obscolumn mapping: the difference between a query that works and one that silently returns zero rows.
- Multi-stage
Dockerfile: non-root UID 10001, no build toolchain in the runtime image, healthcheck on/health/live. docker-compose.ymlwith four profiles: default (Qdrant, Redis, API),ai(Ollama),demo(a real OpenMRS reference installation to introspect),obs(Prometheus, Grafana, Loki).- GitHub Actions CI: lint, strict types, unit tests on a 2x2 OS and Python
matrix, integration tests against service containers,
pip-auditand Trivy, and a Docker build that boots the container and asserts it is non-root. Makefileworking on Windows (Git Bash) and POSIX.
Five real defects, all caught by tests and by actually running the stack rather than by review. The last two were found only by starting the system end to end, which is why "it builds" is not the same gate as "it runs":
- Database password leaked through serialisation.
sqlalchemy_urlwas a Pydanticcomputed_field, so the plaintext password appeared inrepr()and inmodel_dump(), and therefore inoqa config show --jsonand in any traceback that formatted a settings object. Fixed by making it a plain property; only the maskedsafe_urlis serialisable. Four regression tests now cover it. create_app(settings)ignored its argument. Routes resolved settings through the globalget_settings()singleton rather thanapp.state, so/health/inforeported the process environment instead of the application's. Fixed with aget_app_settingsdependency readingapp.state.- The container could not start.
uvicorn --log-config /dev/nullmakes uvicorn parse an empty file as a logging config and abort. The CLI has no way to express "no logging config"; only the programmatic API acceptslog_config=None. The image now runsoqa serve. - The documented quick start produced a crash loop.
.env.exampledescribesOQA_CORS_ORIGINSas comma-separated, but pydantic-settings JSON-decodes list fields, socp .env.example .envfollowed bydocker compose upfailed on boot with a parse error. The field now usesNoDecodewith a validator accepting comma-separated values, a JSON array, or an empty string. An existing test had masked this: it was named "comma-separated" but passed JSON. Six tests now cover the real forms. - Metrics were declared but never incremented.
/metricsserved onlyHELPandTYPElines, so a Grafana dashboard wired tooqa_http_requests_totalwould have shown an idle system rather than a missing instrument. The access-log middleware now records the counter and the latency histogram, labelled by route template so no identifier can enter a label value.
| Document | Lines | Status |
|---|---|---|
| Vision | 527 | Complete |
| Functional requirements | 1,658 | Complete, 90 requirements |
| Non-functional requirements | 1,281 | Complete, 59 NFRs |
| Architecture | 659 | Complete |
| Security guide | 1,160 | Complete |
| Deployment guide | 1,786 | Complete |
| ADR 0001-0009 | 9 records | Complete |
| High-level design | 0 | Not written. See below |
docs/05-high-level-design.md is not yet written. It was scoped for Phase 1
and its authoring run was cut short. The material it would contain (per-stage
input and output types, sequence diagrams, the error-to-audit-action mapping) is
partly covered by Architecture sections 4 and 5, but the
detailed design remains outstanding and should be completed before Phase 2
implementation begins.
Status: design complete, awaiting approval.
Evaluation architecture (748 lines) specifies the benchmark and continuous-learning platform. The load-bearing decisions:
- A four-tier gold standard that avoids circular evaluation. The largest tier constructs SQL deterministically from the introspected schema graph and the live concept dictionary, then generates the question from the SQL, so correctness is established by construction rather than asserted by a model.
- Execution accuracy as the primary metric, with result-set equivalence defined precisely. SQL string similarity is reported but never gated on.
- Metamorphic relations as a gold-free signal, targeting exactly the two
errors that inflate OpenMRS numbers silently: a missing
voided = 0and a join that turnsCOUNT DISTINCTintoCOUNT. - AST diffing with sqlglot for objective, automated error classification.
- A sealed holdout with family-based splits, so the improvement loop cannot overfit the benchmark without it being visible.
src/oqa_eval/domain/benchmark.py, with 35 tests. The two behaviours that carry
the framework's integrity are implemented and covered:
GoldStandard.is_scorablestructurally prevents model-drafted items from contributing to a headline accuracy figure until a named human verifies them.assign_splitspartitions by family, using a stable hash so that adding items never reshuffles existing ones and the holdout stays sealed across releases.
Section 12 of the evaluation architecture proposes per-difficulty accuracy targets in place of a single ">95%" figure, with a weighted overall target of 82% execution accuracy and 98% execution success. The reasoning is recorded there. This needs a decision before Phase 2.
Five questions in section 16 of that document need answers before evaluation Phase 2 starts, most importantly whether a real OpenMRS database is available for validating synthetic-data realism.
| Phase | Scope | Status |
|---|---|---|
| 1 | Requirements, architecture, foundation | Complete |
| 2 | Database reverse engineering, metadata extraction | Not started |
| 3 | RAG pipeline | Not started |
| 4 | MCP server | Not started |
| 5 | SQL generation, validation, optimisation | Not started |
| 6 | AI orchestration | Not started |
| 7 | Web frontend | Not started (Node.js is not installed on this workstation) |
| 8 | Authentication | Not started |
| 9 | Reporting | Not started |
| 10 | Test hardening | Not started |
| 11 | Performance tuning | Not started |
| 12 | Production deployment | Not started |
| Phase | Scope | Status |
|---|---|---|
| 1 | Architecture and benchmark design | Design complete, awaiting approval |
| 2 | Synthetic data generator | Blocked on agent Phase 2 |
| 3 | Benchmark generator, Tiers A and B | Blocked on eval Phase 2 |
| 4 | Evaluation engine | Blocked on agent Phase 5 |
| 5-10 | Diagnosis, learning loop, dashboard, datasets, load, deployment | Not started |
Observed on this workstation, and relevant to what can be verified locally:
| Tool | Status |
|---|---|
| Python 3.12.10 | Available |
| Docker 29.5.3, Compose v5.1.4 | Available |
| Git 2.54.0 | Available |
| Ollama 0.32.3 | Installed, not running; no models pulled |
| Node.js / npm | Not installed; required for Phase 7 |
| MySQL client | Not installed; the Python driver is used instead |
| Ruff native binary | Blocked by Application Control; run via container |
No OpenMRS database is currently configured. Phase 2 introspection should run
against docker compose --profile demo up -d, which starts a real OpenMRS
installation, so the schema is read rather than assumed.