Skip to content

Commit b83bad3

Browse files
committed
feat: use parent BullError that all errors must extend. rename to toUserTranslated. Updated Agents.md and added errorhandling_agent.md. Removed old unused bullError
1 parent 1add1a0 commit b83bad3

38 files changed

Lines changed: 283 additions & 141 deletions

AGENTS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Instructions for AI coding agents working in this repo. Cross-tool standard ([ag
66

77
Bull Bitcoin Mobile: self-custodial Bitcoin + Liquid + Lightning wallet. Flutter/Dart, BLoC, Drift/SQLite, GoRouter, get_it DI. See [README.md](README.md) for product details.
88

9-
**Repo doc map:** [README.md](README.md) = product · [ARCHITECTURE.md](ARCHITECTURE.md) = layer model & design rules (read its "Entry points & code tour") · **AGENTS.md** (this file) = toolchain, conventions, commit/test rules · [FEATURES.md](FEATURES.md) = cross-feature dependency graph.
9+
**Repo doc map:** [README.md](README.md) = product · [ARCHITECTURE.md](ARCHITECTURE.md) = layer model & design rules (read its "Entry points & code tour") · **AGENTS.md** (this file) = toolchain, conventions, commit/test rules · [FEATURES.md](FEATURES.md) = cross-feature dependency graph · [ERRORHANDLING_AGENT.md](ERRORHANDLING_AGENT.md) = error-handling pattern (`BullError` + `toUserTranslated`).
1010

1111
**Composition root** (where to wire a new feature): `lib/main.dart` (`Bull.init()``initLocator()``runApp`) → `lib/locator.dart` (get_it; `AppLocator.setup` registers core, then every `<Feature>Locator.setup`) → `lib/router.dart` (GoRouter; `AppRouter.router` spreads each `<Feature>Router`'s routes). A new feature is added in those last two files.
1212

@@ -57,7 +57,7 @@ Hard rules:
5757
8. **Shared value objects go in `lib/core/primitives/`** (planned per [FEATURES.md](FEATURES.md) — folder doesn't exist yet at audit time, and `Secret`/`Address`/`Amount`/`Fingerprint` are not yet extracted as primitives). When you create a value object that more than one feature would reasonably use, put it in `lib/core/primitives/` from the start — that's how the primitives layer gets built. Before creating one, grep `lib/` for an existing class with the same intent.
5858
9. **Prefer rich domain models** — methods that enforce invariants, not anemic DTOs mirroring DB rows. (Note: [Fowler's anemic-domain anti-pattern](https://www.martinfowler.com/bliki/AnemicDomainModel.html) is widely cited but [not universal](https://blog.inf.ed.ac.uk/sapm/2014/02/04/the-anaemic-domain-model-is-no-anti-pattern-its-a-solid-design/). This is our preference, aligned with ARCHITECTURE.md's "Anemic Domain Models" pitfall.)
5959
10. **No raw colors.** Always pull from the theme. If the user describes a color in plain language, pick the closest theme color that works in both light and dark mode. Don't edit theme files unprompted.
60-
11. **Errors — one sealed family per feature.** Define a sealed `<feature>_error.dart` in `domain/`. Map foreign errors at every boundary; never leak another layer's or feature's error type. **Each error exposes a `toTranslated(BuildContext)`** (dominant convention, 36 uses — `sell_error.dart`, `bitbox_errors.dart`, `replace_by_fee/errors.dart`; not `toTranslation`/`String get message`) returning a localized, user-safe message via `AppLocalizations` (the `context.loc` extension; see the UI Kit "no hardcoded user-facing strings" rule) — never the raw exception or dev detail, which stays in logs. The `sealed` switch makes a missing user message a compile error. **Call it UI-side only** — it takes a `BuildContext`; the bloc holds the `<Feature>Error` in state, the widget renders `error.toTranslated(context)`. A `BuildContext` in `presentation/` is a smell. **The end user never sees a dev string:** the catch-all variant (`unexpected`/`unknown`) returns a **generic localized** message, never the raw `message` — `unexpected: (message) => message` leaks dev detail; log it, show a generic string. Prefer returning a `Result`/sealed outcome for expected, recoverable errors at the repository boundary (`throw` only for programmer errors) — officially "a recommendation, but not a requirement" ([data-layer case study](https://docs.flutter.dev/app-architecture/case-study/data-layer)); don't mass-migrate existing exception code. **Legacy (don't replicate):** some hexagonal features split errors per layer (`domain_errors.dart` / `application_errors.dart` / `presentation_errors.dart`), and `fund_exchange` uses feature-prefixed singular (`fund_exchange_<layer>_error.dart`). Converge to one family per feature over time.
60+
11. **Errors — one sealed family per feature, extending `BullError`.** Full guide, canonical example, and checklist: **[ERRORHANDLING_AGENT.md](ERRORHANDLING_AGENT.md)**. Define a sealed `<feature>_error.dart` in `domain/` as `sealed class <Feature>Error extends BullError` ([`lib/core/errors/bull_error.dart`](lib/core/errors/bull_error.dart)); each variant is a `final class <Something>Error` (always the `Error` suffix), and the catch-all is `Unexpected<Feature>Error`. Map foreign errors at every boundary; never leak another layer's or feature's error type. **Each error exposes a `toUserTranslated(BuildContext)`** returning a localized, user-safe message via `AppLocalizations` (the `context.loc` extension; see the UI Kit "no hardcoded user-facing strings" rule) — never the raw exception or dev detail, which stays in logs. The `sealed` switch makes a missing user message a compile error. **Call it UI-side only** — it takes a `BuildContext`; the bloc holds the `<Feature>Error` in state, the widget renders `error.toUserTranslated(context)`. A `BuildContext` in `presentation/` is a smell. **The end user never sees a dev string:** the catch-all variant (`unexpected`/`unknown`) returns a **generic localized** message, never the raw `message` — `unexpected: (message) => message` leaks dev detail; log it, show a generic string. Prefer returning a `Result`/sealed outcome for expected, recoverable errors at the repository boundary (`throw` only for programmer errors) — officially "a recommendation, but not a requirement" ([data-layer case study](https://docs.flutter.dev/app-architecture/case-study/data-layer)); don't mass-migrate existing exception code. **Legacy (don't replicate):** some hexagonal features split errors per layer (`domain_errors.dart` / `application_errors.dart` / `presentation_errors.dart`), and `fund_exchange` uses feature-prefixed singular (`fund_exchange_<layer>_error.dart`). Converge to one family per feature over time.
6161
12. **Acyclic feature graph.** Check [FEATURES.md](FEATURES.md) before adding a dependency.
6262
13. **Keep the dependency graph live.** Any PR that adds a feature, removes a feature, or changes which other features it consumes via `public/` facades **must** update the mermaid graph in [FEATURES.md](FEATURES.md) in the same commit. The graph is documentation only if it matches the code — if you change one without the other, both become useless.
6363
14. **Folders justify their existence — files don't justify folders.** A tiny piece of code is one file with a role suffix, not a folder of one file:
@@ -148,7 +148,7 @@ final class UnexpectedLabelError extends LabelError {
148148
const UnexpectedLabelError(this.message);
149149
150150
@override
151-
String toTranslated(BuildContext context) => context.loc.oopsSomethingWentWrong;
151+
String toUserTranslated(BuildContext context) => context.loc.oopsSomethingWentWrong;
152152
}
153153
```
154154

@@ -182,6 +182,7 @@ Format: `type(scope): description`
182182

183183
Rules:
184184

185+
- **Never commit (or push) for the developer.** The agent does not run `git commit`/`git push`. When work is ready, leave a clean working tree, summarize what changed and why, and **ask the developer to review and commit themselves** — they must understand the diff before it lands. This holds even when the task says "and commit": stop at a ready-to-commit state and hand off.
185186
- **Atomic.** One logical change per commit. A feature + its refactor = two commits. A fix + cleanup = two commits. Independently revertable.
186187
- **No `Co-Authored-By` trailer.**
187188
- **No `--no-verify`.** Pre-commit must pass on its own.

ARCHITECTURE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,9 @@ Calls flow **down**; data flows **up**, transformed once per boundary: **wire mo
155155
### Error handling
156156

157157
- **One sealed error family per feature** by default (`<feature>_error.dart`). Most features are not deep enough to justify an error file per layer. Map foreign errors at every boundary; never leak another layer's or feature's error type.
158-
- **A user-facing message per error.** Each error carries a `toTranslated(BuildContext)` method (the established codebase convention — see `sell_error.dart`) that returns a clean, localized message for the end user via `AppLocalizations` (accessed through the `context.loc` extension) — never the developer-facing detail. The raw error (exception text, stack, foreign codes) stays in logs; the UI shows only what `toTranslated` returns. This keeps the sealed family the single source of truth for both *what went wrong* (for us) and *what the user sees* (for them), and the `sealed` switch guarantees every error variant has a user message. The **end user must never see a dev error string.** The **catch-all variant** (`unexpected`, `unknown`) returns a **generic localized** message (e.g. `context.loc.somethingWentWrong`), **never** the raw `message` — the `unexpected: (message) => message` shortcut in some current errors leaks dev detail to the user and is the anti-pattern to avoid; log the raw text, show the generic string.
158+
- **A user-facing message per error.** Each error carries a `toUserTranslated(BuildContext)` method (the established codebase convention — see `sell_error.dart`) that returns a clean, localized message for the end user via `AppLocalizations` (accessed through the `context.loc` extension) — never the developer-facing detail. The raw error (exception text, stack, foreign codes) stays in logs; the UI shows only what `toUserTranslated` returns. This keeps the sealed family the single source of truth for both *what went wrong* (for us) and *what the user sees* (for them), and the `sealed` switch guarantees every error variant has a user message. The **end user must never see a dev error string.** The **catch-all variant** (`unexpected`, `unknown`) returns a **generic localized** message (e.g. `context.loc.somethingWentWrong`), **never** the raw `message` — the `unexpected: (message) => message` shortcut in some current errors leaks dev detail to the user and is the anti-pattern to avoid; log the raw text, show the generic string.
159159
- **Prefer a `Result` at the repository boundary** for expected, recoverable failures; `throw` for programmer errors. Dart exceptions are unchecked — callers aren't forced to handle them ([dart.dev](https://dart.dev/language/error-handling)) — so a `Result` makes failure explicit in the signature. The Flutter team offers `Result` but explicitly as *"a recommendation, but not a requirement"* ([data-layer](https://docs.flutter.dev/app-architecture/case-study/data-layer)). Adopt where it pays; don't mass-migrate existing exception code.
160-
- **The flow, end to end.** Repository returns a `Result` carrying a data-layer error → the **use-case** maps that to the feature's sealed `<Feature>Error` (the boundary where foreign errors are translated) → the bloc holds that `<Feature>Error` in its state → the **UI** renders it via `error.toTranslated(context)`. Because `toTranslated` needs a `BuildContext`, it is called UI-side only — never in the bloc (a `BuildContext` in `presentation/` is itself a smell).
160+
- **The flow, end to end.** Repository returns a `Result` carrying a data-layer error → the **use-case** maps that to the feature's sealed `<Feature>Error` (the boundary where foreign errors are translated) → the bloc holds that `<Feature>Error` in its state → the **UI** renders it via `error.toUserTranslated(context)`. Because `toUserTranslated` needs a `BuildContext`, it is called UI-side only — never in the bloc (a `BuildContext` in `presentation/` is itself a smell).
161161

162162
### The facade: a feature's public contract
163163

@@ -201,7 +201,7 @@ The repository's own interface is the canonical port (the Ports-&-Adapters analo
201201
- `domain/entities/` — entities and value objects; **rich** models that enforce their own invariants in the constructor/factory (an invalid instance can't exist), never anemic DB mirrors. Never carry serialization — that's the model's job.
202202
- `domain/repositories/` — the **abstract** repository interfaces (the contracts use-cases depend on).
203203
- `domain/usecases/` — one per user intent; **thin orchestration** only.
204-
- `domain/<feature>_error.dart` — the feature's sealed error family; each variant exposes `toTranslated(context)` for its user-facing message.
204+
- `domain/<feature>_error.dart` — the feature's sealed error family; each variant exposes `toUserTranslated(context)` for its user-facing message.
205205
- `data/datasources/` — stateless wrappers around one external system each; private to their repository.
206206
- `data/<noun>_repository_impl.dart` — the concrete repository implementation.
207207
- `data/models/` — the wire/persistence model, one per entity, always separate from the domain entity (serialization only). `data/mappers/` — model ↔ entity; a file only once it earns one (rule #14), inline/extension when trivial.
@@ -255,7 +255,7 @@ A feature whose data is **shared** does not hold `domain/` + `data/` itself —
255255

256256
For a new user-facing flow in `lib/features/<feature>/`, working **outside-in is fine but design domain-first**:
257257

258-
1. **Domain.** Rich entity + value objects (`domain/entities/`), the `abstract interface class <Noun>Repository` (`domain/repositories/`), the sealed `<feature>_error.dart` (each variant with `toTranslated(context)`), and one `<Verb><Noun>Usecase` per intent (entry method `execute(...)`).
258+
1. **Domain.** Rich entity + value objects (`domain/entities/`), the `abstract interface class <Noun>Repository` (`domain/repositories/`), the sealed `<feature>_error.dart` (each variant with `toUserTranslated(context)`), and one `<Verb><Noun>Usecase` per intent (entry method `execute(...)`).
259259
2. **Data.** Datasource(s) (`data/datasources/`, one external system each), wire model + mapper (`data/models/`, `data/mappers/`), and `<noun>_repository_impl.dart`. The repository returns entities only — never a model (see "boundary rule").
260260
3. **Presentation.** `<Feature>Bloc`/`Cubit` + sealed state/event (`presentation/`). It calls use-cases only — never a repository or datasource.
261261
4. **UI.** Screens/widgets (`ui/`), reusing `lib/core/widgets/` first; all strings via `context.loc.<key>`, all colors from the theme (see AGENTS.md "UI Kit").

0 commit comments

Comments
 (0)