diff --git a/TODO.md b/TODO.md index 189e989..7561fc1 100644 --- a/TODO.md +++ b/TODO.md @@ -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 ` 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 ` warns `failed to serialize ETF profile: WriteFailed` — priority LOW Every `zfin etf VTI` invocation prints diff --git a/src/commands/projections.zig b/src/commands/projections.zig index 1d0151b..8ed75cd 100644 --- a/src/commands/projections.zig +++ b/src/commands/projections.zig @@ -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(