An ASP.NET Core (.NET 10) Web API + React/TypeScript SPA running a retrieval-augmented Q&A
vertical slice over land/title documents: ingest a PDF (or .txt/.md) → extract structured fields
→ chunk → embed → top-k similarity retrieval → answer with citations. Each ingested document is
persisted (original file + metadata), so you also get a documents table and a source-file viewer,
and every citation links back to the document it came from.
It is deployed — a single container on Azure Container Apps, secrets pulled from Key Vault via managed
identity, CI/CD on merge to main (see DEPLOYMENT.md +
ADR-0016). It is
not production-hardened — RBAC, observability, VNet/Private Link and the like are deliberately
out of scope (see CLAUDE.md); access is gated to the owner's Microsoft account
(ADR-0022).
Senior-level judgment made visible: deliberate scope (build vs. stub), a spec- and ADR-first workflow, and an agentic process that's part of the deliverable.
- Ingest (
POST /documents) — parse PDF / decode text → extract fields (best-effort) → chunk → embed → store chunks in the vector store and persist the original file + metadata in the document store. - Ask (
POST /ask) — embed the question → retrieve top-k chunks across the whole corpus → answer grounded only in those passages, cite-or-error (an answer always carries ≥1 citation, or it 409s on an empty store). - Read back (
GET /documents,GET /documents/{id},GET /documents/{id}/file) — list documents with their fields, and open the original file inline in the viewer. - Explore (Dashboard tab) — KPI tiles, documents-by-location and ingest-over-time charts, a
needs-review list, and lease expirations — all aggregated client-side from
GET /documents; the documents table also adds search + CSV export. - Delete (
DELETE /documents/{id}) — multi-select removal of documents from both stores (chunks + file/metadata); idempotent. - Monitor (
GET /usage, Ops / Usage tab) — LLM token usage, estimated cost, request health (success / 4xx / 429 / 5xx), and latency, read live from free Azure Monitor platform metrics (ADR-0020).
Every external dependency sits behind an interface with a live adapter (production default) and an offline adapter (used by the test suite, no cloud credentials). Swapping is a config-section change, never a code change.
| Port | Live default | Offline / fallback |
|---|---|---|
IChatClient |
Azure OpenAI gpt-5.4-mini (ADR-0012) |
Anthropic claude-opus-4-8 (config-swap fallback) |
IEmbeddingClient |
Azure OpenAI text-embedding-3-small, 256-d (ADR-0013) |
Local deterministic hashing (offline/test) |
IVectorStore |
Azure AI Search Free tier (ADR-0017) | In-memory cosine (offline/test) |
IDocumentStore |
Azure Blob Storage (ADR-0018) | In-memory (offline/test) |
IUsageSource |
Azure Monitor platform metrics (ADR-0020) | In-memory (offline/test) |
Backend (backend/) — ASP.NET Core, .NET 10:
dotnet build
dotnet test # fully offline — pins the in-memory + local adapters
dotnet run --project src/LandDoc.Api # listens on http://localhost:5084Frontend (frontend/) — React + TS + Vite:
npm install
npm run dev # http://localhost:5173, dev-proxies /documents + /ask to the API
npm test # Vitest + React Testing Library (mocked client)Run with no cloud credentials by pinning the offline providers (defaults in tests):
ModelClient__EmbeddingProvider=local, ModelClient__ChatProvider=anthropic (needs an Anthropic key) or
a fake, VectorStore__Provider=inmemory, DocumentStore__Provider=inmemory. See
RUNBOOK.md for all config keys.
CLAUDE.md— project constitution (architecture · conventions · guardrails)CONTRIBUTING.md— how to work in this repo + the doc workflowbackend/— ASP.NET Core Web API; modulesIngestion·Extraction·Retrieval·Qa·Usage, ports/adapters underStorage/+Model/frontend/— React/TS SPA (upload → fields → documents table → ask → answer-with-citations → source viewer)knowledge/— living docs, evergreen notes, committed session logs, lessons- Design: PRD · Stack · Architecture · Data model · Data flow · API · Glossary
- Operate: Runbook · Deployment · CI/CD · Azure config · Usage dashboard
- Decide: ADRs · Specs · Lessons
Docs are authored as design intent before code; once code lands,
/wrapkeeps them current.