{"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/.
openapi.yaml spec drives every route via opendapi-first's OpenAPIFirstApp; handlers are thin and matched by operationIddagpipex-fk)# 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":"git@git.aetoskia.com:apps/khata-app.gitsite/lib/ from GSDFC docstringsdocs/mcp/{domain,infra,interfaces,pipelines}/ \u2014 machine-readable API referencesite/api/ from openapi.yaml\u00a9 Aetoskia Internal \u2014 khata-app 0.1.0
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":"amount, balance, a kind (debit/credit/transfer), mcc, memo, date, an optional tag, and an optional entity (the payee).dagpipe.ProgressEvents over SSE; ambiguity pauses are stored for later resolution.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.
__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":"None in worker._maybe_create_resolver \u2014 LLM-assisted resolution is stubbed behind LLM_API_KEY.infra/llm (OpenAI / OpenRouter pools, prompt policies) is the substrate for future resolver work.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 interfaceinterfaces/ 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.
\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:
init() constructs the Mongo engine, repos, services, fetchers, cache store, the SSE bus, and a PipelineOrchestrator wiring, then returns a dict of named services.get_svc(name) (e.g. \"fetch_request_svc\", \"expense_svc\", \"ambiguity_svc\").main.py registers the OpenAPI app from openapi.yaml; worker.py polls for pending fetch requests; interfaces/fetch_request/cli drives the same services from the terminal.implements-style) the port interfaces declared in the domain; the domain never knows a concrete repo.ObjectId; cross references are populated with explicit filter-based x-fk resolution, never Mongo joins._invalidate_cache.__init__ modules with factories; facades.py and init.py hold the composition.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 Contentsinterfaces/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.
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.
User \u2014 id, name, email, phone, password hash, avatar.UserActivity \u2014 audit trail of user-driven actions.UserService (create, read, update, delete, activity log). Routes: GET/POST /users, GET/PATCH/DELETE /users/{id}, activity listing under /users/{id}/activities.Entity \u2014 canonical payee: name, brands, branch, aliases, classification, type.EntityAccount \u2014 an account known to belong to an entity.EntityService \u2014 entity creation, name-based lookup, account linking. Routes: GET/POST /entities, GET/PATCH /entities/{id}, POST /entities/{id}/accounts.Account \u2014 bank account: name, account number, bank, currency, account type, iban, linked entity.AccountType \u2014 savings / current / credit etc.AccountService \u2014 create, read, update, delete, balance helpers. Routes: GET/POST /accounts, GET/PATCH/DELETE /accounts/{id}.Expense \u2014 the core record: amount, balance, kind (debit/credit/transfer), mcc, memo, date, tag, entity, source.ExpenseTag \u2014 tag membership on an expense; ExpenseSource \u2014 provenance (upload/fetch-request).ExpenseService \u2014 CRUD plus tag assignment and entity resolution. Routes: GET/POST /expenses, GET/PATCH/DELETE /expenses/{id}, tag/entity mutation sub-routes.Tag \u2014 name, color, optional description.TagColor \u2014 palette enum for display.TagService \u2014 CRUD. Routes: GET/POST /tags, GET/PATCH/DELETE /tags/{id}.Upload \u2014 records a raw statement upload (file or email) with status.UploadService \u2014 create, read, update status, list. Routes under /uploads.FetchRequest \u2014 a statement ingestion job: user, account, source (file/email), date range, status, retry count.FileSource \u2014 path + optional bank; EmailSource \u2014 mailbox + filters; Bank \u2014 enum of supported banks.FetchRequestService \u2014 CRUD + status transitions (update_status). Routes: GET/POST /fetch-requests, GET/PATCH /fetch-requests/{id}, POST /fetch-requests/{id}/retry.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:
search(filter={...}, limit=...)x-fk resolves referenced ids via exact filtersShared 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(...).
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.
Lives under domain/enrichment/entity/:
identity.py \u2014 normalizes and fingerprints payee strings for matching.resolver.py \u2014 EntityResolver: resolves a raw payee to an entity_id via name lookup + fingerprint + account linkage.pipeline.py \u2014 the enrichment pipeline node that uses the resolver on expense records.utils.py \u2014 helpers for entity display names and brand collapsing.The pipeline is expressed as a dagpipe.Pipeline; the stub pipeline.pyi type-checks against dagpipe directly (see domain/enrichment/entity/pipeline.pyi).
Lives under domain/enrichment/tags/:
flags.py \u2014 tag candidate flags (priority/confidence hints) computed from memo and MCC.pipeline.py \u2014 a dagpipe.Pipeline that assigns a tag_id to expenses based on matched rules.memo/payee fields.PipelineOrchestrator in domain/fetch_pipeline/) chains extraction \u2192 enrichment.entity_id / tag_id are written back onto the expense and persisted.tests/domain/enrichment/ cover fingerprint/identity edge cases, tag flag computation, and pipeline wiring..venv\\Scripts\\pytest.exe tests\\domain\\enrichmentDomain 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.
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.
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":"tabular.py \u2014 generic tabular parsing helpers (used by CSV/XLSX families)files.py, utils.py \u2014 content sniffing, date/amount normalizationambiguity.py \u2014 PauseForResolution + AmbiguityCandidate helpersextractor.py \u2014 the BaseExtractionPipeline template and registryAmount/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.
status == \"pending\").PipelineOrchestrator.run_pipeline(fr_id):fr.source.type + bank,Expense candidates,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.
worker.py polls for pending fetch requests and runs the pipeline.
.venv\\Scripts\\python.exe worker.py\n Knobs (env vars):
Env var Default MeaningWORKER_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:
PauseForResolution \u2192 paused (retry count reset),retry_count + 1.Ambiguity resolution is enabled only when an LLM_API_KEY is present; today worker._maybe_create_resolver() returns None.
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).
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.
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).
Repos in infra/repos/ are generic mongo-ops repos wrapped per model. Key behaviors:
search(filter={...}, limit=...) \u2014 filter-first reads, no magic._invalidate_cache.infra/cache/redis.py with TTL-aware caching.x-fk cross-collection population \u2014 referenced ids (e.g. account_id on expenses) are populated by explicit filter lookups against the target repo, not Mongo joins.infra/http_cache/ caches external HTTP responses:
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 PurposeMONGO_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.
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.
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.
WORKER_MAX_RETRIES bounded so poisoned fetch requests fail fast.REDIS_URL at a real Redis before enabling the HTTP cache.paused on ambiguity; a resolver (LLM) must be wired to auto-advance them.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":"main, keep the three-layer boundary (no backflow)..pyi stub.init.py \u2014 do not by-pass the service registry.ruff check, pytest, py_compile on changed files.Architecture \u00b7 Running The Service
"},{"location":"10_development/01_environment/","title":"\u2699 Environment","text":"Local setup for developing khata-app.
# 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*.
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.
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 Contenttests/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.
x-fk population against an in-memory Mongo.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.
# Summary heading, then a --- separator, then prose. Long modules follow with Notes:/Example: sections.Attributes: for fields, Notes: for invariants, Example: when a snippet clarifies usage.Args: / Returns: / Raises: / Yields: in that conventional order, then optional Notes: / Example:.str | None, the docstring and the stub say str | None; do not write Optional[str].\"\"\"# 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:
... bodies,Generate/validate conveniently with doc-forge's stub tooling, and always run py_compile + ruff on new stubs.
import instead \u2014 e.g. from dagpipe import Pipeline).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).
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.
pygments==2.19.2.docforge.nav.yml, rebuild with --refresh or the nav won't regenerate.ports/ style relative links on the auto-generated index pages; cosmetic only.Environment \u00b7 GSDFC Docstrings \u00b7 Testing
"}]}