1. When to Use This Skill & Threshold Calibration
Use this skill when analyzing Dart and Flutter maintainability, evaluating function readability, or remediating high-complexity findings during code review. Cognitive Complexity measures human mental friction rather than flat branch counts.
- Production Logic Functions: Target score
<= 15(Target Zone:8–15, Not0). Functions exceeding 15 points mandate architectural refactoring. Once a function reaches<= 15, stop decomposing—never shred a12-point function into single-caller pass-through micro-helpers just to chase0. - Stop Rule: Only declarations scoring above the threshold are work
items. When the max score is
<= threshold, report "done, nothing required" and stop. Further refactors (re-inlining, file splits, style edits) are optional and need an explicit user ask. - Extracted Helper Depth & Parameter Cap: Pure extracted helpers must take
<= 4parameters (<= 3preferred) and carry genuine internal depth (sliceScoreAtRoot >= 3or reused across2+call sites). - Test Methods (
_test.dart): Target score<= 40. - Class & File Size Ceilings: Logic classes should remain
<= 150non-comment lines; source files should remain<= 400lines. - Flutter UI Calibration: Do not enforce the 150 LOC ceiling on declarative
Flutter
buildmethods; instead, enforce a Widget Tree Nesting Ceiling of at most 5 horizontal indentation levels before extracting helper widgets.
2. Automated Execution & Scope Resolution
Run the CLI directly (requires Dart SDK 3.12.0+, verify via
dart --version):
- Scope 1 — Targeted (Specific File or Directory):
dart run cognitive_complexity@^2.0.1 --threshold 15 lib/src/auth/ dart run cognitive_complexity@^2.0.1 --threshold 15 --verbose lib/src/auth/--verbose/-vadds aBreakdowncolumn (branches,nesting,boolean_ops,max_depth; also in JSON ascomposition) so a flat score-15 function (Pattern D/E material) is distinguishable from a five-deep pyramid (Pattern A/B material). Top-levelmainof a_test.dartfile is tagged[test entrypoint]. - Scope 2 — Delta (PR, Branch, or Pre-Flight Audit):
dart run cognitive_complexity@^2.0.1 --git-diff origin/main --fail-threshold 15 --fail-on-increase - Scope 3 — Whole-Project (Default Naked Invocation): Invoking
cognitive_complexitywith zero positional paths automatically discovers package roots or workspace members and defaults to analyzinglib/:dart run cognitive_complexity@^2.0.1 --threshold 15 dart run cognitive_complexity@^2.0.1 --threshold 40 test/[!NOTE]
CLI Package Caveat (
lib/ bin/): Zero-argument auto-discovery only inspectslib/. For CLI tools and applications with entrypoints inbin/(ortool/), pass target directories explicitly:dart run cognitive_complexity@^2.0.1 --threshold 15 lib/ bin/ - Optional Review Aid — Shallow Helper Audit (Advisory Only):
Without positional targets,dart run cognitive_complexity:shallow@^2.0.1 lib/ dart run cognitive_complexity:shallow@^2.0.1 lib/ bin/shallowscanslib/in the current directory (passlib/ bin/or package paths explicitly for CLI tools and monorepos). Its findings (likefile_splitplans) are prompts for judgment, never a queue to clear or a CI gate; see Section 5.3 before acting on any of them. This framing applies to consumer repos: a package may keep its own dogfood--fail-on-safe-inlineCI step (this repo does), and never remove an existing CI gate without an explicit ask.
3. The Triage & Confirmation Protocol (Audit Before Action)
When threshold breaches are detected, do not mutate code immediately unless
given an explicit upfront remediation directive or running in an unattended
automated harness (evalin / subagent). When there are no breaches (max
score <= threshold), apply the Stop Rule: report that nothing is required,
make no source edits, and skip Stage 2.
Stage 1: Read-Only Audit & Reporting (Mandatory Stop)
- Mandatory Persistent Artifact: Create
complexity_triage_report.mdin<appDataDir>/brain/<conversation-id>/containing:-
Core Cognitive Complexity Outliers (
> 15Prod /> 40Test) — the only work items: each flagged function, clickable file path with code snippets, current score vs. ceiling (sorted descending by score), recommended pattern (A–F), and unit test status. -
Optional: Shallow Helper Notes (Advisory, Not Work Items): If you ran
cognitive_complexity:shallow, list its findings (SAFE_INLINE,ZERO_HEADROOM,FLATTEN_AND_INLINE,LOAD_BEARING) for review:Classification Helper Declaration Sole Caller Helper Metrics Caller CC ( Before -> After)Suggested Review SAFE_INLINE_helpercaller(depth=0)params=6,LOC=18,CC=20 (base 0) -> 2(+2)Review: consider re-inlining if the helper is pure plumbing, has no same-shape siblings, and its name adds no meaning. - Reading Shallow Classifications:
SAFE_INLINE(CallerCCAfter < 15): Inlining is possible without breaching the caller's budget. It is not a recommendation; apply the Section 5.3 keep/revert heuristics first.ZERO_HEADROOM(CallerCCAfter == 15): Inlining would leave the caller no budget; leave extracted.HIGH_ARITY/ Parameter Record: For sibling helpers sharing high-arity parameter clumps, a shared Dart 3 named record (or an existing domain/state object that already holds those values) usually beats inlining.FLATTEN_AND_INLINE: Only relevant when the caller is itself a work item; flatten with Patterns A/B as part of that fix.
- Reading Shallow Classifications:
-
- Visible Chat Pre-Render: Render a high-level summary (top complexity
outliers, plus any advisory shallow notes) and a clickable link to
complexity_triage_report.mdin visible chat BEFORE invoking the confirmation gate. - Outlier-First Mandate: Prioritize the highest-scoring declaration in the
report (
Score >= 25or top outlier) targeting a post-refactoring score of<= 15.
Stage 2: Interactive User Selection (Confirmation Gate)
Never call ask_question before complexity_triage_report.md is written and
linked in chat. Reference [complexity_triage_report.md](...) in the prompt and
offer:
- (Recommended) Refactor Primary Outlier First: Target the single
highest-scoring declaration, decompose to
<= 15, verify tests, and show diffs. - Selective Batch Refactor: Remediate the top N highest-scoring functions in descending order.
- Report-Only / Exit: Acknowledge scores without code mutation.
4. Pre-Refactoring Assessment, Coverage & Public API Baseline
- Test Baseline: Confirm a unit test exists (
lib/src/foo.dart->test/foo_test.dart), check coverage viadart-collect-coverageif available, and rundart test(orflutter test) to verify a green baseline before editing. If coverage is missing, warn the user (or add a minimal regression test in unattended runs). - Public API Surface Baseline (
api_summary): Whenever a complexity or file-split refactor extracts helpers across files or toucheslib/exports in a Dart package, ensure.dart_tool/package_config.jsonexists and capture a non-empty pre-refactor public surface (withoutpackage_config.json,api_summaryexits0with0package:symbols):test -f .dart_tool/package_config.json || dart pub get dart run api_summary@^1.1.0 > /tmp/api_before.txt && rg -q '^package:' /tmp/api_before.txt
5. Deep Modules & Refactoring Patterns
See references/refactoring_recipes.md for
complete before/after code examples across Patterns A–F and the PR provenance
template.
5.1 Deep Externally, Pure Internally (No Stateful _Populator / _Runner Classes)
- Forbid Single-Use Stateful Helper Classes: Never decompose complex
functions by creating single-use stateful private helper classes
(
_FooPopulator,_BarBuilder,_BazRunner) that hold mutable fields across methods or mutate caller collections in-place. - Require Pure Top-Level / Static Helpers: Extract pure file-private
top-level functions (
_validateItem(...),_parseHeader(...)) orstaticmethods on existing domain types with explicit inputs and immutable return values (Dart 3 named records with<= 3fields or value types). - Forbid Callback Trampolines & Closure-Wrapped Mutable Locals: Never
extract a helper that accepts a callback closure
(
String Function(Object?) pporbool Function() isAborted) just to call back into a local recursive function or read a mutable local variable in the caller, and never slice atry/catchout of a retry loop with anullsentinel return. If a function relies on a nested recursive closure, promote the recursive closure itself (or a stateless private formatter/printer helper withconstconfiguration fields) rather than passing recursion trampolines into extracted branch helpers. - Forbid
.catchError(...)Metric Gaming inasyncFunctions: Never replace idiomatictry { await ... } catch (e, s)with.catchError(...)inside anasyncfunction solely to dodgeCatchClausescoring, and never preserve or introduce zero-allocation fast-path regressions (e.g., allocating a new collection before an early-return fast path). - Forbid Multi-Pass Loop Splitting &
.expandMetric Gaming: Never split a single loop (or nested loop) into multiple sequential loops over the same collection or chain.expand((c) => c)/.forEach(...)solely to shed loop nesting depth.
5.2 In-Place Flattening First (Patterns A & B Before data_flow Extraction)
Always apply in-place flattening (Patterns A and B) before extracting any
single-caller helper—especially for borderline violations (CC 16–22), where
eliminating 1–2 levels of nesting drops the score to <= 15 with zero new
functions and zero parameter plumbing. If a CC 16–22 function owes its
remaining complexity to a nested loop that cannot be flattened in a single
pass using continue/return guards or switch expressions, extract a
cohesive LOAD_BEARING helper for the inner loop or validation pass rather than
splitting the loop into two passes:
- Pattern A (Dart 3 Switch Expressions): Replace nested
if-elseladders with exhaustive table-drivenswitchexpressions (single base penalty). - Pattern B (Guard Clause Inversion &
elseRemoval): Invert nested preconditions into early returns (if (!cond) return;), dropelseafterreturn/break/continue/throw, merge nestedifconditions, and eliminate redundant post-trynull checks only when doing so does not widen thetryblock around user-supplied callbacks whose exceptions must not be caught.
Only when a declaration still exceeds 15 after exhausting Patterns A and
B, run the statement-level data-flow analyzer on candidate line slices:
dart run cognitive_complexity:data_flow@^2.0.1 lib/src/my_file.dart:45-80
Inspect the complexity impact (enclosing_score, slice_score_in_place,
slice_score_at_root, estimated_enclosing_score_after in JSON;
DataFlowResult.enclosingScore, sliceScoreInPlace, sliceScoreAtRoot,
estimatedEnclosingScoreAfter in Dart) and honor any extraction_warnings
(high parameter count >= 5 inputs, low complexity payoff, or shallow
signature-to-complexity ratio):
- Pattern C (3-Tier
data_flowExtraction):- Tier 1 — Pure Functional Decomposition (First Choice): For slices with
2+live outputs (<= 3record fields),<= 4inputs, andslice_score_at_root >= 3, extract a pure private top-level orstaticfunction returning the synthesized Dart 3 named record (final (:data, :errors) = _step(input);). Never create single-use_XxxResultdataclasses or> 3-field anonymous return records for private slices. - Tier 2 — Standard Helper Extraction (Second Choice): For slices with
<= 1output,<= 3inputs, andslice_score_at_root >= 3, extract a pure private top-level or static helper (never avoidhelper that mutates a caller'sMap/Listin place for only one of several parallel steps). - Tier 3 — Encapsulated Method Object (Last Resort): Permitted ONLY when
data_flowon 2+ candidate slices shows>= 3intersectingmutationsvariables—readreferences/method-object.mdbefore applying. If coupled state represents a cohesive domain concept with multiple public behaviors and dedicated tests, promote it to a real public domain class instead.
- Tier 1 — Pure Functional Decomposition (First Choice): For slices with
- Pattern D (Fast-Fail Type Matching): Never silently drop malformed items
with
if (raw case final Map<String, dynamic> m); useif (raw is! Map) { ...; continue; }. - Pattern E (Loop-Body Signal Returns): Return an explicit
_Actionenum orsealed classfrom extracted loop bodies rather thanshouldBreakboolean flags. - Pattern F (Acyclic File Decomposition & Load-Bearing Library Boundaries):
Run
dart run cognitive_complexity:file_split@^2.0.1 lib/src/large_file.dart --target-lines 300for files> 400lines and select the library boundary tier.--target-linesis a physical-line budget for the surviving file; the planner cuts disjoint islands first, then sub-cone leaf groups out of the dominant island, names each cut after its dominant public declaration, and skips Tier-3partfallback cuts that would leave< 25%of the pre-fallback file behind. The header reportslargest resulting file: N lines(appending(target M not met)whenN > M;largest_resulting_file_linesandmeets_targetin--format json, which always emits an array of one report per analyzed file). Oversized-declaration notes (implements X (n/m members are @override), static-promotion hints, embedded-asset hints, and coupled-SCC vs. cohesive-island summaries) appear on bothMove Declarationsin extracted cuts andSurviving Declarations— when>= 50%of a class's members are@override, its size is bound by the interface surface, so narrow the interface or split behind a delegate rather than promoting statics.- Tier 1 (Default — Standalone
lib/src/<topic>.dart, CLI[Cut N - Tier 1]): Use when extracted helpers form a genuine sub-domain with narrow parameter lists (<= 3args) and do not need private_members or library-scoped modifiers of the parent class/library ([Cut N - Tier 2]infile_splitindicates a one-way acyclic cut that requires widening1–3internal_helpers to@internalinsidelib/src/). - Tier 2 (
part/part of, CLI[Cut N - Tier 3]): Use when splitting one cohesive domain where helpers share private_fields, private constructors (._()),sealed/final/interface/basemodifiers, or internal invariants, OR when standalonelib/src/files would require widening visibility and risk leaking internal types via unscopedexport 'src/...';directives. - Breaking Model/Enum Dispatch Back-Edges: When data models/enums are
fused into a single giant SCC with a parser/service class because an
enumor model class defines a dispatch method calling the parser/service, move that dispatch method to anextensionin the surviving file so the models extract cleanly into<stem>_models.dart. - Zero Uncalled Factories / Constructor Parity: Whenever extracting
<stem>_models.dartor adding a convenience factory (Foo.fromBar(...)), verify withrgthat every newly added factory is called by the surviving file and never leave an inline constructor call with divergent field values alongside an unused factory. file_splitplans are advisory: the cuts are dependency-correct, but the suggested file name follows one declaration. Name each new file by what it actually holds (Section 5.3).
- Tier 1 (Default — Standalone
- Pattern G (Advisory Shallow-Helper Review —
shallow): Optionally rundart run cognitive_complexity:shallow@^2.0.1 lib/to list single-caller helpers (HIGH_ARITY,MICRO_HELPER,SIG_HEAVY,CROSS_FILE_SINGLE_CALLER). ASAFE_INLINEfinding means inlining would not breach the caller's budget, not that it should happen; most well-named helpers should stay. Consider inlining only pure plumbing that passes every Section 5.3 check. AFacts:line (shared_param_signature_with,params_subset_of_existing_typein JSON) points at a Parameter Record or Existing State Object remedy instead of inlining;HIGH_ARITYcounts inline record parameters by field count, so packing arguments into an ad-hoc record is not an escape.
5.3 Keep / Revert Heuristics
KEEP (these reliably improve code):
- De-duplication of repeated logic.
- Guard clauses and flattening (Patterns A/B).
- Named step helpers that turn a long function into a readable sequence.
DON'T:
- Inline a helper whose same-shape siblings stay extracted (e.g. inlining
_readEnvironwhile_readCmdline/_readCwdremain helpers). - Inline a helper whose name or doc comment carries meaning the call site would
lose. Watch for inlined bodies whose early
returnnow skips later caller logic. - Make style-only edits to code you are not otherwise fixing.
- Split files without a cohesive name. Name a file by what it holds, put shared
types in a
models-style file, and never create import cycles or barrel files (export ... showre-export lists) when direct imports work.
5.4 Hot Loops: Benchmark Before Splitting
For per-element inner loops (pixel, module, byte, or token kernels), splitting into helpers can cost real throughput even on AOT. Benchmark before and after any split. Prefer leaving the kernel inline with a declaration-level suppression on the line immediately preceding the declaration, plus a reason:
// Hot per-module kernel: splitting into helpers benchmarked slower on AOT.
// cognitive_complexity:ignore
int _scoreModules(List<int> modules, int size) {
// ...
}
6. Verification & Public API Surface Guardrails
- Complexity Check: Run
dart run cognitive_complexity@^2.0.1 --fail-threshold 15 <refactored files>. Optionally rundart run cognitive_complexity:shallow@^2.0.1 <files>as a review aid (Section 5.3); its findings never block. - Mandatory
api_summaryPublic API Surface Verification Gate: Whenever a refactor extracts helpers across files or toucheslib/exports:
Require zero unintended public symbol leaks (and ifdart run api_summary@^1.1.0 > /tmp/api_after.txt diff -u /tmp/api_before.txt /tmp/api_after.txtapi.txtis tracked in the repo, verifygit diff api.txtis empty). - Format, Analyze & Test: Run
dart format .,dart analyze --fatal-infos, anddart test(orflutter test). Plaindart analyzeexits 0 oninfodiagnostics, but many ecosystem CI pipelines run with--fatal-infos; extraction commonly leavesunnecessary_ignore/duplicate_ignore(an// ignore:copied onto both the caller and the extracted helper),unused_import, ordirectives_orderingbehind, so resolve every info before committing. - Blind Self-Review (Diffs
> ~300Changed Lines): Before finishing, have a fresh agent or human reviewer, given only the before/after trees and the diff (no tool output or scores), judge whether each change is an improvement. Revert hunks judged neutral or worse. - PR & Commit Provenance: In interactive sessions, ask the user before
appending the standardized Tool Provenance & Complexity Delta block from
references/refactoring_recipes.md.