mindsystem-cc 3.3.3 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -1
- package/agents/ms-code-simplifier.md +1 -0
- package/agents/ms-flutter-reviewer.md +210 -0
- package/agents/ms-flutter-simplifier.md +42 -101
- package/bin/install.js +8 -0
- package/commands/ms/audit-milestone.md +209 -0
- package/commands/ms/do-work.md +7 -7
- package/commands/ms/execute-phase.md +5 -5
- package/commands/ms/research-project.md +10 -4
- package/mindsystem/templates/config.json +5 -1
- package/mindsystem/workflows/do-work.md +23 -23
- package/mindsystem/workflows/execute-phase.md +20 -20
- package/mindsystem/workflows/map-codebase.md +12 -6
- package/package.json +3 -2
- package/skills/flutter-senior-review/AGENTS.md +869 -0
- package/skills/flutter-senior-review/SKILL.md +205 -0
- package/skills/flutter-senior-review/principles/dependencies-data-not-callbacks.md +75 -0
- package/skills/flutter-senior-review/principles/dependencies-provider-tree.md +85 -0
- package/skills/flutter-senior-review/principles/dependencies-temporal-coupling.md +97 -0
- package/skills/flutter-senior-review/principles/pragmatism-consistent-error-handling.md +130 -0
- package/skills/flutter-senior-review/principles/pragmatism-speculative-generality.md +91 -0
- package/skills/flutter-senior-review/principles/state-data-clumps.md +64 -0
- package/skills/flutter-senior-review/principles/state-invalid-states.md +53 -0
- package/skills/flutter-senior-review/principles/state-single-source-of-truth.md +68 -0
- package/skills/flutter-senior-review/principles/state-type-hierarchies.md +75 -0
- package/skills/flutter-senior-review/principles/structure-composition-over-config.md +105 -0
- package/skills/flutter-senior-review/principles/structure-shared-visual-patterns.md +107 -0
- package/skills/flutter-senior-review/principles/structure-wrapper-pattern.md +90 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flutter-senior-review
|
|
3
|
+
description: Senior engineering principles for Flutter/Dart code reviews. Apply when reviewing, refactoring, or writing Flutter code to identify structural improvements that make code evolvable, not just working.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: forgeblast
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
date: January 2026
|
|
9
|
+
abstract: Code review framework focused on structural improvements that typical reviews miss. Uses 3 core lenses (State Modeling, Responsibility Boundaries, Abstraction Timing) backed by 12 detailed principles organized into 4 categories. Each principle includes detection signals, smell examples, senior solutions, and Dart-specific patterns.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Flutter Senior Review
|
|
13
|
+
|
|
14
|
+
Senior engineering principles for Flutter/Dart code. Apply when reviewing, refactoring, or writing code to identify structural improvements that make code evolvable, not just working.
|
|
15
|
+
|
|
16
|
+
## When to Apply
|
|
17
|
+
|
|
18
|
+
Reference these guidelines when:
|
|
19
|
+
- Reviewing code changes (commits, PRs, patches)
|
|
20
|
+
- Refactoring existing Flutter/Dart code
|
|
21
|
+
- Writing new features or components
|
|
22
|
+
- Identifying why code feels hard to change
|
|
23
|
+
- Planning structural improvements
|
|
24
|
+
|
|
25
|
+
## Senior Mindset
|
|
26
|
+
|
|
27
|
+
Junior and mid-level engineers ask: **"Does this code work?"**
|
|
28
|
+
Senior engineers ask: **"How will this code change? What happens when requirements shift?"**
|
|
29
|
+
|
|
30
|
+
This distinction drives everything. Code that "works" today becomes a liability when:
|
|
31
|
+
- A new state is added and 5 files need coordinated updates
|
|
32
|
+
- A feature toggle requires touching code scattered across the codebase
|
|
33
|
+
- A bug fix in one place breaks assumptions elsewhere
|
|
34
|
+
|
|
35
|
+
Focus on **structural issues that compound over time** - the kind that turn "add a simple feature" into "refactor half the codebase first."
|
|
36
|
+
|
|
37
|
+
Do NOT look for:
|
|
38
|
+
- Style preferences or formatting
|
|
39
|
+
- Minor naming improvements
|
|
40
|
+
- "Nice to have" abstractions
|
|
41
|
+
- Issues already covered by linters/analyzers
|
|
42
|
+
|
|
43
|
+
## Core Lenses
|
|
44
|
+
|
|
45
|
+
Apply these three lenses to every review. They catch 80% of structural issues.
|
|
46
|
+
|
|
47
|
+
### Lens 1: State Modeling
|
|
48
|
+
|
|
49
|
+
**Question:** Can this code represent invalid states?
|
|
50
|
+
|
|
51
|
+
Look for:
|
|
52
|
+
- Multiple boolean flags (2^n possible states, many invalid)
|
|
53
|
+
- Primitive obsession (stringly-typed status, magic numbers)
|
|
54
|
+
- Same decision logic repeated in multiple places
|
|
55
|
+
|
|
56
|
+
**Senior pattern:** Sealed classes where each variant is a valid state. Factory methods that encapsulate decision logic. Compiler-enforced exhaustive handling.
|
|
57
|
+
|
|
58
|
+
Related principles: `state-invalid-states.md`, `state-type-hierarchies.md`, `state-single-source-of-truth.md`, `state-data-clumps.md`
|
|
59
|
+
|
|
60
|
+
### Lens 2: Responsibility Boundaries
|
|
61
|
+
|
|
62
|
+
**Question:** If I remove/modify feature X, how many files change?
|
|
63
|
+
|
|
64
|
+
Look for:
|
|
65
|
+
- Optional feature logic scattered throughout a parent component
|
|
66
|
+
- Widgets with 6+ parameters (doing too much)
|
|
67
|
+
- Deep callback chains passing flags through layers
|
|
68
|
+
|
|
69
|
+
**Senior pattern:** Wrapper components for optional features. Typed data objects instead of flag parades. Each widget has one job.
|
|
70
|
+
|
|
71
|
+
Related principles: `structure-wrapper-pattern.md`, `structure-shared-visual-patterns.md`, `structure-composition-over-config.md`
|
|
72
|
+
|
|
73
|
+
### Lens 3: Abstraction Timing
|
|
74
|
+
|
|
75
|
+
**Question:** Is this abstraction earned or speculative?
|
|
76
|
+
|
|
77
|
+
Look for:
|
|
78
|
+
- Interfaces with only one implementation
|
|
79
|
+
- Factories that create only one type
|
|
80
|
+
- "Flexible" config that's never varied
|
|
81
|
+
- BUT ALSO: Duplicated code that should be unified
|
|
82
|
+
|
|
83
|
+
**Senior pattern:** Abstract when you have 2-3 concrete cases, not before. Extract when duplication causes bugs or drift, not for aesthetics.
|
|
84
|
+
|
|
85
|
+
Related principles: `pragmatism-speculative-generality.md`, `dependencies-data-not-callbacks.md`
|
|
86
|
+
|
|
87
|
+
## Principle Categories
|
|
88
|
+
|
|
89
|
+
| Category | Principles | Focus |
|
|
90
|
+
|----------|------------|-------|
|
|
91
|
+
| State & Types | 1, 3, 6, 10 | Invalid states, type hierarchies, single source of truth, data clumps |
|
|
92
|
+
| Structure | 2, 4, 8 | Feature isolation, visual patterns, composition |
|
|
93
|
+
| Dependencies | 5, 7, 9 | Coupling, provider architecture, temporal coupling |
|
|
94
|
+
| Pragmatism | 11, 12 | Avoiding over-engineering, consistent error handling |
|
|
95
|
+
|
|
96
|
+
## Quick Reference
|
|
97
|
+
|
|
98
|
+
### State & Type Safety
|
|
99
|
+
- **invalid-states** - Replace boolean flag combinations with sealed class hierarchies
|
|
100
|
+
- **type-hierarchies** - Use factories to encapsulate decision logic
|
|
101
|
+
- **single-source-of-truth** - One owner per state, derive the rest via selectors
|
|
102
|
+
- **data-clumps** - Group parameters that travel together into typed objects
|
|
103
|
+
|
|
104
|
+
### Structure & Composition
|
|
105
|
+
- **wrapper-pattern** - Isolate optional feature logic into wrapper components
|
|
106
|
+
- **shared-visual-patterns** - Deduplicate UI with style variants
|
|
107
|
+
- **composition-over-config** - Small focused widgets over god widgets with many flags
|
|
108
|
+
|
|
109
|
+
### Dependencies & Flow
|
|
110
|
+
- **data-not-callbacks** - Pass typed objects, not callback parades
|
|
111
|
+
- **provider-tree** - Root -> branch -> leaf hierarchy for providers
|
|
112
|
+
- **temporal-coupling** - Enforce operation sequences via types, not documentation
|
|
113
|
+
|
|
114
|
+
### Pragmatism
|
|
115
|
+
- **speculative-generality** - Don't abstract until 2-3 concrete cases exist
|
|
116
|
+
- **consistent-error-handling** - One strategy applied everywhere, not ad-hoc try/catch
|
|
117
|
+
|
|
118
|
+
## How to Use
|
|
119
|
+
|
|
120
|
+
Read individual principle files for detailed explanations and code examples:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
principles/state-invalid-states.md
|
|
124
|
+
principles/structure-wrapper-pattern.md
|
|
125
|
+
principles/dependencies-provider-tree.md
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Each principle file contains:
|
|
129
|
+
- Brief explanation of why it matters
|
|
130
|
+
- Detection signals (what to look for)
|
|
131
|
+
- Incorrect code example with explanation
|
|
132
|
+
- Correct code example with explanation
|
|
133
|
+
- Why it matters section
|
|
134
|
+
- Detection questions for self-review
|
|
135
|
+
|
|
136
|
+
## Context Gathering
|
|
137
|
+
|
|
138
|
+
When asked to review code, first identify the target:
|
|
139
|
+
|
|
140
|
+
If target is unclear, ask:
|
|
141
|
+
- What code should be reviewed? (specific files, a feature folder, uncommitted changes, a commit, a patch file)
|
|
142
|
+
- Is there a specific concern or area of focus?
|
|
143
|
+
|
|
144
|
+
For the specified target, gather the relevant code:
|
|
145
|
+
- **Commit**: `git show <commit>`
|
|
146
|
+
- **Patch file**: Read the patch file
|
|
147
|
+
- **Uncommitted changes**: `git diff` and `git diff --cached`
|
|
148
|
+
- **Folder/feature**: Read the relevant files in that directory
|
|
149
|
+
- **Specific file**: Read that file and related files it imports/uses
|
|
150
|
+
|
|
151
|
+
## Analysis Process
|
|
152
|
+
|
|
153
|
+
1. **Read thoroughly** - Understand what the code does, not just its structure
|
|
154
|
+
|
|
155
|
+
2. **Apply the three lenses** - For each lens, note specific instances (or note "no issues found")
|
|
156
|
+
|
|
157
|
+
3. **Check for additional patterns** - If you notice issues beyond the core lenses, consult the principle files for precise diagnosis
|
|
158
|
+
|
|
159
|
+
4. **Prioritize by evolution impact**:
|
|
160
|
+
- High: Will cause cascading changes when requirements shift
|
|
161
|
+
- Medium: Creates friction but contained to one area
|
|
162
|
+
- Low: Suboptimal but won't compound
|
|
163
|
+
|
|
164
|
+
5. **Formulate concrete suggestions** - Name specific extractions, show before/after for the highest-impact change
|
|
165
|
+
|
|
166
|
+
## Output Format
|
|
167
|
+
|
|
168
|
+
```markdown
|
|
169
|
+
## Senior Review: [Target]
|
|
170
|
+
|
|
171
|
+
### Summary
|
|
172
|
+
[1-2 sentences: Overall assessment and the single most important structural opportunity]
|
|
173
|
+
|
|
174
|
+
### Findings
|
|
175
|
+
|
|
176
|
+
#### High Impact: [Issue Name]
|
|
177
|
+
**What I noticed:** [Specific code pattern observed]
|
|
178
|
+
**Why it matters:** [How this will cause problems as code evolves]
|
|
179
|
+
**Suggestion:** [Concrete refactoring - name the types/widgets to extract]
|
|
180
|
+
|
|
181
|
+
#### Medium Impact: [Issue Name]
|
|
182
|
+
[Same structure]
|
|
183
|
+
|
|
184
|
+
#### Low Impact: [Issue Name]
|
|
185
|
+
[Same structure]
|
|
186
|
+
|
|
187
|
+
### No Issues Found
|
|
188
|
+
[If a lens revealed no problems, briefly note: "State modeling: No boolean flag combinations or repeated decision logic detected."]
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
**What's your take on these suggestions? Any context I'm missing?**
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Success Criteria
|
|
196
|
+
|
|
197
|
+
- At least one finding per applicable lens (or explicit "no issues" statement)
|
|
198
|
+
- Each finding tied to evolution impact, not just "could be better"
|
|
199
|
+
- Suggestions are concrete: specific types/widgets named, not vague advice
|
|
200
|
+
- No forced findings - if code is solid, say so
|
|
201
|
+
- User has opportunity to provide context before changes
|
|
202
|
+
|
|
203
|
+
## Full Compiled Document
|
|
204
|
+
|
|
205
|
+
For the complete guide with all principles expanded: `AGENTS.md`
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Reduce Coupling Through Data
|
|
3
|
+
category: dependencies
|
|
4
|
+
impact: MEDIUM-HIGH
|
|
5
|
+
impactDescription: Stabilizes widget APIs
|
|
6
|
+
tags: coupling, parameters, typed-objects, widget-api
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Reduce Coupling Through Data, Not Callbacks
|
|
10
|
+
|
|
11
|
+
Pass typed objects, not callback parades. Child widgets receive a single typed object that encapsulates all the data they need.
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- Widgets have 4+ parameters beyond key and callbacks
|
|
15
|
+
- Boolean flags being passed through multiple widget layers
|
|
16
|
+
- Changing a child's behavior requires changing the parent's call site
|
|
17
|
+
- Parameters that are only used in some conditions
|
|
18
|
+
|
|
19
|
+
**Incorrect (flag parade):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
QuestClaimButton(
|
|
23
|
+
quest: quest,
|
|
24
|
+
onSuccess: onClaimSuccess,
|
|
25
|
+
isExpired: isExpired,
|
|
26
|
+
isTutorial: isTutorial,
|
|
27
|
+
bonus: bonus,
|
|
28
|
+
showMultiplier: showMultiplier,
|
|
29
|
+
isPremiumUser: isPremiumUser,
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Correct (typed data object):**
|
|
34
|
+
|
|
35
|
+
```dart
|
|
36
|
+
// Data object encapsulates all context
|
|
37
|
+
sealed class QuestClaimMode {
|
|
38
|
+
final QuestEntity quest;
|
|
39
|
+
final RewardBonus? bonus;
|
|
40
|
+
|
|
41
|
+
const QuestClaimMode({required this.quest, this.bonus});
|
|
42
|
+
|
|
43
|
+
factory QuestClaimMode.fromContext({
|
|
44
|
+
required QuestEntity quest,
|
|
45
|
+
required UserEntity? user,
|
|
46
|
+
required bool isTutorialQuest,
|
|
47
|
+
}) {
|
|
48
|
+
// Decision logic centralized here
|
|
49
|
+
if (quest.isExpired) return QuestClaimModeExpired(quest: quest);
|
|
50
|
+
if (isTutorialQuest) return QuestClaimModeTutorial(quest: quest);
|
|
51
|
+
return QuestClaimModeNormal(
|
|
52
|
+
quest: quest,
|
|
53
|
+
bonus: user?.applyRewardBonus(quest.reward),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Clean widget API
|
|
59
|
+
QuestClaimButton(
|
|
60
|
+
mode: QuestClaimMode.fromContext(quest: quest, user: user, isTutorial: isTutorial),
|
|
61
|
+
onSuccess: onClaimSuccess,
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Why it matters:**
|
|
66
|
+
- Child widget's API is stable even as requirements change
|
|
67
|
+
- Parent doesn't need to know child's internal decision logic
|
|
68
|
+
- Data dependencies are explicit in the mode type
|
|
69
|
+
- Easier to test: create mode objects directly
|
|
70
|
+
|
|
71
|
+
**Detection questions:**
|
|
72
|
+
- Do widgets have 4+ parameters beyond key and callbacks?
|
|
73
|
+
- Are boolean flags being passed through multiple widget layers?
|
|
74
|
+
- Does changing a child's behavior require changing the parent's call site?
|
|
75
|
+
- Are there parameters that are only used in some conditions?
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Provider Tree Architecture
|
|
3
|
+
category: dependencies
|
|
4
|
+
impact: MEDIUM
|
|
5
|
+
impactDescription: Clarifies data flow
|
|
6
|
+
tags: riverpod, provider, architecture, dependencies
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Provider Tree Architecture
|
|
10
|
+
|
|
11
|
+
Root -> branch -> leaf hierarchy for providers. Clear mental model of which providers depend on which.
|
|
12
|
+
|
|
13
|
+
**Mental model:**
|
|
14
|
+
- **Root providers**: Match app lifecycle, never disposed. Hold core entities (user, games, quests). Referenced from many places. Use `keepAlive: true`.
|
|
15
|
+
- **Branch providers**: Combine/filter/transform core data. May become "core" as app grows (e.g., `squadQuestsProvider`).
|
|
16
|
+
- **Leaf providers**: Screen-specific or ephemeral. Depend on branches, rarely watched by other providers.
|
|
17
|
+
|
|
18
|
+
**Detection signals:**
|
|
19
|
+
- Can't draw the provider dependency graph as a tree
|
|
20
|
+
- Circular or confusing dependency chains
|
|
21
|
+
- "Leaf" providers being watched by other providers
|
|
22
|
+
- Provider doing too much (should split into root + branch)
|
|
23
|
+
|
|
24
|
+
**Incorrect (flat, tangled):**
|
|
25
|
+
|
|
26
|
+
```dart
|
|
27
|
+
// Hard to trace what depends on what
|
|
28
|
+
final userProvider = ...;
|
|
29
|
+
final questsProvider = ...; // watches userProvider
|
|
30
|
+
final gamesProvider = ...; // watches userProvider
|
|
31
|
+
final filteredQuestsProvider = ...; // watches questsProvider, gamesProvider, some other provider
|
|
32
|
+
final screenStateProvider = ...; // watches 5 different providers
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Correct (tree structure):**
|
|
36
|
+
|
|
37
|
+
```dart
|
|
38
|
+
// Root (core entities, keepAlive: true)
|
|
39
|
+
@Riverpod(keepAlive: true)
|
|
40
|
+
Future<User> user(Ref ref) => ref.watch(authProvider.future).then((auth) => auth.user);
|
|
41
|
+
|
|
42
|
+
@Riverpod(keepAlive: true)
|
|
43
|
+
Future<List<Quest>> quests(Ref ref) async {
|
|
44
|
+
final user = await ref.watch(userProvider.future);
|
|
45
|
+
if (user == null) return [];
|
|
46
|
+
return ref.watch(questsApiProvider).fetchQuests(user.id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Branch (derived/filtered, may become core as app grows)
|
|
50
|
+
@riverpod
|
|
51
|
+
Future<List<Quest>> squadQuests(Ref ref) async {
|
|
52
|
+
final quests = await ref.watch(questsProvider.future);
|
|
53
|
+
return quests.where((q) => q.type == QuestType.squad).toList();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@riverpod
|
|
57
|
+
Future<List<Quest>> soloQuests(Ref ref) async {
|
|
58
|
+
final quests = await ref.watch(questsProvider.future);
|
|
59
|
+
return quests.where((q) => q.type == QuestType.solo).toList();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Leaf (screen-specific, ephemeral)
|
|
63
|
+
@riverpod
|
|
64
|
+
class SquadQuestScreenState extends _$SquadQuestScreenState {
|
|
65
|
+
@override
|
|
66
|
+
FutureOr<ScreenState> build() async {
|
|
67
|
+
// Only watches branch providers
|
|
68
|
+
final quests = await ref.watch(squadQuestsProvider.future);
|
|
69
|
+
return ScreenState(quests: quests);
|
|
70
|
+
}
|
|
71
|
+
// Never watched by other providers
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Why it matters:**
|
|
76
|
+
- Clear mental model of data flow
|
|
77
|
+
- Predictable rebuild scope
|
|
78
|
+
- Easier to add new derived providers
|
|
79
|
+
- Natural place for each piece of logic
|
|
80
|
+
|
|
81
|
+
**Detection questions:**
|
|
82
|
+
- Can you draw the provider dependency graph as a tree?
|
|
83
|
+
- Are there circular or confusing dependency chains?
|
|
84
|
+
- Are "leaf" providers being watched by other providers?
|
|
85
|
+
- Is a provider doing too much (should it split into root + branch)?
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Temporal Coupling
|
|
3
|
+
category: dependencies
|
|
4
|
+
impact: MEDIUM
|
|
5
|
+
impactDescription: Catches misuse at compile time
|
|
6
|
+
tags: builder-pattern, type-state, initialization, sequence
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Temporal Coupling (Enforce Sequences)
|
|
10
|
+
|
|
11
|
+
Enforce operation sequences via types, not documentation. Make it impossible to call methods in the wrong order.
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- Methods that must be called before others
|
|
15
|
+
- Comments like "must call X first" or "call after Y"
|
|
16
|
+
- Objects can be in an "invalid" state between operations
|
|
17
|
+
- Tests have setup steps that could be forgotten
|
|
18
|
+
|
|
19
|
+
**Incorrect (implicit sequence):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
class PaymentProcessor {
|
|
23
|
+
void init() { ... }
|
|
24
|
+
void setAmount(int amount) { ... }
|
|
25
|
+
void setCustomer(Customer c) { ... }
|
|
26
|
+
Future<void> process() { ... } // Must call init, setAmount, setCustomer first!
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Easy to misuse:
|
|
30
|
+
final processor = PaymentProcessor();
|
|
31
|
+
processor.process(); // Boom - forgot to init
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct (builder pattern):**
|
|
35
|
+
|
|
36
|
+
```dart
|
|
37
|
+
class PaymentBuilder {
|
|
38
|
+
int? _amount;
|
|
39
|
+
Customer? _customer;
|
|
40
|
+
|
|
41
|
+
PaymentBuilder withAmount(int amount) {
|
|
42
|
+
_amount = amount;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
PaymentBuilder withCustomer(Customer c) {
|
|
47
|
+
_customer = c;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Payment build() {
|
|
52
|
+
assert(_amount != null && _customer != null);
|
|
53
|
+
return Payment(amount: _amount!, customer: _customer!);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Usage is clear
|
|
58
|
+
final payment = PaymentBuilder()
|
|
59
|
+
.withAmount(100)
|
|
60
|
+
.withCustomer(customer)
|
|
61
|
+
.build();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Correct (type-state pattern):**
|
|
65
|
+
|
|
66
|
+
```dart
|
|
67
|
+
// Types enforce valid sequences
|
|
68
|
+
class UninitializedProcessor {
|
|
69
|
+
InitializedProcessor init(Config config) => InitializedProcessor(config);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class InitializedProcessor {
|
|
73
|
+
final Config _config;
|
|
74
|
+
InitializedProcessor(this._config);
|
|
75
|
+
|
|
76
|
+
Future<Result> process(int amount, Customer c) async {
|
|
77
|
+
// Can only be called on initialized processor
|
|
78
|
+
return _processPayment(amount, c);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Misuse is a compile error
|
|
83
|
+
final processor = UninitializedProcessor();
|
|
84
|
+
processor.process(100, customer); // Error: process is not defined on UninitializedProcessor
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Why it matters:**
|
|
88
|
+
- Compiler catches misuse, not runtime
|
|
89
|
+
- Self-documenting: types show valid sequences
|
|
90
|
+
- Impossible to forget required steps
|
|
91
|
+
- Easier onboarding for new developers
|
|
92
|
+
|
|
93
|
+
**Detection questions:**
|
|
94
|
+
- Are there methods that must be called before others?
|
|
95
|
+
- Are there comments like "must call X first" or "call after Y"?
|
|
96
|
+
- Can objects be in an "invalid" state between operations?
|
|
97
|
+
- Do tests have setup steps that could be forgotten?
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Consistent Error Handling
|
|
3
|
+
category: pragmatism
|
|
4
|
+
impact: MEDIUM
|
|
5
|
+
impactDescription: Improves UX and debugging
|
|
6
|
+
tags: error-handling, async-value, riverpod, toast
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Consistent Error Handling Strategy
|
|
10
|
+
|
|
11
|
+
One strategy applied everywhere, not ad-hoc try/catch. Use AsyncValue for state management and centralized error presentation.
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- Errors appear differently across screens (toasts vs dialogs vs inline)
|
|
15
|
+
- try/catch blocks scattered in widget code
|
|
16
|
+
- Inconsistent error messaging
|
|
17
|
+
- No standard retry mechanism
|
|
18
|
+
|
|
19
|
+
**Incorrect (ad-hoc handling):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
// Screen 1: Try/catch with toast
|
|
23
|
+
try {
|
|
24
|
+
await ref.read(saveProvider.notifier).save();
|
|
25
|
+
} catch (e) {
|
|
26
|
+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e')));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Screen 2: Different pattern
|
|
30
|
+
final result = await ref.read(saveProvider.notifier).save();
|
|
31
|
+
if (result.isError) {
|
|
32
|
+
showDialog(...); // Different error UI
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Screen 3: No error handling at all
|
|
36
|
+
await ref.read(saveProvider.notifier).save(); // Hope it works!
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Correct (consistent pattern):**
|
|
40
|
+
|
|
41
|
+
```dart
|
|
42
|
+
// Providers handle errors uniformly with AsyncValue.guard
|
|
43
|
+
@riverpod
|
|
44
|
+
class SaveController extends _$SaveController {
|
|
45
|
+
@override
|
|
46
|
+
FutureOr<void> build() => null;
|
|
47
|
+
|
|
48
|
+
Future<void> save(Data data) async {
|
|
49
|
+
state = const AsyncLoading();
|
|
50
|
+
state = await AsyncValue.guard(() => _repository.save(data));
|
|
51
|
+
// Error stays in state, not thrown
|
|
52
|
+
// Success navigates or shows confirmation
|
|
53
|
+
if (state.hasValue) {
|
|
54
|
+
ref.invalidate(dataProvider);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extension for centralized error listening
|
|
60
|
+
extension WidgetRefEx on WidgetRef {
|
|
61
|
+
void listenOnError<T>(
|
|
62
|
+
ProviderListenable<AsyncValue<T>> provider, {
|
|
63
|
+
bool Function(Object error)? ignoreIf,
|
|
64
|
+
}) {
|
|
65
|
+
listen(provider, (_, next) {
|
|
66
|
+
next.whenOrNull(
|
|
67
|
+
error: (error, _) {
|
|
68
|
+
if (ignoreIf?.call(error) == true) return;
|
|
69
|
+
AppToast.showError(context, error.toString());
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Screens use consistent pattern
|
|
77
|
+
Widget build(context, ref) {
|
|
78
|
+
// Centralized error listening - shows toast on any error
|
|
79
|
+
ref.listenOnError(saveProvider);
|
|
80
|
+
|
|
81
|
+
final saveState = ref.watch(saveProvider);
|
|
82
|
+
|
|
83
|
+
return AppPrimaryButton(
|
|
84
|
+
isLoading: saveState.isLoading,
|
|
85
|
+
onPressed: () => ref.read(saveProvider.notifier).save(data),
|
|
86
|
+
child: Text('Save'),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**For first-load errors (need retry UI):**
|
|
92
|
+
|
|
93
|
+
```dart
|
|
94
|
+
Widget build(context, ref) {
|
|
95
|
+
final dataState = ref.watch(dataProvider);
|
|
96
|
+
|
|
97
|
+
return dataState.when(
|
|
98
|
+
data: (data) => DataContent(data: data),
|
|
99
|
+
loading: () => const AppLoadingIndicator(),
|
|
100
|
+
error: (error, _) => Center(
|
|
101
|
+
child: Column(
|
|
102
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
103
|
+
children: [
|
|
104
|
+
Icon(Icons.error_outline, size: 64, color: context.colorScheme.error),
|
|
105
|
+
const SizedBox(height: 16),
|
|
106
|
+
Text(LocaleKeys.common_error.tr()),
|
|
107
|
+
const SizedBox(height: 16),
|
|
108
|
+
FilledButton.icon(
|
|
109
|
+
onPressed: () => ref.invalidate(dataProvider),
|
|
110
|
+
icon: const Icon(Icons.refresh),
|
|
111
|
+
label: Text(LocaleKeys.common_retry.tr()),
|
|
112
|
+
),
|
|
113
|
+
],
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Why it matters:**
|
|
121
|
+
- Users get consistent experience
|
|
122
|
+
- Developers follow one pattern
|
|
123
|
+
- Error states are explicit and testable
|
|
124
|
+
- Retry logic is standardized
|
|
125
|
+
|
|
126
|
+
**Detection questions:**
|
|
127
|
+
- Do errors appear the same way across all screens?
|
|
128
|
+
- Are there try/catch blocks in widget code?
|
|
129
|
+
- Is error messaging consistent (toasts vs dialogs vs inline)?
|
|
130
|
+
- Is there a standard retry mechanism?
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Speculative Generality
|
|
3
|
+
category: pragmatism
|
|
4
|
+
impact: MEDIUM
|
|
5
|
+
impactDescription: Reduces unnecessary complexity
|
|
6
|
+
tags: abstraction, yagni, over-engineering, interfaces
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Speculative Generality (Know When NOT to Abstract)
|
|
10
|
+
|
|
11
|
+
Don't abstract until 2-3 concrete cases exist. Build for today's requirements; extract abstractions when you have real examples.
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- Interface with only one implementation
|
|
15
|
+
- Factory that only creates one type
|
|
16
|
+
- Configuration options no one uses
|
|
17
|
+
- Abstraction added "in case we need it later"
|
|
18
|
+
|
|
19
|
+
**Incorrect (premature abstraction):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
// "We might need different storage backends someday"
|
|
23
|
+
abstract class StorageStrategy {
|
|
24
|
+
Future<void> save(String key, String value);
|
|
25
|
+
Future<String?> load(String key);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class LocalStorageStrategy implements StorageStrategy {
|
|
29
|
+
@override
|
|
30
|
+
Future<void> save(String key, String value) => _prefs.setString(key, value);
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
Future<String?> load(String key) => _prefs.getString(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class StorageFactory {
|
|
37
|
+
static StorageStrategy create(StorageType type) => switch (type) {
|
|
38
|
+
StorageType.local => LocalStorageStrategy(), // Only one ever used
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// "We might need to support multiple payment providers"
|
|
43
|
+
abstract class PaymentProvider { ... }
|
|
44
|
+
class StripeProvider implements PaymentProvider { ... } // Only one exists
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Correct (direct usage):**
|
|
48
|
+
|
|
49
|
+
```dart
|
|
50
|
+
// Just use the thing directly
|
|
51
|
+
class LocalStorage {
|
|
52
|
+
final SharedPreferences _prefs;
|
|
53
|
+
|
|
54
|
+
LocalStorage(this._prefs);
|
|
55
|
+
|
|
56
|
+
Future<void> save(String key, String value) => _prefs.setString(key, value);
|
|
57
|
+
Future<String?> load(String key) async => _prefs.getString(key);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// When you ACTUALLY need a second implementation, THEN abstract
|
|
61
|
+
// The refactoring is straightforward and you'll know the right abstraction
|
|
62
|
+
// because you have concrete examples of the variation
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**When TO abstract:**
|
|
66
|
+
|
|
67
|
+
```dart
|
|
68
|
+
// You have 2+ real implementations with actual differences
|
|
69
|
+
abstract class AuthProvider {
|
|
70
|
+
Future<User> signIn();
|
|
71
|
+
Future<void> signOut();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class GoogleAuthProvider implements AuthProvider { ... }
|
|
75
|
+
class AppleAuthProvider implements AuthProvider { ... }
|
|
76
|
+
class EmailAuthProvider implements AuthProvider { ... }
|
|
77
|
+
|
|
78
|
+
// The abstraction is earned - you know exactly what varies
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Why it matters:**
|
|
82
|
+
- Less code to maintain
|
|
83
|
+
- Abstractions based on real needs fit better
|
|
84
|
+
- Easier to understand: no indirection to trace
|
|
85
|
+
- YAGNI: You Aren't Gonna Need It
|
|
86
|
+
|
|
87
|
+
**Detection questions:**
|
|
88
|
+
- Is there an interface with only one implementation?
|
|
89
|
+
- Is there a factory that only creates one type?
|
|
90
|
+
- Are there configuration options no one uses?
|
|
91
|
+
- Was this abstraction added "in case we need it later"?
|