Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
20 KiB
JSON
1 line
20 KiB
JSON
{"repo": "amaramrahul/fisqo", "files": {"docs/fisqo-tech-design.md": {"efcb10d750006d9baeeae7f530265a3835284425": {"t": 1786067772.6386304, "n": 19541, "h": "4c526b98de48e637", "c": "# Fisqo — Technical Design Document\n\n---\n\n## High-Level Design\n\n### System Components\n\n| Component | Technology | Responsibility |\n|---|---|---|\n| **Frontend** | React SPA (Vite) | Stage-by-stage filing UI; calls REST API; streams LLM job progress via SSE |\n| **API Server** | Node.js + Express | REST API (`/api/v1/...`); serves React static build in production; manages LLM job queue |\n| **LLM Pipeline** | LangGraph.js | Document classification and income extraction chains; one chain per extraction category |\n| **ORM / DB** | Prisma + SQLite | All app state persisted locally; migrations applied on startup |\n| **File System Layer** | Node.js `fs` | Reads user-supplied statement directories; path-sanitised at API boundary |\n| **Desktop Shell** | Electron | Spawns Express as child process; opens `localhost:3000` in browser; handles native OS interactions (file picker) |\n\n**Monorepo layout:**\n```\npackages/\n core/ — shared types, Zod schemas, Prisma client, FIFO engine, tax computation\n api/ — Express server, route handlers, LLM job queue\n pipeline/ — LangGraph chains, prompt files, LLM provider adapters\n web/ — React SPA\n desktop/ — Electron shell\ndata/\n fx-rates/ — Bundled SBI TT rate CSVs (refreshed at release time)\nschemas/\n ay2026/ — Bundled ITR-2 JSON schema for AY 2026\nevals/\n fixtures/ — Redacted statement samples + reference extraction outputs\ndocs/\n adr/ — Architecture Decision Records\n```\n\n### Distribution\n\nElectron app — bundles Node.js runtime + Express + React build. Installs like a native app on Mac/Windows/Linux. No prerequisites for end users.\n\n- Electron main process spawns the Express server as a child process on a fixed local port.\n- Opens `http://localhost:3000` in the user's default browser on launch.\n- Native OS access (file picker, app data directory) via Electron IPC with `contextIsolation: true`.\n\n### Config File\n\nLLM configuration stored as plain-text JSON at `app.getPath('userData')` (OS app data directory):\n\n```json\n{\n \"llm\": {\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"apiKey\": \"sk-...\"\n }\n}\n```\n\nRead on startup; written when the user saves W1 Stage 1. Not stored in SQLite.\n\n### API Design\n\n- Versioned REST under `/api/v1/...`\n- Mutation endpoints carry an `Idempotency-Key` header; server deduplicates within a 24-hour window.\n- Standardised error shape: `{ code: string, message: string, details?: unknown }`\n- List endpoints use cursor-based pagination; page size capped at 200.\n- Individual endpoint inventory deferred to implementation.\n\n### LLM Provider Abstraction\n\nAll providers implement a single `LlmAdapter` interface:\n\n```ts\ninterface LlmAdapter {\n chat(messages: ChatMessage[], options: LlmOptions): Promise<string>;\n stream(messages: ChatMessage[], options: LlmOptions): AsyncIterable<string>;\n readonly capabilities: { supportsPdfInput: boolean };\n}\n```\n\nProvider is selected at runtime from user config. No provider-specific code leaks into pipeline or route handlers. Supported: **DeepSeek** (primary), **OpenAI**, **Anthropic**, **Google Gemini**. Local model support deferred.\n\n### Model Selection\n\n| Axis | Decision |\n|---|---|\n| **Primary model** | DeepSeek-V3 — primary development and testing model; strong structured JSON extraction at significantly lower cost |\n| **Other providers** | OpenAI (GPT-4o), Anthropic (Claude Sonnet), Google (Gemini) — user-selectable; no automatic fallback |\n| **Context window** | Typical Indian bank statement ≈ 10k–80k tokens. All providers support ≥ 64k; no chunking needed for most documents |\n| **Cost estimate** | DeepSeek-V3: full W2 extraction ≈ $0.05–0.15. Western providers 5–20× higher. |\n\n### PDF Parsing Strategy\n\nProvider-aware via `adapter.capabilities.supportsPdfInput`:\n\n| Mode | How | Providers |\n|---|---|---|\n| **Text extraction (default)** | `pdfjs-dist` extracts text; sent as string | DeepSeek and any text-only provider |\n| **Native PDF** | PDF bytes sent directly in the API message | Claude, Gemini |\n| **Image mode (opt-in)** | Pages converted to PNG via `pdf2pic` | Claude, GPT-4o, Gemini — for scanned PDFs |\n\nDeepSeek (primary) always uses text extraction — it is text-only. Image mode requires switching to a multimodal provider; user can toggle per institution in W2 Stage 1 / W3 Stage 2.\n\n### Communication Modes\n\n| Operation | Protocol | Rationale |\n|---|---|---|\n| CRUD (stage data, rows, config) | Synchronous REST | Simple; easy to test and cache |\n| LLM extraction jobs | SSE (`/api/v1/jobs/:id/events`) | Unidirectional server→client stream; no WebSocket handshake; works through HTTP/1.1 proxies |\n\n### AY Schema and Prompt Registry\n\nA `SchemaRegistry` factory in `packages/core/src/registry.ts` maps an Assessment Year string (e.g. `\"AY2026\"`) to:\n\n- The Zod schema bundle under `schemas/ay{year}/`\n- The prompt directory under `packages/pipeline/prompts/ay{year}/`\n\nRoute handlers and LangGraph chains receive the AY from `Filing.assessmentYear` and call `SchemaRegistry.forYear(ay)` to obtain the correct schemas and prompts. No handler hard-codes a year. If the requested AY is not registered, the server returns `HTTP 422` with error code `UNSUPPORTED_AY`. On app update, the UI shows a banner if a filing's AY schema has been superseded by a newer bundled version, prompting the user to re-validate extracted data.\n\n### Data Sourcing\n\n- User provides statement files (PDF, CSV, XLS) from their local machine.\n- Fisqo validates LLM output **structure** (Zod) but not **semantic correctness** — user is responsible for reviewing extracted values before filing.\n- Before the first cloud LLM job on a filing, UI shows an explicit acknowledgement that financial documents will be transmitted to the configured API endpoint.\n\n---\n\n## Detailed Design\n\n### LangGraph Pipeline\n\nAll LLM work uses deterministic **chains**, not autonomous agents — every extraction task has a fixed input and output schema. One chain per extraction category, independently prompt-tunable and testable:\n\n| Chain | Output Zod type |\n|---|---|\n| `classifyDocuments` | `InstitutionClassification[]` |\n| `extractSalary` | `SalaryRow[]` |\n| `extractDomesticBankInterest` | `InterestRow[]` |\n| `extractOtherDomesticInterest` | `InterestRow[]` |\n| `extractForeignInterest` | `ForeignIncomeRow[]` |\n| `extractDomesticDividends` | `DividendRow[]` |\n| `extractForeignDividends` | `ForeignDividendRow[]` |\n| `extractDomesticCgSummary` | `CgSummaryRow[]` |\n| `extractDomesticCgTransactions` | `TradeRow[]` |\n| `extractForeignCgSummary` | `ForeignCgSummaryRow[]` |\n| `extractForeignCgTransactions` | `ForeignTradeRow[]` |\n| `extractTds` | `TdsRow[]` |\n| `extractFaIncome` | `FaIncomeRow[]` |\n| *(+ FA-period mirrors of interest / dividend / CG chains)* | |\n\n**Prompt versioning**: prompts live in `prompts/{category}.md`, hashed (SHA-256) at load time, hash stored in `LlmJob.promptHash` — enables exact reproduction of any past extraction run.\n\n**Retry / failure**: on Zod parse failure, chain retries up to `maxRetries` (configurable); on exhaustion, job moves to `failed` and stores `rawResponse` + `zodError` for debugging.\n\n### Database Entities\n\nKey entities (full schema in `packages/core/prisma/schema.prisma`):\n\n| Entity | Purpose | Key design notes |\n|---|---|---|\n| `TaxUser` | One per taxpayer — PAN, DOB, Aadhaar | Aadhaar AES-256-GCM encrypted |\n| `Filing` | One per AY per tax user | References AY for ITR schema selection |\n| `Stage` | One row per stage per filing | `(filingId, stageKey)` unique; `status` enum (`NOT_STARTED \\| IN_PROGRESS \\| COMPLETE \\| SKIPPED`) required column — `SKIPPED` records a conscious user bypass without blocking downstream stages; W4 routing requires all prerequisite stages to be `COMPLETE` or `SKIPPED` before unlocking; `data` JSON holds stage output |\n| `Institution` | Document grouping + category selection | `workflowScope: FY \\| CY` — `FY` for Workflow 2 (April–March statements), `CY` for Workflow 3 (January–December FA statements); LangGraph chains receive only institutions matching the active workflow scope, preventing cross-contamination; `cgMode` (summary/transactions), `pdfMode` (auto/image) per institution |\n| `Document` | File path + exclusion flag per institution | — |\n| `IncomeRow` | Extracted or manually-added data row | `data` JSON for category-specific fields; `note` never sent to LLM |\n| `LlmJob` | One job per chain run | Tracks tokens, latency, `promptHash`, status FSM |\n| `FxRate` | SBI TT rate by currency + date | `lastFetchedAt` drives staleness check |\n\n**Key schema decisions:**\n- Flexible income data stored as JSON in `IncomeRow.data` — avoids a column-per-field explosion across 13+ extraction categories.\n- Additive-only migrations — no destructive column drops on existing filings when AY schema changes.\n- `Stage.data` JSON stores stage-specific output; downstream stages read it directly rather than requerying all rows.\n\n### Stage Dependency Graph\n\nRescan of a stage resets all stages in its subtree. Manual-entry stages (W2.10–12, W2.15–17, W2.20) are never in any downstream — rescanning income extraction never clears manual entries.\n\n```\nW2.1 (Doc Discovery)\n └─► W2.2–9, W2.13, W2.14 (income extraction)\n └─► W2.5, W2.7, W2.9, W2.14 (foreign income stages)\n └─► W2.18 (Schedule FSI)\n └─► W2.19 (Schedule TR)\n └─► W3.3–6 (FA income, if started) ← explicit cross-workflow warning shown\n\nW3.1 (FA Setup — Dec 31 FX rate confirmed)\n └─► W3.2 (FA Doc Discovery)\n └─► W3.3–6 (FA income extraction)\n └─► W3.7–14 (FA sub-tables)\n └─► W3.15 (FA Review and Lock)\n```\n\nW3.1 is the root dependency for all W3 calculations. Any user override to the December 31 FX rate cascades invalidation through W3.3–14. W3.15 can only be reached when all sub-table stages are `COMPLETE` or `SKIPPED`.\n\nLlmJob status FSM: `queued → running → succeeded | retrying → failed | superseded`\n\n### FIFO Capital Gains Engine\n\nPure TypeScript in `packages/core/src/fifo.ts`, no LLM. Processes `TradeRow[]` into `CgSummaryRow[]` using FIFO lot matching, with 31 Jan 2018 FMV grandfathering for domestic listed equities. Foreign CG applies FX conversion at acquisition and sale dates separately.\n\n**Corporate action override interface:** The engine accepts an `overrides: TradeRow[]` parameter alongside the LLM-extracted rows. Override rows carry `rowType: 'corporate_action'` and fields: `date`, `actionType` (`SPLIT | BONUS | MERGER | SPINOFF`), quantity delta, and cost basis delta. Before lot-matching, the engine merges and date-sorts extracted rows and override rows; override rows take precedence over any extracted row with the same `(isin, date, actionType)` key. This injection point is populated from user-added rows in the W2 Stage 8 / W3 Stage 5 UI (\"Add adjustment row\") — required when the broker statement omits corporate-action entries.\n\n### SBI FX Rate Data\n\n- Source: [`sbi-fx-ratekeeper`](https://github.com/sahilgupta/sbi-fx-ratekeeper/tree/main/csv_files) CSV files.\n- CSVs bundled under `data/fx-rates/` at release time; seeded into `FxRate` on first run.\n- On startup (background): download and re-seed only if `FxRate.lastFetchedAt` is older than 24 hours. Falls back to existing data if offline.\n- Look-up by currency + exact date. The look-up function returns `{ rate: number, appliedDate: string, requestedDate: string, isFallback: boolean }`. When `isFallback: true` (weekend, bank holiday, or missing entry), the API includes this metadata in the response and the UI renders a tooltip: \"Rate from [appliedDate] used — no rate available for [requestedDate].\" This lets the user confirm the correct fallback rate was applied before filing.\n\n### W3 Lock Mechanism\n\nW3 Stage 15 (\"FA Review and Lock\") is the terminal node of the W3 dependency graph. When the user clicks \"Confirm and Lock\":\n\n- `Stage.status` for the W3.15 row is set to `COMPLETE` and a `lockedAt` ISO timestamp is written into `Stage.data`.\n- All W3 mutation routes (PUT/PATCH on `IncomeRow`, `Stage.data`, or `Institution` where `workflowScope = CY`) include a pre-handler guard: if `w3LockStage.lockedAt` is set and W4 has been initialised, the request is rejected with `HTTP 409 Conflict` and error code `W3_LOCKED`.\n- Async LLM job completions for W3 chains check the lock before writing results; any job that completes after the lock was set discards its output and transitions to status `superseded`.\n- **Unlock path**: the user clicks \"Reopen W3\" in W3.15, which clears `lockedAt` and resets W4 to `NOT_STARTED` with an explicit warning that any generated ODS/ITR JSON must be regenerated.\n\n### W4 Tax Computation Engine\n\nA dedicated, purely rule-based module in `packages/core/src/tax/`. No LLM involvement — all computations are deterministic formula executions over typed inputs.\n\n**Architecture:** DAG of pure TypeScript functions — each node takes typed inputs and returns typed outputs with no side effects, enabling isolated unit testing per node.\n\n**Computation nodes (in dependency order):**\n\n| Node | Inputs | Output |\n|---|---|---|\n| `computeSlabTax` | `totalIncome`, `regime` | tax before rebate |\n| `computeRebate87A` | `totalIncome`, `regime`, `taxBeforeRebate` | rebate (₹60,000 cap under NTR for income ≤ ₹12L) |\n| `computeSurcharge` | `totalIncome`, `taxAfterRebate` | surcharge + marginal relief |\n| `computeCess` | `taxAfterSurcharge` | 4% Health & Education Cess |\n| `computeAMTCredit` | `filingHistory` | credit u/s 115JD if applicable |\n| `computeRelief` | `foreignTaxPaid`, `scheduleFsi` | relief u/s 89/90/91 |\n| `computeCredits` | `tdsRows`, `advanceTaxRows`, `tcsRows` | total prepaid credits |\n| `compute234B` | `assessedTax`, `advanceTaxPaid`, `assessmentDate` | interest; quarterly due dates (Jun 15, Sep 15, Dec 15, Mar 15) are statutory constants in the module |\n| `compute234C` | `quarterlyShortfalls` | interest per quarter |\n| `compute234A` | `taxPayable`, `filingDate`, `dueDate` | interest; computed and shown only if filing date is after the due date |\n| `compute234F` | `totalIncome`, `filingDate`, `dueDate` | late fee (₹1,000 / ₹5,000 depending on income) |\n\n**Inputs sourced from:** W2 Stage 16/17 (TDS, advance tax), W2 Stage 19 (Schedule TR relief amounts), Part B-TI total income (auto-derived from income schedules).\n\n**UI positioning:** Computed values are shown with a banner: \"These figures are computed by Fisqo for reference. Verify with a CA or authorised representative before filing.\" This is a user-facing disclaimer about responsibility — the computation is deterministic and fully implemented.\n\n**Testing:** Unit tests cover known CBDT examples, the 87A rebate cliff at ₹12L, the surcharge marginal relief band, nil advance tax (full 234B), and partial advance tax with one shortfall quarter (234C).\n\n---\n\n## QA\n\n### TDD Approach\n\nTests written before implementation for every non-trivial component:\n\n- Zod output schemas defined per extraction category first — chains must satisfy them.\n- Unit tests for FIFO engine, tax computation, FX look-up written before those modules.\n- API contract tests (request/response shape) written before route handler implementations.\n- Golden eval set and Zod schema written before each LangGraph chain.\n\n### Test Pyramid\n\n| Layer | Coverage |\n|---|---|\n| Unit | FIFO engine, tax computation (Part B-TTI), FX look-up, Zod schema validation |\n| Integration | API routes + DB (real SQLite), stage dependency graph, rescan logic |\n| End-to-end | Stage flows with mocked LLM responses |\n| Snapshot | Generated ITR JSON for fixture filings — explicit update required on any output change |\n\n### LLM Evaluation\n\n- Golden test set: 10–20 redacted statement samples with reference extraction output per income category; lives in `evals/fixtures/`.\n- Each fixture has `metadata.json` (institution type, AY, categories present, redaction notes) and a corresponding reference output JSON.\n- **Primary metric**: field-level extraction accuracy — exact match for amounts/ISINs; fuzzy match for institution names.\n- **Secondary metrics**: tokens per extraction, latency, false-positive institution classification rate.\n- CI runs with mocked LLM; real-LLM eval is manual/opt-in before any prompt version bump or model upgrade.\n\n---\n\n## Release and Build Automation\n\n- **CI**: GitHub Actions — lint, type-check (`strict: true`), unit tests, integration tests, snapshot tests, LLM eval suite (mocked).\n- **Packaging**: `electron-builder` produces platform installers (Mac `.dmg`, Windows `.exe`, Linux `.AppImage`) on each release tag.\n- **AY schema versioning**: each AY's ITR JSON schema under `schemas/ay{year}/`; a filing records its AY at creation and always uses the matching schema; UI shows a banner if a newer AY schema ships with an app update.\n- **Versioning**: semver; breaking API changes increment major version.\n- **Branch strategy**: GitHub Flow — feature branches → main.\n\n---\n\n## Security\n\n- LLM API key stored in plain-text JSON config file at the OS app data directory; users should treat this file as a credential.\n- PAN and Aadhaar stored AES-256-GCM encrypted in SQLite.\n- File path sanitisation: all user-supplied paths resolved to absolute and checked against allowed roots before any `fs` operation; directory traversal rejected with HTTP 400.\n- Electron: `contextIsolation: true`, `nodeIntegration: false`; IPC used for all native OS access.\n- Express binds to `127.0.0.1` only; no inbound traffic from the network.\n\n**LLM input guardrails:**\n- Explicit acknowledgement gate before the first cloud LLM job on a filing — user confirms that financial documents (PAN, account numbers, transaction data) will be transmitted to the configured endpoint.\n- User-supplied custom prompts (W2.1/W3.2) are sandboxed — appended after the system prompt, never replacing it; LLM output still goes through Zod regardless.\n- Document size checked before sending; user warned if approaching the provider's context limit.\n- Single-worker job queue per session — new LLM job requests are enqueued, not parallelised. The `LlmJob` table tracks queue position; the UI shows \"Queued (position N)\" for waiting jobs. This prevents rate-limit exhaustion and gives the user clear visibility into pending cost before jobs run.\n\n**LLM output guardrails:**\n- Zod schema validation on every response before any application code touches it; failure moves the job to `failed`.\n- Post-Zod sanity checks: amounts in plausible ranges, dates within relevant FY/CY, currency codes are valid ISO 4217 — surface as UI warnings, not hard blocks.\n- LLM output is never executed as code and never rendered as HTML.\n- Raw LLM response stored only in `LlmJob.rawResponse` (failure debugging only); never written to log files or transmitted elsewhere.\n\n---\n\n## Monitoring and Observability\n\n- Structured JSON logs to `~/.fisqo/logs/` (rotated, size-capped).\n- Every LangGraph run persists `(promptHash, model, tokensIn, tokensOut, latencyMs, outputValid)` in `LlmJob` — enables per-session cost estimate and extraction replay.\n- UI \"Extraction log\" panel per stage: model used, token count, latency, validation status.\n\n---\n\n## Alternative Designs\n\n| Decision | Chosen | Runner-up | Reason |\n|---|---|---|---|\n| LLM orchestration | LangGraph | LangChain | Explicit state machine; step-level retries; deterministic chain execution |\n| Database | SQLite | PostgreSQL | Zero ops; single-user local app; sufficient data volumes |\n| API style | REST | GraphQL | First-party clients; query shapes are known; REST simpler to test and cache |"}}}} |