8000 feat: structured error codes foundation (MinigrafError, ErrorCategory) — #277 (1/6) by adityamukho · Pull Request #357 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content

feat: structured error codes foundation (MinigrafError, ErrorCategory) — #277 (1/6) - #357

Merged
adityamukho merged 25 commits into
mainfrom
worktree-error-codes-277-foundation
Aug 25, 2026
Merged

adityamukho merged 25 commits into
mainfrom
worktree-error-codes-277-foundation

Conversation

@adityamukho
Copy link
Copy Markdown
Collaborator

Summary

Foundation PR for issue #277 (structured runtime error codes) — 1 of 6 planned PRs. Adds a new src/error.rs module (ErrorCategory, ErrorCode, a REGISTRY mapping codes to message templates, bail_coded!/err_coded! macros, and the public MinigrafError type) and migrates 13 public API functions across src/db.rs and src/query/datalog/prepared.rs from anyhow::Result<T> to Result<T, MinigrafError>.

By design, zero real call sites are migrated to specific error codes in this PR — every error currently falls through to a generic INT-000 catch-all. The 5 follow-up category PRs (PRS, STG, WAL, QRY, API+INT) wire up the 130 documented codes in docs/ERROR_REFERENCE.md one category at a time. Part of #277 — the umbrella issue stays open until all 6 land.

Design spec: docs/superpowers/specs/2026-08-25-structured-error-codes-design.md
Implementation plan: docs/superpowers/plans/2026-08-25-structured-error-codes-foundation.md

Type of change

  • Breaking change (requires migration or version bump)

Minigraf's, WriteTransaction's, and PreparedQuery's public methods now return Result<T, MinigrafError> instead of anyhow::Result<T>. MinigrafError implements std::error::Error, so ? still works against anyhow::Result/Box<dyn Error> callers. Every error's Display/to_string() output now carries a [CODE] prefix — a behavior change beyond the type signature (see CHANGELOG). Bundles into the version bump already pending from the unreleased OpenOptions field addition.

Checklist

  • cargo test passes (1085 tests, 0 failures)
  • cargo clippy --all-features -- -D warnings is clean
  • cargo fmt --check passes
  • New code has tests (unit and/or integration) — 15 unit tests in src/error.rs, 8 integration tests in tests/error_codes_foundation_test.rs
  • Error paths are tested, not just happy paths — including multi-hop .context() chain-walking and the INT-000 fallback path
  • No unwrap() / expect() in library code paths
  • CHANGELOG.md updated under Unreleased
  • I have read PHILOSOPHY.md and this change aligns with it — no new dependencies (no thiserror), ErrorCategory/code stay a stable matchable surface rather than a 130-variant enum, appropriate for a decades-stability commitment

Notes for reviewer

  • ErrorCategory is marked #[non_exhaustive] — a deliberate forward-compatibility choice for a 6-variant public enum meant for downstream match, made during final review.
  • The _inner/wrapper migration pattern (rename the existing function to a private _inner twin with an unchanged body, add a thin pub wrapper doing .map_err(MinigrafError::from)) is applied uniformly across all 13 functions — verified consistent by both per-task and whole-branch review.
  • Known follow-up blocker, recorded in the spec, not fixed here: src/query/datalog/parser.rs is Result<_, String>-based, not anyhow-based, so bail_coded!/err_coded! won't work there as a drop-in replacement the way they do everywhere else. This needs a decision before the PRS category PR (79 of 130 codes, the next PR) is planned — see the spec's Migration phasing section, step 2.
  • This branch was built end-to-end via superpowers:subagent-driven-development — 12 implementer/reviewer task cycles plus a final whole-branch review and one consolidated fix wave. Full history in the commit log.

adityamukho and others added 25 commits August 25, 2026 11:23
…ion tests)

Preflight scan caught the discrepancy before dispatching implementers.
Part of #277.
Foundation types for structured runtime error codes. No call sites
wired up yet — this is pure scaffolding. Part of #277.
Task 1's implementer necessarily added `pub(crate) mod error;` to
src/lib.rs -- cargo test --lib error:: cannot compile/run without the
module being part of the crate tree, a gap the original brief didn't
anticipate. Task 4 was written assuming no such line existed yet and
would have inserted a duplicate `mod error;`, a compile error. Fixed
Task 4's text to upgrade the existing line's visibility instead.
Ruling from Task 1's task review. Part of #277.
Call-site macros that tag an anyhow::Error with a compile-time-checked
ErrorCode, formatted from the code's REGISTRY template. Not yet used
by any real call site. Part of #277.
MinigrafError is the new public error type: category + stable code +
Display as "[CODE] message" + std::error::Error. From<anyhow::Error>
walks the error chain for a CodedError, falling back to INT-000 for
any error not yet migrated to a specific code. Part of #277.
Wires the new error module into the public API surface, documents the
INT-000 catch-all code in ERROR_REFERENCE.md, and adds a test that
fails if REGISTRY and the doc's code list/message templates ever
diverge. Part of #277.
…ded!

These have no production call sites until the first category PR (PRS)
starts using them -- Task 11's clippy -D warnings gate needs this clean
now rather than deferred. Also replaces registry_entry's expect() with
a debug_assert! + defensive fallback, since clippy::expect_used is
denied crate-wide and this was blocking `cargo clippy --all-features
-- -D warnings` independently of the dead-code warnings. Part of #277.
open, open_with_options, in_memory, in_memory_with_options, and the
OpenOptions/OpenOptionsWithPath builders now return
Result<Minigraf, MinigrafError>. Bodies are unchanged (renamed to
_inner twins); every error still surfaces as INT-000 until a later
category PR wires up real codes. Part of #277.
Signature-only change; body renamed to execute_inner, unchanged.
Part of #277.
Signature-only changes; bodies renamed to _inner twins, unchanged.
Part of #277.
Signature-only changes; bodies renamed to _inner twins, unchanged.
Part of #277.
Signature-only changes; bodies renamed to _inner twins, unchanged.
Part of #277.
The mutation happens inside commit_inner, which already declares its
own mut self -- the outer wrapper never mutates self directly. Part
of #277.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7e4uTBWtM8fMZiPZc4bCw
Signature-only change; body renamed to execute_inner, unchanged.
This completes the public API signature migration for #277's
foundation PR. Part of #277.
- Remove unused `pub use` re-exports of FaultConfig and FaultInjectingBackend
  (tests import directly from fault_inject module, not through the re-export)
- Apply rustfmt formatting to match project style
- Verify all clippy checks pass with -D warnings

Part of #277.
…s scope

The unused FaultConfig/FaultInjectingBackend re-export predates this
branch (confirmed via git log against the merge-base with main) and
is unrelated to structured error codes. Reported, not fixed, matching
how Tasks 4/8/9 handled other pre-existing out-of-scope issues in
this same plan. Part of #277.
Line 4 said errors surface as anyhow::Error, but this PR (part of #277)
made db.execute()/db.prepare()/etc. return MinigrafError, as the very
next paragraph already documented. Align line 4 with reality.
…ry entry

From<anyhow::Error>'s fallback branch (no CodedError found in the chain)
hardcoded code/category/message instead of calling registry_entry, so
the emitted Display text never actually matched the documented
'unclassified internal error: {}' template in ERROR_REFERENCE.md. Route
it through registry_entry(ErrorCode::Int000) + format_template instead,
removing the third hardcoded duplicate of INT-000/Internal (REGISTRY and
registry_entry's own fallback were the other two).

Updates one existing test's expected Display text to the now-correct
template-prefixed form; part of #277.
ErrorCategory is a public enum designed for downstream consumers to
match on, in a library targeting decades-long API stability per
PHILOSOPHY.md. Marking it non_exhaustive now, before any release ships
it, means a future 7th category (STG/WAL/QRY/API's categories already
exist as variants; further growth is plausible) won't be a breaking
change for consumers who already have to write a wildcard arm. No
effect within this crate. Part of #277.
…ANGELOG

The #277 foundation entry was missing that every error's Display/
to_string() now carries a [CODE] prefix it didn't have before — a
behavior change beyond the Result<T, MinigrafError> type signature,
affecting anything that string-matches error output (the REPL's printed
errors, and all 7 language-binding repos per cascade-monitoring
practice). Also fixes .code() -> &str to the actual signature,
.code() -> &'static str. Part of #277.
…e advice

During the multi-PR #277 migration window (this Foundation PR is 1 of 6;
only INT-000 exists so far), ordinary user syntax mistakes surface as
INT-000 too, same as genuine internal bugs, since nothing is migrated
to specific codes yet. Reorder the INT section's opening sentences so a
reader hits that transitional caveat before the 'likely indicates a bug
... please file an issue' advice, not after. Part of #277.
…rorCode coverage test

registry_entry's doc comment claimed the sync test
(registry_is_a_subset_of_error_reference_doc) exercises every ErrorCode
variant — false, it iterates REGISTRY, not ErrorCode, so a new
ErrorCode variant added without a matching REGISTRY row is caught by
nothing outside a debug_assert! that's compiled out in release builds.
Correct the comment, and add
error::tests::every_error_code_variant_has_a_registry_entry: an
exhaustive match over ErrorCode with no wildcard arm, so the PRS
category PR's ~79 new variants (and every one after) fail to compile
here if a REGISTRY row is missing, rather than silently falling back to
INT-000 at runtime. Part of #277.
…t chain

Two small gaps flagged as risk for the next (PRS category) PR, which
will be the first real caller of both bail_coded!/err_coded! (multi-hop
.context() chains are routine there) and format_template with varying
arg counts:

- format_template_arg_count_mismatch_debug_asserts: a #[should_panic]
  test confirming the debug_assert_eq! fires under cargo test's debug
  build; in release (debug_assert compiled out) the same call does not
  panic and instead drops the unfilled placeholder's text, per the
  function's existing doc comment.
- minigraf_error_finds_coded_error_through_two_context_hops: extends
  the existing single-hop context-chain test to two .context() calls,
  proving the chain-walk in MinigrafError::from finds a CodedError
  buried multiple hops deep, not just one.

Part of #277.
Final whole-branch review of the Foundation PR found that
src/query/datalog/parser.rs returns Result<_, String> everywhere, not
anyhow::Result -- bail_coded!/err_coded! (which produce anyhow::Error)
won't compile there as a drop-in replacement the way they do
everywhere else in the crate. This doesn't require any code change in
this PR, but must be decided before the PRS category PR (79 of 130
codes, the next PR in the #277 rollout) is planned. Part of #277.
@codecov
codecov Bot commented Aug 25, 2026
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.52427% with 18 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/error.rs 73.13% 18 Missing ⚠️

📢 Thoughts on this report? Let us know!

@adityamukho
adityamukho merged commit d603d5e into main Aug 25, 2026
17 checks passed
@adityamukho
adityamukho deleted the worktree-error-codes-277-foundation branch August 25, 2026 10:35
adityamukho added a commit that referenced this pull request Aug 25, 2026
#366)

* feat: migrate parser.rs to PRS-0xx structured error codes — #277 (2/6)

Wires up all 79 documented PRS-0xx codes (docs/ERROR_REFERENCE.md) in
src/query/datalog/parser.rs via bail_coded!/err_coded!, replacing the
generic INT-000 fallback the #277 foundation PR (#357) left in place
for every category.

parser.rs's internal Result<_, String> signatures (118 error sites)
convert to anyhow::Result<_> so bail_coded!/err_coded! can be used
directly, per the decision recorded in the foundation PR. db.rs's
three parse_datalog_command() call sites (Minigraf::execute,
WriteTransaction::execute, Minigraf::prepare) drop their
.map_err(|e| anyhow::anyhow!("{}", e)) — that stringified and
discarded any CodedError chain before it could reach
MinigrafError::from at the API boundary.

docs/ERROR_REFERENCE.md's PRS "Error text" lines are updated to show
the real {}-templated form (matching the INT-000 convention already
established) instead of a fully-rendered example value, verified
byte-for-byte against error.rs's REGISTRY by the existing
registry_is_a_subset_of_error_reference_doc sync test.

Two documented codes are wired up but structurally unreachable
through the public API: PRS-028 ("window expression cannot be
empty") and PRS-049 ("unexpected end of fact vector") sit behind
conditions their call sites' own guards already rule out. Pre-existing
parser logic, not introduced by this change.

Adds tests/error_codes_prs_test.rs: 16 test functions covering all 77
reachable PRS codes end-to-end through db.execute() and db.prepare(),
asserting both .code() and .category(). Updates the foundation PR's
placeholder assertion (execute_parse_error_returns_coded_error) from
INT-000/Internal to the real PRS-005/Parser it now produces.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7e4uTBWtM8fMZiPZc4bCw

* ci: retrigger workflows (no rust.yml/rust-clippy.yml runs fired on initial push)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0