From adf33b9e21b89317f45c870ae4d6d1ac61ae9689 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 16 May 2026 13:07:29 -0700 Subject: [PATCH] add todo items after review of full application surface --- TODO.md | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) diff --git a/TODO.md b/TODO.md index 8552a04..efc6f15 100644 --- a/TODO.md +++ b/TODO.md @@ -160,6 +160,254 @@ Out of scope for V1: file-format alternatives (SVG, PDF), themed color overrides for export (always uses the active terminal theme), non-chart command output as PNG. +## CLI architecture overhaul — priority MEDIUM + +The CLI surface has grown to 21 top-level commands and the dispatch +machinery in `src/main.zig` is starting to bloat. Three related gaps, +all best addressed together because they share the same refactor +vehicle: + +1. **No per-command `--help`.** `zfin help` shows a wall of every + command and every flag. There's no way to ask "what does + `projections` accept?" without grepping the top-level usage. + Every command should support `zfin --help` printing + just that command's synopsis, flags, and a few examples. +2. **`usage` text is roughly chronological-by-feature.** Re-group it + into workflow sections so users can scan for what they need: + - **Per-symbol lookups:** `perf`, `quote`, `history` (symbol + mode), `divs`, `splits`, `options`, `earnings`, `etf` + - **Portfolio analysis:** `portfolio`, `analysis`, `history` + (portfolio mode), `projections`, `milestones` + - **Time-series & journaling:** `snapshot`, `compare`, + `contributions` + - **Data hygiene:** `audit`, `enrich`, `lookup` + - **Infra:** `cache`, `version`, `interactive` +3. **Per-command flag parsing lives inline in `runCli`.** It's + ~500 lines now. The `projections`, `contributions`, and `compare` + branches each carry ~50 lines of inline flag parsing plus their + own conflict-detection. Each command module should own its own + `parseArgs()`; `runCli` should be pure dispatch. + +The natural mechanism for all three is a **command registry table** +mirroring the `tab_modules` registry in `src/tui.zig`: + +```zig +// src/main.zig +const command_registry = .{ + .perf = @import("commands/perf.zig"), + .quote = @import("commands/quote.zig"), + // ... +}; + +// each command module exposes: +// pub const meta = .{ +// .help = "perf Show trailing returns ...", +// .group = .symbol_lookup, +// .takes_symbol = true, +// .needs_portfolio_path = false, +// }; +// pub fn parseArgs(allocator, args, today) !ParsedArgs +// pub fn run(io, allocator, *DataService, parsed, today, color, *Writer) !void +``` + +A comptime walk over the registry produces: +- The grouped `usage` text (sort by `meta.group`, render section + headers). +- Per-command help dispatch (`zfin --help` prints + `meta.help` plus the parsed flag descriptors). +- The dispatch chain itself (replace the giant `if-else if`). +- The "does this command take a symbol?" / "does it need a + portfolio path?" decisions currently scattered across `runCli`. + +**Driver:** the per-command `--help` is the loudest user-facing gap; +the registry refactor is the cleanest way to land it without +scattering help text across more files. Also unblocks `zfin doctor` +(below) and the unified time-range parser (below) by giving them +a clean module shape to plug into. + +**Open question for when this is picked up:** how to handle the +`history` command's two modes (symbol vs portfolio) under a single +registry entry. Probably one entry with a parser that branches on +`args[0]`, same as today — but the registry shape should accommodate +that without ceremony. + +## `zfin doctor` health-check command — priority LOW + +Front-door command for the file constellation and environment. +Answers "is my zfin setup sane?" without making any changes. + +The configuration surface has grown organically and only the README +explains the file layout: + +- `portfolio.srf` — lots +- `metadata.srf` — classifications +- `accounts.srf` — tax types +- `projections.srf` — retirement config +- `transaction_log.srf` — internal transfers +- `imported_values.srf` — back-history +- `history/*-portfolio.srf` — snapshots +- `~/.config/zfin/keys.srf` — keybinds +- `~/.config/zfin/theme.srf` — colors + +Plus 5 API keys, a cache directory, and an optional `ZFIN_SERVER`. + +### v1 scope (full health check) + +- **File inventory + parse check.** For each of the files above: + does it exist, where was it resolved from (cwd vs `ZFIN_HOME` vs + `~/.config/zfin`), and does it parse cleanly. No fixes. +- **Cross-reference checks.** Every account in `portfolio.srf` + has an `accounts.srf` entry. Every held symbol has a + `metadata.srf` entry (or is opted out). Every snapshot file + parses as a portfolio. `transaction_log.srf` records reference + real account names. +- **Environment audit.** Which API keys are set (presence only, + never the value). Cache size + symbol count. `ZFIN_SERVER` + reachability if set (HEAD/OPTIONS, low timeout). Staleness of + hand-maintained data files (T-bill rates, Shiller `ie_data.csv`) + — same registry as `data/staleness.zig`. +- **Output shape.** Sectioned, color-coded. Every check is + `OK` / `WARN` / `FAIL` with a one-line context. Exit code 0 on + all-OK or warnings only; non-zero only on FAIL. Suitable for + CI / cron / pre-commit. + +### Driver + +The file constellation has grown to nine files in three locations, +plus five API keys, plus the cache, plus the optional server. +Today only the README explains the structure. A `doctor` command +surfaces it, catches regressions, and is the obvious place to point +new users (or to point future-self after a long break). + +### Open question for when this is picked up + +Does this *replace* the portfolio-hygiene portion of `audit`, or +live alongside it? Probably alongside — `audit` is reconciliation +against external broker exports, `doctor` is internal-state +validation. But worth confirming the boundary before implementing +to avoid duplicated checks. + +## TUI tab re-order — priority LOW + +Current tab order interleaves the two natural categories: + +``` +Portfolio Quote Performance Options Earnings Analysis History Projections + P S S S S P P P +``` + +That's `P S S S S P P P` — the per-portfolio tabs (P) are split +across the bar (slot 1, then slots 6-8). Reflects history of feature +additions rather than current shape. + +### Suggested re-order to consider + +``` +Portfolio Analysis History Projections | Quote Performance Options Earnings +``` + +"Your money" tabs first (1-4), "research a symbol" tabs second +(5-8). Categorical split is clean; the 4↔5 boundary is a natural +divider in the tab bar. + +### Alternative orderings worth weighing + +- `Portfolio Analysis Projections History | Quote Performance Earnings Options` + — within each category, ordered by frequency of use rather than + by category cohesion. +- Status quo with just Analysis moved up next to Portfolio (minimum + churn). + +### Mechanics + +The `tab_modules` registry in `src/tui.zig` makes the actual reorder +a one-edit change (reorder fields in the registry literal). Cost is +in retraining muscle memory and updating the README's keybinding +table + screenshots. + +### Doc drift to fix while we're there + +README still says "six tabs," actual count is eight (Portfolio, +Quote, Performance, Options, Earnings, Analysis, History, +Projections). + +## Unified time-range flag parser — priority LOW + +`compare`, `contributions`, `projections --vs`, and `projections +--as-of` all accept overlapping but slightly different time-range +inputs. Today there are two helpers in `src/commands/common.zig` +(`parseAsOfDate`, `parseCommitSpec`) and the conflict-detection +logic (`since != null and before_spec != null`, etc.) is duplicated +across each command's flag-parsing block in `main.zig`. + +### Sketch + +```zig +// src/commands/common.zig +pub const TimeRange = struct { + before: Endpoint, + after: Endpoint, + pub const Endpoint = union(enum) { + date: Date, + commit_spec: CommitSpec, + live, + head, + }; +}; +pub fn parseTimeRange(args: ...) !TimeRange { ... } +``` + +Each command consumes a `TimeRange` and decides which endpoint +combinations make sense: +- `compare` rejects `live`-on-both-sides. +- `contributions` rejects `live` entirely (no meaningful "live + contributions diff"). +- `projections --vs` accepts `live` on either side. + +### Naturally folds into the CLI architecture overhaul + +Once each command has its own `parseArgs()` (per the CLI overhaul +entry above), the time-range parser becomes a shared utility those +parsers call. Could either land standalone or be pulled in as part +of the overhaul. If the overhaul lands first, this is one of its +follow-ups; if this lands first, the overhaul inherits it for free. + +## Split `audit.zig` into per-broker reconcilers — priority LOW + +`src/commands/audit.zig` is 3438 lines — the largest command file +by ~2x. It bundles four logically distinct responsibilities: + +- Portfolio hygiene check (no-flag mode) +- Fidelity positions CSV reconciler (`--fidelity`) +- Schwab per-account positions CSV reconciler (`--schwab`) +- Schwab account-summary stdin parser (`--schwab-summary`) + +### Sketch + +``` +src/commands/audit/ + mod.zig ← thin dispatcher; current public `run()` lives here + hygiene.zig ← portfolio hygiene check (no-flag mode) + fidelity.zig ← --fidelity CSV reconciler + schwab.zig ← --schwab CSV + --schwab-summary stdin reconciler + common.zig ← shared types (Discrepancy, ReconcileResult), formatters +``` + +Adding a new broker (Vanguard, Robinhood, etc.) becomes a one-file +add against a documented contract. The hygiene check can be +referenced from `zfin doctor` (above) without pulling in CSV-parser +baggage. + +### Driver + +Maintenance friction. The next person adding a broker reconciler +— likely future-you — has to navigate 3438 lines to find the +pattern. The split also makes the audit-bug investigations already +in this TODO file (phantom discrepancy on freshly-added lots) easier +to localize. + +Pure internal refactor; no user-visible change. + ## Refactor: trim `src/format.zig` once Money / Date have absorbed their helpers — priority LOW `src/format.zig` is still a ~1700-line grab-bag, but the money- and