{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\ud83e\udde9 Khata App \u2014 Expenses & Cashflow Service","text":"

khata-app is the OpenAPI-first backend for tracking expenses and cashflow. It ingests bank statements (PDF, CSV, XLSX, email, LlamaParse), runs rule-driven enrichment to identify payees and tags, and exposes everything through a spec-driven FastAPI service on MongoDB.

Doc model: this wiki is written for humans \u2014 how-to guides, examples, and testing recipes. The authoritative API contracts live in the code (docstrings) and the machine-readable bundle under docs/mcp/.

"},{"location":"#key-features","title":"\ud83d\ude80 Key Features","text":""},{"location":"#installation","title":"\ud83d\udce6 Installation","text":"
# Local development\npip install -e \".[dev]\"\n\n# App dependencies (from pyproject.toml)\npip install -e .\n

Requires Python \u2265 3.9 (development venv runs 3.13) and a running MongoDB + optional Redis.

"},{"location":"#documentation-structure","title":"\ud83d\udcc1 Documentation Structure","text":"Section Description Overview What Khata is and how it fits the stack Architecture Layers, data flow, and lifecycle rules Components The domain, interfaces, and infra building blocks Domain Resources Users, entities, accounts, tags, expenses, uploads Enrichment Entity resolution and tag assignment pipelines Extraction Pipelines PDF/CSV/XLSX/email/llama extraction families Fetch Request Workflow Worker, SSE, CLI, orchestration Reports & Caching Reports service, HTTP cache, Redis Running The Service Boot, config, seed scripts Development Environment, testing, docstrings, docs builds"},{"location":"#related-resources","title":"\ud83d\udd17 Related Resources","text":"

\u00a9 Aetoskia Internal \u2014 khata-app 0.1.0

"},{"location":"01_overview/","title":"\ud83e\udded Overview","text":"

Khata is an internal expenses and cashflow service. It manages users, entities, accounts, tags, expenses, uploads, and fetch requests on MongoDB, ingesting bank statements from several file formats and turning them into normalized expense records enriched with payee entities and tags.

"},{"location":"01_overview/#what-it-does","title":"\ud83c\udfaf What It Does","text":""},{"location":"01_overview/#where-it-lives","title":"\ud83e\udde9 Where It Lives","text":"

Khata is the read/write service in the wider stack: other services query expenses and tags downstream, while Khata itself pulls documents and external OCR. The domain knows only its own models and ports; all adapters (Mongo, Redis, HTTP, OpenAI-compatible LLM, email, LlamaParse) live in infra.

"},{"location":"01_overview/#notable-trade-offs","title":"\ud83d\udccc Notable Trade-offs","text":"Decision Rationale OpenAPI-first routing Single source of truth for the HTTP surface; handlers are thin no plain __init__ factories Entrypoints wire the object graph explicitly in init.py In-memory LRU + Redis Hot entity/account lookups avoid Mongo round-trips during enrichment x-fk cross-collection population Repos resolve referenced ids with explicit filters instead of Mongo joins YAML pattern registries Rule changes require no code deploy"},{"location":"01_overview/#roadmap-notes","title":"\ud83d\uddfa Roadmap Notes","text":""},{"location":"01_overview/#related","title":"\ud83d\udd17 Related","text":"

Architecture \u00b7 Components \u00b7 Running The Service \u00b7 Development

"},{"location":"02_architecture/","title":"\ud83c\udfdb Architecture","text":"

Khata follows a strict three-layer architecture with one-way data flow and no backflow between layers.

"},{"location":"02_architecture/#layers","title":"\ud83e\uddf1 Layers","text":"Layer Package(s) Responsibility interface interfaces/ HTTP routes (OpenAPI handlers), CLI, SSE bus wiring, DTO mapping domain domain/ Models, ports (abstract contract), services, orchestration, enrichment pipelines infra infra/ Mongo repos, Redis cache, HTTP/email/Llama fetchers, OpenAI-compatible LLM pools

The pipelines/ package holds the hexa extraction runtime \u2014 the stateful, stage-driven extractors for PDF, CSV, XLSX, email, and llama sources. It sits outside the three-layer packages in pyproject.toml (packages = domain*, infra*, interfaces*) but is first-class code.

"},{"location":"02_architecture/#data-flow","title":"\ud83d\udd00 Data Flow","text":"
                         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                         \u2502              INTERFACES                 \u2502\n                         \u2502  openapi handler  \u00b7  sse bus  \u00b7  cli    \u2502\n                         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                            \u2502 DTOs / commands\n                         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                         \u2502                DOMAIN                   \u2502\n                         \u2502  models \u00b7 ports \u00b7 services (rules)      \u2502\n                         \u2502  fetch_pipeline orchestrator            \u2502\n                         \u2502  enrichment (entity / tags)  \u2190 dagpipe  \u2502\n                         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                            \u2502 ports (abstract)\n                         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                         \u2502                INFRA                    \u2502\n                         \u2502  mongo repos \u00b7 redis \u00b7 http \u00b7 llm pools \u2502\n                         \u2502  pdf/email/xlsx fetchers                \u2502\n                         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                            \u2502\n                                  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                                  \u25bc                   \u25bc\n                              MongoDB              Redis / external\n

Interfaces speak DTOs/commands; the domain owns rules and orchestration; infra implements ports and talks to the outside world. No infra import may appear in the domain, and no domain import may appear in infra.

"},{"location":"02_architecture/#wiring","title":"\ud83e\udde9 Wiring","text":"

Entrypoints (main.py, worker.py, init.py) build the object graph explicitly:

"},{"location":"02_architecture/#lifecycle-rules","title":"\ud83d\udd01 Lifecycle Rules","text":""},{"location":"02_architecture/#related","title":"\ud83d\udd17 Related","text":"

Components \u00b7 Domain Resources \u00b7 Extraction Pipelines \u00b7 Running The Service \u00b7 Development

"},{"location":"03_components/","title":"\ud83e\udde9 Components","text":"

A map of the codebase's major pieces and where they live.

"},{"location":"03_components/#packages","title":"\ud83e\udded Packages","text":"Path Contents interfaces/openapi/ OpenAPI-first routing: app bootstrap, handlers, DTO schemas, middlewares interfaces/fetch_request/cli/ Argparse CLI over the fetch-request service (list, get, create-file, create-email, ambiguities, resolve, retry) interfaces/sse/ SSE ProgressEventBus and subscription endpoints interfaces/report/ Report generation endpoints and query models domain/ The three resource groups + enrichment + fetch pipeline (see below) domain/enrichment/ Entity identity/resolution + tag assignment; dagpipe-based pipelines domain/fetch_pipeline/ Fetch-request orchestration: pipeline, ambiguity, orchestrator, progress events domain/fetch_request/ FetchRequest model, file/email sources, bank enum, service infra/repos/ Mongo repositories (mongo-ops) for every domain model infra/adaptors/ Cache store, gitignore-safe secrets, fetch helpers infra/cache/ LRU maps, CacheableDict, Redis cache, TTL decorators infra/http_cache/ HTTP client cache layer with TTL + max-entries eviction infra/email/ IMAP email fetcher and parsed-mailbox traversal infra/llm/ OpenAI / OpenRouter provider pools, policy prompts, DTOs infra/adapters/sse.py SSE response adapter for progress event streaming pipelines/extraction/ Hexa runtime: pdf, csv, xlsx, email, llama extraction families pipelines/enrichment/ Pipeline node construction for entity/tag enrichment stages"},{"location":"03_components/#domain-resource-groups","title":"\ud83c\udfd7 Domain Resource Groups","text":"Group Models include Purpose users User, UserActivity Actors; ownership of expenses/fetch requests entities Entity, EntityAccount Canonical payees with brands/branches accounts Account, AccountType Bank accounts owning expenses and statements expenses Expense, ExpenseKind, ExpenseSource, ExpenseTag The core normalized record tags Tag, TagColor Categorization labels with display colors uploads Upload Raw file/email ingest records fetch_request FetchRequest, FileSource, EmailSource, Bank Declares a statement ingestion job report Report, report models Aggregated outputs"},{"location":"03_components/#extraction-families-pipelinesextraction","title":"\u2699\ufe0f Extraction Families (pipelines/extraction/)","text":"Family Inputs Notes pdf Bank statement PDFs Heuristic, balance-driven reconciliation csv CSV exports Axis/ICICI layouts first xlsx Excel exports Tabular header+row parsing via openpyxl email IMAP mailboxes + attachments EmailFetchingParadigm traversal llama Documents via LlamaParse OCR + markdown distribution over pages

All families share the hexa runtime: BaseExtractionPipeline, Fetcher, ExtractionStage, and the ambiguous flag \u2014 get_families_to_enrich returns families that should proceed to enrichment.

"},{"location":"03_components/#related","title":"\ud83d\udd17 Related","text":"

Architecture \u00b7 Domain Resources \u00b7 Extraction Pipelines \u00b7 Reports & Caching

"},{"location":"04_domain_resources/","title":"\ud83d\udce6 Domain Resources","text":"

Each resource group is a domain package with models, ports (abstract repositories/services), and a service implementation \u2014 a facade over the repo. Everything is defined by GSDFC docstrings and mirrored as .pyi stubs.

"},{"location":"04_domain_resources/#users","title":"\ud83d\udc64 Users","text":""},{"location":"04_domain_resources/#entities","title":"\ud83c\udfe2 Entities","text":""},{"location":"04_domain_resources/#accounts","title":"\ud83c\udfe6 Accounts","text":""},{"location":"04_domain_resources/#expenses","title":"\ud83e\uddfe Expenses","text":""},{"location":"04_domain_resources/#tags","title":"\ud83c\udff7 Tags","text":""},{"location":"04_domain_resources/#uploads","title":"\ud83d\udce4 Uploads","text":""},{"location":"04_domain_resources/#fetch-request","title":"\ud83d\udce5 Fetch Request","text":""},{"location":"04_domain_resources/#repositories-ports","title":"\ud83d\udd01 Repositories (Ports)","text":"

Every group declares abstract repo ports (Ports: db.py, service.py) in the domain and implements them in infra/repos/ with mongo-ops. Repos support:

Shared wiring lives in facades.py (domain) and repositories.py (infra). The service registry keys used in init.py are the string names passed to get_svc(...).

"},{"location":"04_domain_resources/#related","title":"\ud83d\udd17 Related","text":"

Components \u00b7 Enrichment \u00b7 Reports & Caching

"},{"location":"05_enrichment/","title":"\ud83e\udde0 Enrichment","text":"

Enrichment turns raw payee/memo text into canonical entities and tags using YAML pattern registries, executed through dagpipe pipelines.

"},{"location":"05_enrichment/#entity-resolution","title":"\ud83c\udfe2 Entity Resolution","text":"

Lives under domain/enrichment/entity/:

The pipeline is expressed as a dagpipe.Pipeline; the stub pipeline.pyi type-checks against dagpipe directly (see domain/enrichment/entity/pipeline.pyi).

"},{"location":"05_enrichment/#tag-assignment","title":"\ud83c\udff7 Tag Assignment","text":"

Lives under domain/enrichment/tags/:

"},{"location":"05_enrichment/#how-rules-are-wired","title":"\ud83d\udd04 How Rules Are Wired","text":"
  1. The extraction stage yields expenses with raw memo/payee fields.
  2. The orchestrator (PipelineOrchestrator in domain/fetch_pipeline/) chains extraction \u2192 enrichment.
  3. Enrichment nodes receive entity/tag services and the http cache store for external lookups.
  4. Resolved entity_id / tag_id are written back onto the expense and persisted.
"},{"location":"05_enrichment/#testing","title":"\ud83e\uddea Testing","text":""},{"location":"05_enrichment/#related","title":"\ud83d\udd17 Related","text":"

Domain Resources \u00b7 Fetch Request Workflow \u00b7 Development

"},{"location":"06_extraction_pipelines/","title":"\ud83d\udce5 Extraction Pipelines","text":"

pipelines/extraction/ implements the hexa runtime: an extensible, stateful pipeline that fetches a source document and reduces it to normalized expense records, flagging anything ambiguous.

"},{"location":"06_extraction_pipelines/#common-runtime","title":"\ud83c\udfd7 Common Runtime","text":"Concept Where Purpose BaseExtractionPipeline shared Template method: fetch \u2192 stage \u2192 emit; tracks ambiguous state Fetcher shared Loads raw content (file bytes, emails, llama docs) ExtractionStage shared A single reduction step (headers, rows, reconciliation) PauseForResolution shared Raised when numbers can't be reconciled; workflow pauses get_families_to_enrich shared Families that should continue to enrichment

Every family registers a factory so pipelines/enrichment/ can build nodes from config at runtime.

"},{"location":"06_extraction_pipelines/#extraction-families","title":"\ud83d\udcc2 Extraction Families","text":"Family Payload Stages (typical) pdf Bank statement PDFs page split \u2192 text extraction \u2192 balance reconciliation csv CSV exports header detect \u2192 row mapping \u2192 dtype normalization xlsx Excel exports (openpyxl) sheet select \u2192 header detect \u2192 row mapping (Axis/ICICI first) email IMAP mailbox + attachments traversal paradigm \u2192 attachment dispatch to family llama LlamaParse documents page distribution \u2192 markdown parse \u2192 amount extraction"},{"location":"06_extraction_pipelines/#cross-cutting-utilities-pipelinesextractionshared","title":"Cross-cutting utilities (pipelines/extraction/shared/)","text":""},{"location":"06_extraction_pipelines/#ambiguity-reconciliation","title":"\u26a0\ufe0f Ambiguity & Reconciliation","text":"

Amount/balance reconciliation is the safety net: if extracted amounts don't balance against the statement, the pipeline raises PauseForResolution, the fetch request flips to paused, and the candidate is stored. A human (or later the LLM resolver) chooses the correct tuple; the worker resets the retry counter on pause.

"},{"location":"06_extraction_pipelines/#lifecycle-inside-a-run","title":"\ud83d\udd04 Lifecycle Inside a Run","text":"
  1. Worker polls pending fetch requests (status == \"pending\").
  2. PipelineOrchestrator.run_pipeline(fr_id):
  3. picks the family from fr.source.type + bank,
  4. runs extraction stages to raw Expense candidates,
  5. keeps unresolved candidates as ambiguities,
  6. forwards the clean set to enrichment,
  7. Enrichment assigns entity/tag; expenses are persisted; SSE progress events stream the whole way.
"},{"location":"06_extraction_pipelines/#related","title":"\ud83d\udd17 Related","text":"

Components \u00b7 Fetch Request Workflow \u00b7 Development

"},{"location":"07_fetch_request_workflow/","title":"\ud83d\udd04 Fetch Request Workflow","text":"

A fetch request is a statement ingestion job. This is the end-to-end journey and how to drive it.

"},{"location":"07_fetch_request_workflow/#creating-a-fetch-request","title":"\ud83d\udce5 Creating a Fetch Request","text":"

Via API:

# File source\ncurl -X POST http://localhost:8000/fetch-requests \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"user_id\":\"...\",\"account_id\":\"...\",\"source\":{\"type\":\"file\",\"path\":\"/var/statements/axis.pdf\",\"bank\":\"axis\"},\"start_date\":\"2025-04-01\",\"end_date\":\"2025-04-30\"}'\n

Via CLI:

# from repo root, with the khata venv active\n.venv\\Scripts\\python.exe -m interfaces.fetch_request.cli create-file --account \"Axis Savings\" --path /var/statements/axis.pdf --bank axis\n.venv\\Scripts\\python.exe -m interfaces.fetch_request.cli list --status pending\n.venv\\Scripts\\python.exe -m interfaces.fetch_request.cli get <id>\n

CLI subcommands: list, get, create-file, create-email, ambiguities, resolve, retry.

"},{"location":"07_fetch_request_workflow/#the-worker","title":"\u2699\ufe0f The Worker","text":"

worker.py polls for pending fetch requests and runs the pipeline.

.venv\\Scripts\\python.exe worker.py\n

Knobs (env vars):

Env var Default Meaning WORKER_POLL_MIN 5 Min poll interval (seconds); backs off \u00d72 up to MAX when idle WORKER_POLL_MAX 60 Max poll interval WORKER_MAX_RETRIES 3 Failed requests stop being retried beyond this count

Per cycle: fetch status=\"pending\" (limit 100), mark each processing, run the orchestrator, then:

Ambiguity resolution is enabled only when an LLM_API_KEY is present; today worker._maybe_create_resolver() returns None.

"},{"location":"07_fetch_request_workflow/#progress-sse","title":"\ud83d\udce1 Progress & SSE","text":"

ProgressEventBus (interfaces/sse/) emits typed progress events during a run; clients subscribe to the SSE endpoint per fetch request to watch stage transitions. The domain declares the event model in domain/fetch_pipeline/messaging/ (see ProgressEventBus port).

"},{"location":"07_fetch_request_workflow/#orchestration","title":"\ud83e\udde0 Orchestration","text":"

PipelineOrchestrator (domain/fetch_pipeline/utils/pipeline.py) wires the worker's services:

fr_svc \u00b7 ambiguity_svc \u00b7 expense_svc \u00b7 entity_svc \u00b7 tag_svc\npdf/email/xlsx/csv fetchers \u00b7 sse bus \u00b7 http cache store \u00b7 resolved_choices\n

with pause_on_ambiguity=True and a pre-loaded map of previously resolved ambiguity choices so repeats don't re-pause.

"},{"location":"07_fetch_request_workflow/#related","title":"\ud83d\udd17 Related","text":"

Extraction Pipelines \u00b7 Domain Resources \u00b7 Running The Service

"},{"location":"08_reports_and_caching/","title":"\ud83d\udcc8 Reports & Caching","text":"

Reports aggregate expense data; caching keeps the hot paths fast without Mongo round-trips.

"},{"location":"08_reports_and_caching/#reports","title":"\ud83d\udcca Reports","text":"

Report endpoints live under interfaces/report/ and run over the expenses via the report domain models in domain/report/.

Typical flow: GET /reports/{kind}?from=...&to=...&tag=... returns a grouped breakdown (count + total per bucket).

"},{"location":"08_reports_and_caching/#repositories-fk-population","title":"\ud83d\uddc3 Repositories & FK Population","text":"

Repos in infra/repos/ are generic mongo-ops repos wrapped per model. Key behaviors:

"},{"location":"08_reports_and_caching/#http-cache","title":"\ud83c\udf10 HTTP Cache","text":"

infra/http_cache/ caches external HTTP responses:

Config Meaning HTTP_CACHE_TTL Cache time-to-live for responses HTTP_CACHE_MAX_ENTRIES Eviction cap for the cache store REDIS_URL When set, cache lives in Redis; otherwise in-memory

Used by fetchers and enrichment lookups to avoid hammering upstreams during a full-statement import.

"},{"location":"08_reports_and_caching/#config-summary","title":"\u2699\ufe0f Config Summary","text":"Env var Purpose MONGO_URI / MONGO_DB MongoDB connection + database REDIS_URL Redis (cache mirror, optional) HTTP_CACHE_TTL / HTTP_CACHE_MAX_ENTRIES HTTP cache behavior LLAMA_CLOUD_API_KEY LlamaParse (llama family) EMAIL_HOST / EMAIL_USER / EMAIL_PASS IMAP ingestion AUTH_SERVER External auth/service integration WORKER_* Worker polling knobs (see fetch-request workflow) DEBUG Verbose logging"},{"location":"08_reports_and_caching/#related","title":"\ud83d\udd17 Related","text":"

Domain Resources \u00b7 Running The Service \u00b7 Development

"},{"location":"09_running_the_service/","title":"\ud83d\ude80 Running The Service","text":"

Boot sequence, configuration, and operational runbooks.

"},{"location":"09_running_the_service/#boot","title":"\ud83c\udfc3 Boot","text":"
# API (OpenAPI-first app from openapi.yaml)\n.venv\\Scripts\\python.exe main.py\n#  \u2192 http://localhost:8000, /openapi.json, /docs (per spec config)\n\n# Worker (poll loop)\n.venv\\Scripts\\python.exe worker.py\n

main.py calls init() (wiring from init.py), registers handlers, and serves. worker.py runs poll_fetch_requests(resources) in a loop.

"},{"location":"09_running_the_service/#configuration","title":"\ud83d\udd0c Configuration","text":"

Everything is env-var driven (see Reports & Caching for the full table). Minimal .env:

MONGO_URI=mongodb://localhost:27017\nMONGO_DB=khata\nREDIS_URL=redis://localhost:6379/0\nWORKER_POLL_MIN=5\nWORKER_POLL_MAX=60\nWORKER_MAX_RETRIES=3\nDEBUG=1\n

Secret-ish values (LLM keys, IMAP creds) go through the secrets adapter in infra/adaptors/ \u2014 never commit real keys.

"},{"location":"09_running_the_service/#local-workflow","title":"\ud83e\uddd1\ud83d\udcbb Local Workflow","text":"
pip install -e \".[dev]\"\n\n# seed / smoke\n.venv\\Scripts\\pytest.exe tests\\unit -q\n\n# CLI against the live service\n.venv\\Scripts\\python.exe -m interfaces.fetch_request.cli list\n

Requires MongoDB (and Redis for the cache mirror). A local Mongo on mongodb://localhost:27017 is the default dev target.

"},{"location":"09_running_the_service/#production-notes","title":"\u2699\ufe0f Production Notes","text":""},{"location":"09_running_the_service/#related","title":"\ud83d\udd17 Related","text":"

Fetch Request Workflow \u00b7 Development \u00b7 Development \u2014 Environment

"},{"location":"10_development/","title":"\ud83d\udee0 Development","text":"

How to work on Khata safely: environments, testing, documentation conventions, and doc builds.

"},{"location":"10_development/#sections","title":"\ud83d\udcda Sections","text":"Page What it covers Environment Venv setup, deps, package layout Testing Test layout, running, quality gates GSDFC Docstrings The docstring & stub rules for this repo Building Docs doc-forge lib/api/wiki/MCP builds"},{"location":"10_development/#contributing-checklist","title":"\u2795 Contributing Checklist","text":"
  1. Branch off main, keep the three-layer boundary (no backflow).
  2. Every public symbol gets a GSDFC docstring + updated .pyi stub.
  3. Wire services through init.py \u2014 do not by-pass the service registry.
  4. Run the gates: ruff check, pytest, py_compile on changed files.
  5. Regenerate docs (see Building Docs) when API/docstrings change.
"},{"location":"10_development/#related","title":"\ud83d\udd17 Related","text":"

Architecture \u00b7 Running The Service

"},{"location":"10_development/01_environment/","title":"\u2699 Environment","text":"

Local setup for developing khata-app.

"},{"location":"10_development/01_environment/#create-the-venv","title":"\u25b6 Create The Venv","text":"
# PowerShell, from repo root\npython -m venv .venv\n.\\.venv\\Scripts\\Activate.ps1\npip install -e \".[dev,doc]\"\n

The repo is Python 3.13 compatible (dev venv runs 3.13.5). pyproject.toml declares the app dist as packages domain*, infra*, interfaces*.

"},{"location":"10_development/01_environment/#key-tools","title":"\ud83d\udce6 Key Tools","text":"Tool Where Notes pytest .venv\\Scripts\\pytest.exe Unit + integration tests ruff .venv\\Scripts\\ruff.exe Lint/format checks doc-forge .venv\\Scripts\\doc-forge.exe Docs engine (lib/api/wiki/MCP) mypy (not installed) Optional type gate; stubs currently lint-clean instead

doc-forge is installed from the local aetos source (pip install -e \"C:\\Users\\vishe\\WorkSpace\\code\\aetos\\doc-forge[mkdocs,mcp]\"); it isn't on PyPI.

"},{"location":"10_development/01_environment/#pin-note-python-313","title":"\ud83d\udca1 Pin Note (Python 3.13)","text":"

MkDocs highlighter crashes on pygments 2.20 under 3.13 (filename=None bug). Keep pygments==2.19.2 in the khata venv:

.\\.venv\\Scripts\\python.exe -m pip install \"pygments==2.19.2\"\n
"},{"location":"10_development/01_environment/#package-map","title":"\ud83e\udded Package Map","text":"
khata-app/\n\u251c\u2500\u2500 main.py, worker.py, init.py   # entrypoints + wiring\n\u251c\u2500\u2500 openapi.yaml                   # API contract (drives routing)\n\u251c\u2500\u2500 interfaces/                    # openapi handlers, sse, report, cli\n\u251c\u2500\u2500 domain/                        # models \u00b7 ports \u00b7 services \u00b7 enrichment\n\u251c\u2500\u2500 infra/                         # mongo repos \u00b7 cache \u00b7 fetchers \u00b7 llm pools\n\u251c\u2500\u2500 pipelines/                     # extraction families + enrichment nodes\n\u251c\u2500\u2500 tests/                         # unit + integration suites\n\u251c\u2500\u2500 docs/                          # lib sources, api, wiki, mcp bundles (generated + authored)\n\u251c\u2500\u2500 docforge.nav.yml               # lib navigation spec\n\u2514\u2500\u2500 py.typed (in each package)     # typed marker for stub distribution\n
"},{"location":"10_development/01_environment/#related","title":"\ud83d\udd17 Related","text":"

Building Docs \u00b7 Testing \u00b7 Running The Service

"},{"location":"10_development/02_testing/","title":"\ud83e\uddea Testing","text":"

What's covered and how to run the gates.

"},{"location":"10_development/02_testing/#layout","title":"\ud83d\udcc1 Layout","text":"Path Content tests/unit/ Pure unit tests (domain, extraction stages, enrichment helpers) tests/domain/ Domain sub-suites: resources, enrichment, fetch pipeline tests/ (integration) Mongo-backed repo/service integration tests"},{"location":"10_development/02_testing/#running","title":"\ud83d\ude80 Running","text":"
# Full unit suite\n.\\.venv\\Scripts\\pytest.exe tests\\unit -q\n\n# A single area\n.\\.venv\\Scripts\\pytest.exe tests\\domain\\enrichment -q\n\n# Lint gate (baseline ~79 pre-existing errors on main; don't add new ones)\n.\\.venv\\Scripts\\ruff.exe check domain infra interfaces pipelines\n\n# Syntax gate on everything including new stubs\n.\\.venv\\Scripts\\python.exe -m py_compile .\\main.py .\\worker.py\nGet-ChildItem -Recurse -Filter *.py -Path domain,infra,interfaces,pipelines | `\n  ForEach-Object { .\\.venv\\Scripts\\python.exe -m py_compile $_.FullName }\n

Current health: 1190 passed, 1 skipped; all *.pyi lint-clean after the F821 fixes in the enrichment pipeline stubs.

"},{"location":"10_development/02_testing/#whats-heavily-tested","title":"\ud83e\udde9 What's Heavily Tested","text":""},{"location":"10_development/02_testing/#stub-checks","title":"\u26a0\ufe0f Stub Checks","text":"

Stubs mirror public signatures exactly. When a signature changes, the .pyi must change with it or the type gate breaks. Two stubs import dagpipe.Pipeline instead of a rogue string forward-ref:

# domain/enrichment/entity/pipeline.pyi\n# domain/enrichment/tags/pipeline.pyi\nfrom dagpipe import Pipeline\ndef load() -> Pipeline: ...\n
"},{"location":"10_development/02_testing/#related","title":"\ud83d\udd17 Related","text":"

Environment \u00b7 GSDFC Docstrings \u00b7 Building Docs

"},{"location":"10_development/03_gsdfc_docstrings/","title":"\ud83d\udcdd GSDFC Docstrings","text":"

khata-app adheres to the GSDFC docstring conventions (see the doc-forge GSDFC guide). The rules below are the ones enforced in this repo.

"},{"location":"10_development/03_gsdfc_docstrings/#core-rules","title":"\ud83c\udfd7 Core Rules","text":""},{"location":"10_development/03_gsdfc_docstrings/#format-cheat-sheet","title":"\ud83d\udcc4 Format Cheat-Sheet","text":"
\"\"\"# Summary\n\nImperative one-liner of what this does.\n\n---\n\nArgs:\n    name (str):\n        Description on the next line.\n\nReturns:\n    bool:\n        Description.\n\"\"\"\n

A typical module docstring:

\"\"\"# Summary\n\n...one-liner...\n\n---\n\nNotes:\n- point one.\n- point two.\n\"\"\"\n
"},{"location":"10_development/03_gsdfc_docstrings/#stub-files","title":"\ud83d\uddc3 Stub Files","text":"

Every module gets a sibling .pyi stub mirroring the public surface only:

Generate/validate conveniently with doc-forge's stub tooling, and always run py_compile + ruff on new stubs.

"},{"location":"10_development/03_gsdfc_docstrings/#anti-patterns","title":"\u274c Anti-Patterns","text":""},{"location":"10_development/03_gsdfc_docstrings/#related","title":"\ud83d\udd17 Related","text":"

Building Docs \u00b7 Testing \u00b7 Development

"},{"location":"10_development/04_building_docs/","title":"\ud83d\udee0 Building Docs","text":"

All documentation for khata-app is produced with doc-forge (installed into the khata venv from the local aetos source).

"},{"location":"10_development/04_building_docs/#what-gets-built","title":"\ud83d\udccb What Gets Built","text":"Target Command (from repo root) Output Lib (mkdocs) doc-forge build --mkdocs --module <pkg> [--refresh] docs/lib/\u2026 + docs/mkdocs.lib.yml, site/lib/ MCP bundles doc-forge build --mcp --module <pkg> --out-dir docs\\mcp\\<pkg> docs/mcp/<pkg>/ (index.json, nav.json, modules/) API (OpenAPI) doc-forge build --api --openapi-spec <spec.json> docs/api/ + docs/mkdocs.api.yml Wiki doc-forge build --wiki --site-name \"Khata App \u2014 Wiki\" nav from docs/wiki/ + docs/mkdocs.wiki.yml Serve doc-forge serve local preview

Builds must run from the repo root so Griffe discovers the packages.

"},{"location":"10_development/04_building_docs/#lib-builds","title":"\ud83d\udcda Lib Builds","text":"
# first time per package\n.\\.venv\\Scripts\\doc-forge.exe build --mkdocs --module domain\n.\\.venv\\Scripts\\doc-forge.exe build --mkdocs --module infra\n.\\.venv\\Scripts\\doc-forge.exe build --mkdocs --module interfaces\n.\\.venv\\Scripts\\doc-forge.exe build --mkdocs --module pipelines\n\n# after docstring/nav changes, refresh and set the nice site name\n.\\.venv\\Scripts\\doc-forge.exe build --mkdocs --module domain --refresh --site-name \"Khata App \u2014 Library Reference\" --project-name khata-app\n

docforge.nav.yml drives the nav groups:

home: lib/index.md\ngroups:\n  Domain: [lib/domain/index.md, lib/domain/enrichment/index.md, lib/domain/fetch_pipeline/index.md, lib/domain/fetch_request/index.md]\n  Infra: [lib/infra/index.md]\n  Interfaces: [lib/interfaces/index.md]\n  Pipelines: [lib/pipelines/index.md]\n
"},{"location":"10_development/04_building_docs/#mcp-bundles","title":"\ud83e\udde0 MCP Bundles","text":"

Serve the API as machine-readable resources an MCP client can consume:

.\\.venv\\Scripts\\doc-forge.exe build --mcp --module domain --out-dir docs\\mcp\\domain\n.\\.venv\\Scripts\\doc-forge.exe build --mcp --module infra --out-dir docs\\mcp\\infra\n.\\.venv\\Scripts\\doc-forge.exe build --mcp --module interfaces --out-dir docs\\mcp\\interfaces\n.\\.venv\\Scripts\\doc-forge.exe build --mcp --module pipelines --out-dir docs\\mcp\\pipelines\n
"},{"location":"10_development/04_building_docs/#api-build","title":"\ud83c\udf10 API Build","text":"

build --api reads JSON (not YAML). Convert openapi.yaml first:

.\\.venv\\Scripts\\python.exe -c \"import yaml,json,pathlib; p=pathlib.Path('docs/openapi.json'); p.write_text(json.dumps(yaml.safe_load(pathlib.Path('openapi.yaml').read_text(encoding='utf-8')),indent=2),encoding='utf-8')\"\n.\\.venv\\Scripts\\doc-forge.exe build --api --openapi-spec docs\\openapi.json\n
"},{"location":"10_development/04_building_docs/#wiki-build","title":"\ud83d\udcd6 Wiki Build","text":"

The wiki is hand-authored under docs/wiki/; doc-forge only derives the nav:

.\\.venv\\Scripts\\doc-forge.exe build --wiki --site-name \"Khata App \u2014 Wiki\" --project-name khata-app\n

Nav labels come from filenames (01_overview.md \u2192 Overview); tune docs/mkdocs.wiki.yml after generation for nicer titles.

"},{"location":"10_development/04_building_docs/#known-gotchas","title":"\u26a0\ufe0f Known Gotchas","text":""},{"location":"10_development/04_building_docs/#related","title":"\ud83d\udd17 Related","text":"

Environment \u00b7 GSDFC Docstrings \u00b7 Testing

"}]}