Audit Presentation Layer
Statically scans Flutter presentation-layer source and test files against
bundled rule docs. Emits a violations table, then offers targeted fixes.
Rule source
Rules are bundled locally in skills/audit-presentation-layer/rules/.
This skill does not delegate to ai_toolkit/ — it is self-contained.
Phase 0 — Resolve input and platform
Step 1 — Resolve path
Read the user's request and extract one of:
- Single file: a path ending in
.dart
- Feature folder: a path containing a
presentation/ directory
If neither is clear, ask exactly one question:
"Provide a widget file path or a feature folder path containing a presentation/ directory."
Do not proceed until a path is confirmed.
Step 2 — Resolve target platforms
- Check the user's request for a
--platform=<value> argument.
- Accepted values:
web, android, ios, mobile, all.
mobile expands to {android, ios}. all expands to {web, android, ios}.
- If no
--platform argument, read pubspec.yaml from the project root.
Look for the flutter: { platforms: { ... } } map (present after
flutter create --platforms). Extract the declared platform keys
(web, android, ios, linux, macos, windows). Use only
web, android, ios from this set.
- If
pubspec.yaml has no platforms map (or cannot be read), fall back to all.
Precedence: --platform arg > pubspec flutter.platforms > all (fallback).
State the resolved target at the start of Phase 4 output, e.g.:
Target platforms: android, ios (from pubspec)
Target platforms: web (from --platform=web)
Target platforms: all (fallback — no platforms map in pubspec)
Step 3 — Rule gating
Before applying any rule in Phase 3, check the rule's Platforms: tag in
rules/CATALOG.md. Skip the rule if its platform set does not intersect the
resolved target set. Rules tagged all always run.
Phase 1 — Load rule catalog
Read skills/audit-presentation-layer/rules/CATALOG.md in full before scanning.
Do not read individual rule doc files yet — the catalog contains all heuristics
needed for Phase 3. Open a specific rule doc only if you need to clarify a
borderline case or produce a more detailed fix explanation.
Phase 2 — Discover files
Single-file mode
Target file = the provided .dart path.
Check whether a mirrored *_test.dart exists under test/src/:
lib/src/features/<feature>/presentation/<name>.dart
→ test/src/features/<feature>/presentation/<name>_test.dart
Include the test file in the scan if it exists.
If the provided file is NOT under presentation/, classify it as domain-file
(skip the test mirror check) and apply only UI-STR-01 to it.
Folder mode
Spawn an Explore subagent:
Agent(
subagent_type="Explore",
prompt="List all .dart files (excluding .g.dart, .freezed.dart) recursively
under <input_path>. For each file report:
- Relative path
- Whether it is a *_test.dart file
- Line count (approximate)
- Widget class name and superclass if visible in first 20 lines
Report as a plain table."
)
Also check for the mirrored test/src/ path of the given lib/ folder and
include all *_test.dart files found there.
Also collect non-presentation/ dart files under the same feature root
(e.g. application/, domain/, data/ siblings of presentation/).
Classify each file:
widget — non-test dart file under presentation/
widget-test — *_test.dart file mirroring a presentation widget
domain-file — dart file outside presentation/ in the same feature tree
Phase 3 — Scan
For each file:
- Read the full file contents.
- Apply every heuristic in
rules/CATALOG.md relevant to the file type and
not gated out by the platform target (see Phase 0 Step 3):
widget files → apply: RIV-WIDGET-, REBUILD-, EXTRACT-, COHESION-01, COUPLING-, LAYOUT-, SIDE-FX-01, ROBOT-04, ROUTER-, RESPONSIVE-*, WEB-01
widget-test files → apply: ROBOT-01, ROBOT-02, ROBOT-03, ROBOT-05
domain-file files → apply: UI-STR-01 only
- For each match: record
{file, line_number, rule_id, severity, message, fix_hint, autofix_safe}.
Heuristic application notes:
- RIV-WIDGET-01: flag
ref.watch( lines where the enclosing method is NOT named build. Look at method declarations above the line to determine context.
- RIV-WIDGET-02: flag
ref.watch(someProvider) result where the return value is immediately accessed with .fieldName (within 3 lines) and no .select( appears on the watch call.
- RIV-WIDGET-03: flag
Consumer( blocks where the builder: body exceeds 50 lines.
- RIV-WIDGET-04: flag
ref.read( lines inside a build(BuildContext method span.
- REBUILD-01: inside
build spans, flag all-literal constructor calls (SizedBox(, EdgeInsets.*(, Icon(Icons., literal-only Text(, Divider() not preceded by const and not covered by an enclosing const (check ~3 lines above).
- REBUILD-02: flag
MediaQuery.of(context).<prop> single-property accesses (size, padding, viewInsets, platformBrightness, textScaler); fix is the scoped MediaQuery.<prop>Of(context) accessor.
- REBUILD-03: flag
AnimatedBuilder(/ListenableBuilder(/ValueListenableBuilder( spans with no child: argument and a builder: body > ~10 lines.
- REBUILD-04: in files with
setState(, measure the enclosing class's build( span; flag each setState( call when that span > 50 lines.
- EXTRACT-01: flag
build( method declarations whose span (signature to matching brace) exceeds 80 lines.
- EXTRACT-02: flag top-level
Widget name( declarations (column 0) and static Widget name( anywhere in widget files.
- COHESION-01: for widget classes with one non-Key constructor field of a non-primitive project type, count distinct
<field>.<member> accesses in the class body; flag the field when ≤ 2 distinct members are read.
- COUPLING-01: in
presentation/ files, flag import lines whose path contains /data/.
- COUPLING-02: in
features/<name>/presentation/ files, flag imports matching features/<other>/presentation/ where <other> ≠ own feature.
- ROBOT-01: flag any
find.text( in *_test.dart files.
- ROBOT-02: flag any
find.byTooltip( in *_test.dart files.
- ROBOT-03: flag all
pumpAndSettle( lines in test files that also contain CircularProgressIndicator or LinearProgressIndicator.
- ROBOT-04: for each interactive widget found (see catalog for list), check if the same file declares a Key for it; flag if missing.
- ROBOT-05: flag public
find…() methods in Robot classes (method name starts with find but no leading _).
- ROUTER-01: flag
context.push( and GoRouter.of(context).push( in presentation/ source files.
- ROUTER-02: flag
AppBar( in *_screen.dart files where leading: is not present in the same AppBar(…) span.
- LAYOUT-01: flag any file with more than one
Scaffold( occurrence.
- LAYOUT-02: flag
Widget _ methods inside widget class bodies.
- SIDE-FX-01: flag
showDialog(, Navigator.push(, ScaffoldMessenger.of(context).show, addPostFrameCallback( inside build(BuildContext method spans.
- UI-STR-01: flag long string literals (> ~20 chars, > 3 words) in files outside
presentation/.
- RESPONSIVE-01: flag
MediaQuery.of(context).size or MediaQuery.sizeOf(context) used in an if/ternary branch for layout decisions; also flag width: / height: values ≥ 100 on Container(/SizedBox( constructor spans (proxy for hard-coded structural sizing, not small decorative values).
- RESPONSIVE-02: flag width-like expressions (
constraints.maxWidth, size.width, width) compared against 3–4 digit numeric literals in if/ternary/switch conditions; skip when the value comes from a named constant (e.g. AppBreakpoints.compact).
- RESPONSIVE-03: within
Row( spans, flag the Row( line when ≥ 2 children carry width: <num> and no Flexible(/Expanded( appears in the span.
- RESPONSIVE-04: flag literal
crossAxisCount: <num> in SliverGridDelegateWithFixedCrossAxisCount( and GridView.count( spans, unless computed from constraints/width.
- WEB-01 (web target only): flag
GestureDetector( or InkWell( blocks containing onTap: where no MouseRegion, Focus, or FocusableActionDetector appears as an ancestor within the same build method span (~20 lines above). Skip occurrences inside Flutter's built-in button/tile classes.
Phase 4 — Report
Emit the violations grouped by file. Begin with the resolved platform line:
## Audit Results
**Target platforms**: android, ios (from pubspec)
### lib/.../sign_in_screen.dart
| Line | Rule ID | Severity | Message |
|------|---------|----------|---------|
| 42 | RIV-WIDGET-02 | warning | ref.watch(authProvider) accesses single field — add .select() |
| 88 | LAYOUT-02 | warning | Widget _buildForm() is a build helper — extract to widget class |
### test/.../sign_in_screen_test.dart
| Line | Rule ID | Severity | Message |
|------|---------|----------|---------|
| 55 | ROBOT-01 | error | find.text('Login') — breaks i18n; use find.byKey(SignInScreen.loginButtonKey) |
| 73 | ROBOT-03 | error | pumpAndSettle() used in file containing CircularProgressIndicator — use pump() |
---
**Summary**: 4 violations across 2 files (1 error, 2 warnings, 1 info)
If no violations are found, say so explicitly:
No violations found in <path>. All checked rules pass.
Phase 5 — Fix prompt
After the report, ask:
Apply fixes for which rule IDs? (comma-separated list, "all", or "none")
Auto-fix safe: RIV-WIDGET-02, REBUILD-01, REBUILD-02, ROBOT-05
Requires judgment: RIV-WIDGET-01, RIV-WIDGET-03, RIV-WIDGET-04, REBUILD-03,
REBUILD-04, EXTRACT-01, EXTRACT-02, COHESION-01, COUPLING-01,
COUPLING-02, ROBOT-01, ROBOT-02, ROBOT-03, ROBOT-04,
ROUTER-01, ROUTER-02, LAYOUT-01, LAYOUT-02, SIDE-FX-01,
UI-STR-01, RESPONSIVE-01, RESPONSIVE-02, RESPONSIVE-03,
RESPONSIVE-04, WEB-01
On response:
- "none" or no response: done.
- "all" or specific IDs:
- For each violation matching the selected IDs:
- If
autofix_safe: true: apply the edit directly, show diff.
- If
autofix_safe: false: show the specific change needed and ask the
user to confirm before editing. Provide the exact code transformation.
- After all edits, re-run Phase 3 on touched files only.
- Confirm which violations were resolved.
Never edit files that were not explicitly approved by the user.
Usage examples
audit the presentation layer of features/booking
audit this widget: lib/src/features/auth/presentation/sign_in_screen.dart
find UI violations in features/flight_plan/presentation/
/audit-presentation-layer apps/pollicino_viewer/lib/src/features/booking/presentation/
/audit-presentation-layer lib/src/features/home/presentation/ --platform=web
/audit-presentation-layer lib/src/features/auth/presentation/ --platform=mobile
Notes
- Paths are relative to the project root — always resolve from there.
- This skill does not shell out to
dart analyze; it reads files directly.
- It does not overlap with
riverpod-reviewer (which audits provider declarations)
or flutter-analyze-targeted (which runs the Dart analyzer).
- To add or modify rules, edit
skills/audit-presentation-layer/rules/CATALOG.md only.
- Platform gating: rules tagged
platforms: all always run. Rules tagged
mobile are skipped on web-only targets; rules tagged web are skipped on
mobile-only targets. When pubspec.yaml has no flutter.platforms map, the
target defaults to all so no rule is ever silently skipped on legacy projects.
1---2name: audit-presentation-layer3description: Audit a Flutter presentation-layer file or folder (screens, widgets, pages, related widget tests) against the project's documented UI guidelines — Riverpod v3 widget rules, rebuild isolation (const subtrees, scoped MediaQuery, builder child caching, setState blast radius), widget extraction and cohesion/coupling (oversized builds, function widgets, Law of Demeter params, layer/cross-feature imports), Robot Testing pattern, GoRouter conventions, layout antipatterns, side-effect handling, responsive layout (named breakpoints, flex rows, adaptive grids), and web interaction affordances. Platform-aware: auto-detects target platforms from pubspec.yaml and gates rules accordingly; override with --platform=web|android|ios|mobile|all. Emits a violations table with file:line and rule ID, then offers to apply fixes. Use proactively when the user says "audit presentation layer", "audit this widget", "review this widget", "check UI guidelines", "find UI violations", "presentation audit", "lint widgets", or asks to verify4---56# Audit Presentation Layer78Statically scans Flutter presentation-layer source and test files against9bundled rule docs. Emits a violations table, then offers targeted fixes.1011## Rule source1213Rules are bundled locally in `skills/audit-presentation-layer/rules/`.14This skill does **not** delegate to `ai_toolkit/` — it is self-contained.1516---1718## Phase 0 — Resolve input and platform1920### Step 1 — Resolve path2122Read the user's request and extract one of:2324- **Single file**: a path ending in `.dart`25- **Feature folder**: a path containing a `presentation/` directory2627If neither is clear, ask exactly one question:2829> "Provide a widget file path or a feature folder path containing a `presentation/` directory."3031Do not proceed until a path is confirmed.3233### Step 2 — Resolve target platforms34351. Check the user's request for a `--platform=<value>` argument.36 - Accepted values: `web`, `android`, `ios`, `mobile`, `all`.37 - `mobile` expands to `{android, ios}`. `all` expands to `{web, android, ios}`.382. If no `--platform` argument, read `pubspec.yaml` from the project root.39 Look for the `flutter: { platforms: { ... } }` map (present after40 `flutter create --platforms`). Extract the declared platform keys41 (`web`, `android`, `ios`, `linux`, `macos`, `windows`). Use only42 `web`, `android`, `ios` from this set.433. If `pubspec.yaml` has no `platforms` map (or cannot be read), fall back to `all`.4445**Precedence**: `--platform` arg > pubspec `flutter.platforms` > `all` (fallback).4647State the resolved target at the start of Phase 4 output, e.g.:48- `Target platforms: android, ios (from pubspec)`49- `Target platforms: web (from --platform=web)`50- `Target platforms: all (fallback — no platforms map in pubspec)`5152### Step 3 — Rule gating5354Before applying any rule in Phase 3, check the rule's `Platforms:` tag in55`rules/CATALOG.md`. Skip the rule if its platform set does not intersect the56resolved target set. Rules tagged `all` always run.5758---5960## Phase 1 — Load rule catalog6162Read `skills/audit-presentation-layer/rules/CATALOG.md` in full before scanning.6364Do not read individual rule doc files yet — the catalog contains all heuristics65needed for Phase 3. Open a specific rule doc only if you need to clarify a66borderline case or produce a more detailed fix explanation.6768---6970## Phase 2 — Discover files7172### Single-file mode7374Target file = the provided `.dart` path.7576Check whether a mirrored `*_test.dart` exists under `test/src/`:7778```79lib/src/features/<feature>/presentation/<name>.dart80→ test/src/features/<feature>/presentation/<name>_test.dart81```8283Include the test file in the scan if it exists.8485If the provided file is NOT under `presentation/`, classify it as `domain-file`86(skip the test mirror check) and apply only UI-STR-01 to it.8788### Folder mode8990Spawn an Explore subagent:9192```93Agent(94 subagent_type="Explore",95 prompt="List all .dart files (excluding .g.dart, .freezed.dart) recursively96 under <input_path>. For each file report:97 - Relative path98 - Whether it is a *_test.dart file99 - Line count (approximate)100 - Widget class name and superclass if visible in first 20 lines101 Report as a plain table."102)103```104105Also check for the mirrored `test/src/` path of the given lib/ folder and106include all `*_test.dart` files found there.107108Also collect non-`presentation/` dart files under the same feature root109(e.g. `application/`, `domain/`, `data/` siblings of `presentation/`).110111Classify each file:112- `widget` — non-test dart file under `presentation/`113- `widget-test` — `*_test.dart` file mirroring a presentation widget114- `domain-file` — dart file outside `presentation/` in the same feature tree115116---117118## Phase 3 — Scan119120For each file:1211221. Read the full file contents.1232. Apply every heuristic in `rules/CATALOG.md` relevant to the file type **and**124 not gated out by the platform target (see Phase 0 Step 3):125 - `widget` files → apply: RIV-WIDGET-*, REBUILD-*, EXTRACT-*, COHESION-01, COUPLING-*, LAYOUT-*, SIDE-FX-01, ROBOT-04, ROUTER-*, RESPONSIVE-*, WEB-01126 - `widget-test` files → apply: ROBOT-01, ROBOT-02, ROBOT-03, ROBOT-05127 - `domain-file` files → apply: UI-STR-01 only1283. For each match: record `{file, line_number, rule_id, severity, message, fix_hint, autofix_safe}`.129130Heuristic application notes:131132- **RIV-WIDGET-01**: flag `ref.watch(` lines where the enclosing method is NOT named `build`. Look at method declarations above the line to determine context.133- **RIV-WIDGET-02**: flag `ref.watch(someProvider)` result where the return value is immediately accessed with `.fieldName` (within 3 lines) and no `.select(` appears on the watch call.134- **RIV-WIDGET-03**: flag `Consumer(` blocks where the `builder:` body exceeds 50 lines.135- **RIV-WIDGET-04**: flag `ref.read(` lines inside a `build(BuildContext` method span.136- **REBUILD-01**: inside `build` spans, flag all-literal constructor calls (`SizedBox(`, `EdgeInsets.*(`, `Icon(Icons.`, literal-only `Text(`, `Divider(`) not preceded by `const` and not covered by an enclosing `const` (check ~3 lines above).137- **REBUILD-02**: flag `MediaQuery.of(context).<prop>` single-property accesses (`size`, `padding`, `viewInsets`, `platformBrightness`, `textScaler`); fix is the scoped `MediaQuery.<prop>Of(context)` accessor.138- **REBUILD-03**: flag `AnimatedBuilder(`/`ListenableBuilder(`/`ValueListenableBuilder(` spans with no `child:` argument and a `builder:` body > ~10 lines.139- **REBUILD-04**: in files with `setState(`, measure the enclosing class's `build(` span; flag each `setState(` call when that span > 50 lines.140- **EXTRACT-01**: flag `build(` method declarations whose span (signature to matching brace) exceeds 80 lines.141- **EXTRACT-02**: flag top-level `Widget name(` declarations (column 0) and `static Widget name(` anywhere in widget files.142- **COHESION-01**: for widget classes with one non-Key constructor field of a non-primitive project type, count distinct `<field>.<member>` accesses in the class body; flag the field when ≤ 2 distinct members are read.143- **COUPLING-01**: in `presentation/` files, flag `import` lines whose path contains `/data/`.144- **COUPLING-02**: in `features/<name>/presentation/` files, flag imports matching `features/<other>/presentation/` where `<other>` ≠ own feature.145- **ROBOT-01**: flag any `find.text(` in `*_test.dart` files.146- **ROBOT-02**: flag any `find.byTooltip(` in `*_test.dart` files.147- **ROBOT-03**: flag all `pumpAndSettle(` lines in test files that also contain `CircularProgressIndicator` or `LinearProgressIndicator`.148- **ROBOT-04**: for each interactive widget found (see catalog for list), check if the same file declares a Key for it; flag if missing.149- **ROBOT-05**: flag public `find…()` methods in Robot classes (method name starts with `find` but no leading `_`).150- **ROUTER-01**: flag `context.push(` and `GoRouter.of(context).push(` in `presentation/` source files.151- **ROUTER-02**: flag `AppBar(` in `*_screen.dart` files where `leading:` is not present in the same `AppBar(…)` span.152- **LAYOUT-01**: flag any file with more than one `Scaffold(` occurrence.153- **LAYOUT-02**: flag `Widget _` methods inside widget class bodies.154- **SIDE-FX-01**: flag `showDialog(`, `Navigator.push(`, `ScaffoldMessenger.of(context).show`, `addPostFrameCallback(` inside `build(BuildContext` method spans.155- **UI-STR-01**: flag long string literals (> ~20 chars, > 3 words) in files outside `presentation/`.156- **RESPONSIVE-01**: flag `MediaQuery.of(context).size` or `MediaQuery.sizeOf(context)` used in an `if`/ternary branch for layout decisions; also flag `width:` / `height:` values ≥ 100 on `Container(`/`SizedBox(` constructor spans (proxy for hard-coded structural sizing, not small decorative values).157- **RESPONSIVE-02**: flag width-like expressions (`constraints.maxWidth`, `size.width`, `width`) compared against 3–4 digit numeric literals in `if`/ternary/`switch` conditions; skip when the value comes from a named constant (e.g. `AppBreakpoints.compact`).158- **RESPONSIVE-03**: within `Row(` spans, flag the `Row(` line when ≥ 2 children carry `width: <num>` and no `Flexible(`/`Expanded(` appears in the span.159- **RESPONSIVE-04**: flag literal `crossAxisCount: <num>` in `SliverGridDelegateWithFixedCrossAxisCount(` and `GridView.count(` spans, unless computed from constraints/width.160- **WEB-01** _(web target only)_: flag `GestureDetector(` or `InkWell(` blocks containing `onTap:` where no `MouseRegion`, `Focus`, or `FocusableActionDetector` appears as an ancestor within the same `build` method span (~20 lines above). Skip occurrences inside Flutter's built-in button/tile classes.161162---163164## Phase 4 — Report165166Emit the violations grouped by file. Begin with the resolved platform line:167168```169## Audit Results170171**Target platforms**: android, ios (from pubspec)172173### lib/.../sign_in_screen.dart174| Line | Rule ID | Severity | Message |175|------|---------|----------|---------|176| 42 | RIV-WIDGET-02 | warning | ref.watch(authProvider) accesses single field — add .select() |177| 88 | LAYOUT-02 | warning | Widget _buildForm() is a build helper — extract to widget class |178179### test/.../sign_in_screen_test.dart180| Line | Rule ID | Severity | Message |181|------|---------|----------|---------|182| 55 | ROBOT-01 | error | find.text('Login') — breaks i18n; use find.byKey(SignInScreen.loginButtonKey) |183| 73 | ROBOT-03 | error | pumpAndSettle() used in file containing CircularProgressIndicator — use pump() |184185---186**Summary**: 4 violations across 2 files (1 error, 2 warnings, 1 info)187```188189If no violations are found, say so explicitly:190191```192No violations found in <path>. All checked rules pass.193```194195---196197## Phase 5 — Fix prompt198199After the report, ask:200201```202Apply fixes for which rule IDs? (comma-separated list, "all", or "none")203Auto-fix safe: RIV-WIDGET-02, REBUILD-01, REBUILD-02, ROBOT-05204Requires judgment: RIV-WIDGET-01, RIV-WIDGET-03, RIV-WIDGET-04, REBUILD-03,205 REBUILD-04, EXTRACT-01, EXTRACT-02, COHESION-01, COUPLING-01,206 COUPLING-02, ROBOT-01, ROBOT-02, ROBOT-03, ROBOT-04,207 ROUTER-01, ROUTER-02, LAYOUT-01, LAYOUT-02, SIDE-FX-01,208 UI-STR-01, RESPONSIVE-01, RESPONSIVE-02, RESPONSIVE-03,209 RESPONSIVE-04, WEB-01210```211212On response:213214- **"none"** or no response: done.215- **"all"** or specific IDs:216 1. For each violation matching the selected IDs:217 - If `autofix_safe: true`: apply the edit directly, show diff.218 - If `autofix_safe: false`: show the specific change needed and ask the219 user to confirm before editing. Provide the exact code transformation.220 2. After all edits, re-run Phase 3 on touched files only.221 3. Confirm which violations were resolved.222223Never edit files that were not explicitly approved by the user.224225---226227## Usage examples228229- `audit the presentation layer of features/booking`230- `audit this widget: lib/src/features/auth/presentation/sign_in_screen.dart`231- `find UI violations in features/flight_plan/presentation/`232- `/audit-presentation-layer apps/pollicino_viewer/lib/src/features/booking/presentation/`233- `/audit-presentation-layer lib/src/features/home/presentation/ --platform=web`234- `/audit-presentation-layer lib/src/features/auth/presentation/ --platform=mobile`235236---237238## Notes239240- Paths are relative to the project root — always resolve from there.241- This skill does not shell out to `dart analyze`; it reads files directly.242- It does not overlap with `riverpod-reviewer` (which audits provider declarations)243 or `flutter-analyze-targeted` (which runs the Dart analyzer).244- To add or modify rules, edit `skills/audit-presentation-layer/rules/CATALOG.md` only.245- **Platform gating**: rules tagged `platforms: all` always run. Rules tagged246 `mobile` are skipped on web-only targets; rules tagged `web` are skipped on247 mobile-only targets. When `pubspec.yaml` has no `flutter.platforms` map, the248 target defaults to `all` so no rule is ever silently skipped on legacy projects.