You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: use parent BullError that all errors must extend. rename to toUserTranslated. Updated Agents.md and added errorhandling_agent.md. Removed old unused bullError
**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.
12
12
@@ -57,7 +57,7 @@ Hard rules:
57
57
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.
58
58
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.)
59
59
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.
61
61
12.**Acyclic feature graph.** Check [FEATURES.md](FEATURES.md) before adding a dependency.
62
62
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.
63
63
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 {
-**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.
185
186
-**Atomic.** One logical change per commit. A feature + its refactor = two commits. A fix + cleanup = two commits. Independently revertable.
186
187
-**No `Co-Authored-By` trailer.**
187
188
-**No `--no-verify`.** Pre-commit must pass on its own.
Copy file name to clipboardExpand all lines: ARCHITECTURE.md
+4-4Lines changed: 4 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -155,9 +155,9 @@ Calls flow **down**; data flows **up**, transformed once per boundary: **wire mo
155
155
### Error handling
156
156
157
157
-**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.
159
159
-**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).
161
161
162
162
### The facade: a feature's public contract
163
163
@@ -201,7 +201,7 @@ The repository's own interface is the canonical port (the Ports-&-Adapters analo
201
201
-`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.
202
202
-`domain/repositories/` — the **abstract** repository interfaces (the contracts use-cases depend on).
203
203
-`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.
205
205
-`data/datasources/` — stateless wrappers around one external system each; private to their repository.
206
206
-`data/<noun>_repository_impl.dart` — the concrete repository implementation.
207
207
-`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 —
255
255
256
256
For a new user-facing flow in `lib/features/<feature>/`, working **outside-in is fine but design domain-first**:
257
257
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(...)`).
259
259
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").
260
260
3.**Presentation.**`<Feature>Bloc`/`Cubit` + sealed state/event (`presentation/`). It calls use-cases only — never a repository or datasource.
261
261
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