user-errors management...any non-user error panics like before

This commit is contained in:
Emil Lerch 2026-05-19 12:28:05 -07:00
parent 4297fda67a
commit fa39749980
Signed by: lobo
GPG key ID: A7B62D657EF764F8
24 changed files with 105 additions and 2 deletions

1
.gitignore vendored
View file

@ -9,3 +9,4 @@ coverage/
# `ZFIN_HOME=examples/<name> zfin projections` immediately.
!examples/**/*.srf
scripts/
.tmp/

View file

@ -308,6 +308,30 @@ Ask the user instead.**
stop. You do not commit. You do not prepare a commit. You hand off the
working tree and wait.
### NEVER write to `/tmp`. Use `$(cwd)/.tmp` instead.
- **`/tmp` is off-limits for any temporary file the assistant creates.**
Not for scratch scripts, not for redirected stdout, not for `cp`
backups, not for "just for a second" experiments. The user's workflow
treats `/tmp` as their own space; assistant-created files there
pollute it across sessions and hide from `git status`.
- Use `$(cwd)/.tmp/` (a `.tmp` directory at the repository root) for
every transient file. `mkdir -p .tmp` if it doesn't exist;
`.gitignore` it (or rely on a project-level `.gitignore` rule that
catches it). Files in `.tmp` show up in `ls` next to the work,
surface in `git status` if the ignore rule is wrong, and get
cleaned up at the end of the task with a single `rm -rf .tmp/*`.
- This applies to:
- Bash redirections: `cmd > .tmp/out` not `cmd > /tmp/out`.
- Test fixture scratch files.
- `cp file /tmp/file.bak` for "I'll restore in a sec" backups —
use `cp file .tmp/file.bak` instead.
- Heredoc-created throwaway scripts to test a Zig snippet
(`zig run /tmp/foo.zig``zig run .tmp/foo.zig`).
- The ONE exception: temp files created by the system / by other
tools (mise installations, zig's own cache symlinks, etc.) that
legitimately use `/tmp`. Don't try to redirect those.
### Documentation-file conventions
- **`TODO.md` holds only what's still open.** Git already tracks what was

View file

@ -25,6 +25,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{UnexpectedArg},
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -2278,6 +2278,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{ UnexpectedArg, EmptyFile, NoAccountsFound, UnexpectedHeader },
};
pub fn parseArgs(_: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -33,6 +33,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{ MissingSubcommand, UnexpectedArg, UnknownSubcommand },
};
/// Data types to show in the stats table (skip candles_meta and meta internal bookkeeping).

View file

@ -126,6 +126,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{ InvalidDate, MissingDateArg, PortfolioLoadFailed, SameDate, SnapshotNotFound, UnexpectedArg },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -226,6 +226,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{ DuplicateEndpoint, InvalidArg, MissingOpenDate, PrepareFailed, ResolveFailed, UnexpectedArg },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -25,6 +25,7 @@ pub const meta: framework.Meta = .{
\\ zfin divs T # historical AT&T dividends
\\
,
.user_errors = error{ MissingSymbol, UnexpectedArg },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -31,6 +31,7 @@ pub const meta: framework.Meta = .{
\\ zfin earnings AAPL | head -8 # last two years of quarters
\\
,
.user_errors = error{ MissingSymbol, UnexpectedArg },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -41,6 +41,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{ MissingArg, UnexpectedArg },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -26,6 +26,7 @@ pub const meta: framework.Meta = .{
\\ zfin etf TQQQ # leveraged (warning surfaced)
\\
,
.user_errors = error{ MissingSymbol, UnexpectedArg },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -17,6 +17,7 @@
//! .synopsis = "Show 1y/3y/5y/10y trailing returns",
//! .help = "...", // multi-line; rendered by `--help`
//! .uppercase_first_arg = true,
//! .user_errors = error{ MissingSymbol, UnexpectedArg },
//! };
//!
//! pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs;
@ -125,8 +126,47 @@ pub const Meta = struct {
/// optional opt-in; the `false` answer is just as load-bearing
/// as `true`.
uppercase_first_arg: bool,
/// Error set listing the command's user-level errors errors
/// where the command has already printed a useful message to
/// stderr and the dispatcher should just return exit 1 silently.
/// Anything NOT in this set propagates to Zig's panic handler
/// with a stack trace, signaling a genuine bug worth investigating.
///
/// Examples:
/// - `error.MissingSymbol` (parseArgs printed "requires a symbol
/// argument") → user-level.
/// - `error.SnapshotNotFound` (run printed "No snapshot at or
/// before X") → user-level.
/// - `error.OutOfMemory`, `error.Unexpected*` not user-level;
/// these should crash visibly so they don't get swallowed.
///
/// No default every command author must enumerate the errors
/// their `parseArgs` and `run` deliberately return as user
/// signals. Commands with no user-level errors (`version`)
/// declare `error{}`. Adding a new `return error.X` to a
/// command means you also add `X` here if it's user-level
/// the explicit list IS the contract.
user_errors: type,
};
/// Returns true if `err` is a member of the comptime-known
/// `UserErrors` error set. Used by the framework dispatcher to
/// decide whether a `Module.run` error is a user-level signal
/// (return exit 1 silently) or an internal bug (propagate to
/// Zig's panic handler).
///
/// Implemented as a comptime-unrolled equality chain over the
/// error set's variants. Cost is one comparison per variant; the
/// largest user-error set today is on the order of 10 variants,
/// so this is cheap.
pub fn isUserError(comptime UserErrors: type, err: anyerror) bool {
inline for (@typeInfo(UserErrors).error_set orelse return false) |variant| {
if (err == @field(anyerror, variant.name)) return true;
}
return false;
}
// Refresh policy
/// Cache-freshness policy for the invocation. Set via the global
@ -432,6 +472,7 @@ const ProbeModule = struct {
.synopsis = "test probe",
.help = "Usage: zfin probe\n",
.uppercase_first_arg = false,
.user_errors = error{},
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
@ -481,6 +522,7 @@ test "printCommandHelp: appends newline when meta.help lacks one" {
.synopsis = "x",
.help = "no trailing newline",
.uppercase_first_arg = false,
.user_errors = error{},
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{};
@ -536,6 +578,7 @@ test "printGroupedUsage: groups in canonical order with headers + synopses" {
.synopsis = "alpha synopsis",
.help = "alpha help\n",
.uppercase_first_arg = false,
.user_errors = error{},
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{};
@ -550,6 +593,7 @@ test "printGroupedUsage: groups in canonical order with headers + synopses" {
.synopsis = "beta synopsis",
.help = "beta help\n",
.uppercase_first_arg = false,
.user_errors = error{},
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{};
@ -592,6 +636,7 @@ test "printGroupedUsage: omits empty groups" {
.synopsis = "only synopsis",
.help = "h\n",
.uppercase_first_arg = false,
.user_errors = error{},
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{};

View file

@ -83,6 +83,7 @@ pub const meta: framework.Meta = .{
\\DATE accepts YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y).
\\
,
.user_errors = error{ UnexpectedArg, MissingFlagValue, InvalidFlagValue, UnknownMetric, UnknownResolution },
};
pub const Error = error{

View file

@ -29,6 +29,7 @@ pub const meta: framework.Meta = .{
\\ zfin lookup 037833100 # → AAPL
\\
,
.user_errors = error{ MissingCusip, UnexpectedArg },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -62,6 +62,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{ InvalidStep, MissingStep, NoData, UnexpectedArg },
};
pub const RunError = error{

View file

@ -33,6 +33,7 @@ pub const meta: framework.Meta = .{
\\ zfin options AAPL --ntm 12
\\
,
.user_errors = error{ MissingSymbol, UnexpectedArg, MissingFlagValue, InvalidFlagValue },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -33,6 +33,7 @@ pub const meta: framework.Meta = .{
\\ zfin perf NVDA # individual stock
\\
,
.user_errors = error{ MissingSymbol, UnexpectedArg },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -30,6 +30,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{UnexpectedArg},
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -108,6 +108,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{ UnexpectedArg, MissingFlagValue, InvalidFlagValue, MutuallyExclusive, NoSnapshot, PortfolioLoadFailed },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -30,6 +30,7 @@ pub const meta: framework.Meta = .{
\\ zfin quote spy # symbols are case-insensitive
\\
,
.user_errors = error{ MissingSymbol, UnexpectedArg },
};
/// Quote data extracted from the real-time API (or synthesized from candles).

View file

@ -96,6 +96,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{ UnexpectedArg, BadMetadata, NoCommitBeforeDate, NoMetadata, PathMissingInRev, WriteFailed },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -25,6 +25,7 @@ pub const meta: framework.Meta = .{
\\ zfin splits NVDA # 10:1 (2024-06-10), 4:1 (2021-07-20), ...
\\
,
.user_errors = error{ MissingSymbol, UnexpectedArg },
};
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {

View file

@ -33,6 +33,7 @@ pub const meta: framework.Meta = .{
\\
,
.uppercase_first_arg = false,
.user_errors = error{UnexpectedArg},
};
/// Parse `--verbose`/`-v`. Unknown args produce an error on stderr.

View file

@ -376,8 +376,21 @@ fn runCli(init: std.process.Init) !u8 {
try cmd_framework.normalizeFirstArg(allocator, cmd_args)
else
cmd_args;
const parsed = Module.parseArgs(&ctx, dispatched_args) catch return 1;
try Module.run(&ctx, parsed);
const parsed = Module.parseArgs(&ctx, dispatched_args) catch |err| {
// parseArgs errors: if the command declared this
// error as user-level, exit 1 silently (the
// command already printed a stderr message).
// Otherwise propagate so genuine bugs (OOM, etc.)
// surface with a stack trace.
if (cmd_framework.isUserError(Module.meta.user_errors, err)) return 1;
return err;
};
Module.run(&ctx, parsed) catch |err| {
// Same treatment for run errors: user-level errors
// become exit 1; everything else propagates.
if (cmd_framework.isUserError(Module.meta.user_errors, err)) return 1;
return err;
};
try out.flush();
return 0;
}