Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
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.
2026-08-07 12:53:47 +08:00

1 line
20 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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 |"}}}}