1. When to Use This Skill
Use this skill when analyzing Dart and Flutter codebase maintainability, evaluating function readability, or remediating high-complexity findings during code review or static analysis audits.
Unlike Cyclomatic Complexity (which linearly counts control flow branching paths and punishes declarative table switches), Cognitive Complexity measures the mental friction required for a human engineer to read and simulate control flow. Rely on deterministic evaluation to target structures matching these indicators:
- Deeply Nested Control Flow: Functions exhibiting multiple layers of
enclosing conditionals (
if,for,while), where horizontal indentation obscures logic. - Convoluted Conditional Trees: Functions employing verbose
if-elseorelse ifchains instead of modern Dart 3 exhaustive pattern matching or table-driven switch expressions. - Monolithic Method Bodies: Functions breaching operational threshold ceilings.
- God Classes: Logic classes exceeding structural line-count targets
(excluding declarative Flutter
buildmethods).
2. Automated Execution & Scope Resolution
Execute the official package CLI directly in the terminal to retrieve exact complexity scores deterministically without LLM arithmetic or AST interpretation.
SDK Compatibility Note: Executing
dart run cognitive_complexity@^0.2.4requires Dart SDK version 3.12.0 or greater installed in the host environment. Verify compatibility viadart --versionbefore initiating scans.
Select the execution scope based on the user's task instructions:
Scope 1: Targeted (Specific File, Directory, or Class)
When the user references a discrete component (e.g., "check complexity in
lib/src/auth/" or "audit order_service.dart"), pass explicit targets:
dart run cognitive_complexity@^0.2.4 --threshold 15 lib/src/auth/
Scope 2: Delta (Pull Request, Branch, or Pre-flight Audit)
When reviewing a feature branch, active commit stack, or PR, avoid full-project scanning. Isolate evaluation strictly to modified declarations against trunk:
dart run cognitive_complexity@^0.2.4 --git-diff origin/main --fail-threshold 15 --fail-on-increase
With both flags set, only increases that exceed the threshold fail — healthy
sub-threshold increases (e.g. added error handling) are reported without
blocking. Omit --fail-threshold only when a strict any-increase ratchet is
explicitly desired.
Scope 3: Whole-Project (Default Naked Invocation)
When invoked without targeting parameters ("scan my project for complexity" or
/cognitive-complexity), audit the standard source and test roots:
# Production logic target (threshold 15)
dart run cognitive_complexity@^0.2.4 --threshold 15 lib/
# Test harness target (threshold 40)
dart run cognitive_complexity@^0.2.4 --threshold 40 test/
3. Actionable Thresholds & Calibration
- Production Logic Functions: Target score
<= 15. Functions exceeding 15 points mandate architectural refactoring. - Test Methods (
_test.dart): Target score<= 40. Test suites tolerate higher setup sequences before decomposition is required. - Class Size Ceiling: Logic classes (services, domain objects, controllers)
should remain
<= 150non-comment lines. - Flutter UI Calibration: Do not enforce the 150 LOC class ceiling on
declarative Flutter
buildmethods, as widget wrappers consume vertical space without increasing cognitive logic load. Instead, enforce a Widget Tree Nesting Ceiling of maximum 5 horizontal indentation levels before extracting discrete helper widget classes.
4. The Triage & Confirmation Protocol (Audit Before Action)
Discovering high-complexity functions during an audit does not grant permission to autonomously refactor the entire repository. To prevent unwanted diff bloat and preserve historical code stability, adhere to a strict 2-stage workflow:
Stage 1: Read-Only Audit & Reporting (Mandatory Stop)
When threshold breaches are detected, do not mutate code immediately. Output a ranked Markdown Complexity Triage Report directly in chat (or to an artifact for extensive findings) containing:
- Flagged function name and clickable file local path.
- Current complexity score versus operational ceiling (sorted descending by score).
- Recommended refactoring strategy (Pattern A, B, D, E, or a Pattern C tier) and unit test status.
Invariant: Outlier-First Mandate
- Target the Primary Outlier: When remediating complexity, prioritize the highest-scoring declaration in the report (e.g. Score >= 25 or top outlier) to achieve the highest measurable reduction.
- Prioritize High-Impact Reductions: Avoid selecting only minor sub-threshold helpers while leaving severe complexity outliers unaddressed.
- Decomposition Target: Apply Dart 3 pattern matching, guard clauses, and
method decomposition targeting a post-refactoring score of
<= 15. For massive legacy functions (score > 60), safe phased decomposition across isolated PRs is permitted if a single pass would exceed reviewable diff limits.
Stage 2: Interactive User Selection (Confirmation Gate)
Pause execution and prompt the user (via interactive choice or chat) to select the desired sequencing:
- (Recommended) Refactor Primary Outlier First: Target the single highest-scoring declaration in the report, decompose towards score <= 15, verify via unit tests, and present diffs cleanly.
- Selective Batch Refactor: Remediate the top N highest-scoring functions in descending order.
- Report-Only / Exit: Acknowledge complexity scores without code mutation.
Explicit Bypass & Non-Interactive Fallback:
- Direct Directives: Skip Stage 1 triage if given an explicit remediation directive upfront (e.g., "Refactor
processOrderinlib/src/order.dartto fix complexity" or "Refactor the top complexity outlier").- Non-Interactive Execution: In unattended or automated evaluation workflows (e.g.
evalinor subagents), proceed with Option 1 (Refactor Primary Outlier) automatically after verifying baseline tests pass.
5. Pre-Refactoring Assessment & Test Coverage Gate
High cognitive complexity strongly correlates with brittle, untested legacy logic. Before undertaking structural refactoring on flagged functions, enforce this verification baseline:
- Test Harness Mapping: Confirm an accompanying unit test file exists for
the target declaration (e.g.,
lib/src/foo.dart->test/foo_test.dart). - Coverage Audit & Execution:
- If the
dart-collect-coveragecompanion skill is available in your agent runtime, invoke it to check line and branch coverage on the targeted declarations. - At minimum, execute the relevant test suite (
dart test test/foo_test.dartorflutter test test/foo_test.dart) to confirm a passing green regression baseline before touching code.
- If the
- Low-Coverage Safety Gate: If tests are missing or coverage around the
flagged function is inadequate:
- Interactive Sessions: Pause execution and warn the user. Offer options to (1) write unit tests first, or (2) proceed with surgical refactoring.
- Automated Task Execution: Log a low-coverage warning and author a minimal regression test before modifying complex logic.
6. Dart Refactoring Patterns
When remediation is required for declarations flagged by the scanner, apply these Dart-specific architectural refactorings:
Pattern A: Replace Nested If-Else with Dart 3 Switch Expression
In Dart 3, an entire exhaustive switch expression incurs a single base penalty,
regardless of how many pattern arms it contains. Converting deeply nested
if-else trees into declarative tables removes repeated branching penalties and
flattens nesting.
Before: Nested Conditional Ladders (Score: 11)
int resolveTimeout(String protocol, bool isSecure, int retryCount) {
if (protocol == 'http') {
if (isSecure) {
if (retryCount > 3) {
return 5000;
} else {
return 3000;
}
} else {
return 1000;
}
} else if (protocol == 'ftp') {
return isSecure ? 10000 : 2000;
}
return 0;
}
After: Table-Driven Switch Expression (Score: 1)
int resolveTimeout(String protocol, bool isSecure, int retryCount) =>
switch ((protocol, isSecure, retryCount)) {
('http', true, > 3) => 5000,
('http', true, _) => 3000,
('http', false, _) => 1000,
('ftp', true, _) => 10000,
('ftp', false, _) => 2000,
_ => 0,
};
Pattern B: Guard Clause Inversion (Flattening Nesting Depth)
Invert conditional checks into early guard return statements
(if (!condition) return;). Every early exit strips away a layer of nesting
multiplication from subsequent downstream logic.
Before: Pyramid of Nesting (Score: 11)
Future<void> syncPayload(User? user, Payload? data) async {
if (user != null) {
if (user.hasPermission) {
if (data != null && data.isValid) {
for (final item in data.items) {
await repository.save(item);
}
}
}
}
}
After: Early Exit Guard Clauses (Score: 5)
Future<void> syncPayload(User? user, Payload? data) async {
if (user == null || !user.hasPermission) return;
if (data == null || !data.isValid) return;
for (final item in data.items) {
await repository.save(item);
}
}
Pattern C: The 3-Tier Decomposition Rubric (Anti-Goodhart)
Anti-Goodhart Guardrail: Do NOT blindly wrap monolithic functions in single-use runner classes with mutable instance variables just to bring scores below 15. Reducing the metric must never sacrifice transparent data flow or hide bugs.
Deterministic 3-Tier Selection via data_flow
Do not manually estimate or guess which tier a candidate slice belongs to. Run the companion statement-level data-flow analyzer (on-demand form; same SDK 3.12.0+ requirement as the scanner) on each line slice you intend to extract:
dart run cognitive_complexity:data_flow@^0.2.4 lib/src/my_file.dart:45-80
Its report (inputs, mutations, live outputs, control-flow escapes, and a
synthesized Dart 3 record signature) selects the tier:
Tier 1 — Pure Functional Decomposition (First Choice):
- Selection: Cleanly extractable slice with 2+ live outputs.
- Idiom: Extract a static or top-level function returning the synthesized
Dart 3 named record signature verbatim (
final (:data, :errors) = _stepOne(input);). - Dataclass Boundary: Private, file-local slices use named records at ANY
output count—do not create single-use
_XxxResultdataclasses for them. Reserve dedicated dataclasses only for values that cross public API boundaries or require specialized invariants/methods. - State Scoping: If the helper does not read or mutate class instance
state (
this), declare it as a private top-level function (or static method) to guarantee referential transparency.
Tier 2 — Standard Helper Extraction (Second Choice):
- Selection: Cleanly extractable slice with <= 1 live output and <= 3 inputs.
- Idiom: Extract a standard private helper method or private top-level function returning that single value.
Control-Flow Escapes & Loop Bodies:
- Apply natural seams: enlarge the slice to include the entire enclosing loop
or state machine and re-run
data_flow. - If the loop body itself is the hotspot, extract it with an explicit signal
return (Pattern E)—never a
shouldBreakboolean flag. - Use guard-clause inversion (Pattern B) when the escape exists only to skip nested conditions.
- Apply natural seams: enlarge the slice to include the entire enclosing loop
or state machine and re-run
Tier 3 — Encapsulated Method Object (Last Resort):
- Mutation-Web Check Gate: Permitted ONLY if
data_flowreports on at least two distinct candidate slices show that the intersection of theirmutationsvariable names contains 3 or more entries (the same mutable variables thread through every candidate extraction). - Mechanics: Read references/method-object.md for extraction mechanics and mandatory idioms. Do not load or apply it speculatively.
- Mutation-Web Check Gate: Permitted ONLY if
Domain-Modeling Exit: When the same tightly coupled mutable state keeps
resurfacing across a function (a parser's buffer + cursor, a traversal's
queue + visited), the code may be asking to become a real, cohesively
named domain class (Parser, GraphTraversal) with a public, unit-tested
API. To qualify for the exit, the class MUST have cohesive entity state, more
than one public behavior, and its own dedicated test suite. Single-use private
facades with one run() method remain subject to the Tier 3 gate.
Pattern D: Fast-Fail Type Matching & Silent Data Swallowing
When refactoring loops and type checks to reduce branching, never replace explicit type casts with pattern matching that silently drops data.
Flawed Structure (Silent Failure):
for (final raw in rawTasks) {
// SILENTLY DROPS malformed data if raw is not a Map
if (raw case final Map<String, dynamic> taskMap) {
_applyTask(taskMap);
}
}
Correct Structure (Fast-Fail Preservation):
for (final raw in rawTasks) {
if (raw is! Map<String, dynamic>) {
errors.add('Malformed task item (expected Map, got ${raw.runtimeType}): $raw');
continue;
}
_applyTask(raw);
}
Pattern E: Loop-Body Extraction with Signal Returns
When a loop body must be extracted but contains break/continue targeting the
loop, never smuggle the control flow through boolean flags (shouldBreak soup)
— that raises cognitive load and hides termination conditions. Return an
explicit signal and keep the loop keywords at the loop site:
enum _ScanAction { proceed, skip, halt }
// Pure, independently testable helper.
_ScanAction _classify(Entry entry, Set<String> seen) {
if (seen.contains(entry.id)) return _ScanAction.skip;
if (entry.isTerminal) return _ScanAction.halt;
return _ScanAction.proceed;
}
outer:
for (final entry in entries) {
switch (_classify(entry, seen)) {
case _ScanAction.skip:
continue;
case _ScanAction.halt:
break outer;
case _ScanAction.proceed:
process(entry);
}
}
Use a sealed class instead of an enum when the signal must carry a payload.
Asynchronous classification works identically:
switch (await _classify(entry, seen)). The exhaustive switch keeps every
termination path visible at the loop site. Prefer extracting the entire loop
when data_flow shows it forms a natural seam; use Pattern E when the loop body
alone is the hotspot.
7. Verification Guardrails
Run these verification commands before committing refactored code:
- Complexity Audit: Run
dart run cognitive_complexity@^0.2.4 --fail-threshold 15 <refactored files>scoped to the files you touched. Pre-existing breaches elsewhere in the project do not invalidate the refactor. - Code Presentation: Run
dart format .(orflutter format .) to maintain uniform syntactic styling. - Static Analysis: Run
dart analyze(orflutter analyze) to ensure zero static warnings, lint violations, or un-awaited asynchronous gaps. - Test Fidelity:
- Check
pubspec.yaml: ifsdk: flutteris declared, runflutter test; otherwise rundart test. - In multi-package workspaces, run tests across all dependent packages.
- Check
8. Pull Request & Commit Provenance Protocol
When staging refactored code and preparing a commit message or Pull Request:
1. User Confirmation Gate
- Interactive Sessions: Before writing the PR description or commit body,
explicitly prompt the user in chat or via the harness confirmation tool (e.g.
ask_question) whether to include a Tool Provenance & Complexity Delta block. - User Prompt Inclusion: When the user explicitly requests inclusion (or confirms via prompt), append the standardized markdown block below. In unattended or automated workflows, output the summary to chat or step summaries rather than modifying commit bodies without user confirmation.
2. Standardized Provenance Block Format
When confirmed by the user, include the following markdown block in the PR description or commit body so reviewers understand where the changes originated, see the quantified readability improvements, and can rerun the audit locally:
### 🤖 Tool Provenance & Complexity Delta
This refactoring was guided by
[`cognitive_complexity`](https://pub.dev/packages/cognitive_complexity)
(`v{version}`).
<!-- mdformat off(prevent table wrapping) -->
| Target Declaration | Pre-Score | Post-Score | Operational Ceiling |
| :----------------------------------- | :-----------: | :------------: | :-----------------: |
| `{declaration_name}` (`{file_path}`) | `{pre_score}` | `{post_score}` | `<={threshold}` |
<!-- mdformat on -->
To reproduce or re-evaluate cognitive complexity scores:
```bash
{exact_command_line}
```
<!-- If statement data-flow analysis was used during decomposition: -->
```bash
dart run cognitive_complexity:data_flow@^0.2.4 {file}:{start_line}-{end_line}
```
3. Version Resolution
Determine the package version dynamically:
- Check
pubspec.lockin the workspace or rundart run cognitive_complexity@^0.2.4 --version. - If invoked with a specific version constraint (e.g.
cognitive_complexity@^0.2.4), use that exact version.