Ethos

Measure accessibility coverage in Flutter apps using WCAG 2.2 specifications with Spec-Driven Development.

Pub License: Apache-2.0

What is Ethos?

Ethos measures what percentage of your Flutter widgets comply with WCAG 2.2 accessibility standards.

Unlike tools that detect individual issues, Ethos calculates coverage metrics for each rule, giving you a clear picture of your app's overall accessibility maturity.

๐Ÿ“Š Overall Coverage: 44.6%
Compliance Level: NONE

๐Ÿ“‹ Coverage by Rule:
  โœ… Semantic Labels:  96% (26/27)
  โ„น๏ธ  Color Contrast:   NO DATA  โ“˜ 82 indeterminate
  โ„น๏ธ  Touch Targets:    NO DATA  โ“˜ 905 indeterminate
  โš ๏ธ  Keyboard Nav:    66% (8/12)  โ€” CRITICAL
  โš ๏ธ  Focus Order:     0%  (0/1)   โ€” CRITICAL
  โš ๏ธ  Non-text Content: 15% (7/45) โ€” CRITICAL
  โ„น๏ธ  Resize Text:      NO DATA (no hardcoded scale โ€” โœ… correct)

Features

  • โœ… WCAG 2.2 alignment โ€” coverage metrics, not one-off issue lists.
  • โœ… Honest AST analysis with package:analyzer โ€” no regex, no guessing.
  • โœ… Indeterminate accounting โ€” values from themes or runtime variables are reported separately and never inflate pass/fail ratios.
  • โœ… Built-in spec, zero setup โ€” the WCAG 2.2 rules ship inside the package. You don't copy any YAML file.
  • โœ… Optional ethos.yaml โ€” teach Ethos about your own design-system widgets and colors in five minutes.
  • โœ… Theme-aware contrast โ€” resolves theme.textTheme.X automatically from your ThemeData, and accepts explicit color_aliases for custom style variables.
  • โœ… Deep analysis mode (--deep) โ€” uses AnalysisContextCollection to resolve cross-file references, class hierarchies, and type information. Emits Stream<AnalysisProgress> events for live progress. Falls back to standard mode automatically if the project is not ready.
  • โœ… Pluggable detector registry โ€” add or replace rules without touching the core engine.
  • โœ… Watch mode (ethos watch) โ€” re-analyzes only the changed file on every save, prints the full report with โ–ฒ/โ–ผ deltas, and highlights new findings.
  • โœ… Flutter test integration โ€” package:ethos/ethos_test.dart provides expect(report, meetsAccessibilityLevel(WcagLevel.a)) matchers and EthosTestHelper.analyzeSource(dartCode) for inline detector unit tests.

Installation

As a CLI

dart pub global activate ethos

# Verify installation
ethos --version

# From inside your Flutter project:
cd ./my_flutter_app
ethos

As a library

dependencies:
  ethos: ^0.8.2
import 'package:ethos/ethos.dart';

void main() async {
  final analyzer = await CoverageAnalyzer.forProject('./my_flutter_app');
  final report  = await analyzer.analyze();
  print('Coverage:   ${report.overallCoverage}%');
  print('Compliance: ${report.complianceLevel}');
}

You do not copy any spec file. The built-in WCAG 2.2 spec lives inside the package.


Quick start (local development)

git clone https://github.com/gearscrafter/ethos.git
cd ethos
dart pub get

# Run against the bundled fixtures
dart run example/main.dart

# Install locally as a global command
dart pub global activate --source path .

# Verify installation
ethos --version

# Analyze from inside a Flutter project โ€” no -p needed
cd ./my_flutter_app
ethos
ethos --deep -v

Standard vs Deep analysis

Ethos has two analysis modes:

Standard Deep (--deep)
Speed Fast (seconds) Slower (10โ€“60s)
Cross-file resolution โœ— โœ…
Class hierarchy traversal โœ— โœ…
Variable type resolution โœ— โœ…
Progress stream โœ— โœ…
Requires flutter pub get โœ— โœ… (auto-detects)

Standard mode is ideal for quick checks and CI gates. Deep mode is for comprehensive audits โ€” it finds widgets that standard mode misses because they are defined in a different file from where they are used.

# Standard (from inside the project)
ethos

# Deep โ€” with live progress
ethos --deep -v

Deep mode falls back to standard automatically if the project context cannot be built (e.g. flutter pub get has not been run).

Deep mode as a library

final deepAnalyzer = await DeepAnalyzer.forProject('./my_app');

await for (final event in deepAnalyzer.analyze()) {
  switch (event) {
    case AnalysisLoadingContext(:final totalFiles):
      print('Loading $totalFiles files...');
    case AnalysisAnalyzingFile(:final current, :final total):
      print('[$current/$total]');
    case AnalysisWarning(:final message):
      print('โš ๏ธ  $message');
    case AnalysisComplete():
      final report = (event as AnalysisComplete).report;
      print(report.toJsonString());
    default:
      break;
  }
}

Getting started with ethos init

If your project uses a custom design system, the first thing to do after installing Ethos is run ethos init. It scans your project for custom widgets and unresolvable color expressions, then generates a starter ethos.yaml with everything pre-filled โ€” you just review and fill in the values it can't infer.

# from inside your project
ethos init

Example output:

๐Ÿ” Scanning ./my_app for custom widgets and color tokens...
   Scanned 189 Dart files

   Found 12 custom widget(s), 5 color expression(s)

โœ… Generated: ./my_app/ethos.yaml

๐Ÿ“ฆ Custom widgets (fill in role and label_arg):
   CircleIconBtn                  47 uses
   AppBtn                         23 uses
   WonderIllustration             18 uses
   AppHeader                       9 uses

๐ŸŽจ Color expressions (add hex values to enable contrast checks):
   $styles.text.body              31 uses
   $styles.colors.offWhite        14 uses

Next steps:
  1. Open ./my_app/ethos.yaml
  2. Set role: for each widget_alias
  3. Uncomment label_arg, size_guaranteed, keyboard_ready as needed
  4. Fill in hex values under color_aliases
  5. Run: ethos -v

The generated ethos.yaml looks like this โ€” nothing is invented, only discovered. Values that require human knowledge are left as comments:

# ethos.yaml โ€” generated by `ethos init`

widget_aliases:

  # CircleIconBtn โ€” used 47 times
  CircleIconBtn:
    role: button           # button | text | input โ€” REQUIRED
    # label_arg: ???       # e.g. semanticLabel, a11yLabel, label
    # size_guaranteed: true
    # keyboard_ready: true

  # AppBtn โ€” used 23 times
  AppBtn:
    role: button
    # label_arg: ???

color_aliases:
  # used 31 times
  # "$styles.text.body":
  #   foreground: "#REPLACE_ME"
  #   background: "#REPLACE_ME"

Once you fill in the values and re-run ethos -p ./my_app, the widgets that were previously invisible (indeterminate) will start contributing to your coverage score.


Live feedback with ethos watch

ethos watch performs an initial full scan and then re-analyzes only the file you just saved. Every change prints the full report with coverage deltas so you can see the impact of each edit immediately.

# from inside your project
ethos watch

# With deep analysis on each change:
ethos watch --deep

Example output after saving a file:

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
๐Ÿ”„  buttons.dart changed (14:23:05)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

โœ… Initial scan complete (189 files)

๐Ÿ“Š Overall:  71.4%  โ–ฒ +2.1%  ยท Compliance: A

โœ… Semantic Labels           85.0% (17/20)   โ–ฒ +5.0%
โ„น๏ธ  Color Contrast           100.0% (8/8)
โœ… Touch Targets             100.0% (8/8)
โš ๏ธ  Keyboard Nav              66.7% (8/12)   โ–ผ -3.3%
โœ… Focus Order                95.0% (19/20)
โš ๏ธ  Non-text Content          15.6% (7/45)   CRITICAL
โ„น๏ธ  Resize Text               NO DATA

๐Ÿ”Ž Findings in buttons.dart:
  โœ— line 42 โ€” GestureDetector: Tap gesture has no keyboard alternative

Watching for changes... (Ctrl+C to stop)

Watch mode observes lib/, test/, and example/ with a 300 ms debounce. Generated files (.g.dart, .freezed.dart) are ignored automatically.

Watch mode as a library

final engine = await WatchEngine.forProject('./my_app');

// Initial full scan
final baseline = await engine.initialScan(
  onProgress: (current, total, path) {
    print('[$current/$total] $path');
  },
);

// Re-analyze on file change
final (newReport, diff) = await engine.reanalyzeFile(changedPath);

print('Overall delta: ${diff.overallDelta > 0 ? "โ–ฒ" : "โ–ผ"} ${diff.overallDelta.abs().toStringAsFixed(1)}%');
print('New critical rules: ${diff.newCritical}');
print('Resolved rules: ${diff.resolvedCritical}');

Testing your Flutter project's accessibility

Ethos ships a separate test utilities library so you can write accessibility assertions directly inside your existing dart test or flutter_test suites.

Setup

Add Ethos as a dev dependency:

# pubspec.yaml
dev_dependencies:
  ethos: ^0.8.2
  test: ^1.24.0   # or flutter_test if you're in a Flutter project

Project-level accessibility test

Analyze your whole lib/ folder once in setUpAll and assert against the report. This is the recommended pattern for CI gates:

// test/accessibility_test.dart
import 'package:test/test.dart';
import 'package:ethos/ethos_test.dart';

void main() {
  group('Accessibility coverage', () {
    late CoverageReport report;

    setUpAll(() async {
      report = await EthosTestHelper.analyzeProject('lib/');
    });

    test('meets WCAG Level A', () {
      expect(report, meetsAccessibilityLevel(WcagLevel.a));
    });

    test('semantic labels above 80%', () {
      expect(report, hasRuleCoverage(
        'wcag_1_3_1_semantics_label',
        greaterThan(80),
      ));
    });

    test('no critical failures', () {
      expect(report, hasNoCriticalFailures());
    });

    test('no image accessibility issues', () {
      expect(report, hasNoFindingsFor('wcag_1_1_1_non_text_content'));
    });
  });
}

Run it with:

dart test test/accessibility_test.dart
# or
flutter test test/accessibility_test.dart

Exit code 1 when any assertion fails โ€” plugs straight into CI.

Detector unit tests with inline source

EthosTestHelper.analyzeSource parses a Dart snippet in memory without touching the filesystem. Use it to test specific widgets in isolation:

import 'package:test/test.dart';
import 'package:ethos/ethos_test.dart';

void main() {
  group('Icon accessibility', () {
    test('Icon with semanticLabel passes', () async {
      final report = await EthosTestHelper.analyzeSource(
        "Icon(Icons.search, semanticLabel: 'Search artifacts')",
      );
      expect(report, passesRule('wcag_1_1_1_non_text_content'));
    });

    test('Icon without semanticLabel fails', () async {
      final report = await EthosTestHelper.analyzeSource('Icon(Icons.close)');
      final cov = report.coverage['wcag_1_1_1_non_text_content'];
      expect(cov?.findings, isNotEmpty);
    });
  });

  group('GestureDetector accessibility', () {
    test('with Semantics ancestor passes', () async {
      final report = await EthosTestHelper.analyzeSource('''
        Semantics(
          label: 'Open profile',
          child: GestureDetector(
            onTap: () => navigate(),
            child: Icon(Icons.person),
          ),
        )
      ''');
      expect(report, passesRule('wcag_1_3_1_semantics_label'));
    });

    test('without Semantics fails', () async {
      final report = await EthosTestHelper.analyzeSource('''
        GestureDetector(onTap: () => navigate(), child: Icon(Icons.settings))
      ''');
      final cov = report.coverage['wcag_1_3_1_semantics_label'];
      expect(cov?.findings, isNotEmpty);
    });
  });

  group('Color contrast', () {
    test('black on white passes', () async {
      final report = await EthosTestHelper.analyzeSource(
        "Text('Hello', style: TextStyle(color: Colors.black, backgroundColor: Colors.white))",
      );
      expect(report, passesRule('wcag_1_4_3_contrast_minimum'));
    });

    test('light grey on white fails', () async {
      final report = await EthosTestHelper.analyzeSource(
        "Text('Hello', style: TextStyle(color: Color(0xFFCCCCCC), backgroundColor: Color(0xFFFFFFFF)))",
      );
      final cov = report.coverage['wcag_1_4_3_contrast_minimum'];
      expect(cov?.findings, isNotEmpty);
    });
  });
}

Available matchers

Matcher Description
meetsAccessibilityLevel(WcagLevel.a) Overall coverage meets Level A (โ‰ฅ70%), AA (โ‰ฅ85%), or AAA (โ‰ฅ95%)
passesRule('rule_id') A specific rule is not below its critical threshold
hasRuleCoverage('rule_id', greaterThan(80)) Rule coverage satisfies any Matcher
hasNoCriticalFailures() No rule is below its critical threshold
hasNoFindingsFor('rule_id') No findings for a specific rule

One-liner smoke test

For a quick CI check with no setup:

test('no critical a11y failures', () async {
  await EthosTestHelper.expectNoCriticalFailures('lib/');
});

Throws a descriptive TestFailure listing every critical rule if any exist.

Ethos works out of the box โ€” the built-in spec already covers Flutter's standard widgets (GestureDetector, InkWell, IconButton, TextField, etc.).

Most real apps wrap controls in their own design-system components and define colors in a custom style object. Drop an ethos.yaml next to your pubspec.yaml to teach Ethos about them:

# ethos.yaml โ€” OPTIONAL. Ethos auto-detects it; no flag needed.

widget_aliases:
  # Key = your widget's class name exactly as written in code.
  CircleIconBtn:
    role: button             # button | text | input
    label_arg: semanticLabel # which arg carries the accessible label
    size_guaranteed: true    # already wraps a >= 48ร—48 target internally?
    keyboard_ready: true     # keyboard-operable out of the box?

  AppButton:
    role: button
    label_arg: a11yLabel

color_aliases:
  # Teach Ethos about your design-system color expressions so the contrast
  # rule can compute real WCAG ratios instead of reporting "indeterminate".
  "$styles.text.body":
    foreground: "#212121"   # required โ€” the text color
    background: "#FFFFFF"   # optional โ€” the default background color

  "$styles.colors.primary":
    foreground: "#1565C0"

# Optional: tighten a threshold without rewriting the spec.
# rule_overrides:
#   wcag_1_4_3_contrast_minimum:
#     critical_threshold: 95

widget_aliases

Field Detector Effect
role: button Semantic Labels, Keyboard, Touch Target Widget counts as an interactive control.
label_arg Semantic Labels Look for the semantic label in this argument.
size_guaranteed Touch Target Size Auto-PASS โ€” already โ‰ฅ 48ร—48 internally.
keyboard_ready Keyboard Accessibility Auto-PASS โ€” keyboard-operable out of the box.

color_aliases

Maps a design-system style expression to concrete hex colors. Both #RRGGBB and #AARRGGBB formats are accepted. When background is omitted, the element remains indeterminate.

No ethos.yaml? Ethos still runs on the built-in spec. For a vanilla Flutter project that's already useful; for a project with a design system, the aliases make all the difference โ€” and deep mode can discover many of them automatically.


Supported rules

Seven built-in rules, all backed by RecursiveAstVisitor on real Dart AST. Deep mode runs enhanced versions of rules 1 and 2.

1. Semantic Labels โ€” wcag_1_3_1_semantics_label (WCAG 1.3.1 ยท Level A)

Custom interactive widgets must have an accessible label.

  • In scope: GestureDetector, InkWell, InkResponse with tap-like gestures, plus any role: button alias from ethos.yaml.
  • Pass: wrapped in Semantics(label: '<non-empty literal>') as ancestor or descendant; or the alias label_arg is a non-empty literal.
  • Indeterminate: label is a variable, interpolation, or runtime call.
  • Excluded automatically: excludeFromSemantics: true, drag/pan-only gestures, onTap: () {} (block-parent), tap-to-dismiss patterns.
  • Deep mode: also follows widget definitions across files and detects cross-method Semantics wrappers.
// โœ… PASS โ€” Semantics as ancestor
Semantics(
  label: 'Open profile',
  child: GestureDetector(onTap: () {}, child: Icon(Icons.person)),
)

// โœ… PASS โ€” Semantics as descendant also works
GestureDetector(
  onTap: () => navigate(),
  child: Semantics(label: 'Go to settings', child: Icon(Icons.settings)),
)

// โŒ FAIL
GestureDetector(onTap: () => navigate(), child: Icon(Icons.settings))

2. Minimum Color Contrast โ€” wcag_1_4_3_contrast_minimum (WCAG 1.4.3 ยท Level AA)

Text must have at least 4.5:1 contrast (3:1 for large text โ‰ฅ 18 pt). Resolution is attempted in layers:

  1. Inline literals โ€” TextStyle(color: Color(0xFF...), backgroundColor: ...).
  2. ThemeData extraction โ€” resolves theme.textTheme.bodyLarge etc.
  3. color_aliases โ€” resolves design-system expressions from ethos.yaml.
  4. Deep mode only โ€” follows variable references across files.
// โœ… PASS โ€” ratio 21:1
Text('Hello', style: TextStyle(color: Colors.black, backgroundColor: Colors.white))

// โŒ FAIL โ€” ratio ~1.6:1
Text('Hello', style: TextStyle(color: Color(0xFFCCCCCC), backgroundColor: Colors.white))

// โ“˜ INDETERMINATE in standard mode; resolved in deep mode
Text('Hello', style: TextStyle(color: bodyColor))  // bodyColor defined elsewhere

3. Touch Target Size โ€” wcag_2_5_5_target_size_enhanced (WCAG 2.5.5 ยท Level AAA)

Interactive elements must be at least 48ร—48 logical pixels.

  • Auto-pass: IconButton, FloatingActionButton; aliases with size_guaranteed: true.
  • Verifiable: custom widget in a SizedBox/Container with literal dimensions.
  • Indeterminate: size from a variable or intrinsic content.

4. Keyboard Accessibility โ€” wcag_2_1_1_keyboard (WCAG 2.1.1 ยท Level A)

All interactive functionality must be reachable by keyboard.

  • Pass: Material controls; GestureDetector under Focus/FocusScope/ Shortcuts/KeyboardListener; aliases with keyboard_ready: true.
  • Fail: GestureDetector.onTap with no keyboard path.
  • Excluded: excludeFromSemantics: true (visual-only wrappers).

5. Focus Order โ€” wcag_2_4_3_focus_order (WCAG 2.4.3 ยท Level A)

Multi-input layouts must declare explicit focus management.

  • In scope: Form widgets, or layouts with 2+ focusable inputs.
  • Pass: declares FocusNode, FocusScope, FocusTraversalGroup, or autofocus: true.

6. Non-text Content โ€” wcag_1_1_1_non_text_content (WCAG 1.1.1 ยท Level A)

All images and icons that convey information must have a text alternative. Purely decorative content must be explicitly excluded from the semantic tree.

  • In scope: Image, Image.asset, Image.network, Image.file, SvgPicture.asset, SvgPicture.network, Icon.
  • Pass: wrapped in Semantics(label: '...'), or excludeFromSemantics: true (decorative); for Icon, a non-empty literal semanticLabel: argument.
  • Indeterminate: label is a runtime variable.
  • Fail: no label and no explicit decoration marker.
// โœ… PASS โ€” informative image with label
Semantics(
  label: 'Photo of the Colosseum',
  child: Image.network(url),
)

// โœ… PASS โ€” decorative image explicitly excluded
Image.asset('assets/bg.png', excludeFromSemantics: true)

// โœ… PASS โ€” icon with semantic label
Icon(Icons.search, semanticLabel: 'Search artifacts')

// โŒ FAIL โ€” image without any accessibility annotation
Image.network(artifactUrl)

// โŒ FAIL โ€” icon without semanticLabel
Icon(Icons.close)

7. Resize Text โ€” wcag_1_4_4_resize_text (WCAG 1.4.4 ยท Level AA)

Text must be resizable up to 200% without loss of content. Hardcoding textScaleFactor or textScaler to a fixed value ignores system font-size preferences set by users with visual impairments.

  • In scope: only Text and MediaQuery widgets that explicitly set textScaleFactor or textScaler. Widgets without these args are correct by default and are not counted.
  • Pass: textScaleFactor: null (inherits system), or variable-based scaling that cannot be verified statically.
  • Fail: literal textScaleFactor, TextScaler.noScaling, or TextScaler.linear(<literal>).
// โœ… PASS โ€” inherits system preference (the right default)
Text('Hello')

// โœ… PASS โ€” explicitly null (same as default)
Text('Hello', textScaleFactor: null)

// โŒ FAIL โ€” locks font size, ignores accessibility settings
Text('Hello', textScaleFactor: 1.0)

// โŒ FAIL โ€” forces no scaling
Text('Hello', textScaler: TextScaler.noScaling)

NO DATA is a good sign for Resize Text. It means your project does not override text scaling anywhere โ€” which is exactly what WCAG requires.


CLI reference

-p is optional in all commands โ€” when omitted, Ethos uses the current working directory. Run cd my_flutter_app first and then:

ethos                        # standard analysis of current directory
ethos --deep -v              # deep mode with progress
ethos init                   # generate ethos.yaml here
ethos watch                  # watch for changes here
ethos watch --deep           # watch + deep

Or pass -p explicitly from anywhere:

ethos -p <project-path> [options]
ethos init -p <project-path>
ethos watch -p <project-path>

Options (analyze):
  -p, --project-path   Path to the Flutter project (default: current directory)
  -c, --config         Path to a custom ethos.yaml (default: auto-detect)
  -r, --report-type    Output format: human | json | markdown | coverage
                       (default: human)
  -o, --output         Write report to this file instead of stdout
  -d, --deep           Deep analysis: resolves types and cross-file references.
                       Slower but more precise. Requires `flutter pub get`.
                       Falls back to standard mode if project is not ready.
  -v, --verbose        Show progress details (written to stderr)
  -h, --help           Show this help

Options (watch):
  -p, --project-path   Path to the Flutter project (default: current directory)
  -d, --deep           Use deep analysis on each change
  -h, --help           Show this help

More examples:
  ethos -p ./my_app -r json -o report.json
  ethos -p ./my_app -r markdown -o report.md
  ethos init -p ./my_app -o path/to/ethos.yaml

Verbose logs go to stderr so ethos -r json | jq works cleanly. Exit code 1 when any rule is below its critical threshold โ€” useful as a CI gate.


Compliance levels

Level Minimum coverage Description
AAA โ‰ฅ 95% Enhanced accessibility
AA โ‰ฅ 85% Strong accessibility (typical target)
A โ‰ฅ 70% Basic accessibility
NONE < 70% Does not meet minimum standards

Library API reference

// Standard entry point
final analyzer = await CoverageAnalyzer.forProject('./my_app');
final report = await analyzer.analyze();

// Deep entry point
final deepAnalyzer = await DeepAnalyzer.forProject('./my_app');
await for (final event in deepAnalyzer.analyze()) {
  if (event is AnalysisComplete) {
    final report = (event as AnalysisComplete).report;
    print(report.toJsonString());
  }
}

// Watch entry point
final engine = await WatchEngine.forProject('./my_app');
final baseline = await engine.initialScan();
final (newReport, diff) = await engine.reanalyzeFile(changedPath);

// Output
print(report.overallCoverage);   // double 0โ€“100
print(report.complianceLevel);   // 'A' | 'AA' | 'AAA' | 'NONE'
print(report.toJsonString());    // JSON for CI pipelines

Two separate import paths keep test utilities out of production builds:

import 'package:ethos/ethos.dart';       // main API โ€” use in production code
import 'package:ethos/ethos_test.dart';  // matchers + helpers โ€” use in tests only

Architecture

ethos/
โ”œโ”€โ”€ bin/
โ”‚   โ””โ”€โ”€ analyze.dart               # CLI (analyze + init + watch subcommands)
โ”œโ”€โ”€ lib/
โ”‚   โ”œโ”€โ”€ ethos.dart                 # Public barrel export
โ”‚   โ””โ”€โ”€ src/
โ”‚       โ”œโ”€โ”€ models/
โ”‚       โ”‚   โ”œโ”€โ”€ spec.dart          # Spec, Rule, WidgetAlias, WidgetRole
โ”‚       โ”‚   โ”œโ”€โ”€ ethos_config.dart  # EthosConfig, ColorAlias, RuleOverride
โ”‚       โ”‚   โ””โ”€โ”€ coverage_report.dart  # CoverageReport, RuleCoverage, Finding
โ”‚       โ”œโ”€โ”€ specs/v1/
โ”‚       โ”‚   โ”œโ”€โ”€ wcag_2_2.yaml          # Source spec โ€” edit this
โ”‚       โ”‚   โ””โ”€โ”€ wcag_2_2_embedded.dart # Generated constant โ€” do not edit
โ”‚       โ””โ”€โ”€ analyzer/
โ”‚           โ”œโ”€โ”€ coverage_analyzer.dart  # Standard engine
โ”‚           โ”œโ”€โ”€ spec_loader.dart
โ”‚           โ”œโ”€โ”€ detector_registry.dart
โ”‚           โ”œโ”€โ”€ rule_detector.dart      # RuleDetector interface
โ”‚           โ”œโ”€โ”€ ast/widget_visitor.dart
โ”‚           โ”œโ”€โ”€ utils/
โ”‚           โ”‚   โ”œโ”€โ”€ color_resolver.dart
โ”‚           โ”‚   โ””โ”€โ”€ theme_extractor.dart
โ”‚           โ”œโ”€โ”€ detectors/              # 7 standard detectors
โ”‚           โ”‚   โ”œโ”€โ”€ semantic_labels_detector.dart
โ”‚           โ”‚   โ”œโ”€โ”€ contrast_detector.dart
โ”‚           โ”‚   โ”œโ”€โ”€ touch_target_detector.dart
โ”‚           โ”‚   โ”œโ”€โ”€ keyboard_detector.dart
โ”‚           โ”‚   โ”œโ”€โ”€ focus_order_detector.dart
โ”‚           โ”‚   โ”œโ”€โ”€ non_text_content_detector.dart
โ”‚           โ”‚   โ””โ”€โ”€ resize_text_detector.dart
โ”‚           โ”œโ”€โ”€ deep/
โ”‚           โ”‚   โ”œโ”€โ”€ deep_analyzer.dart       # Deep engine (Stream)
โ”‚           โ”‚   โ”œโ”€โ”€ deep_detector.dart       # DeepDetector interface
โ”‚           โ”‚   โ”œโ”€โ”€ analysis_progress.dart   # Sealed class: 6 event types
โ”‚           โ”‚   โ”œโ”€โ”€ resolved_file.dart       # ResolvedFile + ProjectIndex
โ”‚           โ”‚   โ””โ”€โ”€ detectors/
โ”‚           โ”‚       โ”œโ”€โ”€ cross_file_semantic_labels_detector.dart
โ”‚           โ”‚       โ””โ”€โ”€ resolved_contrast_detector.dart
โ”‚           โ”œโ”€โ”€ watch/
โ”‚           โ”‚   โ””โ”€โ”€ watch_engine.dart        # Incremental cache + ReportDiff
โ”‚           โ””โ”€โ”€ init/
โ”‚               โ”œโ”€โ”€ widget_discovery.dart    # Scans for custom widgets/colors
โ”‚               โ””โ”€โ”€ ethos_yaml_generator.dart # Generates starter ethos.yaml
โ”œโ”€โ”€ example/
โ”‚   โ”œโ”€โ”€ main.dart
โ”‚   โ””โ”€โ”€ fixtures/
โ”‚       โ”œโ”€โ”€ ethos.yaml
โ”‚       โ””โ”€โ”€ lib/
โ””โ”€โ”€ tool/
    โ””โ”€โ”€ embed_spec.dart

Roadmap

v1.0.0

  • Animation preferences detector (wcag_2_3_1) โ€” flag AnimationController and transitions that don't respect MediaQuery.disableAnimations.
  • Configurable rule subset โ€” run only the rules you care about.
  • Stable public API with full WCAG 2.2 Level AA coverage.

Contributing

Contributions are welcome. High-value areas:

  • Additional WCAG 2.2 detectors.
  • Improved theme/$styles resolution for the contrast rule.
  • CI/CD integration examples (GitHub Actions, GitLab CI).

License

Apache-2.0 โ€” see LICENSE.

Author

@gearscrafter โ€” Mobile Developer.

Resources


Made with โค๏ธ for inclusive Flutter apps.

Libraries

ethos
Ethos โ€” Measure accessibility coverage in Flutter apps using WCAG 2.2 specifications with Spec-Driven Development.
ethos_test
Test utilities for Ethos โ€” matchers and helpers for use in dart test or flutter_test suites.