user-errors management...any non-user error panics like before
This commit is contained in:
parent
4297fda67a
commit
fa39749980
24 changed files with 105 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,3 +9,4 @@ coverage/
|
|||
# `ZFIN_HOME=examples/<name> zfin projections` immediately.
|
||||
!examples/**/*.srf
|
||||
scripts/
|
||||
.tmp/
|
||||
|
|
|
|||
24
AGENTS.md
24
AGENTS.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 .{};
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ pub const meta: framework.Meta = .{
|
|||
\\
|
||||
,
|
||||
.uppercase_first_arg = false,
|
||||
.user_errors = error{ InvalidStep, MissingStep, NoData, UnexpectedArg },
|
||||
};
|
||||
|
||||
pub const RunError = error{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
17
src/main.zig
17
src/main.zig
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue