update AGENTS.md
This commit is contained in:
parent
d7a86cd639
commit
b7b492e494
1 changed files with 84 additions and 303 deletions
387
AGENTS.md
387
AGENTS.md
|
|
@ -1,45 +1,13 @@
|
||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file is zfin's source-of-truth for assistant agents. It
|
||||||
|
holds project-specific architecture, conventions, and gotchas
|
||||||
|
plus the universal hard rules that apply on top.
|
||||||
|
|
||||||
|
Read the ABSOLUTE PROHIBITIONS section first.
|
||||||
|
|
||||||
## ⛔ ABSOLUTE PROHIBITIONS — READ FIRST ⛔
|
## ⛔ ABSOLUTE PROHIBITIONS — READ FIRST ⛔
|
||||||
|
|
||||||
### Zig 0.16.0 reference — read the release notes
|
|
||||||
|
|
||||||
This codebase is on Zig 0.16.0. The 0.16 release was a major
|
|
||||||
I/O-as-an-interface refactor that reshaped the standard library.
|
|
||||||
Before making non-trivial changes (especially anything touching
|
|
||||||
`std.Io`, `std.fs`, `std.process`, `std.http`, `std.Thread`,
|
|
||||||
allocators, or `std.time`), **read the release notes** at:
|
|
||||||
|
|
||||||
https://ziglang.org/download/0.16.0/release-notes.html
|
|
||||||
|
|
||||||
Key migrations that bit us repeatedly during the 0.16 upgrade and
|
|
||||||
will bite future work too:
|
|
||||||
|
|
||||||
- `std.io` → `std.Io` (namespace rename, no deprecation alias).
|
|
||||||
- `std.fs.cwd()` → `std.Io.Dir.cwd()`. All file ops take `io`.
|
|
||||||
- `std.process.Child.init(argv, alloc)` → `std.process.spawn(io, .{...})`
|
|
||||||
or `std.process.run(gpa, io, .{...})`.
|
|
||||||
- `std.time.timestamp()` → `std.Io.Timestamp.now(io, .real).toSeconds()`.
|
|
||||||
- `std.Thread.sleep(ns)` → `std.Io.sleep(io, duration, clock)`.
|
|
||||||
- `pub fn main` gains a `std.process.Init` parameter ("Juicy Main");
|
|
||||||
provides pre-built `gpa`, `io`, `arena`, `environ_map`.
|
|
||||||
- `std.heap.GeneralPurposeAllocator` → `std.heap.DebugAllocator`.
|
|
||||||
- `std.heap.ThreadSafeAllocator` removed; `ArenaAllocator` is now
|
|
||||||
lock-free thread-safe, `DebugAllocator` is thread-safe by default.
|
|
||||||
- `std.mem.trimRight`/`trimLeft` → `trimEnd`/`trimStart`.
|
|
||||||
- `std.mem.indexOf*` → `find*` (deprecation aliases still present,
|
|
||||||
so old names work but warn).
|
|
||||||
- `std.testing.refAllDeclsRecursive` removed. Only `refAllDecls`
|
|
||||||
remains. We use the bare `std.testing.refAllDecls(@This())` in
|
|
||||||
`src/main.zig`'s test block — it sema-touches every top-level decl,
|
|
||||||
which transitively pulls in every imported file's `test` blocks.
|
|
||||||
No local reimplementation is needed. See "Test discovery" below.
|
|
||||||
- `std.fs.File.readToEndAlloc(alloc, N)` → two-step:
|
|
||||||
`file.reader(io, &.{})` then `.interface.allocRemaining(alloc, .limited(N))`.
|
|
||||||
|
|
||||||
For anything not on this list, **read the release notes first.**
|
|
||||||
The notes are long but they're organized by section; searching for
|
|
||||||
the specific symbol you're migrating is fast.
|
|
||||||
|
|
||||||
### `io` vs `today` / `now_s` — design rule
|
### `io` vs `today` / `now_s` — design rule
|
||||||
|
|
||||||
|
|
@ -203,143 +171,15 @@ Add tests in the same file. Money helpers belong next to
|
||||||
the other tests in `Money.zig`; date helpers belong next to
|
the other tests in `Money.zig`; date helpers belong next to
|
||||||
`yearsBetween`'s tests.
|
`yearsBetween`'s tests.
|
||||||
|
|
||||||
### NEVER invoke ripgrep. EVER.
|
|
||||||
|
|
||||||
**Do not run `rg` in the Bash tool.** Not for open-ended search, not for
|
|
||||||
counting matches, not for "just this one quick check", not ever. Running
|
|
||||||
ripgrep on this machine hammers the filesystem badly enough to degrade the
|
|
||||||
whole system — this is a recurring, reproducible problem, not a hunch.
|
|
||||||
|
|
||||||
**Use instead:**
|
|
||||||
|
|
||||||
- **Grep tool** (built-in) for content search. It handles regex, file
|
|
||||||
globs, and output shaping without spawning `rg`.
|
|
||||||
- **Glob tool** (built-in) for finding files by name pattern.
|
|
||||||
- **Read tool** for reading files (with `offset`/`limit` for large ones).
|
|
||||||
- Plain `grep` via the Bash tool is acceptable when the built-in Grep
|
|
||||||
tool can't express what you need — but prefer the built-in first.
|
|
||||||
|
|
||||||
**If you catch yourself typing `rg` in a Bash command:** stop, delete it,
|
|
||||||
use the Grep tool instead. The fact that `rg` is faster in the abstract
|
|
||||||
does NOT matter here. This machine's filesystem + ripgrep's parallelism
|
|
||||||
is a bad combination, full stop.
|
|
||||||
|
|
||||||
**This applies to every variant:** `rg`, `ripgrep`, piping through
|
|
||||||
`rg`, backgrounded `rg`, `rg --files`, etc. All banned.
|
|
||||||
|
|
||||||
### NEVER delete or modify build caches. EVER.
|
|
||||||
|
|
||||||
**This means:**
|
|
||||||
|
|
||||||
- **NEVER** run `rm -rf .zig-cache` or `rm -rf .zig-cache/*` or any variant.
|
|
||||||
- **NEVER** run `rm -rf ~/.cache/zig` or touch anything under `~/.cache/zig/`.
|
|
||||||
- **NEVER** touch `~/.cache/zls/` or any other tool cache.
|
|
||||||
- **NEVER** suggest deleting the cache as a "fix" — it is not a fix, it is
|
|
||||||
damage. Deleting `.zig-cache` while ZLS or another `zig build` is running
|
|
||||||
creates a corrupt state where the build runner's expected cache entry
|
|
||||||
(`.zig-cache/o/<hash>/build`) references a path that no longer exists,
|
|
||||||
producing the error `failed to spawn build runner .zig-cache/o/<hash>/build:
|
|
||||||
FileNotFound`. Recovering from this on the affected machine requires
|
|
||||||
killing every concurrent `zig` process (including ZLS) and waiting for
|
|
||||||
filesystem state to re-stabilize — it is NOT a simple retry.
|
|
||||||
- **NEVER** touch the cache "just to force a rebuild". Zig's cache is
|
|
||||||
content-addressed. It does not get stale in a way that deletion fixes.
|
|
||||||
If a build result looks wrong, the bug is in the source, not the cache.
|
|
||||||
Use `touch src/somefile.zig` if you truly need to invalidate one file's
|
|
||||||
cache line. Do not nuke the whole directory.
|
|
||||||
- **NEVER** suggest a "different cache directory" as a workaround without
|
|
||||||
an explicit, specific reason and explicit user approval. `--cache-dir`
|
|
||||||
and `--global-cache-dir` flags exist; they are not toys.
|
|
||||||
|
|
||||||
**If a test result seems wrong or cached incorrectly:** the answer is
|
|
||||||
ALWAYS to investigate the source code or build graph, not to delete
|
|
||||||
cache. See "Test discovery" below — 99% of the time the "cached wrong
|
|
||||||
result" is actually a test discovery problem, not a cache problem.
|
|
||||||
|
|
||||||
**If you find yourself typing `rm -rf` anywhere near a cache path: STOP.
|
|
||||||
Ask the user instead.**
|
|
||||||
|
|
||||||
### NEVER run destructive git operations without explicit permission.
|
|
||||||
|
|
||||||
- No `git reset --hard`, `git clean -fdx`, `git push --force`, `git checkout .`
|
|
||||||
on files with uncommitted work, unless the user asks for that specific
|
|
||||||
operation by name.
|
|
||||||
|
|
||||||
### NEVER run `git stash`. EVER.
|
|
||||||
|
|
||||||
- **`git stash` is banned outright.** Not `git stash push`, not `git stash --keep-index`,
|
|
||||||
not `git stash pop`, not `git stash apply`, not even read-only `git stash list`.
|
|
||||||
No variant. No "just to test something." No "I'll pop it right back."
|
|
||||||
- The reason: `git stash pop` can conflict on overlapping lines and leave
|
|
||||||
unresolved conflict markers in the working tree. The recovery requires
|
|
||||||
hand-resolving the markers, which trashes whatever curated index state
|
|
||||||
the user had in flight. This has bitten the user before and it's bitten
|
|
||||||
them again because of me. The rule is absolute now.
|
|
||||||
- If you think you need `git stash` to verify something (e.g. "does the
|
|
||||||
staged-only state build cleanly in isolation?"), the answer is: **don't
|
|
||||||
verify it that way.** Either:
|
|
||||||
- Read `git diff --cached` and reason about whether the staged hunks
|
|
||||||
are coherent on their own, OR
|
|
||||||
- Ask the user to verify after they commit, OR
|
|
||||||
- If verification is critical, ask the user to do the stash themselves
|
|
||||||
with their tools — but recommend against it because of this rule.
|
|
||||||
- **There is no exception clause for `git stash`.** Not even "the user
|
|
||||||
said it's OK this once" — the previous "one-time exception" for git
|
|
||||||
staging operations is what led to the stash incident. Direct exceptions
|
|
||||||
for `git add -p` / `git restore --staged` for staging-management remain
|
|
||||||
permitted with explicit user approval, but `git stash` is permanently
|
|
||||||
off-limits regardless of consent.
|
|
||||||
|
|
||||||
### NEVER run `git add`, `git commit`, or `git push`. EVER.
|
|
||||||
|
|
||||||
- **The user commits. You do not.** Do not stage files. Do not create commits.
|
|
||||||
Do not amend commits. Do not push. Do not suggest running these commands
|
|
||||||
yourself "to save a step". This includes `git add -p`, `git add .`,
|
|
||||||
`git add <file>`, `git commit -m ...`, `git commit --amend`, `git push`,
|
|
||||||
and any `gh pr create` that would auto-stage or auto-commit.
|
|
||||||
- If you are tempted to run any of these because "the work is done and it
|
|
||||||
seems logical to commit" — STOP. The user has a review-and-commit workflow.
|
|
||||||
Your job ends at a clean working tree with the changes ready to review.
|
|
||||||
- The ONLY exception is when the user says, verbatim in the current turn,
|
|
||||||
"commit this" / "make a commit" / "push it" / similar direct imperative.
|
|
||||||
Do not extrapolate from earlier intent, a plan that mentioned milestones,
|
|
||||||
or any indirect signal. If in doubt, ask — don't commit.
|
|
||||||
- When a milestone plan says "STOP POINT — user reviews and commits": you
|
|
||||||
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
|
### Documentation-file conventions
|
||||||
|
|
||||||
- **`TODO.md` holds only what's still open.** Git already tracks what was
|
- **`TODO.md` holds only what's still open.** Git already tracks
|
||||||
done and when. Do NOT add "DONE" markers, completion status, strikethrough,
|
what was done and when. Do NOT add "DONE" markers, completion
|
||||||
or "shipped in …" blurbs to TODO entries — just delete the section when
|
status, strikethrough, or "shipped in ..." blurbs to TODO
|
||||||
the work is finished. If follow-up work came out of the finished task,
|
entries. Just delete the section when the work is finished. If
|
||||||
add it as a new top-level section; don't leave the parent entry around
|
follow-up work came out of the finished task, add it as a new
|
||||||
as a historical wrapper.
|
top-level section; don't leave the parent entry around as a
|
||||||
|
historical wrapper.
|
||||||
- **`REPORT.md` is untracked on purpose.** It's a personal workflow doc
|
- **`REPORT.md` is untracked on purpose.** It's a personal workflow doc
|
||||||
living in the repo root only until it moves to the user's portfolio
|
living in the repo root only until it moves to the user's portfolio
|
||||||
directory. Edit it freely when asked; don't treat it as part of the
|
directory. Edit it freely when asked; don't treat it as part of the
|
||||||
|
|
@ -347,140 +187,84 @@ Ask the user instead.**
|
||||||
|
|
||||||
### Lint warnings — there are no "pre-existing" warnings
|
### Lint warnings — there are no "pre-existing" warnings
|
||||||
|
|
||||||
**zlint warnings get fixed, period.** Do NOT excuse a warning by
|
**Lint warnings get fixed, period.** Do NOT excuse a warning by
|
||||||
saying it was "pre-existing in the file" or "inherited from a copy"
|
saying it was "pre-existing in the file" or "inherited from a
|
||||||
or "the same style as elsewhere." That's how lint debt accumulates
|
copy" or "the same style as elsewhere." That's how lint debt
|
||||||
to the point where the pre-commit hook (`zlint --deny-warnings`)
|
accumulates to the point where the deny-warnings flag on the
|
||||||
becomes a tripwire that everyone routes around.
|
pre-commit hook becomes a tripwire that everyone routes around.
|
||||||
|
|
||||||
The rule:
|
The rule:
|
||||||
|
|
||||||
1. **Before any commit, run zlint on every file you touched in
|
1. **Before any commit, run zlint on every file you touched
|
||||||
the change.** If zlint reports any warnings on those files,
|
in the change.** If zlint reports any warnings on those
|
||||||
fix them in that change. There is no "I didn't introduce it,
|
files, fix them in that change. There is no "I didn't
|
||||||
not my problem" — once you've touched the file, the warnings
|
introduce it, not my problem." Once you've touched the file,
|
||||||
are yours.
|
the warnings are yours.
|
||||||
|
|
||||||
2. **If you find pre-existing warnings in a file you didn't
|
2. **If you find pre-existing warnings in a file you didn't
|
||||||
intend to touch (e.g. you ran zlint across the whole tree
|
intend to touch** (e.g. you ran zlint across the whole
|
||||||
for a sanity check), fix them in a SEPARATE commit and call
|
tree for a sanity check), fix them in a SEPARATE commit and
|
||||||
that out to the user explicitly so they can keep the commits
|
call that out to the user explicitly so they can keep the
|
||||||
clean.** Do not silently bundle drive-by lint fixes into a
|
commits clean. Do not silently bundle drive-by lint fixes
|
||||||
feature commit; the diff becomes harder to review and the
|
into a feature commit.
|
||||||
lint-debt origin gets buried.
|
|
||||||
|
|
||||||
3. **Common warning kinds and the right fix:**
|
3. **Never report a lint result by saying "0 errors, N warnings,
|
||||||
- `suppressed-errors` (`catch {}` / `catch "fallback"`):
|
but they're all pre-existing."** Either fix the warnings in
|
||||||
replace with `try` (propagate to the caller). If the call
|
this change OR report "0 errors, 0 warnings on the files I
|
||||||
genuinely cannot propagate (e.g. an stderr write inside an
|
touched. The wider tree has N warnings I haven't addressed
|
||||||
error-reporting path where the secondary error doesn't
|
in this change; flagging for follow-up" — and only after
|
||||||
matter), use `catch |err| std.log.debug(...)` or rewrite to
|
you've confirmed by file that none of the wider-tree warnings
|
||||||
not need the catch. The `catch {}` form is almost never the
|
are in files you modified.
|
||||||
right answer.
|
|
||||||
- `unsafe-undefined`: add a `// SAFETY: <reason>` comment on
|
|
||||||
the line with `undefined` explaining why it's safe (e.g.
|
|
||||||
"buffer immediately overwritten by bufPrint below").
|
|
||||||
- `unused-decls`: delete the decl. Don't leave dead imports
|
|
||||||
or constants around "in case."
|
|
||||||
|
|
||||||
4. **Never report a lint result by saying "0 errors, N warnings,
|
zfin uses `zlint --deny-warnings` on the pre-commit hook. The
|
||||||
but they're all pre-existing."** Either:
|
common zlint warning kinds and the right fix:
|
||||||
- Fix the warnings in this change (preferred when N is small
|
|
||||||
or the file is yours), OR
|
|
||||||
- Report "0 errors, 0 warnings on the files I touched. The
|
|
||||||
wider tree has N warnings I haven't addressed in this
|
|
||||||
change; flagging for follow-up" — and only after you've
|
|
||||||
confirmed by file that none of the wider-tree warnings
|
|
||||||
are in files you modified.
|
|
||||||
|
|
||||||
The `--deny-warnings` flag is on the pre-commit hook for a
|
- `suppressed-errors` (`catch {}` / `catch "fallback"`):
|
||||||
reason: every warning is a real signal that the codebase asked
|
replace with `try` (propagate to the caller). If the call
|
||||||
the linter to flag and someone hasn't dealt with. Treat them as
|
genuinely cannot propagate (e.g. an stderr write inside an
|
||||||
errors at write-time, not as background noise to ignore.
|
error-reporting path where the secondary error doesn't
|
||||||
|
matter), use `catch |err| std.log.debug(...)` or rewrite to
|
||||||
|
not need the catch. The `catch {}` form is almost never the
|
||||||
|
right answer.
|
||||||
|
- `unsafe-undefined`: add a `// SAFETY: <reason>` comment on
|
||||||
|
the line with `undefined` explaining why it's safe (e.g.
|
||||||
|
"buffer immediately overwritten by bufPrint below").
|
||||||
|
- `unused-decls`: delete the decl. Don't leave dead imports
|
||||||
|
or constants around "in case."
|
||||||
|
|
||||||
### Em-dash usage — ASK FIRST
|
|
||||||
|
|
||||||
If you're about to write an em-dash (`—`) anywhere (code, tests, doc
|
|
||||||
comments, commit messages, AGENTS.md prose), **stop and check whether a
|
|
||||||
regular ASCII hyphen (`-`) would do.** Most of the time it would. Em-dashes
|
|
||||||
look nice in prose but they create real problems:
|
|
||||||
|
|
||||||
- **In tabular output / TUI cells**, `—` is 3 bytes / 1 display column.
|
|
||||||
Zig's `{s:>N}` formatter pads by byte count, so any column containing
|
|
||||||
em-dashes will be 2 visual columns short per em-dash. We've fixed this
|
|
||||||
bug at least twice; don't reintroduce it. If you genuinely need a
|
|
||||||
multibyte sentinel for "no data", use `fmt.padRightToCols` /
|
|
||||||
`fmt.centerDash` (display-column-aware) — or just hard-code the cell
|
|
||||||
as a literal const string when the cell width is fixed and the dash
|
|
||||||
position is static (no point computing what you already know). Add
|
|
||||||
an alignment test that compares the multibyte row's `displayCols`
|
|
||||||
against an ASCII row.
|
|
||||||
- **In code identifiers and string literals**, em-dashes look fine in your
|
|
||||||
editor and break grep on the user's machine when they're searching with
|
|
||||||
ASCII `-`. If a future grep for "expected-return" or "as-of" silently
|
|
||||||
misses your "expected—return" string, that's a bug surface.
|
|
||||||
- **In commit messages and prose docs**, em-dashes are an AI tell. The
|
|
||||||
user reads commit messages and won't appreciate the codebase looking
|
|
||||||
like it was written by ChatGPT.
|
|
||||||
|
|
||||||
**Rule of thumb:**
|
|
||||||
|
|
||||||
- Use `-` (ASCII hyphen) for: ranges (`1-5y`), compound modifiers
|
|
||||||
(`forecast-vs-actual`), CLI flag names (`--return-backtest`), code
|
|
||||||
identifiers, and string concatenation in tables.
|
|
||||||
- Use `—` (em-dash) only when you're displaying it to the user as a
|
|
||||||
meaningful sentinel (e.g. "no data" cell in a table), AND you've handled
|
|
||||||
the display-column padding correctly.
|
|
||||||
- If you find yourself reaching for `—` in prose for a parenthetical
|
|
||||||
aside, **switch to a regular dash, comma, or parens.** Em-dashes have
|
|
||||||
become a stylistic AI tic and the user has explicitly asked to keep
|
|
||||||
them out unless they earn their place.
|
|
||||||
|
|
||||||
When in doubt, **ask**. A one-line "I'm about to use `—` here for X, OK?"
|
|
||||||
is much cheaper than reverting after the user notices.
|
|
||||||
|
|
||||||
### Errors carry information — never throw it away
|
### Errors carry information — never throw it away
|
||||||
|
|
||||||
When you catch an error, the caller's first question is **"why did
|
When you catch an error, the caller's first question is **"why
|
||||||
this fail?"** A user-facing error message that says only
|
did this fail?"** A user-facing error message that says only
|
||||||
`"FetchFailed"` or `"could not parse portfolio file"` is failing
|
`"FetchFailed"` or `"could not parse portfolio file"` is failing
|
||||||
the user — they have to read source code to figure out what
|
the user — they have to read source code to figure out what
|
||||||
happened. Three habits cause this:
|
happened. Three habits cause this:
|
||||||
|
|
||||||
**(1) Bare `catch {}` and `catch return error.X`.** The error
|
**(1) Bare `catch {}` and `catch return error.X`.** Capture as
|
||||||
value carries the upstream's classification (`RateLimited`,
|
`catch |err|` and at minimum include `@errorName(err)` (or `{t}`
|
||||||
`Unauthorized`, `NotFound`, `ParseError`, etc.). Discarding it
|
format) in any user-visible message. Better: re-raise a more
|
||||||
reduces N distinct conditions to one opaque "something went
|
specific error variant from the caller's error set.
|
||||||
wrong." Fix: capture as `catch |err|` and at minimum include
|
|
||||||
`@errorName(err)` (or `{t}` format) in any user-visible message.
|
|
||||||
Better: re-raise a more specific error variant from the caller's
|
|
||||||
error set so the next layer up can decide what to do.
|
|
||||||
|
|
||||||
**(2) Generic `error.X` collapse points in service-layer code.**
|
**(2) Generic `DataError.FetchFailed` collapse in service.zig.**
|
||||||
When `service.zig:getX` does `provider.fetchX(...) catch return
|
When `service.zig:getX` does `provider.fetchX(...) catch return
|
||||||
DataError.FetchFailed`, the provider's specific error
|
DataError.FetchFailed`, the provider's specific error
|
||||||
(`RateLimited` vs `Unauthorized` vs `NotFound`) is permanently
|
(`RateLimited` vs `Unauthorized` vs `NotFound`) is permanently
|
||||||
erased. Fix: either expand the service-layer error set
|
erased. Fix: either expand `DataError` to mirror the provider
|
||||||
(`DataError`) to mirror the provider distinctions, OR pass the
|
distinctions, OR pass the inner error through via `catch |err| {
|
||||||
inner error through via `catch |err| { log.warn(...
|
log.warn(... @errorName(err) ...); return ...; }` so stderr
|
||||||
@errorName(err) ...); return ...; }` so the stderr path tells
|
tells the user something useful even when the typed return value
|
||||||
the user something useful even if the typed return value is
|
is collapsed.
|
||||||
collapsed.
|
|
||||||
|
|
||||||
**(3) User-facing stderr messages that don't say what the actual
|
**(3) User-facing stderr messages must name the error.** A CLI
|
||||||
error name was.** A CLI command that catches an error MUST print
|
command that catches an error MUST print `@errorName(err)` (or a
|
||||||
`@errorName(err)` (or a deliberate human translation per error
|
deliberate human translation per error kind) in the stderr
|
||||||
kind) in the stderr message. `"Error: Failed to fetch data for
|
message. `"Error: Failed to fetch data for symbol"` is
|
||||||
symbol"` is unhelpful. `"Error fetching AAPL: RateLimited (free
|
unhelpful. `"Error fetching AAPL: RateLimited (free tier 5/min,
|
||||||
tier 5/min — wait 60s)"` is helpful. The error name alone is
|
wait 60s)"` is helpful.
|
||||||
enough for the user to look it up; the human translation is a
|
|
||||||
nice-to-have on top.
|
|
||||||
|
|
||||||
**The grep test:** if you're catching an error in user-facing
|
**The grep test:** `cli.stderrPrint` calls with hardcoded "Error:
|
||||||
code, the message should mention either `@errorName` / `{t}` or
|
failed to ..." strings and no error context fail this test.
|
||||||
reference a specific error variant by name. `cli.stderrPrint`
|
|
||||||
calls with hardcoded "Error: failed to ..." strings and no error
|
|
||||||
context fail this test.
|
|
||||||
|
|
||||||
### NEVER put PII in tests, fixtures, comments, or docs
|
### NEVER put PII in tests, fixtures, comments, or docs
|
||||||
|
|
||||||
|
|
@ -879,26 +663,27 @@ shape with example signatures.
|
||||||
|
|
||||||
### `anytype` is almost never the right answer — pause and ask first
|
### `anytype` is almost never the right answer — pause and ask first
|
||||||
|
|
||||||
Empirically, every time `anytype` looked necessary in this codebase
|
Empirically, every time `anytype` looked necessary in this
|
||||||
it turned out not to be. Concrete-typed parameters always worked
|
codebase it turned out not to be. Concrete-typed parameters
|
||||||
once we actually tried them. **Before adding any new `anytype`
|
always worked once we actually tried them. **Before adding any
|
||||||
parameter, stop and reconsider.** The discussion that surfaces "no,
|
new `anytype` parameter, stop and reconsider.** The discussion
|
||||||
this can be `*App` after all" is short and worth having every time.
|
that surfaces "no, this can be `*App` after all" is short and
|
||||||
|
worth having every time.
|
||||||
|
|
||||||
Common reasons people reach for `anytype` and what to do instead:
|
Common reasons people reach for `anytype` and what to do instead:
|
||||||
|
|
||||||
- **"I want to avoid a circular import."** Test the assumption.
|
- **"I want to avoid a circular import."** Test the assumption.
|
||||||
Zig resolves `a.zig ↔ b.zig` cycles fine in most cases — file
|
Zig resolves `a.zig ↔ b.zig` cycles fine in most cases — file
|
||||||
structs are evaluated lazily, and the cycle only fails if
|
structs are evaluated lazily, and the cycle only fails if
|
||||||
evaluation actually loops. Just write the concrete type and run
|
evaluation actually loops. Just write the concrete type and
|
||||||
`zig build`. If it fails, the answer is usually to extract the
|
run `zig build`. If it fails, the answer is usually to extract
|
||||||
shared type to a third file, not to weaken the contract with
|
the shared type to a third file, not to weaken the contract
|
||||||
`anytype`. (See: the `tab_framework.zig` ↔ `tui.zig` cycle —
|
with `anytype`. (See: the `tab_framework.zig` ↔ `tui.zig` cycle
|
||||||
caught me once; turned out Zig handled it cleanly.)
|
— caught me once; turned out Zig handled it cleanly.)
|
||||||
- **"This function is genuinely polymorphic over many types."**
|
- **"This function is genuinely polymorphic over many types."**
|
||||||
In Zig, the right shape for runtime polymorphism is usually
|
In Zig, the right shape for runtime polymorphism is usually
|
||||||
`*anyopaque` + an explicit cast at the boundary, paired with
|
`*anyopaque` + an explicit cast at the boundary, paired with a
|
||||||
a vtable struct of fn pointers. That's harder to abuse than
|
vtable struct of fn pointers. That's harder to abuse than
|
||||||
`anytype` and the cast site documents the type contract
|
`anytype` and the cast site documents the type contract
|
||||||
unambiguously. Compile-time polymorphism over a known set of
|
unambiguously. Compile-time polymorphism over a known set of
|
||||||
types is better expressed with `comptime T: type` parameters.
|
types is better expressed with `comptime T: type` parameters.
|
||||||
|
|
@ -910,8 +695,8 @@ Common reasons people reach for `anytype` and what to do instead:
|
||||||
the method.
|
the method.
|
||||||
- **"It's a test helper that takes any struct."** This is the
|
- **"It's a test helper that takes any struct."** This is the
|
||||||
one case where `anytype` is sometimes OK — generic test
|
one case where `anytype` is sometimes OK — generic test
|
||||||
utilities like `std.testing.expectEqual`. But check whether
|
utilities like `std.testing.expectEqual`. But check whether a
|
||||||
a concrete type would do.
|
concrete type would do.
|
||||||
|
|
||||||
The real cost of `anytype` is that it punches a hole in the type
|
The real cost of `anytype` is that it punches a hole in the type
|
||||||
contract: the compiler can't catch wrong-shape arguments at the
|
contract: the compiler can't catch wrong-shape arguments at the
|
||||||
|
|
@ -977,7 +762,3 @@ command.
|
||||||
| [SRF](https://git.lerch.org/lobo/srf) | Cache file format, portfolio/watchlist parsing, serialization |
|
| [SRF](https://git.lerch.org/lobo/srf) | Cache file format, portfolio/watchlist parsing, serialization |
|
||||||
| [libvaxis](https://github.com/rockorager/libvaxis) (v0.6.0) | Terminal UI rendering |
|
| [libvaxis](https://github.com/rockorager/libvaxis) (v0.6.0) | Terminal UI rendering |
|
||||||
| [z2d](https://github.com/vancluever/z2d) (v0.11.0) | Pixel chart rendering (Kitty graphics protocol) |
|
| [z2d](https://github.com/vancluever/z2d) (v0.11.0) | Pixel chart rendering (Kitty graphics protocol) |
|
||||||
|
|
||||||
## Build system rules
|
|
||||||
|
|
||||||
- **Never use `addAnonymousImport`** in `build.zig`. Always use `b.addModule()` + `addImport()`. Anonymous imports cause "file belongs to multiple modules" errors and make dependency wiring opaque.
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue