user visible error rather than crash on projections --vs <date> with imported data

This commit is contained in:
Emil Lerch 2026-05-19 13:37:08 -07:00
parent 8fe9257c60
commit f10508932e
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 17 additions and 23 deletions

22
TODO.md
View file

@ -631,28 +631,6 @@ malformed flag" behavior (above) means
`zfin interactive --default-keys --bogus-flag` is accepted; the
bogus flag is silently dropped. Same fix as above.
### `projections --vs <date>` crashes with `FileNotFound` when as-of resolves to imported source — priority MEDIUM
Repro: `zfin projections --vs 2025-01-01` (any date that resolves
to imported_values rather than a real snapshot) →
`error: FileNotFound` panic with stack trace.
Root cause: `loadAsOfContext` in `src/commands/projections.zig:760-762`
calls `history.loadSnapshotAt` unconditionally, but the resolution
returned by `resolveAsOfSnapshot` can have `source == .imported`
when the only available data point at that date is from
imported_values, not a real snapshot file. `loadSnapshotAt` then
fails to open a file that doesn't exist.
`runBands` at line 369 has the same setup but correctly branches:
`if (resolution.?.source == .snapshot) { loadSnapshotAt(...) } else { loadProjectionContextFromImported(...) }`.
`loadAsOfContext` should mirror that branching.
Pre-existing bug from before the framework refactor. Surfaced
during the sanity check because the new dispatcher correctly
identifies `FileNotFound` as not a user-level error and propagates
with stack trace. Pre-fix the same crash happened, just hidden.
### `etf <SYMBOL>` warns `failed to serialize ETF profile: WriteFailed` — priority LOW
Every `zfin etf VTI` invocation prints

View file

@ -742,9 +742,18 @@ fn extractKeyMetrics(ctx: view.ProjectionContext) KeyMetrics {
/// Build a `ProjectionContext` against a historical snapshot date.
///
/// Caller owns `snap_bundle_out.*` on success it must outlive the
/// Caller owns `snap_bundle_out.*` on success - it must outlive the
/// returned context because allocations borrow symbol strings from
/// the snapshot's backing buffer.
///
/// Imported-only resolutions (where the requested date predates any
/// real snapshot but is covered by `imported_values.srf`) are NOT
/// supported here: the imported-only path needs live-portfolio
/// composition plumbed through additional outparams that this helper
/// doesn't expose. Callers that hit this case get `error.NoSnapshot`
/// after a clear stderr message, mirroring the user-visible behavior
/// of "no snapshot at that date." See the `--vs` follow-up TODO for
/// the parity work that would make this branch fully supported.
fn loadAsOfContext(
io: std.Io,
allocator: std.mem.Allocator,
@ -758,6 +767,13 @@ fn loadAsOfContext(
snap_bundle_out: *history.LoadedSnapshot,
) !view.ProjectionContext {
resolution_out.* = resolveAsOfSnapshot(io, va, file_path, requested_date) catch |err| return err;
if (resolution_out.source != .snapshot) {
// Imported-only resolution: no snapshot file exists at the
// resolved date, so `loadSnapshotAt` would crash with
// FileNotFound. Bail with a clear message instead.
try cli.stderrPrint(io, "Error: --vs does not yet support back-dating to imported-only periods (no snapshot at that date).\n");
return error.NoSnapshot;
}
const hist_dir = try history.deriveHistoryDir(va, file_path);
snap_bundle_out.* = try history.loadSnapshotAt(io, allocator, hist_dir, resolution_out.actual);
return try view.loadProjectionContextAsOf(