round-5 follow-up r2: migrate all thiserror Display attributes to catalog

Completes the i18n sweep started in the previous commit. All
remaining hand-rolled user-facing English strings inside
thiserror #[error(...)] attributes have been moved into the
catalog. Drops the thiserror dependency entirely.

Twelve error types migrated:

- dsl::action::UnknownAction         → parse.custom.unknown_action
- dsl::parser::ParseError            → parse.error_wrapper + parse.empty
- dsl::value::ValueError             → value.{type_mismatch,format}
- persistence::csv_io::CsvError      → persistence.csv.*
- persistence::mod::PersistenceError → persistence.{io,encode}
- persistence::yaml::YamlError       → persistence.yaml.*
- persistence::migrations::MigrateError → persistence.migrate.*
- project::lock::LockError           → project.lock.*
- project::naming::NamingError       → project.naming.*
- project::naming::UserNameError     → project.user_name.*
- project::mod::ProjectError         → project.{path_not_found,...}
- project::mod::SafeDeleteError      → project.safe_delete.*
- archive::ArchiveError              → archive.*
- cli::ArgsError                     → cli.*
- db::DbError                        → db.error.*

Pattern per type: drop thiserror::Error derive, write manual
Display calling crate::t!(), keep #[from] semantics via
explicit From impls, override Error::source() where applicable
so #[source]-style chaining is preserved.

Why this matters (user rationale): "fine to have fallbacks for
errors that are purely technical, but lift the output to a
place where it can be localized later and where an adjustment
with friendly text is easily possible if any of them become
part of the happy path." All surface strings now live in
en-US.yaml and can be reworded or localized without touching
Rust source.

Tests: 769 passing, 0 failed, 1 ignored. Clippy clean with
-D warnings. Cargo.toml: drop thiserror = "2.0.18".
This commit is contained in:
claude@clouddev1
2026-05-13 21:24:51 +00:00
parent 1e06490572
commit 6ca297579e
17 changed files with 680 additions and 117 deletions
+105 -2
View File
@@ -153,6 +153,15 @@ error:
headline: "UPDATE requires at least one assignment."
# ---- Help text (CLI banner + in-app `help` command) ------------------
# ---- CLI argument-parsing errors (stderr before TUI starts) ---------
cli:
missing_value: "missing value for --{flag}"
invalid_value: "invalid value for --{flag}: {value} (expected one of: {expected})"
unknown_argument: "unknown argument: {arg}"
multiple_paths: "only one project path may be supplied; got both `{first}` and `{second}`"
resume_with_path: |-
--resume and a positional <project-path> are mutually exclusive; pass one or the other
help:
# CLI usage banner. Printed by `--help` / `-h` and on
# argument-parse failure. Multi-line block; consumers
@@ -279,6 +288,10 @@ parse:
# caret pointer (visualising the failure column) is printed
# on its own preceding line via `parse.caret`.
error: "parse error: {detail}"
# Wrapper used by `ParseError::Display` (so any to_string()
# call on a parse error renders consistently with the in-app
# error rendering).
error_wrapper: "could not parse command: {detail}"
# Custom (try_map / source-slice) error messages raised by
# the DSL parser. These were hand-written strings in
# `src/dsl/parser.rs` until the catalog migration brought
@@ -292,6 +305,7 @@ parse:
on_action_specified_twice: "`on {target}` specified twice"
change_column_flags_exclusive: "`--force-conversion` and `--dont-convert` are mutually exclusive — pick one."
unknown_type: "unknown type '{found}' (expected one of: {expected})"
unknown_action: "unknown referential action '{found}' (expected one of: {expected})"
# Caret pointer showing where in the input the parser
# failed. `{padding}` is the leading whitespace; the
# template appends `^` so the rendered line places the
@@ -444,9 +458,38 @@ project:
# --resume CLI failures printed to stderr before the TUI
# starts (ADR-0015 §7). Wording stays one line for clean
# piping; the runtime prepends `rdbms-playground: ` from
# `cli.binary_prefix` itself.
# the calling code itself.
resume_recorded_missing: "--resume: recorded project `{path}` no longer exists"
resume_no_previous: "--resume: no previous project recorded under `{data_root}`"
# Project-lock errors (single-instance enforcement, ADR-0015 §6).
lock:
already_held: |-
project is already open in another rdbms-playground process (pid {pid} on host `{hostname}`); close that process or remove `{path}` if you're sure it's not running
write: "could not write lock file `{path}`: {source}"
read: "could not read existing lock file `{path}`: {source}"
# Temp-project name generation failures.
naming:
wordlist_too_small: "wordlist must contain at least 3 entries; found {count}"
too_many_collisions: |-
could not generate a non-colliding temp project name after {attempts} attempts
# User-typed project name validation failures.
user_name:
empty: "project name cannot be empty"
leading_dot: "project name cannot start with `.`"
invalid_char: "project name cannot contain `{ch}`; use letters, digits, `-`, `_`, or `.` only"
# ProjectError variants (ProjectError Display path).
data_root_unavailable: |-
could not determine the OS-standard data directory; pass --data-dir to override
path_not_found: "project path `{path}` does not exist"
not_a_project: |-
path `{path}` does not look like a project directory (no `project.yaml` and no `playground.db`)
already_exists: "path `{path}` already exists; pick a different name or remove it first"
io: "filesystem error at `{path}`: {source}"
# SafeDeleteError — surfaces in logs when temp-project cleanup
# refuses to delete a path (ADR-0015 §13).
safe_delete:
refused: "refusing to delete `{path}`: {reason}"
io: "io error on `{path}`: {source}"
# ---- DSL failure wrapper + advanced-mode placeholder + fatal --------
dsl:
@@ -458,6 +501,51 @@ dsl:
# output that follows.
running: "running: {input}"
# ---- Value-validation errors (per-column at bind time) --------------
value:
type_mismatch: "column `{column}` expects {expected_human}, got {got}"
format: "column `{column}`: {message}"
# ---- Archive / zip errors (export / import) -------------------------
archive:
io: "io error on `{path}`: {source}"
zip: "zip error on `{path}`: {message}"
export_sequence_exhausted: |-
could not pick an export filename for `{project}` in `{target_dir}`: all sequence numbers up to {limit} are taken
import_collision_exhausted: |-
destination `{path}` already exists and the auto-suffix retries (-02 through -{limit}) are also taken; use `import <zip> as <target>` to choose a different name
invalid_zip: "zip is malformed: {detail}"
not_a_project_archive: |-
zip does not contain a project (no `project.yaml` under a single top-level folder)
multiple_top_folders: "zip contains more than one top-level folder; refusing to extract"
unsafe_entry: "zip entry `{entry}` would escape the target directory; refusing to extract"
# ---- Persistence-layer errors (CSV/YAML/IO) -------------------------
# These were thiserror Display attributes pre-round-6. Most surface
# only as the inner `{message}` of `fatal.persistence` or as the
# wrapped detail inside `DbError::PersistenceFatal`.
persistence:
io: "could not {operation} `{path}`: {source}"
encode: "could not encode {kind} for `{path}`: {message}"
csv:
empty: "CSV is empty"
invalid_utf8: "invalid UTF-8 in CSV body"
unterminated_quote: "unterminated quoted field"
yaml:
syntax: "project.yaml syntax error: {detail}"
unsupported_version: "unsupported project.yaml version: {version} (expected 1)"
unknown_type: "unknown user-facing column type `{raw}` for `{table}.{column}`"
unknown_action: "unknown referential action `{raw}`"
migrate:
version_parse: "could not read version field from project.yaml: {detail}"
newer_than_supported: |-
project.yaml is at version {file} but this build only understands up to version {latest}; upgrade the application or restore an older project.yaml
no_migrator: |-
no migrator registered for version {version} (programmer error: registry latest_version disagrees with migrators length)
step_failed: "migrator from v{from} to v{to} failed: {source}"
bad_output: "migrator produced an unparseable result: {detail}"
io: "io error during migration on `{path}`: {source}"
# ---- Advanced-mode placeholder until SQL parser lands (Q1) ----------
advanced_mode:
not_implemented: "advanced mode SQL not implemented yet — echo: {input}"
@@ -542,8 +630,23 @@ messages:
set_verbose: "messages: verbose"
unknown: "unknown messages mode '{value}' (expected 'short' or 'verbose')"
# ---- Cascade-effect summaries (per ADR-0014 delete reporting) -------
# ---- Database-error fallback wording + cascade summaries ------------
db:
# DbError variants — fallback Display wording for paths that
# bypass the structured friendly translator (fatal banners,
# plain to_string() calls). The normal path goes through
# `friendly::translate_error` which routes by the `kind`
# field and renders catalog wording from `error.*` instead.
error:
sqlite: "database error: {message}"
unsupported: "operation not supported: {detail}"
invalid_value: "invalid value: {detail}"
persistence_fatal: "could not {operation} `{path}`: {message}"
rebuild_row_failed: |-
unable to load row {row_number} from `{csv_path}` into table `{table}`: {detail}
worker_gone: "database worker is no longer available"
io: "io error: {detail}"
cascade:
# Per-relationship cascade summary appended to a delete
# success note. The same template handles cascade,