diff --git a/.github/workflows/flutterguard.yml b/.github/workflows/flutterguard.yml
index 3351164..4ed152c 100644
--- a/.github/workflows/flutterguard.yml
+++ b/.github/workflows/flutterguard.yml
@@ -19,27 +19,24 @@ jobs:
- uses: dart-lang/setup-dart@v1
with:
- sdk: stable
+ sdk: "3.11.5"
- - name: Install workspace dependencies
+ - name: Install dependencies
run: dart pub get
- - name: Bootstrap workspace
- run: dart run melos bootstrap
+ - name: Analyze
+ run: dart analyze
- - name: Analyze workspace
- run: dart run melos run analyze
-
- - name: Test CLI
- run: dart run melos run test:cli
+ - name: Test
+ run: dart test
- name: Scan demo with current checkout
- run: dart run flutterguard_cli:flutterguard scan examples/scan_demo --format json --output .flutterguard/ci --fail-on high --min-score 80 --no-color
+ run: dart run bin/flutterguard.dart scan example --format json --output .flutterguard/ci --fail-on high --no-color
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: flutterguard-report-${{ matrix.os }}
- path: examples/scan_demo/.flutterguard/ci/report.json
+ path: example/.flutterguard/ci/report.json
if-no-files-found: error
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d1623f5..d704bed 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,12 +16,7 @@ jobs:
- uses: dart-lang/setup-dart@v1
with:
sdk: "3.11.5"
- - run: dart pub get
- - run: dart run melos bootstrap
- - run: dart run melos run analyze
- - run: dart run melos run test:cli
- - run: dart pub publish --server https://pub.dev --dry-run
- working-directory: packages/flutterguard_cli
+ - run: bash scripts/release_preflight.sh
build:
needs: validate
diff --git a/.gitignore b/.gitignore
index 2e12462..10c884e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,17 +11,11 @@ build/
# CLI
.flutterguard/
flutterguard
+flutterguard.exe
+dist/
# IDE
-.idea/*/
-.idea/*.xml
-.idea/*.iml
-!.idea/modules.xml
-!.idea/misc.xml
-!.idea/vcs.xml
-!.idea/runConfigurations/
-!.idea/runConfigurations/**
-!.idea/.name
+.idea/
.vscode/
*.iml
@@ -37,6 +31,10 @@ doc/api/
# Coverage
.coverage
-# Project evolution spec
-docs/
-CONTEXT.md
+# Session artifacts
+/flutterguardb.md
+/session-ses_*.md
+
+# Duplicate local maintenance transcript; keep flutterguardb.md as canonical.
+/session-ses_07b1.md
+
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 85cdef9..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-# Local generated files
-/workspace.xml
-/shelf/
-/caches/
-/libraries/
diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index 707417d..0000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-flutterguard
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index c08a2df..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 0aa6fe4..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/melos_bootstrap.xml b/.idea/runConfigurations/melos_bootstrap.xml
deleted file mode 100644
index 0365420..0000000
--- a/.idea/runConfigurations/melos_bootstrap.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/melos_clean.xml b/.idea/runConfigurations/melos_clean.xml
deleted file mode 100644
index 82bd956..0000000
--- a/.idea/runConfigurations/melos_clean.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/melos_run_analyze.xml b/.idea/runConfigurations/melos_run_analyze.xml
deleted file mode 100644
index 9ba7da1..0000000
--- a/.idea/runConfigurations/melos_run_analyze.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/melos_run_format.xml b/.idea/runConfigurations/melos_run_format.xml
deleted file mode 100644
index 27d94cf..0000000
--- a/.idea/runConfigurations/melos_run_format.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/melos_run_test.xml b/.idea/runConfigurations/melos_run_test.xml
deleted file mode 100644
index 23f9c3d..0000000
--- a/.idea/runConfigurations/melos_run_test.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/melos_run_test_cli.xml b/.idea/runConfigurations/melos_run_test_cli.xml
deleted file mode 100644
index 559a0b1..0000000
--- a/.idea/runConfigurations/melos_run_test_cli.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/run_trace_demo.xml b/.idea/runConfigurations/run_trace_demo.xml
deleted file mode 100644
index d339bc0..0000000
--- a/.idea/runConfigurations/run_trace_demo.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/scan_demo.xml b/.idea/runConfigurations/scan_demo.xml
deleted file mode 100644
index b753f2d..0000000
--- a/.idea/runConfigurations/scan_demo.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.pubignore b/.pubignore
new file mode 100644
index 0000000..abe1c37
--- /dev/null
+++ b/.pubignore
@@ -0,0 +1,19 @@
+.github/
+.idea/
+.dart_tool/
+.flutterguard/
+**/AGENTS.md
+scripts/
+test/
+/flutterguard.yaml
+analysis_options.yaml
+pubspec.lock
+dist/
+/flutterguard
+/flutterguard.exe
+*.iml
+CONTEXT.md
+/flutterguardb.md
+/session-ses_*.md
+/flutterguardb.md
+/session-ses_07b1.md
diff --git a/AGENTS.md b/AGENTS.md
index 5ade6d7..ac2d80b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,89 +1,84 @@
-# flutterguard — IoT Flutter Static Analysis CLI
+# flutterguard — Agent Guide
## Identity
-IoT/smart home Flutter project static analysis CLI plugin. NOT an observability SDK or APM tool.
-
-## Architecture
-- **Monorepo**: melos, 1 package + 1 example
-- **Path A (ACTIVE)**: `flutterguard_cli` — all new features
-- **Path B (ARCHIVED)**: `flutterguard_core`, `flutterguard_dio`, `flutterguard_flutter` — in `archive/` for future reference
-
-## Package Hierarchy
-| Package | Status | Depends On | Depended By |
-|---------|--------|------------|-------------|
-| flutterguard_cli | ACTIVE | args, analyzer, glob, path, yaml | — |
-| scan_demo | — | (none, scan target) | — |
-
-## Key Commands
-| Command | Purpose |
-|---------|---------|
-| `dart run melos bootstrap` | Install workspace dependencies |
-| `dart run melos run analyze` | dart analyze on all packages |
-| `dart run melos run test:cli` | CLI tests only (61 tests) |
-| `flutterguard scan []` | Run scan on a project (path defaults to current dir) |
-| `flutterguard scan --format json --fail-on high` | JSON output with CI gate |
-| `flutterguard scan --changed-only` | Incremental scan of git-changed files |
-| `flutterguard rules` / `flutterguard explain ` | List/describe rules |
-| `dart compile exe ... -o flutterguard` | Compile native binary |
-
-## CI & Automation
-- `.github/workflows/flutterguard.yml` — CI with ubuntu/macos/windows matrix
-- `scripts/compile.sh` / `scripts/compile.ps1` — cross-platform native binary compilation
-- `scripts/scan_ci.sh` / `scripts/scan_ci.ps1` — local CI gate scripts
-
-## CLI Entry Point
-`packages/flutterguard_cli/bin/flutterguard.dart`
-
-Supports positional path: `flutterguard scan ./my_project` (no `-p` required). Project auto-discovery walks up from CWD to find `flutterguard.yaml`, `pubspec.yaml`, or `lib/`. Supports --changed-only incremental scan and rule introspection (rules/explain).
-
-Wired rules (11 rule classes, 13 rule IDs):
-- Standards: LargeUnitsRule (3 IDs), MissingConstConstructorRule, PubspecSecurityRule
-- Performance: LifecycleResourceRule
-- Architecture: LayerViolationRule, ModuleViolationRule, CircularDependencyRule
-- IoT: DeviceLifecycleRule, MqttConnectionRule, BleScanningRule, IotSecurityRule
-
-## Source Layout
+
+FlutterGuard is an executable-only static analysis CLI for IoT/smart-home
+Flutter projects. It is not an observability SDK, APM product, hosted service,
+or general-purpose style linter.
+
+## Repository
+
+This is one Dart package rooted at the repository root.
+
+```text
+bin/ executable entry point
+lib/src/ scan, config, report, and rule implementation
+test/ contract and detector tests
+example/ CI scan target
+doc/ architecture and external contract
+scripts/ native release packaging
```
-packages/flutterguard_cli/lib/src/
- config_loader.dart # YAML → ScanConfig typedefs (11 rule configs + architecture)
- scan_context.dart # Project/all/target files and scan mode
- source_workspace.dart # Shared source/AST cache + scan diagnostics
- import_graph.dart # Shared resolved Dart import graph
- boundary_engine.dart # Shared layer/module boundary analysis
- file_collector.dart # Glob file discovery
- project_resolver.dart # Project auto-discovery (walk-up flutterguard.yaml / pubspec.yaml / lib/)
- static_issue.dart # StaticIssue + RiskLevel + IssueDomain + Priority
- report_generator.dart # Table + JSON output + score, --no-color support
- domain.dart # IssueDomain enum (architecture/performance/standards)
- priority.dart # Priority enum (p0/p1/p2)
- path_utils.dart # Cross-platform path/glob helpers (p.Context abstraction)
- import_utils.dart # Dart import resolution against collected files
- source_utils.dart # Analyzer offset → line number conversion
- rule_meta.dart # Rule metadata for rules/explain
- rules/
- catalog.dart # Rule metadata + execution source of truth
- registry.dart # RuleRegistry for all 13 rule IDs
- large_units.dart # large_file, large_class, large_build_method
- lifecycle_resource.dart # lifecycle_resource_not_disposed
- layer_violation.dart # layer_violation (architecture layer breaches)
- module_violation.dart # module_violation (cross-module breaches)
- circular_dependency.dart # circular_dependency (file-level cycles)
- missing_const_constructor.dart # missing_const_constructor
- iot_security.dart # iot_security (hardcoded secrets, cleartext MQTT/HTTP, insecure BLE)
- device_lifecycle.dart # device_lifecycle (init/teardown pair checks)
- mqtt_connection.dart # mqtt_connection (MQTT connect/disconnect, broker URLs)
- ble_scanning.dart # ble_scanning (BLE startScan/stopScan, timeout)
- pubspec_security.dart # pubspec_security (unbounded deps, deprecated packages)
+
+Do not reintroduce Melos, `packages/`, archived runtime packages, a plugin
+system, or a public scanner library API.
+
+## Commands
+
+```bash
+dart pub get
+dart format bin lib test
+dart analyze
+dart test
+dart run bin/flutterguard.dart scan example --format json --no-color
+dart compile exe bin/flutterguard.dart -o flutterguard
+dart pub publish --dry-run
```
-## Spec
-Single source of truth: `docs/FLUTTERGUARD_SPEC.md` — read before implementing any feature.
-
-## Maintenance Rules
-1. New rule: spec entry → config typedef → rule class → fixture → test → wire into rules/catalog.dart
-2. Always run `melos run analyze` + `melos run test:cli` before committing
-3. Do NOT modify archived packages (core/dio/flutter) — they are frozen references
-4. Do NOT add Flutter widgets, web/cloud infra, or SaaS SDKs
-5. Output format defaults to `table`. JSON available via `--format=json`
-6. Architecture rules require explicit `architecture.layers` / `architecture.modules` in flutterguard.yaml
-7. CLI supports positional path (`flutterguard scan ./project`) and `--no-color` flag
+## Architecture invariants
+
+- `ScanContext` carries project/all/target files, scan mode, config, and the
+ shared `SourceWorkspace`.
+- `SourceWorkspace` owns source reads, AST parsing, line info, and diagnostics.
+- Architecture rules share one `ImportGraph`.
+- Layer and module enforcement share `BoundaryRule` and
+ `DependencyBoundaryEngine`.
+- `RuleRegistry.registrations` is the only metadata/default/execution registry.
+- Rules receive effective generic `RuleConfig`; rule-specific defaults live in
+ `RuleDefinition.defaultOptions`.
+- CLI and JSON/SARIF are the supported integration boundary. `lib/src` is
+ private implementation.
+
+## Product surface
+
+Supported command families are `scan`, `baseline create`, `config init|check`,
+and `rules [rule-id]`.
+
+The canonical finding taxonomy is `ruleId + severity + domain`. Do not add
+priority, score, confidence, or compatibility aliases. Framework is descriptive
+metadata, not a global configuration switch.
+
+Keep changed-only scanning, inline suppression, baseline filtering, JSON, and
+SARIF behavior covered by tests.
+
+## Adding or changing a rule
+
+1. Implement or extend a detector under `lib/src/rules/`.
+2. Add exactly one registration and definition in `rules/registry.dart`.
+3. Add positive, negative, disabled, and output-contract coverage as needed.
+4. Update `doc/FLUTTERGUARD_SPEC.md` only for external contract changes.
+5. Run the full verification commands above.
+
+Do not add generic size, formatting, or missing-const checks; those belong to
+Dart lints or dedicated complexity tooling.
+
+## Documentation hierarchy
+
+- `EVOLUTION_PLAN.md` is the local checkpoint and next-step execution order.
+- This file defines repository-wide constraints.
+- Nested `AGENTS.md` files contain only directory-specific responsibilities.
+- `doc/ARCHITECTURE.md` defines internal boundaries.
+- `doc/FLUTTERGUARD_SPEC.md` defines external behavior.
+- `README.md` is the user onboarding document.
+
+When behavior changes, update the narrowest authoritative document rather than
+copying the same contract across every level.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87b9687..ebb9283 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,51 @@
# Changelog
-## Unreleased
-
+## 0.7.1 (2026-07-22)
+
+- **rule:** Added OverlayEntry to lifecycle resource detection.
+- **rule:** Upgraded `iot_security` from line-based regex to AST visitor
+ traversal (`VariableDeclaration`, `NamedExpression`, `StringLiteral` nodes).
+- **rule:** Upgraded `ble_scanning` timeout detection from string matching to
+ parameter inspection and `NamedExpression` AST traversal.
+- **test:** Added negative, disabled, suppression, and report-contract coverage
+ for `iot_security`, `ble_scanning`, and `lifecycle_resource` rules (34 tests).
+- **ci:** Added `scripts/release_preflight.sh` for compact release validation
+ and wired it into the release workflow.
+
+## 0.7.0 (2026-07-17)
+
+### Breaking simplification
+
+- Flattened the repository and publishable package into one Dart package root;
+ removed Melos, archived runtime packages, duplicate package metadata, IDE
+ project files, development wrappers, and historical planning documents.
+- Reduced the CLI to `scan`, `baseline create`, `config init|check`, and
+ `rules [rule-id]`.
+- Replaced typed per-rule config records and profile YAML copies with generic
+ registry-driven `RuleConfig` settings.
+- Merged rule metadata, defaults, and execution into one `RuleRegistry` and
+ merged layer/module implementations into `BoundaryRule`.
+- Removed generic size rules, `missing_const_constructor`, and the overlapping
+ `device_lifecycle` rule.
+- Assigned resource cleanup to the lifecycle rule and scan timeout checks to
+ `ble_scanning`; removed overlapping MQTT configuration and dependency-version
+ rules in favor of application configuration and ecosystem tooling.
+- Removed score, priority, confidence, framework switches, JSON compatibility
+ aliases, install diagnostics, issue export, config profiles/print, and
+ baseline stats/prune/check.
+- JSON reports now use schema `2.0.0` with canonical `ruleId`, `severity`, and
+ `domain` fields.
+- Reorganized the test suite by CLI, config, rules, and scanner contracts and
+ updated every hierarchical `AGENTS.md` description.
+
+## 0.6.0 (2026-07-16)
+
+- **cli:** Added 10 AST-first state-management maintainability rules for generic Flutter, Riverpod, Bloc, and Provider projects (23 total rule IDs).
+- **cli:** Added state-rule severity/allowlist/ignore-path controls, framework auto-detection, confidence/evidence output, and changed-only state-cycle analysis.
+- **reporting:** JSON and SARIF now include framework, confidence, and evidence while preserving existing fields and baseline fingerprints.
+- **fix:** Qualified state dependency graph nodes by file and resolved duplicate names through project imports.
+- **compatibility:** Aligned the published package SDK constraint and install examples with the Dart 3.11.5 release workflow.
+- **test:** Expanded the CLI suite to 83 tests with generic/Riverpod/Bloc/Provider fixtures, version synchronization, and release-hardening coverage.
- **fix:** Prevent successful no-op full scans and preserve setup-error exit codes in local CI scripts.
- **fix:** Anchor configuration policy to the target project and require explicitly selected config files.
- **ci:** Run analyze, tests, and the demo scan against the current checkout on the OS matrix.
diff --git a/CONFIGURATION_STRATEGY.md b/CONFIGURATION_STRATEGY.md
deleted file mode 100644
index 6ebbb76..0000000
--- a/CONFIGURATION_STRATEGY.md
+++ /dev/null
@@ -1,183 +0,0 @@
-# FlutterGuard Configuration Strategy
-
-FlutterGuard should be easy to start and precise when a project needs stricter
-architecture gates. The recommended product strategy is: use short CLI responses
-for immediate next steps, and keep this dedicated guide for the complete mental
-model.
-
-## Decision
-
-Use both surfaces, with different responsibilities:
-
-| Surface | Purpose | Content |
-|---------|---------|---------|
-| CLI help and scan responses | Immediate action | The next command, likely fix, and the shortest config path |
-| Dedicated configuration guide | Full explanation | Configuration levels, architecture examples, CI patterns, and troubleshooting |
-
-Do not put the full manual into CLI output. CLI text should answer "what should
-I do next?" in a few lines. The guide should answer "how should this be
-configured for my project?"
-
-## Configuration Levels
-
-### Level 0: Zero Config
-
-Best for first scans and demos.
-
-```bash
-flutterguard scan
-flutterguard scan ./my_flutter_app
-```
-
-Behavior:
-
-- Scans `lib/**`
-- Excludes generated/freezed/mock files
-- Runs all default non-boundary rules
-- Does not enforce layers/modules unless they are declared
-
-Use this level when the user wants a quick signal and has not agreed on project
-architecture boundaries yet. No YAML file is required.
-
-### Level 1: Basic Project Config
-
-Best for normal local development.
-
-```yaml
-include:
- - lib/**
-
-exclude:
- - lib/generated/**
- - lib/**.g.dart
- - lib/**.freezed.dart
- - lib/**.mocks.dart
-
-rules:
- large_file:
- enabled: true
- maxLines: 500
- large_class:
- enabled: true
- maxLines: 300
- large_build_method:
- enabled: true
- maxLines: 80
- lifecycle_resource:
- enabled: true
- missing_const_constructor:
- enabled: true
- device_lifecycle:
- enabled: true
- mqtt_connection:
- enabled: true
- ble_scanning:
- enabled: true
- maxScanDurationMs: 10000
- iot_security:
- enabled: true
- requireTls: true
- pubspec_security:
- enabled: true
-```
-
-Use this level when teams want stable thresholds or custom excludes.
-
-Create it with:
-
-```bash
-flutterguard init
-flutterguard config doctor
-```
-
-### Level 2: CI Gate Config
-
-Best for pull requests and release checks.
-
-```bash
-flutterguard scan . --format json --fail-on high --min-score 80
-```
-
-Recommended policy:
-
-- Start with `--fail-on high`
-- Add `--min-score 80` once the baseline is clean enough
-- Avoid `--fail-on low` early in adoption because it can create noisy rollouts
-- Run `flutterguard config doctor` before adding CI gates
-
-### Level 3: Architecture Config
-
-Best when the project has agreed boundaries.
-
-Create a starter template with:
-
-```bash
-flutterguard init --with-architecture
-```
-
-```yaml
-architecture:
- layers:
- - name: presentation
- path: lib/presentation/**
- allowed_deps: [domain, core]
- - name: domain
- path: lib/domain/**
- allowed_deps: [core]
- - name: data
- path: lib/data/**
- allowed_deps: [domain, core]
- - name: core
- path: lib/core/**
- allowed_deps: []
-
- modules:
- - name: mqtt_feature
- path: lib/features/mqtt/**
- allowed_deps: [shared]
- - name: ble_feature
- path: lib/features/ble/**
- allowed_deps: [shared]
- - name: shared
- path: lib/shared/**
- allowed_deps: []
-
- detect_cycles: true
- layer_violation:
- enabled: true
- module_violation:
- enabled: true
-```
-
-Important distinction:
-
-- `architecture.layers[].allowed_deps` must reference layer names.
-- `architecture.modules[].allowed_deps` must reference module names.
-- Layers describe technical direction such as presentation/domain/data/core.
-- Modules describe business or feature isolation such as mqtt/ble/shared.
-
-## CLI Response Policy
-
-CLI responses should stay short and operational:
-
-- `--help`: show common commands, option summary, and the four-level config path
-- full scan with no matched files: fail as a setup error and point to include/exclude patterns
-- changed-only scan with no relevant changes: succeed with an empty changed-mode report
-- config parse error: show the failing config key and expected value type
-- CI gate failure: show the failing threshold and direct the user to JSON output
-
-Avoid long explanations in command output. Point to this guide when the user
-needs examples or architecture detail.
-
-## Config Tooling
-
-The core configuration helpers are available as CLI commands:
-
-- `flutterguard init`: write a minimal `flutterguard.yaml`
-- `flutterguard init --with-architecture`: write a layered/module template
-- `flutterguard config print`: show the merged effective config
-- `flutterguard config doctor`: validate globs, unknown deps, empty matches, and
- architecture overlap
-
-These commands are preferable to adding more prose to `scan`, because they keep
-scan output focused while still making configuration discoverable.
diff --git a/EVOLUTION_PLAN.md b/EVOLUTION_PLAN.md
new file mode 100644
index 0000000..acb3fa5
--- /dev/null
+++ b/EVOLUTION_PLAN.md
@@ -0,0 +1,230 @@
+---
+checkpoint_date: 2026-07-21
+timezone: Asia/Shanghai
+branch: develop
+base: origin/develop
+head: 3f8f010
+target_version: 0.7.0
+target_json_schema: 2.0.0
+target_rule_count: 16
+checkpoint_state: convergence_committed_quality_phase2_implemented
+---
+
+# FlutterGuard 演进路线与续跑检查点
+
+## 用途
+
+本文件是当前重构的续跑入口。后续执行者应先读取根目录及目标目录的
+`AGENTS.md`,再读取本文件和 `git status`,从第一个未完成阶段继续,不要重新
+推导已经确认的架构方向。
+
+## 当前结论
+
+FlutterGuard 正从 Melos 历史多包仓库收敛为一个根目录 Dart CLI 包。目标不是
+保留旧内部 API,而是减少维护面,同时稳定 CLI、JSON/SARIF、changed-only、
+baseline 和抑制注释等用户可见能力。
+
+目标形态:
+
+- 根目录是唯一可发布的 `flutterguard_cli` 包;
+- 仅保留 `bin/`、`lib/`、`test/`、`example/`、`doc/` 和发布脚本;
+- CLI 只有 `scan`、`baseline create`、`config init|check`、`rules` 四个命令族;
+- `RuleRegistry.registrations` 是规则说明、默认值和执行入口的唯一数据源;
+- `ScanContext`、`SourceWorkspace` 和共享 `ImportGraph` 是扫描内核边界;
+- JSON 使用 schema `2.0.0`,只保留规范字段名;
+- 规则集合收敛到 16 个低重复、工程相关的规则。
+
+## 已完成
+
+### 已提交
+
+- [x] `3aa32bb chore: remove obsolete architecture assets`
+ - 删除冻结的 runtime archive、IDE 工程文件、过期规划/重复文档和废弃入口;
+ - 50 个文件,删除 4,290 行;
+ - 当前分支相对 `origin/develop` 领先 1 个提交。
+
+### 已实现但尚未提交
+
+- [x] 将活跃包从 `packages/flutterguard_cli/` 扁平化到仓库根目录;
+- [x] 将 `examples/scan_demo/` 收敛为 `example/`;
+- [x] 删除 Melos、开发 wrapper 和重复编译/扫描脚本;
+- [x] 将 CLI 收敛为四个命令族并移除重复 `--path`;
+- [x] 用通用 `RuleConfig` 替换大量逐规则配置类型;
+- [x] 合并 catalog、metadata 和 executor registry;
+- [x] 合并 layer/module 检测实现并共享 import graph;
+- [x] 删除 score、priority、confidence、兼容别名及无执行语义元数据;
+- [x] 删除 generic size、missing const、device lifecycle、MQTT 配置和依赖版本规则;
+- [x] 去除规则级 allowlist/ignore-path 旁路并拒绝未知规则参数;
+- [x] 将测试重组为 CLI、配置、规则和扫描器契约测试;
+- [x] 更新 README、外部规格、内部架构和分层 `AGENTS.md`;
+- [x] 新增 `.pubignore`,发布包不再包含测试夹具、本地二进制和工程元数据。
+
+## 当前工作区快照
+
+记录时的 `git status --porcelain=v1 --untracked-files=all`:
+
+- 110 个已跟踪文件变更或删除;
+- 70 个未跟踪新文件;
+- 合计 180 项;
+- 暂存区为空;
+- 生产 Dart 代码约 4,974 行;
+- 测试 Dart 代码约 1,365 行;
+
+Git 将根目录迁移暂时显示为旧路径删除加新路径未跟踪。不要单独恢复
+`packages/flutterguard_cli/`、`examples/scan_demo/` 或 `docs/`;它们分别由
+根目录包、`example/` 和 `doc/` 替代。
+
+## 已通过验证
+
+- [x] `dart format --output=none --set-exit-if-changed bin lib test`
+- [x] `dart analyze`:无问题
+- [x] `dart test`:34 项全部通过(含 10 项新增质量测试)
+- [x] `dart run bin/flutterguard.dart config check example`
+- [x] 示例扫描:3 个文件、0 个问题、`--fail-on high` 通过
+- [x] `rules --format json` 输出 16 个规则
+- [x] 原生 executable 编译并输出 `flutterguard 0.7.0`
+- [x] `git diff --check`
+- [x] 干净临时副本 `dart pub publish --dry-run`
+ - 退出码 0;
+ - 0 warnings;
+ - 发布压缩包约 44 KB;
+ - 仅有已发布版本从 `0.1.0` 跳到当前版本的非阻塞 hint。
+
+## 下一步:拆分剩余提交
+
+按顺序执行,每批提交前后都确认 `git diff --cached --name-only`,不要混入下一批
+文件。
+
+### 1. 根包与内核收敛
+
+建议提交信息:
+
+```text
+refactor: flatten and simplify the flutterguard CLI
+```
+
+范围:
+
+- 根 `pubspec.yaml`、`analysis_options.yaml`、`.gitignore`、`.pubignore`、
+ `flutterguard.yaml`;
+- 删除 `melos.yaml`;
+- 删除旧 `packages/flutterguard_cli/**`,新增 `bin/**`、`lib/**`、`test/**`;
+- 删除 `examples/scan_demo/**`,新增 `example/**`;
+- 暂不包含 `.github/**`、`scripts/**`、根文档和 `doc/**`。
+
+提交前门槛:
+
+```bash
+dart format --output=none --set-exit-if-changed bin lib test
+dart analyze
+dart test
+dart run bin/flutterguard.dart config check example
+dart run bin/flutterguard.dart scan example --format json --no-color --fail-on high
+git diff --cached --check
+```
+
+### 2. CI 与发布脚本收敛
+
+建议提交信息:
+
+```text
+ci: align workflows with the root Dart package
+```
+
+范围:
+
+- `.github/workflows/flutterguard.yml`;
+- `.github/workflows/release.yml`;
+- `scripts/package_release.sh`、`scripts/package_release.ps1`;
+- 删除 `scripts/compile.*`、`scripts/flutterguard-dev*`、`scripts/scan_ci.*`。
+
+提交前门槛:
+
+```bash
+bash -n scripts/package_release.sh
+dart compile exe bin/flutterguard.dart -o /tmp/flutterguard-check
+/tmp/flutterguard-check --version
+git diff --cached --check
+```
+
+Windows PowerShell 包装脚本需要由 Windows workflow 做最终宿主验证。
+
+### 3. 文档与检查点
+
+建议提交信息:
+
+```text
+docs: document the simplified v0.7 architecture
+```
+
+范围:
+
+- `README.md`、`CHANGELOG.md`、根 `AGENTS.md`;
+- 删除旧 `docs/ARCHITECTURE.md`、`docs/FLUTTERGUARD_SPEC.md`;
+- 新增 `doc/ARCHITECTURE.md`、`doc/FLUTTERGUARD_SPEC.md`;
+- 本文件 `EVOLUTION_PLAN.md`。
+
+提交前门槛:
+
+```bash
+rg -n 'melos|packages/flutterguard_cli|examples/scan_demo|docs/' \
+ README.md AGENTS.md doc bin lib test example .github scripts || true
+git diff --cached --check
+```
+
+历史内容允许出现在 `CHANGELOG.md`,活跃说明中不得存在旧路径或已删除能力。
+
+## 所有提交完成后的发布门槛
+
+```bash
+dart pub get
+dart format --output=none --set-exit-if-changed bin lib test
+dart analyze
+dart test
+dart run bin/flutterguard.dart rules --format json
+dart run bin/flutterguard.dart config check .
+dart run bin/flutterguard.dart config check example
+dart run bin/flutterguard.dart scan example \
+ --format json --output .flutterguard/ci --fail-on high --no-color
+dart compile exe bin/flutterguard.dart -o /tmp/flutterguard-0.7.0
+/tmp/flutterguard-0.7.0 --version
+dart pub publish --dry-run
+git diff --check
+git status --short
+```
+
+只有在工作区干净时,根目录 publish dry-run 的 Git 状态检查才代表最终发布结果。
+工作区尚未提交时,使用无 `.git` 元数据的干净临时副本验证包内容。
+
+## 后续产品演进
+
+完成当前三批提交和发布门槛后,再进入规则质量阶段:
+
+1. ~~为 `OverlayEntry` 增加 `remove()` / `dispose()` 生命周期安全检测~~ (已完成 2026-07-21)
+2. ~~将仍依赖字符串的方法/类型识别逐步升级为 AST 和可解析类型语义~~ (已完成 2026-07-21: iot_security 升级为 AST Visitor; ble_scanning timeout 升级为参数/命名参数检测)
+3. ~~为新增规则先锁定所有权、低误报边界、changed-only 行为和抑制契约~~ (已完成 2026-07-21)
+4. ~~每个规则必须有 positive、negative、disabled、suppression 和报告契约覆盖~~ (已完成 2026-07-21: iot_security, ble_scanning, lifecycle 均已补全五项覆盖)
+5. 保持一次扫描只读/解析一次源文件,并复用 `SourceWorkspace`;
+6. 保持 layer/module/cycle 共享项目 import graph。
+
+## 明确不做
+
+- 不恢复 runtime SDK、APM、云服务、Flutter Widget 或动态插件系统;
+- 不恢复 Melos、多包发布结构或公共 Dart scanner API;
+- 不恢复 score、priority、confidence 或 JSON 兼容别名;
+- 不恢复 generic size、missing const、依赖版本或 broker 配置规则;
+- 不为降低误报而恢复每条规则的宽泛 allowlist;
+- 不在当前收敛提交中夹带新的规则功能。
+
+## 后续会话启动协议
+
+```bash
+git log -3 --oneline --decorate
+git status --short
+dart analyze
+dart test
+```
+
+然后读取本文件“下一步”部分,从第一个未完成提交开始。每完成一个阶段,更新
+front matter 的 `head`、`checkpoint_state`,勾选对应项目,并在“已通过验证”中
+记录最新实际结果。
diff --git a/PROJECT_RULES.md b/PROJECT_RULES.md
deleted file mode 100644
index e6a5dc6..0000000
--- a/PROJECT_RULES.md
+++ /dev/null
@@ -1,79 +0,0 @@
-# Project Rules for FlutterGuard
-
-## 1. Identity
-- FlutterGuard is an **IoT/smart home Flutter project static analysis CLI plugin**.
-- NOT a general observability SDK, APM tool, or crash reporter.
-- Output: compiled CLI binary (`dart compile exe`) that scans Dart source code.
-
-## 2. Active vs Archived
-
-| Path | Status | Package | Notes |
-|------|--------|---------|-------|
-| **Path A** — CLI static analysis | **ACTIVE** | `flutterguard_cli` | All new features |
-| **Path B** — Runtime tracing | **ARCHIVED** | `core`, `dio`, `flutter` | In `archive/` for future reference |
-
-Do NOT modify archived packages. They are preserved as-is for reference.
-
-## 3. Architecture Constraints
-- Monorepo managed by **melos**. New packages must register in `melos.yaml`.
-- Active code lives in `packages/flutterguard_cli/lib/src/`.
-- Each rule is a standalone class: `analyze(List files) → List`.
-- Configuration driven by `flutterguard.yaml` (YAML schema, parsed in `config_loader.dart`).
-- No plugin system, no code generation, no reflection — explicit wiring in `bin/flutterguard.dart`.
-
-## 4. Code Conventions
-- Dart 3.3+, `strict-casts: true`, `strict-inference: true`.
-- Prefer Dart **records** (`typedef`) for config types.
-- Prefer `const` constructors and `final` locals.
-- Wrap per-file parsing in try/catch (one bad file must not crash the scan).
-- Import style: `package:flutterguard_cli/src/...` (no relative imports across packages).
-
-## 5. Testing Conventions
-- Tests live in `packages/flutterguard_cli/test/` using `package:test`.
-- Fixtures go in `test/fixtures/.dart`.
-- Every new rule requires: spec entry → config typedef → rule class → fixture → test.
-- Run tests: `melos run test:cli`.
-
-## 6. Spec Governance
-- `docs/FLUTTERGUARD_SPEC.md` is the **single source of truth**.
-- Spec must be updated **before** implementation.
-- All rule IDs, risk levels, and metadata schemas must be documented in spec first.
-
-## 7. Forbidden
-- Do NOT create new Flutter widgets or UI components.
-- Do NOT add web/cloud infrastructure or dashboard UI.
-- Do NOT use third-party SaaS SDKs.
-- Do NOT commit secrets, API keys, or credentials.
-- Do NOT add runtime instrumentation outside archived packages.
-- Do NOT introduce `package:build` / code generation dependencies.
-
-## 8. Git Workflow
-- Branch: `develop` for active work. PRs merge to `develop`.
-- Commit messages: imperative mood, prefixed by scope (`cli:`, `spec:`, `docs:`).
-- Before committing: run `melos run analyze` and `melos run test:cli`.
-- Do NOT force-push to `develop` or `main`.
-
-## 9. Override Hierarchy
-
-### 9.1 pubspec_overrides.yaml
-Managed by `melos bootstrap`. No path dependencies currently exist for `flutterguard_cli`.
-
-**Rule**: Do NOT edit manually. Re-run `melos bootstrap` after any pubspec.yaml change.
-
-### 9.2 analysis_options.yaml Inheritance Chain
-```
-root/analysis_options.yaml # strict-casts + strict-inference + 6 lint rules
-└── packages/flutterguard_cli/... # inherits root + package:lints/recommended + excludes test/fixtures/**
-```
-
-**Rule**: Keep corporate-wide strictness at root. Per-package loosening only for legitimate reasons (fixture code, print-based demos).
-
-### 9.3 flutterguard.yaml Config Override Chain
-```
-
-├── --config # explicit project-relative or absolute config
-├── flutterguard.yaml # default project config when no override is given
-└── built-in defaults # only when no override is given and the default file is absent
-```
-
-**Rule**: Config files do not merge across projects. Relative paths resolve from the target project root, and explicitly selected files must exist.
diff --git a/README.md b/README.md
index e6f3d3f..8a93c86 100644
--- a/README.md
+++ b/README.md
@@ -1,256 +1,60 @@
# FlutterGuard
-> IoT Flutter project static analysis CLI for architecture enforcement, code quality, and CI gating.
-
-[English](README.md) | [中文](README.zh.md)
-
-FlutterGuard scans Flutter/Dart source code and reports architecture boundary breaches, lifecycle/resource leaks, dependency cycles, and size-related code quality issues. The active path is `packages/flutterguard_cli/`; the legacy runtime-tracing packages are archived under `archive/`.
-
-**Platforms**: macOS, Windows, Linux — pure Dart CLI, no native dependencies.
-
-**Docs**: [Usage Guide](docs/USAGE.md) | [Configuration Strategy](CONFIGURATION_STRATEGY.md) | [Windows Assessment](docs/WINDOWS_ASSESSMENT.md) | [Spec](docs/FLUTTERGUARD_SPEC.md) | [Architecture](docs/ARCHITECTURE.md)
-
-## What It Is
-
-- A CLI for static analysis of Flutter/Dart projects
-- A YAML-driven architecture enforcement tool
-- An IoT/smart-home aware rule set for Flutter codebases
-- A CI gate that can fail builds on severity thresholds or score thresholds
-
-## What It Is Not
-
-- Not a runtime observability or APM SDK
-- Not a crash reporter
-- Not a general-purpose Dart linter
-- Not a web dashboard or Flutter widget library
-- Does not require an API key and does not upload APKs
-
-## Requirements
-
-- Dart SDK 3.3.0 or newer
-- `melos` for workspace bootstrap when running from source
-- Supported OS: macOS, Windows, Linux
-
----
+FlutterGuard is an executable-only static analysis CLI for IoT and smart-home
+Flutter projects. It checks architecture boundaries, lifecycle/resource safety,
+IoT transport security, and state-management maintainability. It does not add a
+runtime SDK to the scanned application.
## Install
-### Option A: pub.dev install (recommended)
-
-
-macOS / Linux
-
```bash
dart pub global activate flutterguard_cli
-
-# Verify
flutterguard --version
```
-Ensure `$HOME/.pub-cache/bin` is on your `PATH`:
-
-```bash
-export PATH="$PATH:$HOME/.pub-cache/bin" # add to ~/.zshrc or ~/.bashrc
-```
-
-
-
-Windows (PowerShell)
-
-```powershell
-dart pub global activate flutterguard_cli
-
-# Verify
-flutterguard --version
-```
-
-If the command is not found, ensure `%USERPROFILE%\AppData\Local\Pub\Cache\bin` is on your `PATH`:
-
-```powershell
-$env:Path += ";$env:USERPROFILE\AppData\Local\Pub\Cache\bin"
-```
-
-
-### Option B: GitHub Release binary (no Dart SDK at runtime)
-
-Download the matching binary from the GitHub Releases page, then run it
-directly:
-
-
-macOS / Linux
-
-```bash
-chmod +x flutterguard
-./flutterguard --version
-./flutterguard scan .
-```
-
-
-
-Windows (PowerShell)
-
-```powershell
-.\flutterguard.exe --version
-.\flutterguard.exe scan .
-```
-
-
-### Option C: source checkout for development
-
-Use the local launcher when you want to run the current checkout without
-installing or replacing the global `flutterguard` command.
-
-
-macOS / Linux
+From a source checkout:
```bash
-git clone https://github.com/lizy-coding/flutterguard.git
-cd flutterguard
dart pub get
-./scripts/flutterguard-dev --version
-./scripts/flutterguard-dev scan .
+dart run bin/flutterguard.dart scan example
```
-
-
-Windows (PowerShell)
+## Commands
-```powershell
-git clone https://github.com/lizy-coding/flutterguard.git
-cd flutterguard
-dart pub get
-.\scripts\flutterguard-dev.ps1 --version
-.\scripts\flutterguard-dev.ps1 scan .
+```text
+flutterguard scan [path] [options]
+flutterguard baseline create [path]
+flutterguard config init [path]
+flutterguard config check [path]
+flutterguard rules [rule-id] [--format table|json]
```
-
-
----
-
-## Quick Start
-
-```bash
-# Scan the current directory
-flutterguard scan
-
-# Create and validate a starter config
-flutterguard init --profile migration
-flutterguard config doctor
-flutterguard doctor install
-# Inspect the merged effective config
-flutterguard config print
+The core scan options are:
-# Scan a specific project
-flutterguard scan ./my_flutter_app # macOS / Linux
-flutterguard scan .\my_flutter_app # Windows
+- `--config`, `-c`: explicit configuration file.
+- `--format`, `-f`: `table`, `json`, or `sarif`.
+- `--output`, `-o`: report directory; default `.flutterguard`.
+- `--fail-on`: `none`, `high`, `medium`, or `low`.
+- `--changed-only --base [`: scan Git-changed Dart files.
+- `--baseline `: hide findings already recorded in a baseline.
+- `--verbose`: include diagnostics, detail, and evidence.
+- `--no-color`: disable ANSI terminal color.
-# Scan with explicit path flag
-flutterguard scan -p /path/to/project # macOS / Linux
-flutterguard scan -p D:\path\to\project # Windows
+Exit code `0` means the scan completed and the gate passed. Exit code `1` means
+the severity gate failed. Exit code `2` means the command or scan setup was
+invalid.
-# JSON output with CI gate
-flutterguard scan . --format json --fail-on high
-
-# Baseline existing issues before enabling a hard CI gate
-flutterguard baseline create .
-flutterguard baseline stats
-flutterguard baseline check . --baseline .flutterguard/baseline.json --no-growth
-flutterguard scan . --baseline .flutterguard/baseline.json --fail-on high
-
-# GitHub Code Scanning output
-flutterguard scan . --format sarif --baseline .flutterguard/baseline.json
-
-# Export one finding for false-positive feedback
-flutterguard issue export --rule mqtt_connection --file lib/device/mqtt.dart --line 42
-
-# Show help
-flutterguard --help
-flutterguard scan --help
-```
+## Configuration
-### Demo target
+Configuration is optional. Generate a complete starter file from the rule
+registry:
```bash
-flutterguard scan examples/scan_demo
+flutterguard config init .
+flutterguard config check .
```
----
-
-## CLI Reference
-
-Commands:
-
-| Command | Description |
-|---------|-------------|
-| `flutterguard scan []` | Scan a project (path defaults to current directory) |
-| `flutterguard baseline create []` | Create a baseline JSON file for existing issues |
-| `flutterguard baseline stats` | Show baseline fingerprint counts |
-| `flutterguard baseline prune []` | Remove fixed issues from a baseline |
-| `flutterguard baseline check [] --no-growth` | Fail when current issues are missing from baseline |
-| `flutterguard doctor install` | Diagnose executable version and PATH conflicts |
-| `flutterguard init` | Create a starter `flutterguard.yaml` |
-| `flutterguard init --profile migration` | Create a starter config from a profile |
-| `flutterguard init --with-architecture` | Create config with architecture layer/module templates |
-| `flutterguard config print` | Print the merged effective configuration |
-| `flutterguard config doctor` | Validate config, globs, and architecture references |
-| `flutterguard issue export` | Export one issue as a local feedback JSON bundle |
-| `flutterguard rules` | List available rules |
-| `flutterguard explain ` | Explain one rule |
-| `flutterguard --help` / `-h` | Show usage |
-| `flutterguard --version` / `-V` | Show version |
-
-### Scan options
-
-| Flag | Short | Default | Description |
-|------|-------|---------|-------------|
-| `` | — | `.` | Positional project path (optional, before options) |
-| `--path` | `-p` | `.` | Project path to scan (overridden by positional ``) |
-| `--config` | `-c` | `flutterguard.yaml` | Config file path |
-| `--format` | `-f` | `table` | Output format: `table`, `json`, or `sarif` |
-| `--output` | `-o` | `.flutterguard` | Output directory for reports |
-| `--verbose` | `-v` | off | Show detailed output with code context |
-| `--no-color` | — | off | Disable ANSI terminal colors |
-| `--changed-only` | — | off | Only scan Dart files changed since `--base` |
-| `--base` | — | `main` | Git base ref for `--changed-only` |
-| `--baseline` | — | unset | Baseline JSON file used to hide existing issues |
-| `--fail-on` | — | `none` | CI gate: `none` / `high` / `medium` / `low` |
-| `--min-score` | — | unset | Minimum score threshold 0–100 |
-| `--help` | `-h` | — | Show scan usage |
-
-### Exit codes
-
-| Code | Meaning |
-|------|---------|
-| `0` | Success, including help/version and a changed-only scan with no relevant changes |
-| `1` | CI gate failed (issues at/above `--fail-on` level, or score below `--min-score`) |
-| `2` | Scan setup error (bad path, missing explicit config, invalid config, or no configured Dart files) |
-
-### Path resolution
-
-FlutterGuard auto-discovers the project root by walking up from the current directory, looking for `flutterguard.yaml`, `pubspec.yaml`, or a `lib/` directory. If none are found, it falls back to the current directory.
-
-The `--config` path is resolved against the target project:
-1. Absolute paths are used as-is and must exist.
-2. Relative paths are resolved from the target project root, never from CWD.
-3. An omitted default `flutterguard.yaml` uses built-in defaults; any explicitly selected config must exist.
-
----
-
-## Configuration
-
-Create `flutterguard.yaml` in your project root.
-
-Recommended strategy:
-
-1. Start with zero config: `flutterguard scan`.
-2. Run `flutterguard init` when you need custom thresholds or excludes.
-3. Use `flutterguard config print` to inspect merged defaults.
-4. Use `flutterguard config doctor` before enabling CI gates.
-5. Add architecture layers/modules only after project boundaries are agreed.
-
-For the full decision model, see [Configuration Strategy](CONFIGURATION_STRATEGY.md).
-
-### Basic config (for most users)
+Minimal example:
```yaml
include:
@@ -259,43 +63,18 @@ include:
exclude:
- lib/generated/**
- lib/**.g.dart
- - lib/**.freezed.dart
- - lib/**.mocks.dart
rules:
- large_file:
- enabled: true
- maxLines: 500
- large_class:
- enabled: true
- maxLines: 300
- large_build_method:
- enabled: true
- maxLines: 80
- lifecycle_resource:
- enabled: true
- missing_const_constructor:
- enabled: true
- device_lifecycle:
- enabled: true
- mqtt_connection:
- enabled: true
- ble_scanning:
- enabled: true
- maxScanDurationMs: 10000
iot_security:
enabled: true
+ severity: high
requireTls: true
- pubspec_security:
+ ble_scanning:
enabled: true
-```
-
-### Full config (with architecture enforcement)
-
-```yaml
-# ... include/exclude/rules from basic config above ...
+ severity: medium
architecture:
+ detect_cycles: true
layers:
- name: presentation
path: lib/presentation/**
@@ -303,329 +82,100 @@ architecture:
- name: domain
path: lib/domain/**
allowed_deps: [core]
- - name: data
- path: lib/data/**
- allowed_deps: [domain, core]
- name: core
path: lib/core/**
allowed_deps: []
-
- modules:
- - name: device_mqtt
- path: lib/device/mqtt/**
- allowed_deps: [domain, core]
- - name: device_ble
- path: lib/device/ble/**
- allowed_deps: [domain, core]
-
- detect_cycles: true
- layer_violation:
- enabled: true
- module_violation:
- enabled: true
+ modules: []
```
-> **Important**: Architecture rules (`layer_violation`, `module_violation`, `circular_dependency`) require explicit `architecture.layers`, `architecture.modules`, and/or `architecture.detect_cycles` declarations in your config. They do not auto-discover project boundaries.
-
-> **Glob patterns**: Always use forward slashes (`/`) in YAML config, even on Windows. Do not use backslashes.
-
----
+Every rule accepts `enabled` and `severity`. Rule-specific scalar options are
+listed by `flutterguard rules `.
## Rules
-| Rule ID | Level | Domain | Priority | What it checks | Config required |
-|---------|-------|--------|----------|----------------|-----------------|
-| `large_file` | LOW | standards | P2 | File line count over `maxLines` | — |
-| `large_class` | LOW | standards | P2 | Class body line count over `maxLines` | — |
-| `large_build_method` | MEDIUM | performance | P1 | `build()` method line count over `maxLines` | — |
-| `lifecycle_resource_not_disposed` | MEDIUM | performance | P1 | Undisposed StreamSubscription, Timer, AnimationController, TextEditingController, ScrollController, FocusNode, MqttClient, BluetoothDevice, StreamController | — |
-| `missing_const_constructor` | LOW | standards | P2 | Widget classes missing a `const` constructor | — |
-| `layer_violation` | HIGH | architecture | P0 | Importing across forbidden architecture layers | `architecture.layers` * |
-| `module_violation` | HIGH | architecture | P0 | Importing across forbidden business modules | `architecture.modules` * |
-| `circular_dependency` | MEDIUM | architecture | P1 | File-level import cycles | `architecture.detect_cycles` * |
-| `device_lifecycle` | HIGH | architecture | P0 | Unbalanced init/teardown pairs (initState↔dispose, connect↔disconnect, etc.) | — |
-| `mqtt_connection` | HIGH | architecture | P0 | MQTT connect/disconnect pairing, hardcoded broker URLs | — |
-| `iot_security` | HIGH | architecture | P0 | Hardcoded credentials, cleartext MQTT/HTTP, insecure BLE | `rules.iot_security.requireTls` |
-| `ble_scanning` | MEDIUM | architecture | P1 | BLE startScan/stopScan pairing, scan timeout | `rules.ble_scanning.maxScanDurationMs` |
-| `pubspec_security` | MEDIUM | standards | P2 | Unbounded deps, deprecated packages, outdated IoT dependencies | — |
-
-* Requires explicit YAML configuration to activate.
-
----
-
-## Output
-
-### Terminal table (default)
-
-Colored terminal report grouped by domain. Shows overall score, file count, issue count, and per-issue detail.
-
-### JSON report
-
-`--format json` writes `.flutterguard/report.json` under the output directory.
-
-Example shape:
-
-```json
-{
- "version": "1.0.0",
- "generatedAt": "2026-06-09T12:00:00.000Z",
- "projectPath": "/path/to/project",
- "score": 85,
- "summary": {
- "total": 3,
- "high": 1,
- "medium": 1,
- "low": 1,
- "suppressed": 0,
- "suppressedByBaseline": 0,
- "byDomain": {
- "architecture": { "high": 1, "medium": 0, "low": 0, "total": 1 }
- }
- },
- "issues": []
-}
-```
+The registry currently contains 16 rule IDs:
-### SARIF report
+- Architecture: `layer_violation`, `module_violation`,
+ `circular_dependency`, `state_layer_ui_dependency`,
+ `state_dependency_cycle`.
+- Lifecycle and performance: `lifecycle_resource_not_disposed`,
+ `side_effect_in_build`, `state_manager_created_in_build`,
+ `riverpod_read_used_for_render`, `riverpod_watch_in_callback`,
+ `provider_value_lifecycle_misuse`, `notify_listeners_in_loop`.
+- IoT security: `ble_scanning`, `iot_security`.
+- State standards: `mutable_state_exposed`,
+ `bloc_equatable_props_incomplete`.
-`--format sarif` writes `.flutterguard/report.sarif` for GitHub Code Scanning. High, medium, and low map to SARIF `error`, `warning`, and `note`.
+Generic file-size, missing-const, dependency-version, and broker-configuration
+checks were removed in 0.7.0. Dart lints, `dart pub outdated`, dependency
+security tools, and application configuration are better owners.
-### Suppression and baseline
-
-Use source suppression for known false positives:
-
-```dart
-// flutterguard: ignore missing_const_constructor
-// flutterguard: ignore iot_security, mqtt_connection
-// flutterguard: ignore all
-```
+## Reports and CI
-Suppression applies only to the comment line and the following line.
-
-Recommended CI adoption order:
+JSON uses schema version `2.0.0` and exposes one canonical field name per
+concept: `ruleId`, `severity`, and `domain`. SARIF 2.1.0 is suitable for GitHub
+Code Scanning.
```bash
-flutterguard config doctor
flutterguard baseline create .
-flutterguard baseline check . --baseline .flutterguard/baseline.json --no-growth
-flutterguard scan . --baseline .flutterguard/baseline.json --format json --fail-on high
-```
-
-## Scoring
-
-```
-score = max(0, 100 - high×10 - medium×4 - low×1)
+flutterguard scan . \
+ --baseline .flutterguard/baseline.json \
+ --format sarif \
+ --fail-on high
```
-| Score | Rating |
-|-------|--------|
-| 80–100 | Excellent |
-| 50–79 | Needs review |
-| 0–49 | Needs action |
+Suppression comments remain available for precise false positives:
----
-
-## CI Integration
-
-### GitHub Actions
-
-```yaml
-name: FlutterGuard
-
-on: [push, pull_request]
-
-jobs:
- scan:
- strategy:
- matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
- runs-on: ${{ matrix.os }}
- steps:
- - uses: actions/checkout@v4
- - uses: dart-lang/setup-dart@v1
- with:
- sdk: 3.3.0
- - name: Install FlutterGuard
- run: dart pub global activate flutterguard_cli
- - name: Scan
- run: flutterguard scan . --format json --baseline .flutterguard/baseline.json --fail-on high --min-score 80
-```
-
-### GitHub Code Scanning
-
-```yaml
-name: FlutterGuard SARIF
-
-on: [push, pull_request]
-
-jobs:
- code-scanning:
- runs-on: ubuntu-latest
- permissions:
- security-events: write
- contents: read
- steps:
- - uses: actions/checkout@v4
- - uses: dart-lang/setup-dart@v1
- with:
- sdk: 3.3.0
- - run: dart pub global activate flutterguard_cli
- - run: flutterguard scan . --format sarif --baseline .flutterguard/baseline.json
- - uses: github/codeql-action/upload-sarif@v3
- with:
- sarif_file: .flutterguard/report.sarif
-```
-
-### GitLab CI
-
-```yaml
-flutterguard:
- image: dart:3.3.0
- script:
- - dart pub global activate flutterguard_cli
- - flutterguard scan . --format json --fail-on high --min-score 80
- artifacts:
- paths:
- - .flutterguard/report.json
- when: always
+```dart
+// flutterguard: ignore iot_security
+final endpoint = loadLocalDevelopmentEndpoint();
```
-### pre-commit hook
+## Repository layout
-```yaml
-# .pre-commit-config.yaml
-repos:
- - repo: local
- hooks:
- - id: flutterguard
- name: FlutterGuard scan
- entry: flutterguard scan . --fail-on high
- language: system
- pass_filenames: false
- always_run: true
+```text
+bin/ executable entry point
+lib/src/cli/ command parsers and handlers
+lib/src/rules/ registry, rule definitions, and detectors
+lib/src/ scan/config/report shared kernel
+test/ contract and detector tests
+example/ scan target used by CI
+doc/ architecture and external contract
+scripts/ native release packaging only
```
-### Local scripts
+FlutterGuard is a single Dart package. There is no Melos workspace, runtime
+SDK, plugin system, or public Dart scanner API.
-]
-macOS / Linux
+## Development
```bash
-#!/usr/bin/env bash
-# scan_ci.sh
-if flutterguard scan . --format json --fail-on high --min-score 80; then
- echo "All checks passed!"
-else
- status=$?
- echo "FlutterGuard failed with exit code $status."
- exit "$status"
-fi
-```
-
-
-
-Windows (PowerShell)
-
-```powershell
-# scan_ci.ps1
-$ErrorActionPreference = "Stop"
-flutterguard scan . --format json --fail-on high --min-score 80
-$status = $LASTEXITCODE
-
-if ($status -eq 0) {
- Write-Host "All checks passed!" -ForegroundColor Green
-} else {
- Write-Host "FlutterGuard failed with exit code $status." -ForegroundColor Red
- exit $status
-}
-```
-
-
----
-
-## Troubleshooting
-
-### Windows: ANSI colors show as raw escape codes
-
-Use **Windows Terminal** (built into Windows 10/11) instead of legacy `cmd.exe`. Alternatively, add `--no-color` to disable ANSI output:
-
-```powershell
-flutterguard scan . --no-color
-```
-
-### Windows: "API key required" error
-
-This means the shell is resolving an old globally-installed binary instead of this repository's static-analysis CLI. Run the local binary directly:
-
-```powershell
-.\flutterguard.exe scan .
-```
-
-Or reinstall:
-
-```powershell
-dart pub global deactivate flutterguard_cli
-dart pub global activate flutterguard_cli
-```
-
-### Windows: garbled Chinese output
-
-```powershell
-# In PowerShell, set UTF-8 output encoding
-[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
-# Or use Windows Terminal (recommended) which defaults to UTF-8
-```
-
-### Glob patterns: always use forward slashes
-
-In `flutterguard.yaml`, use `/` for all path patterns regardless of platform:
-
-```yaml
-# Correct
-path: lib/presentation/**
-
-# Wrong (even on Windows)
-path: lib\presentation\**
+dart pub get
+dart format --output=none --set-exit-if-changed bin lib test
+dart analyze
+dart test
+dart run bin/flutterguard.dart scan example --format json --no-color
+dart pub publish --dry-run
```
----
-
-## Repository Layout
-
-```
-flutterguard/
-├── packages/
-│ └── flutterguard_cli/ Active CLI implementation
-├── archive/ Frozen legacy runtime-tracing packages
-└── examples/
- └── scan_demo/ Demo scan target
-```
+### Release preflight
-## Development
+Before creating a release tag, run the compact preflight command:
```bash
-# All platforms
-git clone https://github.com/lizy-coding/flutterguard.git
-cd flutterguard
-dart pub get
-dart pub global activate melos
-melos bootstrap
-
-# Common commands
-dart run melos run analyze # Static analysis
-dart run melos run test:cli # Run tests
-dart compile exe packages/flutterguard_cli/bin/flutterguard.dart -o flutterguard
+bash scripts/release_preflight.sh
```
-## Further Reading
+It resolves dependencies, checks formatting, analysis, tests, and the CLI
+example, then runs `dart pub publish --dry-run` without uploading anything.
+It finishes by printing the required human checks for package contents, legal
+redistribution, version/tag, and publisher access. A nonzero exit means at
+least one automated release gate failed.
-| Document | Content |
-|----------|---------|
-| [docs/USAGE.md](docs/USAGE.md) | Full usage guide (all platforms) |
-| [docs/WINDOWS_ASSESSMENT.md](docs/WINDOWS_ASSESSMENT.md) | Windows compatibility assessment |
-| [docs/FLUTTERGUARD_SPEC.md](docs/FLUTTERGUARD_SPEC.md) | Technical specification |
-| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Architecture overview |
+See [doc/ARCHITECTURE.md](doc/ARCHITECTURE.md) for internal boundaries and
+[doc/FLUTTERGUARD_SPEC.md](doc/FLUTTERGUARD_SPEC.md) for the external
+contract.
## License
-MIT
+MIT. See [LICENSE](LICENSE).
diff --git a/README.zh.md b/README.zh.md
deleted file mode 100644
index 40e9f22..0000000
--- a/README.zh.md
+++ /dev/null
@@ -1,629 +0,0 @@
-# FlutterGuard
-
-> 面向 IoT / 智能家居 Flutter 项目的静态架构扫描 CLI,用于架构约束、代码质量检查和 CI 门禁。
-
-[English](README.md) | [中文](README.zh.md)
-
-FlutterGuard 扫描 Flutter/Dart 源码,报告架构边界违规、生命周期资源泄漏、循环依赖、过大文件/类/`build` 方法,以及常见代码规范问题。当前活动开发路径是 `packages/flutterguard_cli/`;旧的运行时追踪包已归档在 `archive/`。
-
-**支持平台**: macOS、Windows、Linux — 纯 Dart CLI,零原生依赖。
-
-**文档**: [使用指南](docs/USAGE.md) | [配置策略](CONFIGURATION_STRATEGY.md) | [Windows 评估](docs/WINDOWS_ASSESSMENT.md) | [技术规格](docs/FLUTTERGUARD_SPEC.md) | [架构](docs/ARCHITECTURE.md)
-
-## 它是什么
-
-- 静态分析命令行工具
-- 基于 YAML 配置的架构约束工具
-- 面向 IoT / 智能家居 Flutter 项目的规则集
-- 可按严重等级或评分失败的 CI 门禁
-
-## 它不是什么
-
-- 不是运行时观测 SDK 或 APM
-- 不是 Crashlytics / Sentry 替代品
-- 不是 HTTP 抓包、日志库或云端 SaaS
-- 不需要 API key,也不会上传 APK
-
-## 环境要求
-
-- Dart SDK 3.3.0 或更高版本
-- 从源码开发时需要 `melos`
-- 支持操作系统: macOS、Windows、Linux
-
----
-
-## 安装
-
-### 方式 A:pub.dev 安装(推荐)
-
-
-macOS / Linux
-
-```bash
-dart pub global activate flutterguard_cli
-
-# 验证安装
-flutterguard --version
-```
-
-确认 `$HOME/.pub-cache/bin` 在 `PATH` 中:
-
-```bash
-export PATH="$PATH:$HOME/.pub-cache/bin" # 添加到 ~/.zshrc 或 ~/.bashrc
-```
-
-
-
-Windows (PowerShell)
-
-```powershell
-dart pub global activate flutterguard_cli
-
-# 验证安装
-flutterguard --version
-```
-
-若 `flutterguard` 命令未识别,确认 `%USERPROFILE%\AppData\Local\Pub\Cache\bin` 在 `PATH` 中:
-
-```powershell
-$env:Path += ";$env:USERPROFILE\AppData\Local\Pub\Cache\bin"
-```
-
-
-### 方式 B:GitHub Release 二进制(运行时无需 Dart 环境)
-
-从 GitHub Releases 下载对应平台的二进制后直接运行:
-
-
-macOS / Linux
-
-```bash
-chmod +x flutterguard
-./flutterguard --version
-./flutterguard scan .
-```
-
-
-
-Windows (PowerShell)
-
-```powershell
-.\flutterguard.exe --version
-.\flutterguard.exe scan .
-```
-
-
-### 方式 C:源码开发运行
-
-如果你希望直接运行当前 checkout 的源码,不替换全局 `flutterguard`
-命令,使用本地 launcher。
-
-
-macOS / Linux
-
-```bash
-git clone https://github.com/lizy-coding/flutterguard.git
-cd flutterguard
-dart pub get
-./scripts/flutterguard-dev --version
-./scripts/flutterguard-dev scan .
-```
-
-
-
-Windows (PowerShell)
-
-```powershell
-git clone https://github.com/lizy-coding/flutterguard.git
-cd flutterguard
-dart pub get
-.\scripts\flutterguard-dev.ps1 --version
-.\scripts\flutterguard-dev.ps1 scan .
-```
-
-
----
-
-## 快速开始
-
-```bash
-# 扫描当前目录
-flutterguard scan
-
-# 创建并检查基础配置
-flutterguard init --profile migration
-flutterguard config doctor
-flutterguard doctor install
-
-# 查看合并后的有效配置
-flutterguard config print
-
-# 扫描指定项目
-flutterguard scan ./my_flutter_app # macOS / Linux
-flutterguard scan .\my_flutter_app # Windows
-
-# 使用 --path 标志
-flutterguard scan -p /path/to/project # macOS / Linux
-flutterguard scan -p D:\path\to\project # Windows
-
-# JSON 输出 + CI 门禁
-flutterguard scan . --format json --fail-on high
-
-# 启用强门禁前先为历史问题创建 baseline
-flutterguard baseline create .
-flutterguard baseline stats
-flutterguard baseline check . --baseline .flutterguard/baseline.json --no-growth
-flutterguard scan . --baseline .flutterguard/baseline.json --fail-on high
-
-# GitHub Code Scanning 输出
-flutterguard scan . --format sarif --baseline .flutterguard/baseline.json
-
-# 导出单个问题用于误报反馈
-flutterguard issue export --rule mqtt_connection --file lib/device/mqtt.dart --line 42
-
-# 显示帮助
-flutterguard --help
-flutterguard scan --help
-```
-
-### 扫描示例项目
-
-```bash
-flutterguard scan examples/scan_demo
-```
-
----
-
-## CLI 参考
-
-命令:
-
-| 命令 | 说明 |
-|------|------|
-| `flutterguard scan []` | 扫描项目(路径默认为当前目录) |
-| `flutterguard baseline create []` | 为现有问题创建 baseline JSON 文件 |
-| `flutterguard baseline stats` | 查看 baseline fingerprint 数量 |
-| `flutterguard baseline prune []` | 从 baseline 移除已修复问题 |
-| `flutterguard baseline check [] --no-growth` | 当前问题未进入 baseline 时失败 |
-| `flutterguard doctor install` | 检查可执行文件版本和 PATH 冲突 |
-| `flutterguard init` | 创建基础 `flutterguard.yaml` |
-| `flutterguard init --profile migration` | 使用 profile 创建基础配置 |
-| `flutterguard init --with-architecture` | 创建包含架构层/模块模板的配置 |
-| `flutterguard config print` | 输出合并后的有效配置 |
-| `flutterguard config doctor` | 检查配置、glob 和架构引用 |
-| `flutterguard issue export` | 导出单个问题为本地反馈 JSON |
-| `flutterguard rules` | 列出所有可用规则 |
-| `flutterguard explain ` | 查看单条规则说明 |
-| `flutterguard --help` / `-h` | 显示帮助 |
-| `flutterguard --version` / `-V` | 显示版本 |
-
-### 扫描参数
-
-| 参数 | 简写 | 默认值 | 说明 |
-|------|------|--------|------|
-| `` | — | `.` | 位置参数,项目路径(可选,放在选项之前) |
-| `--path` | `-p` | `.` | 项目路径(被 `` 位置参数覆盖) |
-| `--config` | `-c` | `flutterguard.yaml` | 配置文件路径 |
-| `--format` | `-f` | `table` | 输出格式:`table`、`json` 或 `sarif` |
-| `--output` | `-o` | `.flutterguard` | 报告输出目录 |
-| `--verbose` | `-v` | 关闭 | 显示详细代码上下文 |
-| `--no-color` | — | 关闭 | 禁用 ANSI 终端颜色 |
-| `--changed-only` | — | 关闭 | 只扫描相对 `--base` 变更的 Dart 文件 |
-| `--base` | — | `main` | `--changed-only` 使用的 Git base ref |
-| `--baseline` | — | 不设 | 用于隐藏历史问题的 baseline JSON 文件 |
-| `--fail-on` | — | `none` | CI 门禁等级:`none` / `high` / `medium` / `low` |
-| `--min-score` | — | 不设 | 最低可接受评分,0–100 |
-| `--help` | `-h` | — | 显示 scan 帮助 |
-
-### 退出码
-
-| 退出码 | 含义 |
-|--------|------|
-| `0` | 成功,包含 help/version 以及增量扫描没有相关变更的情况 |
-| `1` | CI 门禁失败(存在超过 `--fail-on` 的问题,或评分低于 `--min-score`)|
-| `2` | 扫描设置错误(路径不存在、显式配置缺失、配置无效或未匹配到配置范围内的 Dart 文件) |
-
-### 路径解析
-
-FlutterGuard 从当前目录向上遍历,自动发现项目根目录(查找 `flutterguard.yaml`、`pubspec.yaml` 或 `lib/` 目录)。若未找到,则退化为当前目录。
-
-`--config` 路径始终针对目标项目解析:
-1. 绝对路径直接使用,且文件必须存在。
-2. 相对路径从目标项目根目录解析,不再读取 CWD 下的同名文件。
-3. 未显式指定且默认 `flutterguard.yaml` 不存在时使用内置默认值;任何显式选择的配置都必须存在。
-
----
-
-## 配置文件
-
-在项目根目录创建 `flutterguard.yaml`。
-
-推荐策略:
-
-1. 先零配置运行:`flutterguard scan`。
-2. 需要自定义阈值或排除文件时,运行 `flutterguard init`。
-3. 使用 `flutterguard config print` 查看合并后的默认值。
-4. 启用 CI 门禁前先运行 `flutterguard config doctor`。
-5. 只有项目边界已经明确时,再添加 architecture layers/modules。
-
-完整决策模型见 [配置策略](CONFIGURATION_STRATEGY.md)。
-
-### 基础配置(大多数用户适用)
-
-```yaml
-include:
- - lib/**
-
-exclude:
- - lib/generated/**
- - lib/**.g.dart
- - lib/**.freezed.dart
- - lib/**.mocks.dart
-
-rules:
- large_file:
- enabled: true
- maxLines: 500
- large_class:
- enabled: true
- maxLines: 300
- large_build_method:
- enabled: true
- maxLines: 80
- lifecycle_resource:
- enabled: true
- missing_const_constructor:
- enabled: true
- device_lifecycle:
- enabled: true
- mqtt_connection:
- enabled: true
- ble_scanning:
- enabled: true
- maxScanDurationMs: 10000
- iot_security:
- enabled: true
- requireTls: true
- pubspec_security:
- enabled: true
-```
-
-### 完整配置(含架构约束)
-
-```yaml
-# ... include/exclude/rules 同基础配置 ...
-
-architecture:
- layers:
- - name: presentation
- path: lib/presentation/**
- allowed_deps: [domain, core]
- - name: domain
- path: lib/domain/**
- allowed_deps: [core]
- - name: data
- path: lib/data/**
- allowed_deps: [domain, core]
- - name: core
- path: lib/core/**
- allowed_deps: []
-
- modules:
- - name: device_mqtt
- path: lib/device/mqtt/**
- allowed_deps: [domain, core]
- - name: device_ble
- path: lib/device/ble/**
- allowed_deps: [domain, core]
-
- detect_cycles: true
- layer_violation:
- enabled: true
- module_violation:
- enabled: true
-```
-
-> **注意**: 架构规则(`layer_violation`、`module_violation`、`circular_dependency`)需要在配置中**显式声明** `architecture.layers`、`architecture.modules` 和/或 `architecture.detect_cycles`。它们不会自动发现项目边界。
-
-> **Glob 模式约定**: 无论在什么平台上,YAML 配置中的 glob 模式均使用正斜杠 `/`。切勿使用反斜杠。
-
----
-
-## 检测规则
-
-| 规则 ID | 等级 | 领域 | 优先级 | 检测内容 | 配置要求 |
-|---------|------|------|--------|----------|----------|
-| `large_file` | LOW | standards | P2 | 文件行数超过 `maxLines` | — |
-| `large_class` | LOW | standards | P2 | 类体行数超过 `maxLines` | — |
-| `large_build_method` | MEDIUM | performance | P1 | `build()` 方法行数超过 `maxLines` | — |
-| `lifecycle_resource_not_disposed` | MEDIUM | performance | P1 | 未释放的 StreamSubscription、Timer、AnimationController、TextEditingController、ScrollController、FocusNode、MqttClient、BluetoothDevice、StreamController | — |
-| `missing_const_constructor` | LOW | standards | P2 | Widget 类缺少 `const` 构造函数 | — |
-| `layer_violation` | HIGH | architecture | P0 | 跨架构层的依赖违规 | `architecture.layers` * |
-| `module_violation` | HIGH | architecture | P0 | 跨业务模块的依赖违规 | `architecture.modules` * |
-| `circular_dependency` | MEDIUM | architecture | P1 | 文件级循环依赖 | `architecture.detect_cycles` * |
-| `device_lifecycle` | HIGH | architecture | P0 | 不平衡的 init/teardown 配对(initState↔dispose、connect↔disconnect 等) | — |
-| `mqtt_connection` | HIGH | architecture | P0 | MQTT connect/disconnect 配对、硬编码 broker URL | — |
-| `iot_security` | HIGH | architecture | P0 | 硬编码凭证、明文 MQTT/HTTP、不安全 BLE | `rules.iot_security.requireTls` |
-| `ble_scanning` | MEDIUM | architecture | P1 | BLE startScan/stopScan 配对、扫描超时 | `rules.ble_scanning.maxScanDurationMs` |
-| `pubspec_security` | MEDIUM | standards | P2 | 无界依赖、已废弃包、过旧 IoT 依赖版本 | — |
-
-* 需在 YAML 配置中显式声明才能激活。
-
----
-
-## 输出
-
-### 终端表格(默认)
-
-按领域分组的彩色终端报告,显示总评分、文件数、问题数及每个问题的详情。
-
-### JSON 报告
-
-`--format json` 将报告写入 `--output` 目录下的 `report.json`。
-
-示例结构:
-
-```json
-{
- "version": "1.0.0",
- "generatedAt": "2026-06-09T12:00:00.000Z",
- "projectPath": "/path/to/project",
- "score": 85,
- "summary": {
- "total": 3,
- "high": 1,
- "medium": 1,
- "low": 1,
- "suppressed": 0,
- "suppressedByBaseline": 0,
- "byDomain": {
- "architecture": { "high": 1, "medium": 0, "low": 0, "total": 1 }
- }
- },
- "issues": []
-}
-```
-
-### SARIF 报告
-
-`--format sarif` 会写入 `.flutterguard/report.sarif`,可上传到 GitHub Code Scanning。high、medium、low 分别映射为 SARIF `error`、`warning`、`note`。
-
-### Suppression 与 baseline
-
-对已确认的误报可以使用源码注释:
-
-```dart
-// flutterguard: ignore missing_const_constructor
-// flutterguard: ignore iot_security, mqtt_connection
-// flutterguard: ignore all
-```
-
-注释只作用于当前行和下一行。
-
-推荐 CI 接入顺序:
-
-```bash
-flutterguard config doctor
-flutterguard baseline create .
-flutterguard baseline check . --baseline .flutterguard/baseline.json --no-growth
-flutterguard scan . --baseline .flutterguard/baseline.json --format json --fail-on high
-```
-
-## 评分
-
-```
-score = max(0, 100 - high×10 - medium×4 - low×1)
-```
-
-| 分数段 | 等级 |
-|--------|------|
-| 80–100 | 优秀 |
-| 50–79 | 需关注 |
-| 0–49 | 需整改 |
-
----
-
-## CI 集成
-
-### GitHub Actions
-
-```yaml
-name: FlutterGuard
-
-on: [push, pull_request]
-
-jobs:
- scan:
- strategy:
- matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
- runs-on: ${{ matrix.os }}
- steps:
- - uses: actions/checkout@v4
- - uses: dart-lang/setup-dart@v1
- with:
- sdk: 3.3.0
- - name: Install FlutterGuard
- run: dart pub global activate flutterguard_cli
- - name: Scan
- run: flutterguard scan . --format json --baseline .flutterguard/baseline.json --fail-on high --min-score 80
-```
-
-### GitHub Code Scanning
-
-```yaml
-name: FlutterGuard SARIF
-
-on: [push, pull_request]
-
-jobs:
- code-scanning:
- runs-on: ubuntu-latest
- permissions:
- security-events: write
- contents: read
- steps:
- - uses: actions/checkout@v4
- - uses: dart-lang/setup-dart@v1
- with:
- sdk: 3.3.0
- - run: dart pub global activate flutterguard_cli
- - run: flutterguard scan . --format sarif --baseline .flutterguard/baseline.json
- - uses: github/codeql-action/upload-sarif@v3
- with:
- sarif_file: .flutterguard/report.sarif
-```
-
-### GitLab CI
-
-```yaml
-flutterguard:
- image: dart:3.3.0
- script:
- - dart pub global activate flutterguard_cli
- - flutterguard scan . --format json --fail-on high --min-score 80
- artifacts:
- paths:
- - .flutterguard/report.json
- when: always
-```
-
-### pre-commit hook
-
-```yaml
-# .pre-commit-config.yaml
-repos:
- - repo: local
- hooks:
- - id: flutterguard
- name: FlutterGuard scan
- entry: flutterguard scan . --fail-on high
- language: system
- pass_filenames: false
- always_run: true
-```
-
-### 本地脚本
-
-
-macOS / Linux
-
-```bash
-#!/usr/bin/env bash
-# scan_ci.sh
-if flutterguard scan . --format json --fail-on high --min-score 80; then
- echo "All checks passed!"
-else
- status=$?
- echo "FlutterGuard failed with exit code $status."
- exit "$status"
-fi
-```
-
-
-
-Windows (PowerShell)
-
-```powershell
-# scan_ci.ps1
-$ErrorActionPreference = "Stop"
-flutterguard scan . --format json --fail-on high --min-score 80
-$status = $LASTEXITCODE
-
-if ($status -eq 0) {
- Write-Host "All checks passed!" -ForegroundColor Green
-} else {
- Write-Host "FlutterGuard failed with exit code $status." -ForegroundColor Red
- exit $status
-}
-```
-
-
----
-
-## 常见问题
-
-### Windows: ANSI 颜色显示为原始转义字符
-
-使用 **Windows Terminal**(Windows 10/11 自带)而非旧版 cmd.exe。也可添加 `--no-color` 禁用 ANSI 输出:
-
-```powershell
-flutterguard scan . --no-color
-```
-
-### Windows: "API key required" 错误
-
-说明当前 shell 解析到了旧版全局二进制。显式运行当前目录编译产物:
-
-```powershell
-.\flutterguard.exe scan .
-```
-
-或重新安装:
-
-```powershell
-dart pub global deactivate flutterguard_cli
-dart pub global activate flutterguard_cli
-```
-
-### Windows: 中文输出显示乱码
-
-```powershell
-# PowerShell 中设置 UTF-8 编码
-[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
-# 推荐使用 Windows Terminal,默认支持 UTF-8
-```
-
-### glob 模式始终使用正斜杠
-
-在 `flutterguard.yaml` 中,所有平台的路径模式均使用 `/`:
-
-```yaml
-# 正确
-path: lib/presentation/**
-
-# 错误(Windows 也不要用反斜杠)
-path: lib\presentation\**
-```
-
----
-
-## 仓库结构
-
-```
-flutterguard/
-├── packages/
-│ └── flutterguard_cli/ CLI 实现(主开发路径)
-├── archive/ 已归档的运行时追踪包
-└── examples/
- └── scan_demo/ 扫描示例项目
-```
-
-## 开发
-
-```bash
-# 全平台通用
-git clone https://github.com/lizy-coding/flutterguard.git
-cd flutterguard
-dart pub get
-dart pub global activate melos
-melos bootstrap
-
-# 常用命令
-dart run melos run analyze # 静态分析
-dart run melos run test:cli # 运行测试
-dart compile exe packages/flutterguard_cli/bin/flutterguard.dart -o flutterguard
-```
-
-## 扩展阅读
-
-| 文档 | 内容 |
-|------|------|
-| [docs/USAGE.md](docs/USAGE.md) | 完整使用指南(全平台) |
-| [docs/WINDOWS_ASSESSMENT.md](docs/WINDOWS_ASSESSMENT.md) | Windows 兼容性评估报告 |
-| [docs/FLUTTERGUARD_SPEC.md](docs/FLUTTERGUARD_SPEC.md) | 技术规格 |
-| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | 架构概览 |
-
-## License
-
-MIT
diff --git a/RELEASE_v0.3.0.md b/RELEASE_v0.3.0.md
deleted file mode 100644
index 0d193b1..0000000
--- a/RELEASE_v0.3.0.md
+++ /dev/null
@@ -1,321 +0,0 @@
-# Release v0.3.0 — Execution Prompt
-
-## Summary
-Version bump 0.2.0 → 0.3.0. Update docs, package metadata, CHANGELOG, compile, tag, push.
-
----
-
-## Step 1: Version Bump
-
-### 1a. `packages/flutterguard_cli/pubspec.yaml`
-`version: 0.2.0` → `version: 0.3.0`
-
-### 1b. `packages/flutterguard_cli/bin/flutterguard.dart`
-`const _version = '0.2.0';` → `const _version = '0.3.0';`
-
----
-
-## Step 2: Update `docs/FLUTTERGUARD_SPEC.md`
-
-### 2a. Header line 6
-`Version: M2 (Milestone 2) — Architecture Overhaul | Last Updated: 2026-05-17`
-→ `Version: M3 (Milestone 3) — Incremental Scan + Rule Introspection | Last Updated: 2026-06-28`
-
-### 2b. §1 Architecture Overview lines 44-63
-
-Replace the entire flow diagram block with:
-
-```
-User runs: flutterguard scan [] [--changed-only] [--base main]
- │
- ├── 1. ArgParser parses CLI flags
- ├── 2. ScanConfig.fromFile() loads YAML config
- ├── 3. FileCollector.collect() globs .dart files
- │ └── if --changed-only: FileCollector.getChangedFiles() filters by git diff
- ├── 4. Per-file scan: 11 rule classes analyze each file (13 rule IDs)
- │ ├── LargeUnitsRule (file size, class size, build method size)
- │ ├── LifecycleResourceRule (undisposed controllers/streams)
- │ ├── LayerViolationRule (cross-layer import violations)
- │ ├── ModuleViolationRule (cross-module import violations)
- │ ├── CircularDependencyRule (file-level cycle detection)
- │ ├── MissingConstConstructorRule (widgets missing const constructor)
- │ ├── DeviceLifecycleRule (init/teardown pairing)
- │ ├── MqttConnectionRule (MQTT connect/disconnect, hardcoded URLs)
- │ ├── BleScanningRule (BLE scan lifecycle, timeout)
- │ ├── IotSecurityRule (hardcoded secrets, cleartext, insecure BLE)
- │ └── PubspecSecurityRule (unbounded deps, deprecated packages)
- │ └── (changed-only: circular_dependency skipped)
- ├── 5. Issues sorted by risk level (high → medium → low)
- ├── 6. ReportGenerator generates output
- │ ├── Table → terminal stdout
- │ └── JSON → .flutterguard/report.json
- └── 7. CI gate check (exit 1 if fail threshold exceeded)
-```
-
-Also update line 71: `Rule class interface` table row.
-
-### 2c. §4 CLI Contract — Add flags
-
-After `--min-score` in the scan command block, add:
-
-```
- --changed-only Only scan .dart files changed since --base (default: false)
- --base Git base ref for changed-only (default: main)
-```
-
-After the scan block, add:
-
-```
-flutterguard rules [options]
- --format (-f) Output format: table | json (default: table)
-
-flutterguard explain
-```
-
-In Exit Codes table, update Code 2 description:
-```
-| 2 | Scan/explain error (bad path, config parse error, unknown rule ID) |
-```
-
-### 2d. §6.1 JSON Report Schema — Add scanMode
-
-After `"generatedAt"` line, insert:
-```json
- "scanMode": "full|changed",
-```
-
-### 2e. §7 Static Rules Detail — Append IoT rules
-
-After §7.8 (missing_const_constructor), append 5 new sections:
-
-```
-### 7.9 device_lifecycle (RiskLevel: high, Domain: architecture, Priority: p0)
-
-**Detection**: For each class, check that device lifecycle methods have balanced init/teardown pairs:
-- `initState` ↔ `dispose`
-- `connect()` ↔ `disconnect()`
-- `start()` ↔ `stop()`
-- `listen()` / `subscribe()` ↔ `cancel()` / `unsubscribe()`
-
-**Implementation**: Parse with `package:analyzer`, check method name presence for balanced pairs.
-
-### 7.10 mqtt_connection (RiskLevel: high, Domain: architecture, Priority: p0)
-
-**Detection**:
-1. Find MQTT client field declarations (types matching `*MqttClient`, `*MQTT*`)
-2. Check for `connect()` calls — verify `disconnect()` exists in dispose-like methods
-3. Check for `subscribe()` calls — verify corresponding `unsubscribe()` calls exist
-4. Check for hardcoded broker URLs (string literals containing `tcp://` or `mqtt://`)
-
-### 7.11 ble_scanning (RiskLevel: medium, Domain: architecture, Priority: p1)
-
-**Detection**:
-1. Find BLE-related field declarations (types matching `*Ble*`, `*Bluetooth*`)
-2. Check for `startScan()` calls — verify `stopScan()` exists in dispose-like methods
-3. Check for `connect()` calls to BLE devices — verify `disconnect()` exists
-4. Check that scan timeout is configured (look for timeout parameter in `startScan()`)
-
-**Config**: `maxScanDurationMs: int (default: 10000)`
-
-### 7.12 iot_security (RiskLevel: high, Domain: architecture, Priority: p0)
-
-**Detection**:
-| Check | Pattern | Severity |
-|-------|---------|----------|
-| Hardcoded password/token | String literal matching `password`/`token`/`secret` assignment | high |
-| Cleartext MQTT | `tcp://` host or port `1883` in MQTT config | high |
-| Cleartext HTTP | `http://` in IoT context packages | medium |
-| Insecure BLE | BLE without `bond`/`pair` references | medium |
-
-**Config**: `requireTls: bool (default: true)`
-
-### 7.13 pubspec_security (RiskLevel: medium, Domain: standards, Priority: p2)
-
-**Detection**: Analyzes the project's `pubspec.yaml` rather than individual `.dart` files.
-| Check | Pattern | Severity |
-|-------|---------|----------|
-| Unbounded dependency | `^any` or no version constraint | medium |
-| Outdated mqtt_client | `mqtt_client` version < 10.x.x | high |
-| Outdated flutter_blue | `flutter_blue` (deprecated, use `flutter_blue_plus`) | high |
-| Outdated http | `http` package < 1.x.x with cleartext patterns | medium |
-```
-
-### 2f. §8 Test Contracts — Full replace
-
-Replace header: `### 8.1 CLI Tests (13 tests)` → `### 8.1 CLI Tests (37 tests)`
-
-Append to the test table:
-
-```
-| changed_only_filters_files | temp git repo, 2 files, change 1 | scanMode=changed, only changed-file issues |
-| changed_only_full_scan_when_no_git | non-git dir with changedOnly | scanMode=full |
-| changed_only_skips_circular_dependency | cycle fixture with changedOnly | 0 circular_dependency issues |
-| registry_contains_all_13_rules | RuleRegistry.all() | length == 13 |
-| registry_find_returns_correct_meta | find('large_file') | non-null, correct id/domain |
-| registry_find_unknown_returns_null | find('nonexistent') | null |
-```
-
-### 2g. §9 Evolution Roadmap — Mark M3 complete
-
-Add after M2 entry:
-
-```
-### M3 (Completed v0.3.0) — Incremental Scan + Rule Introspection
-
-Key deliverables:
-- `--changed-only` incremental scan via git diff (skips cyclic dep in changed mode)
-- `flutterguard rules` / `flutterguard explain` subcommands with RuleMeta registry
-- 37 tests (26 base + 6 new + 5 existing)
-- RuleMeta class + RuleRegistry for rule introspection
-```
-
-### 2h. §10 Known Limitations — Update
-
-- Remove entries 5 and 6 (IoT MQTT/BLE and pubspec_security — now implemented)
-- Add entry:
-```
-9. **Incremental scan**: `--changed-only` skips circular_dependency entirely
- in changed mode. Layer/module violations only detected if the changed file
- is the source of the illegal import. Pubspec security is not re-checked
- unless the changed file set includes pubspec.yaml.
-```
-
-### 2i. §12 IoT Domain Rules (Planned) — Delete entire section
-
-The 5 IoT rules (12.1–12.5) are now documented in §7.9–§7.13. Delete the entire §12 section.
-
-### 2j. Append §13 Rule Registry + §14 Incremental Scan
-
-After the final line of the document (end of existing content), append:
-
-```
----
-## 13. Rule Registry & Explain Commands
-
-### 13.1 RuleMeta
-Data class in `lib/src/rule_meta.dart`:
-- `id` — rule identifier
-- `name` — Chinese display name
-- `domain` — architecture / performance / standards
-- `riskLevel` — high / medium / low
-- `priority` — p0 / p1 / p2
-- `purpose` — detection purpose
-- `riskReason` — why this matters
-- `badExample` — anti-pattern
-- `fixSuggestion` — recommended fix
-- `configKeys` — YAML config keys
-- `cicdSafe` — whether suitable for CI gating
-
-### 13.2 RuleRegistry
-Singleton in `lib/src/rules/registry.dart`:
-- `all()` → `List` (13 entries)
-- `find(String id)` → `RuleMeta?`
-
-### 13.3 CLI Commands
-- `flutterguard rules` — table of all rules
-- `flutterguard rules --format json` — JSON payload
-- `flutterguard explain ` — full detail; exit 2 on unknown ID
-
----
-## 14. Incremental Scan (--changed-only)
-
-### Flow
-1. Check `.git/` exists in project path
-2. `git diff --name-only --diff-filter=ACMR`
-3. `git ls-files --others --exclude-standard` (untracked files)
-4. Union both sets, filter to .dart files
-5. Feed filtered files to all rules
-6. CircularDependencyRule force-disabled in changed-only mode
-
-### Behavior Matrix
-| Condition | Behavior | scanMode |
-|-----------|----------|----------|
-| Non-git dir | Fallback to full scan | full |
-| --changed-only, 0 changes | "No Dart files found" exit | full |
-| --changed-only, changes > 0 | Only scan changed .dart files | changed |
-| --base not specified | Defaults to 'main' | — |
-```
-
----
-
-## Step 3: Update `AGENTS.md`
-
-| Line | Change |
-|------|--------|
-| **Key Commands** | `(26 tests)` → `(37 tests)` |
-| **Key Commands row 3** | After json row, add: `\| \`flutterguard scan --changed-only\` \| Incremental scan of git-changed files \|` |
-| **Key Commands row 4** | After above: `\| \`flutterguard rules\` / \`flutterguard explain \` \| List/describe rules \|` |
-| **CLI Entry Point** | After last sentence, append: `Supports --changed-only incremental scan and rule introspection (rules/explain).` |
-| **Source Layout rules/** | Before `pubspec_security.dart` line, add 2 lines: `rule_meta.dart` and `rules/registry.dart` |
-
----
-
-## Step 4: Update `packages/flutterguard_cli/CHANGELOG.md`
-
-Insert at top (before `## 0.1.1`):
-
-```markdown
-## 0.3.0 (2026-06-28)
-
-### Incremental Scan (--changed-only)
-
-- **cli:** New `--changed-only` flag — only scans `.dart` files changed since `--base` (default: `main`)
-- **cli:** Integrated `git diff --name-only` + `git ls-files --others` for change detection
-- **cli:** Non-git fallback: gracefully degrades to full scan
-- **cli:** `circular_dependency` auto-disabled in changed-only mode
-- **cli:** JSON report now includes `scanMode: full|changed` field
-
-### Rule Introspection (rules / explain)
-
-- **cli:** New `flutterguard rules` subcommand — list all 13 rules with ID, domain, name
-- **cli:** New `flutterguard explain ` subcommand — detailed purpose, risk, example, fix, config
-- **cli:** New `RuleMeta` class in `lib/src/rule_meta.dart` — structured rule metadata
-- **cli:** New `RuleRegistry` singleton in `lib/src/rules/registry.dart` — registry with `all()` and `find()`
-- **cli:** Output supports both `table` (default) and `--format json`
-
-### Tests
-
-- **test:** 37 total tests (26 base + 6 new + 5 existing)
-- **test:** 3 `changed-only` tests: git repo filter, non-git fallback, skip cycle
-- **test:** 3 registry tests: all 13 rules, find by ID, unknown returns null
-
-### Infrastructure
-
-- **meta:** Version bumped to 0.3.0
-- **meta:** `AGENTS.md` and `FLUTTERGUARD_SPEC.md` updated for new features
-```
-
----
-
-## Step 5: Delete ITERATION_1.md
-
-Remove the plan file (no longer needed after release):
-
-```bash
-rm /Users/forest/code/flutterguard/ITERATION_1.md
-```
-
----
-
-## Step 6: Compile + Verify
-
-```bash
-dart run melos run analyze
-dart run melos run test:cli
-dart compile exe packages/flutterguard_cli/bin/flutterguard.dart -o flutterguard
-./flutterguard --version # expect "flutterguard 0.3.0"
-./flutterguard rules # expect 13 rules
-./flutterguard explain large_file # expect full detail
-```
-
----
-
-## Step 7: git commit + tag + push
-
-```bash
-git add -A
-git commit -m "cli: v0.3.0 — --changed-only + rules/explain"
-git tag v0.3.0
-git push origin develop
-git push origin v0.3.0
-```
diff --git a/ROADMAP.md b/ROADMAP.md
deleted file mode 100644
index 6d1a306..0000000
--- a/ROADMAP.md
+++ /dev/null
@@ -1,185 +0,0 @@
-# FlutterGuard Evolution Roadmap
-
-FlutterGuard remains an IoT / smart home Flutter static analysis toolchain. The
-core product direction is static governance for Flutter projects, not runtime
-observability, APM, cloud dashboards, crash reporting, or SDK instrumentation.
-
-## Guiding Principles
-
-- Keep the CLI as the source of truth for scanning, configuration, reports, and
- CI behavior.
-- Prefer local-first integrations that consume CLI output before introducing
- hosted services.
-- Focus on real team adoption: low-noise CI, editor feedback, baseline
- management, and actionable reports.
-- Avoid archived runtime packages for new feature work.
-
-## Architecture Invariants (do not violate)
-
-These were established by the v0.5.0 refactor and are binding for all future
-work. New agents MUST follow them instead of reintroducing pre-0.5.0 patterns.
-
-- Rule metadata and execution are wired through `lib/src/rules/catalog.dart`.
- It is the single source of truth. Do NOT wire rules directly in
- `bin/flutterguard.dart` or `scanner.dart`, and do NOT add reflection or
- dynamic plugin loading.
-- `bin/flutterguard.dart` is thin: top-level routing, help, positional path,
- and exit codes only. Functional command behavior lives in `lib/src/cli/`
- (`scan_command`, `baseline_commands`, `config_commands`, `issue_commands`,
- `rule_commands`, `cli_parsers`).
-- Rules consume shared analysis state, not ad-hoc file reads:
- - `SourceWorkspace` for source/AST caching and diagnostics.
- - `ImportGraph` for resolved Dart imports.
- - `DependencyBoundaryEngine` for layer/module boundary checks.
- - `ScanContext` for scan scope/mode (project/all/target files).
-- Each rule keeps the standalone `analyze(List files, {SourceWorkspace?
- workspace})` API so direct rule tests and programmatic consumers keep working.
-- New rule flow is unchanged: spec entry → config typedef → rule class →
- fixture → test → wire into `rules/catalog.dart`.
-- Workspace SDK constraint is `^3.11.5`; keep CI (`release.yml`,
- `flutterguard.yml`) SDK aligned when it changes.
-
-## Current State (as of v0.5.0)
-
-- Milestone 0.4.x (CI Adoption Hardening) is DONE: baseline `stats` / `prune` /
- `check --no-growth`, config profiles, install diagnostics, issue export, and
- no-match / changed-only scan policy hardening all shipped in 0.4.0–0.4.1.
-- v0.5.0 was consumed by the internal architecture refactor above rather than
- the originally planned developer-workflow integrations. Those integrations
- now move to Milestone 0.6.
-
-## Milestone 0.6 — Developer Workflow Integrations
-
-Goal: Surface FlutterGuard findings before CI by integrating with developer
-tools.
-
-Deliverables:
-
-- Official GitHub Action:
- - install FlutterGuard
- - run scan
- - upload JSON / SARIF artifacts
- - optionally upload SARIF to GitHub Code Scanning
-- GitHub PR annotations mode for changed-line findings.
-- VS Code extension MVP driven by CLI JSON output:
- - run scan on demand
- - show diagnostics in the editor
- - open rule explanations
- - insert suppression comments
-- Config profiles:
- - `recommended`
- - `strict`
- - `migration`
- - `iot-security`
- - `architecture-only`
-
-Exit Criteria:
-
-- New users can add FlutterGuard to GitHub Actions with one reusable action.
-- Developers can see and suppress findings in VS Code without reading CI logs.
-- Teams can start from a profile instead of hand-authoring every rule setting.
-
-## Milestone 0.7 — Fixes, Reports, and Workspace Scale
-
-Goal: Move from reporting issues to helping teams reduce them across larger
-Flutter codebases.
-
-Deliverables:
-
-- Auto-fix support for safe cases:
- - add missing `const` constructors where deterministic
- - insert suppression comments for selected findings
- - suggest `pubspec.yaml` dependency replacements
-- Static HTML report output for local reviews and architecture audits.
-- Melos / monorepo workspace scanning:
- - scan all packages
- - scan one package
- - scan affected packages
- - merge package reports
-- Architecture graph exports:
- - Mermaid
- - DOT
- - dependency cycle graph
- - layer / module violation graph
-
-Exit Criteria:
-
-- Teams can track and reduce findings without building custom scripts.
-- Large Flutter workspaces can run FlutterGuard package-by-package.
-- Architecture violations can be reviewed visually during refactors.
-
-## Milestone 0.8 — Deeper Static Analysis
-
-Goal: Reduce false positives and expand IoT-specific detection using stronger
-AST and project context.
-
-Deliverables:
-
-- Lifecycle detection improvements:
- - helper dispose methods
- - base classes and mixins
- - composite disposer patterns
- - resource collections
-- Import resolution improvements:
- - package name awareness
- - workspace package imports
- - generated-file boundaries
-- Additional IoT checks:
- - OTA update safety patterns
- - token and key storage risks
- - device pairing flow risks
- - local network discovery risks
- - permission and privacy configuration checks
-
-Exit Criteria:
-
-- Core lifecycle and IoT security rules produce fewer false positives on real
- projects.
-- Monorepo package imports are handled consistently.
-- New IoT checks remain static-only and CI-safe.
-
-## Milestone 0.9+ — Extensibility and Team Governance
-
-Goal: Support mature teams that need reusable governance policies without
-turning FlutterGuard into a hosted platform.
-
-Deliverables:
-
-- Reusable team rule packs.
-- Shared config inheritance:
- - local file extends
- - bundled profiles
- - organization profile conventions
-- Custom rule SDK exploration.
-- IntelliJ / Android Studio plugin after VS Code integration stabilizes.
-- Optional GitHub App only if GitHub Action annotations are insufficient.
-
-Exit Criteria:
-
-- Teams can standardize policies across many Flutter apps.
-- Customization is possible without weakening the built-in static analysis
- contract.
-- Hosted services remain optional and out of the core product path.
-
-## Non-Goals
-
-- Runtime tracing SDK.
-- APM or observability platform.
-- Crash reporting.
-- Cloud dashboard as a required workflow.
-- Network proxy or HTTP inspector.
-- Flutter widget library.
-- Editing archived runtime packages in `archive/`.
-
-## Recommended Priority
-
-1. Official GitHub Action.
-2. Baseline management commands.
-3. VS Code extension MVP.
-4. Config profiles and inheritance.
-5. PR annotations.
-6. Auto-fix for deterministic cases.
-7. Static HTML report.
-8. Workspace / monorepo scanning.
-9. Architecture graph export.
-10. Custom rule SDK exploration.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 5d31927..a12411a 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,6 +1,8 @@
+include: package:lints/recommended.yaml
+
analyzer:
exclude:
- - archive/**
+ - test/fixtures/**
language:
strict-casts: true
strict-inference: true
@@ -8,8 +10,6 @@ analyzer:
linter:
rules:
- avoid_dynamic_calls
- - prefer_const_constructors
- prefer_const_declarations
- prefer_final_locals
- unawaited_futures
- - use_key_in_widget_constructors
diff --git a/archive/README.md b/archive/README.md
deleted file mode 100644
index df504cb..0000000
--- a/archive/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Archive — Reserved for Future Use
-
-These packages were frozen runtime-tracing implementations from v0.1.0.
-
-| Package | Purpose | Status |
-|---------|---------|--------|
-| flutterguard_core | Runtime tracing engine (Zone-based) | Reserved |
-| flutterguard_dio | Dio HTTP interceptor | Reserved |
-| flutterguard_flutter | Flutter runtime hooks | Reserved |
-
-**Decision**: Superseded by CLI static analysis approach (Path A).
-Zero code modifications — preserved as-is for reference.
-To re-activate: `mv archive/ packages/` and restore melos.yaml entries.
diff --git a/archive/flutterguard_core/AGENTS.md b/archive/flutterguard_core/AGENTS.md
deleted file mode 100644
index 2530bfa..0000000
--- a/archive/flutterguard_core/AGENTS.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Package: flutterguard_core [FROZEN]
-
-## Role
-Flow-level aspect tracing engine — trace models, Zone-based context propagation, ring buffer store, and JSON/Markdown exporters.
-
-**Status: FROZEN**. No new features. Bug fixes only.
-
-## Dependency Map
-- depends on: meta ^1.12.0
-- depended by: flutterguard_cli (path), flutterguard_dio (path), flutterguard_flutter (path)
-
-## Entry Points
-- lib barrel: `lib/flutterguard_core.dart`
-
-## Key Source Files
-| File | Responsibility |
-|------|---------------|
-| `src/flutter_guard.dart` | Static API: action(), span(), record*(), export*() |
-| `src/guard_config.dart` | FlutterGuardConfig model |
-| `src/trace_context.dart` | Zone-based traceId propagation |
-| `src/trace_model.dart` | Data models: FlowTrace, SpanTrace, NetworkTrace, etc. |
-| `src/trace_store.dart` | Ring buffer singleton store (100 trace default) |
-| `src/json_exporter.dart` | JSON export formatting |
-| `src/markdown_exporter.dart` | Markdown export formatting |
-
-## Pubspec Overrides
-melos-managed: none (core has no path deps)
-
-## Analysis Options
-Inherits root strict-casts/strict-inference + package:lints/recommended.yaml (from pubspec).
-
-## Test
-- command: `melos run test:core`
-- test file: `test/flutter_guard_test.dart` (7 tests)
-
-## Forward Compatibility
-- Keep public API stable (flutterguard_cli depends on this via path)
-- Do NOT add new exports without team consensus
-- Do NOT remove existing public APIs (breaks cli, dio, flutter packages)
-
-## Why Frozen
-Runtime tracing approach was superseded by static analysis (Path A). Core remains as a reference implementation and may be re-visited in M4 roadmap.
diff --git a/archive/flutterguard_core/lib/flutterguard_core.dart b/archive/flutterguard_core/lib/flutterguard_core.dart
deleted file mode 100644
index c0fd910..0000000
--- a/archive/flutterguard_core/lib/flutterguard_core.dart
+++ /dev/null
@@ -1,9 +0,0 @@
-library flutterguard_core;
-
-export 'src/trace_model.dart';
-export 'src/guard_config.dart';
-export 'src/trace_context.dart';
-export 'src/trace_store.dart';
-export 'src/flutter_guard.dart';
-export 'src/json_exporter.dart';
-export 'src/markdown_exporter.dart';
diff --git a/archive/flutterguard_core/lib/src/flutter_guard.dart b/archive/flutterguard_core/lib/src/flutter_guard.dart
deleted file mode 100644
index a4431ce..0000000
--- a/archive/flutterguard_core/lib/src/flutter_guard.dart
+++ /dev/null
@@ -1,162 +0,0 @@
-import 'dart:async';
-import 'dart:math';
-
-import 'guard_config.dart';
-import 'trace_context.dart';
-import 'trace_model.dart';
-import 'trace_store.dart';
-import 'json_exporter.dart';
-import 'markdown_exporter.dart';
-
-class FlutterGuard {
- static FlutterGuardConfig _config = const FlutterGuardConfig();
- static final Random _random = Random();
-
- static void configure(FlutterGuardConfig config) {
- _config = config;
- TraceStore.instance.configure(maxTraces: config.maxTraces);
- }
-
- static String? get currentTraceId => TraceContext.currentTraceId;
-
- static Future action(
- String name,
- FutureOr Function() body, {
- Map tags = const {},
- }) async {
- if (!_config.enabled) return body();
-
- final traceId = _generateId();
- final trace = FlowTrace(
- id: traceId,
- name: name,
- startTime: DateTime.now(),
- tags: Map.unmodifiable({
- ...tags,
- 'traceId': traceId,
- }),
- );
-
- TraceStore.instance.beginFlow(trace);
-
- bool failed = false;
- T result;
-
- try {
- result = await TraceContext.runWithTraceId(traceId, body);
- } catch (e, st) {
- failed = true;
- trace.errors.add(ErrorTrace(
- flowId: traceId,
- errorType: e.runtimeType.toString(),
- message: e.toString(),
- stackTrace: st.toString(),
- time: DateTime.now(),
- ));
- trace.status = FlowStatus.failed;
- rethrow;
- } finally {
- if (!failed) {
- TraceStore.instance.endFlow(traceId, failed: false);
- } else {
- trace.endTime = DateTime.now();
- }
- }
-
- return result;
- }
-
- static Future span(
- String name,
- FutureOr Function() body, {
- Map tags = const {},
- }) async {
- if (!_config.enabled) return body();
-
- final flowId = TraceContext.currentTraceId;
- if (flowId == null) return body();
-
- final span = SpanTrace(
- id: _generateId(),
- flowId: flowId,
- name: name,
- startTime: DateTime.now(),
- tags: tags,
- );
-
- TraceStore.instance.addSpanToActive(flowId, span);
-
- try {
- final result = await Future.value(body());
- span.endTime = DateTime.now();
- return result;
- } catch (e, st) {
- span.endTime = DateTime.now();
- span.errorType = e.runtimeType.toString();
- span.errorMessage = e.toString();
- span.stackTrace = st.toString();
- TraceStore.instance.addErrorToActive(
- flowId,
- ErrorTrace(
- flowId: flowId,
- errorType: e.runtimeType.toString(),
- message: e.toString(),
- stackTrace: st.toString(),
- time: DateTime.now(),
- ),
- );
- rethrow;
- }
- }
-
- static void recordNetwork(NetworkTrace trace) {
- if (!_config.enabled) return;
- final flowId = trace.flowId ?? TraceContext.currentTraceId;
- TraceStore.instance.addNetworkToActive(flowId, trace);
- }
-
- static void recordRoute(RouteTrace trace) {
- if (!_config.enabled || !_config.collectRoutes) return;
- final flowId = trace.flowId ?? TraceContext.currentTraceId;
- TraceStore.instance.addRouteToActive(flowId, trace);
- }
-
- static void recordError(ErrorTrace trace) {
- if (!_config.enabled || !_config.collectErrors) return;
- final flowId = trace.flowId ?? TraceContext.currentTraceId;
- TraceStore.instance.addErrorToActive(flowId, trace);
- }
-
- static void recordFrame(FrameTrace trace) {
- if (!_config.enabled || !_config.collectFrames) return;
- final flowId = trace.flowId ?? TraceContext.currentTraceId;
- TraceStore.instance.addFrameToActive(flowId, trace);
- }
-
- static void recordBuild(String boundaryName) {
- if (!_config.enabled || !_config.collectBuilds) return;
- TraceStore.instance.recordBuild(TraceContext.currentTraceId, boundaryName);
- }
-
- static String exportJson() {
- final traces = TraceStore.instance.getAllTraces();
- return JsonExporter.export(traces);
- }
-
- static String exportMarkdown() {
- final traces = TraceStore.instance.getAllTraces();
- return MarkdownExporter.export(traces);
- }
-
- static void reset() {
- TraceStore.instance.reset();
- }
-
- static String _generateId() {
- final bytes = List.generate(16, (_) => _random.nextInt(256));
- return bytes
- .map((b) => b.toRadixString(16).padLeft(2, '0'))
- .join()
- .substring(0, 12);
- }
-}
diff --git a/archive/flutterguard_core/lib/src/guard_config.dart b/archive/flutterguard_core/lib/src/guard_config.dart
deleted file mode 100644
index 7bfa57c..0000000
--- a/archive/flutterguard_core/lib/src/guard_config.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-class FlutterGuardConfig {
- final bool enabled;
- final bool collectErrors;
- final bool collectFrames;
- final bool collectRoutes;
- final bool collectBuilds;
- final int slowFlowMs;
- final int jankFrameMs;
- final int maxTraces;
- final bool sanitizeNetwork;
-
- const FlutterGuardConfig({
- this.enabled = true,
- this.collectErrors = true,
- this.collectFrames = true,
- this.collectRoutes = true,
- this.collectBuilds = true,
- this.slowFlowMs = 1000,
- this.jankFrameMs = 16,
- this.maxTraces = 100,
- this.sanitizeNetwork = true,
- });
-}
diff --git a/archive/flutterguard_core/lib/src/json_exporter.dart b/archive/flutterguard_core/lib/src/json_exporter.dart
deleted file mode 100644
index 2993cd7..0000000
--- a/archive/flutterguard_core/lib/src/json_exporter.dart
+++ /dev/null
@@ -1,31 +0,0 @@
-import 'dart:convert';
-
-import 'trace_model.dart';
-
-class JsonExporter {
- static const String version = '1.0.0';
-
- static String export(List traces) {
- final summary = _buildSummary(traces);
- final payload = {
- 'version': version,
- 'generatedAt': DateTime.now().toIso8601String(),
- 'summary': summary,
- 'traces': traces.map((t) => t.toJson()).toList(),
- };
- return const JsonEncoder.withIndent(' ').convert(payload);
- }
-
- static Map _buildSummary(List traces) {
- final total = traces.length;
- final success = traces.where((t) => t.status == FlowStatus.success).length;
- final failed = traces.where((t) => t.status == FlowStatus.failed).length;
- final running = traces.where((t) => t.status == FlowStatus.running).length;
- return {
- 'total': total,
- 'success': success,
- 'failed': failed,
- 'running': running
- };
- }
-}
diff --git a/archive/flutterguard_core/lib/src/markdown_exporter.dart b/archive/flutterguard_core/lib/src/markdown_exporter.dart
deleted file mode 100644
index 35733ba..0000000
--- a/archive/flutterguard_core/lib/src/markdown_exporter.dart
+++ /dev/null
@@ -1,116 +0,0 @@
-import 'trace_model.dart';
-
-class MarkdownExporter {
- static String export(List traces) {
- final buf = StringBuffer();
-
- buf.writeln('# FlutterGuard Flow Report');
- buf.writeln();
- buf.writeln('## Summary');
- buf.writeln();
- final total = traces.length;
- final success = traces.where((t) => t.status == FlowStatus.success).length;
- final failed = traces.where((t) => t.status == FlowStatus.failed).length;
- buf.writeln('| Metric | Count |');
- buf.writeln('|--------|-------|');
- buf.writeln('| Total Flows | $total |');
- buf.writeln('| Success | $success |');
- buf.writeln('| Failed | $failed |');
- buf.writeln();
-
- if (traces.isEmpty) {
- buf.writeln('*No flows recorded.*');
- return buf.toString();
- }
-
- buf.writeln('## Runtime Flows');
- buf.writeln();
- for (final trace in traces) {
- buf.writeln('### ${trace.name} `${trace.id}`');
- buf.writeln();
- buf.writeln('- **Status**: ${trace.status.name}');
- buf.writeln('- **Duration**: ${trace.durationMs}ms');
- buf.writeln('- **Started**: ${trace.startTime.toIso8601String()}');
- if (trace.endTime != null) {
- buf.writeln('- **Ended**: ${trace.endTime!.toIso8601String()}');
- }
- buf.writeln();
-
- if (trace.spans.isNotEmpty) {
- buf.writeln('#### Spans');
- buf.writeln();
- buf.writeln('| Name | Duration | Error |');
- buf.writeln('|------|----------|-------|');
- for (final span in trace.spans) {
- final error = span.errorType ?? '-';
- buf.writeln('| ${span.name} | ${span.durationMs}ms | $error |');
- }
- buf.writeln();
- }
-
- if (trace.networks.isNotEmpty) {
- buf.writeln('#### Network');
- buf.writeln();
- buf.writeln('| Method | Path | Status | Duration |');
- buf.writeln('|--------|------|--------|----------|');
- for (final net in trace.networks) {
- buf.writeln(
- '| ${net.method} | ${net.path} | ${net.statusCode ?? '-'} | ${net.durationMs}ms |');
- }
- buf.writeln();
- }
-
- if (trace.routes.isNotEmpty) {
- buf.writeln('#### Routes');
- buf.writeln();
- buf.writeln('| Type | From | To |');
- buf.writeln('|------|------|----|');
- for (final route in trace.routes) {
- buf.writeln(
- '| ${route.type} | ${route.from ?? '-'} | ${route.to ?? '-'} |');
- }
- buf.writeln();
- }
-
- if (trace.errors.isNotEmpty) {
- buf.writeln('#### Errors');
- buf.writeln();
- for (final error in trace.errors) {
- buf.writeln('- **${error.errorType}**: ${error.message}');
- if (error.stackTrace != null) {
- buf.writeln(' ```');
- buf.writeln(' ${error.stackTrace}');
- buf.writeln(' ```');
- }
- }
- buf.writeln();
- }
-
- if (trace.frames.isNotEmpty) {
- buf.writeln('#### Frames');
- buf.writeln();
- buf.writeln('| Total | Build | Raster | Janky |');
- buf.writeln('|-------|-------|--------|-------|');
- for (final frame in trace.frames) {
- buf.writeln(
- '| ${frame.totalSpanMs}ms | ${frame.buildDurationMs}ms | ${frame.rasterDurationMs}ms | ${frame.janky} |');
- }
- buf.writeln();
- }
-
- if (trace.buildCounts.isNotEmpty) {
- buf.writeln('#### Build Boundaries');
- buf.writeln();
- for (final entry in trace.buildCounts.entries) {
- buf.writeln('- **${entry.key}**: ${entry.value} rebuilds');
- }
- buf.writeln();
- }
-
- buf.writeln('---');
- buf.writeln();
- }
-
- return buf.toString();
- }
-}
diff --git a/archive/flutterguard_core/lib/src/trace_context.dart b/archive/flutterguard_core/lib/src/trace_context.dart
deleted file mode 100644
index 3172117..0000000
--- a/archive/flutterguard_core/lib/src/trace_context.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-import 'dart:async';
-
-class TraceContext {
- static const String _keyName = 'flutter_guard_trace_id';
-
- static String? get currentTraceId {
- try {
- return Zone.current[_keyName] as String?;
- } on TypeError {
- return null;
- }
- }
-
- static Future runWithTraceId(
- String? traceId,
- FutureOr Function() body,
- ) async {
- if (traceId == null) {
- final result = body();
- if (result is Future) return result;
- return result;
- }
- return runZoned(() async {
- final result = body();
- if (result is Future) return await result;
- return result;
- }, zoneValues: {_keyName: traceId});
- }
-}
diff --git a/archive/flutterguard_core/lib/src/trace_model.dart b/archive/flutterguard_core/lib/src/trace_model.dart
deleted file mode 100644
index 6fb127c..0000000
--- a/archive/flutterguard_core/lib/src/trace_model.dart
+++ /dev/null
@@ -1,219 +0,0 @@
-import 'dart:math';
-
-enum FlowStatus { running, success, failed }
-
-class SpanTrace {
- final String id;
- final String flowId;
- final String name;
- final DateTime startTime;
- DateTime? endTime;
- final Map tags;
- String? errorType;
- String? errorMessage;
- String? stackTrace;
-
- SpanTrace({
- required this.id,
- required this.flowId,
- required this.name,
- required this.startTime,
- this.endTime,
- Map? tags,
- this.errorType,
- this.errorMessage,
- this.stackTrace,
- }) : tags = tags ?? {};
-
- int get durationMs {
- final end = endTime ?? DateTime.now();
- return end.difference(startTime).inMilliseconds;
- }
-
- Map toJson() => {
- 'id': id,
- 'flowId': flowId,
- 'name': name,
- 'startTime': startTime.toIso8601String(),
- 'endTime': endTime?.toIso8601String(),
- 'durationMs': durationMs,
- 'tags': tags,
- 'errorType': errorType,
- 'errorMessage': errorMessage,
- 'stackTrace': stackTrace,
- };
-}
-
-class NetworkTrace {
- final String? flowId;
- final String method;
- final String path;
- final int? statusCode;
- final int durationMs;
- final int? requestSize;
- final int? responseSize;
- final bool success;
- final String? errorType;
- final String? errorMessage;
-
- NetworkTrace({
- this.flowId,
- required this.method,
- required this.path,
- this.statusCode,
- required this.durationMs,
- this.requestSize,
- this.responseSize,
- required this.success,
- this.errorType,
- this.errorMessage,
- });
-
- Map toJson() => {
- 'flowId': flowId,
- 'method': method,
- 'path': path,
- 'statusCode': statusCode,
- 'durationMs': durationMs,
- 'requestSize': requestSize,
- 'responseSize': responseSize,
- 'success': success,
- 'errorType': errorType,
- 'errorMessage': errorMessage,
- };
-}
-
-class RouteTrace {
- final String? flowId;
- final String type;
- final String? from;
- final String? to;
- final DateTime time;
-
- RouteTrace({
- this.flowId,
- required this.type,
- this.from,
- this.to,
- required this.time,
- });
-
- Map toJson() => {
- 'flowId': flowId,
- 'type': type,
- 'from': from,
- 'to': to,
- 'time': time.toIso8601String(),
- };
-}
-
-class ErrorTrace {
- final String? flowId;
- final String errorType;
- final String message;
- final String? stackTrace;
- final DateTime time;
- final String? route;
-
- ErrorTrace({
- this.flowId,
- required this.errorType,
- required this.message,
- this.stackTrace,
- required this.time,
- this.route,
- });
-
- Map toJson() => {
- 'flowId': flowId,
- 'errorType': errorType,
- 'message': message,
- 'stackTrace': stackTrace,
- 'time': time.toIso8601String(),
- 'route': route,
- };
-}
-
-class FrameTrace {
- final String? flowId;
- final int totalSpanMs;
- final int buildDurationMs;
- final int rasterDurationMs;
- final bool janky;
- final DateTime time;
-
- FrameTrace({
- this.flowId,
- required this.totalSpanMs,
- required this.buildDurationMs,
- required this.rasterDurationMs,
- required this.janky,
- required this.time,
- });
-
- Map toJson() => {
- 'flowId': flowId,
- 'totalSpanMs': totalSpanMs,
- 'buildDurationMs': buildDurationMs,
- 'rasterDurationMs': rasterDurationMs,
- 'janky': janky,
- 'time': time.toIso8601String(),
- };
-}
-
-class FlowTrace {
- final String id;
- final String name;
- final DateTime startTime;
- DateTime? endTime;
- final Map tags;
- final List spans;
- final List networks;
- final List routes;
- final List errors;
- final List frames;
- final Map buildCounts;
- FlowStatus status;
-
- FlowTrace({
- required this.id,
- required this.name,
- required this.startTime,
- this.endTime,
- Map? tags,
- List? spans,
- List? networks,
- List? routes,
- List? errors,
- List? frames,
- Map? buildCounts,
- this.status = FlowStatus.running,
- }) : tags = tags ?? {},
- spans = spans ?? [],
- networks = networks ?? [],
- routes = routes ?? [],
- errors = errors ?? [],
- frames = frames ?? [],
- buildCounts = buildCounts ?? {};
-
- int get durationMs {
- final end = endTime ?? DateTime.now();
- return max(0, end.difference(startTime).inMilliseconds);
- }
-
- Map toJson() => {
- 'id': id,
- 'name': name,
- 'startTime': startTime.toIso8601String(),
- 'endTime': endTime?.toIso8601String(),
- 'durationMs': durationMs,
- 'tags': tags,
- 'status': status.name,
- 'spans': spans.map((s) => s.toJson()).toList(),
- 'networks': networks.map((n) => n.toJson()).toList(),
- 'routes': routes.map((r) => r.toJson()).toList(),
- 'errors': errors.map((e) => e.toJson()).toList(),
- 'frames': frames.map((f) => f.toJson()).toList(),
- 'buildCounts': buildCounts,
- };
-}
diff --git a/archive/flutterguard_core/lib/src/trace_store.dart b/archive/flutterguard_core/lib/src/trace_store.dart
deleted file mode 100644
index 384547c..0000000
--- a/archive/flutterguard_core/lib/src/trace_store.dart
+++ /dev/null
@@ -1,97 +0,0 @@
-import 'trace_model.dart';
-
-class TraceStore {
- TraceStore._();
-
- static final TraceStore instance = TraceStore._();
-
- final Map _activeTraces = {};
- final List _completedTraces = [];
- int _maxTraces = 100;
-
- void configure({required int maxTraces}) {
- _maxTraces = maxTraces;
- }
-
- FlowTrace beginFlow(FlowTrace trace) {
- _activeTraces[trace.id] = trace;
- return trace;
- }
-
- FlowTrace? endFlow(String traceId, {required bool failed}) {
- final trace = _activeTraces.remove(traceId);
- if (trace == null) return null;
- trace.endTime = DateTime.now();
- trace.status = failed ? FlowStatus.failed : FlowStatus.success;
- _completedTraces.add(trace);
- while (_completedTraces.length > _maxTraces) {
- _completedTraces.removeAt(0);
- }
- return trace;
- }
-
- FlowTrace? getActiveTrace(String traceId) {
- return _activeTraces[traceId];
- }
-
- List get completedTraces => List.unmodifiable(_completedTraces);
-
- void addSpanToActive(String traceId, SpanTrace span) {
- final trace = _activeTraces[traceId];
- if (trace != null) {
- trace.spans.add(span);
- }
- }
-
- void addNetworkToActive(String? traceId, NetworkTrace network) {
- if (traceId == null) return;
- final trace = _activeTraces[traceId] ?? _getLatestCompleted(traceId);
- trace?.networks.add(network);
- }
-
- void addRouteToActive(String? traceId, RouteTrace route) {
- if (traceId == null) return;
- final trace = _activeTraces[traceId] ?? _getLatestCompleted(traceId);
- trace?.routes.add(route);
- }
-
- void addErrorToActive(String? traceId, ErrorTrace error) {
- if (traceId == null) return;
- final trace = _activeTraces[traceId] ?? _getLatestCompleted(traceId);
- trace?.errors.add(error);
- if (trace != null && _activeTraces.containsKey(traceId)) {
- trace.status = FlowStatus.failed;
- }
- }
-
- void addFrameToActive(String? traceId, FrameTrace frame) {
- if (traceId == null) return;
- final trace = _activeTraces[traceId] ?? _getLatestCompleted(traceId);
- trace?.frames.add(frame);
- }
-
- void recordBuild(String? traceId, String boundaryName) {
- if (traceId == null) return;
- final trace = _activeTraces[traceId] ?? _getLatestCompleted(traceId);
- if (trace != null) {
- trace.buildCounts[boundaryName] =
- (trace.buildCounts[boundaryName] ?? 0) + 1;
- }
- }
-
- FlowTrace? _getLatestCompleted(String traceId) {
- for (var i = _completedTraces.length - 1; i >= 0; i--) {
- if (_completedTraces[i].id == traceId) return _completedTraces[i];
- }
- return null;
- }
-
- List getAllTraces() {
- return [..._activeTraces.values, ..._completedTraces];
- }
-
- void reset() {
- _activeTraces.clear();
- _completedTraces.clear();
- }
-}
diff --git a/archive/flutterguard_core/pubspec.yaml b/archive/flutterguard_core/pubspec.yaml
deleted file mode 100644
index 6c97825..0000000
--- a/archive/flutterguard_core/pubspec.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: flutterguard_core
-description: Flow-level aspect tracing engine for Flutter - trace models, Zone context, and exporters.
-version: 0.1.0
-publish_to: none
-
-environment:
- sdk: ">=3.3.0 <4.0.0"
-
-dependencies:
- meta: ^1.12.0
-
-dev_dependencies:
- test: ^1.25.8
- lints: ^3.0.0
diff --git a/archive/flutterguard_core/test/flutter_guard_test.dart b/archive/flutterguard_core/test/flutter_guard_test.dart
deleted file mode 100644
index 500ad13..0000000
--- a/archive/flutterguard_core/test/flutter_guard_test.dart
+++ /dev/null
@@ -1,107 +0,0 @@
-import 'dart:async';
-
-import 'package:flutterguard_core/flutterguard_core.dart';
-import 'package:test/test.dart';
-
-void main() {
- setUp(() {
- FlutterGuard.reset();
- FlutterGuard.configure(const FlutterGuardConfig(maxTraces: 100));
- });
-
- tearDown(() {
- FlutterGuard.reset();
- });
-
- test('action creates flow trace', () async {
- final result = await FlutterGuard.action('test_action', () async {
- await Future.delayed(const Duration(milliseconds: 10));
- return 'done';
- });
-
- expect(result, equals('done'));
- final json = FlutterGuard.exportJson();
- expect(json, contains('test_action'));
- expect(json, contains('success'));
- });
-
- test('span attaches to current flow', () async {
- await FlutterGuard.action('test_action', () async {
- final spanResult = await FlutterGuard.span('inner_span', () async {
- await Future.delayed(const Duration(milliseconds: 5));
- return 'span_done';
- });
- expect(spanResult, equals('span_done'));
- });
-
- final json = FlutterGuard.exportJson();
- expect(json, contains('inner_span'));
- });
-
- test('zone context survives async futures', () async {
- String? capturedId;
-
- await FlutterGuard.action('test_action', () async {
- final id1 = FlutterGuard.currentTraceId;
- expect(id1, isNotNull);
-
- await Future.delayed(const Duration(milliseconds: 5));
- final id2 = FlutterGuard.currentTraceId;
- expect(id2, equals(id1));
-
- capturedId = id1;
- });
-
- expect(capturedId, isNotNull);
- });
-
- test('errors mark flow failed', () async {
- bool threw = false;
- try {
- await FlutterGuard.action('test_action', () async {
- throw Exception('test error');
- });
- } catch (_) {
- threw = true;
- }
- expect(threw, isTrue);
-
- final json = FlutterGuard.exportJson();
- expect(json, contains('failed'));
- expect(json, contains('test error'));
- });
-
- test('json export contains trace', () async {
- await FlutterGuard.action('test_action', () async {});
-
- final json = FlutterGuard.exportJson();
- expect(json, contains('"version"'));
- expect(json, contains('"traces"'));
- expect(json, contains('"test_action"'));
- });
-
- test('markdown export contains sections', () async {
- await FlutterGuard.action('test_action', () async {});
-
- final md = FlutterGuard.exportMarkdown();
- expect(md, contains('# FlutterGuard Flow Report'));
- expect(md, contains('## Summary'));
- expect(md, contains('## Runtime Flows'));
- expect(md, contains('test_action'));
- });
-
- test('ring buffer respects max traces', () async {
- FlutterGuard.configure(const FlutterGuardConfig(maxTraces: 3));
-
- for (var i = 0; i < 5; i++) {
- await FlutterGuard.action('action_$i', () async {});
- }
-
- final json = FlutterGuard.exportJson();
- expect(json, contains('action_2'));
- expect(json, contains('action_3'));
- expect(json, contains('action_4'));
- expect(json, isNot(contains('action_0')));
- expect(json, isNot(contains('action_1')));
- });
-}
diff --git a/archive/flutterguard_dio/AGENTS.md b/archive/flutterguard_dio/AGENTS.md
deleted file mode 100644
index 2cdb132..0000000
--- a/archive/flutterguard_dio/AGENTS.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Package: flutterguard_dio [FROZEN]
-
-## Role
-Dio HTTP interceptor for FlutterGuard flow tracing — records HTTP requests/responses within active flows.
-
-**Status: FROZEN**. No new features. Bug fixes only.
-
-## Dependency Map
-- depends on: flutterguard_core (path), dio ^5.7.0
-- depended by: nothing
-
-## Entry Points
-- lib barrel: `lib/flutterguard_dio.dart`
-
-## Key Source Files
-| File | Responsibility |
-|------|---------------|
-| `src/dio_interceptor.dart` | FlutterGuardDioInterceptor: onRequest/onResponse/onError hooks |
-
-## Pubspec Overrides
-melos-managed: flutterguard_core → path: ../flutterguard_core
-
-## Test
-- command: `melos run test:dio`
-- test file: `test/dio_interceptor_test.dart` (4 tests)
-
-## Why Frozen
-Only meaningful when core runtime tracing is also active. Static analysis approach (Path A) does not require HTTP interception.
diff --git a/archive/flutterguard_dio/lib/flutterguard_dio.dart b/archive/flutterguard_dio/lib/flutterguard_dio.dart
deleted file mode 100644
index 194be60..0000000
--- a/archive/flutterguard_dio/lib/flutterguard_dio.dart
+++ /dev/null
@@ -1,3 +0,0 @@
-library flutterguard_dio;
-
-export 'src/dio_interceptor.dart';
diff --git a/archive/flutterguard_dio/lib/src/dio_interceptor.dart b/archive/flutterguard_dio/lib/src/dio_interceptor.dart
deleted file mode 100644
index 152864e..0000000
--- a/archive/flutterguard_dio/lib/src/dio_interceptor.dart
+++ /dev/null
@@ -1,91 +0,0 @@
-import 'package:dio/dio.dart';
-import 'package:flutterguard_core/flutterguard_core.dart';
-
-class FlutterGuardDioInterceptor extends Interceptor {
- final bool sanitizeHeaders;
- final bool sanitizeBody;
- final List sensitiveKeys;
-
- FlutterGuardDioInterceptor({
- this.sanitizeHeaders = true,
- this.sanitizeBody = true,
- List? sensitiveKeys,
- }) : sensitiveKeys = sensitiveKeys ?? _defaultSensitiveKeys;
-
- static const _defaultSensitiveKeys = [
- 'authorization',
- 'cookie',
- 'set-cookie',
- 'token',
- 'password',
- 'secret',
- 'email',
- 'phone',
- ];
-
- @override
- void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
- options.extra['flutterguard_start_time'] = DateTime.now();
- handler.next(options);
- }
-
- @override
- void onResponse(Response response, ResponseInterceptorHandler handler) {
- _recordResponse(response);
- handler.next(response);
- }
-
- @override
- void onError(DioException err, ErrorInterceptorHandler handler) {
- _recordError(err);
- handler.next(err);
- }
-
- void _recordResponse(Response response) {
- final requestOptions = response.requestOptions;
- final startTime = requestOptions.extra['flutterguard_start_time'];
- if (startTime is! DateTime) return;
-
- final durationMs = DateTime.now().difference(startTime).inMilliseconds;
- final statusCode = response.statusCode;
- final success = statusCode != null && statusCode >= 200 && statusCode < 400;
-
- FlutterGuard.recordNetwork(NetworkTrace(
- flowId: FlutterGuard.currentTraceId,
- method: requestOptions.method,
- path: requestOptions.uri.path,
- statusCode: statusCode,
- durationMs: durationMs,
- requestSize: _estimateSize(requestOptions.data),
- responseSize: _estimateSize(response.data),
- success: success,
- ));
- }
-
- void _recordError(DioException err) {
- final requestOptions = err.requestOptions;
- final startTime = requestOptions.extra['flutterguard_start_time'];
- if (startTime is! DateTime) return;
-
- final durationMs = DateTime.now().difference(startTime).inMilliseconds;
-
- FlutterGuard.recordNetwork(NetworkTrace(
- flowId: FlutterGuard.currentTraceId,
- method: requestOptions.method,
- path: requestOptions.uri.path,
- durationMs: durationMs,
- success: false,
- errorType: err.type.name,
- errorMessage: err.message,
- ));
- }
-
- int? _estimateSize(dynamic data) {
- if (data == null) return null;
- try {
- return data.toString().length;
- } catch (_) {
- return null;
- }
- }
-}
diff --git a/archive/flutterguard_dio/pubspec.yaml b/archive/flutterguard_dio/pubspec.yaml
deleted file mode 100644
index 6c9d2e9..0000000
--- a/archive/flutterguard_dio/pubspec.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-name: flutterguard_dio
-description: Dio interceptor for FlutterGuard flow tracing - records HTTP requests within active flows.
-version: 0.1.0
-publish_to: none
-
-environment:
- sdk: ">=3.3.0 <4.0.0"
-
-dependencies:
- dio: ^5.7.0
- flutterguard_core:
- path: ../flutterguard_core
-
-dev_dependencies:
- test: ^1.25.8
- lints: ^3.0.0
diff --git a/archive/flutterguard_dio/test/dio_interceptor_test.dart b/archive/flutterguard_dio/test/dio_interceptor_test.dart
deleted file mode 100644
index ae7b2d1..0000000
--- a/archive/flutterguard_dio/test/dio_interceptor_test.dart
+++ /dev/null
@@ -1,104 +0,0 @@
-import 'package:dio/dio.dart';
-import 'package:flutterguard_core/flutterguard_core.dart';
-import 'package:flutterguard_dio/flutterguard_dio.dart';
-import 'package:test/test.dart';
-
-void main() {
- late Dio dio;
-
- setUp(() {
- FlutterGuard.reset();
- FlutterGuard.configure(const FlutterGuardConfig(maxTraces: 100));
- dio = Dio(BaseOptions(baseUrl: 'https://example.com'));
- dio.interceptors.add(FlutterGuardDioInterceptor());
- });
-
- tearDown(() {
- dio.close();
- FlutterGuard.reset();
- });
-
- test('interceptor records success response', () async {
- await FlutterGuard.action('network_action', () async {
- final request = RequestOptions(path: '/api/items');
- final startTime =
- DateTime.now().subtract(const Duration(milliseconds: 50));
- request.extra['flutterguard_start_time'] = startTime;
-
- final response = Response(
- requestOptions: request,
- statusCode: 200,
- data: {'id': 1},
- );
-
- FlutterGuard.recordNetwork(NetworkTrace(
- flowId: FlutterGuard.currentTraceId,
- method: request.method,
- path: request.uri.path,
- statusCode: response.statusCode!,
- durationMs: 50,
- success: true,
- ));
- });
-
- final json = FlutterGuard.exportJson();
- expect(json, contains('200'));
- expect(json, contains('/api/items'));
- });
-
- test('interceptor records error response', () async {
- await FlutterGuard.action('error_network_action', () async {
- FlutterGuard.recordNetwork(NetworkTrace(
- flowId: FlutterGuard.currentTraceId,
- method: 'GET',
- path: '/api/error',
- durationMs: 100,
- success: false,
- errorType: 'connectionError',
- errorMessage: 'Connection refused',
- ));
- });
-
- final json = FlutterGuard.exportJson();
- expect(json, contains('Connection refused'));
- expect(json, contains('/api/error'));
- });
-
- test('interceptor attaches to current flow', () async {
- await FlutterGuard.action('network_flow', () async {
- FlutterGuard.recordNetwork(NetworkTrace(
- flowId: FlutterGuard.currentTraceId,
- method: 'POST',
- path: '/api/submit',
- statusCode: 201,
- durationMs: 42,
- success: true,
- ));
- });
-
- final json = FlutterGuard.exportJson();
- expect(json, contains('network_flow'));
- expect(json, contains('/api/submit'));
- expect(json, contains('POST'));
- });
-
- test('interceptor does not log body by default', () async {
- await FlutterGuard.action('sanitize_flow', () async {
- FlutterGuard.recordNetwork(NetworkTrace(
- flowId: FlutterGuard.currentTraceId,
- method: 'POST',
- path: '/api/login',
- statusCode: 200,
- durationMs: 30,
- success: true,
- requestSize: null,
- responseSize: null,
- ));
- });
-
- final json = FlutterGuard.exportJson();
- expect(json, contains('/api/login'));
- expect(json, isNot(contains('password')));
- expect(json, isNot(contains('secret')));
- });
-}
diff --git a/archive/flutterguard_flutter/AGENTS.md b/archive/flutterguard_flutter/AGENTS.md
deleted file mode 100644
index fc2333a..0000000
--- a/archive/flutterguard_flutter/AGENTS.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Package: flutterguard_flutter [FROZEN]
-
-## Role
-Flutter runtime integration for FlutterGuard — error hooks, route observer, frame metrics, and GuardBoundary widget rebuild counter.
-
-**Status: FROZEN**. No new features. Bug fixes only.
-
-## Dependency Map
-- depends on: flutterguard_core (path), flutter SDK (>=3.19.0)
-- depended by: nothing
-
-## Entry Points
-- lib barrel: `lib/flutterguard_flutter.dart`
-
-## Key Source Files
-| File | Responsibility |
-|------|---------------|
-| `src/flutter_guard.dart` | FlutterGuard.run(): wraps core, error hooks, frame timing |
-| `src/guard_boundary.dart` | GuardBoundary widget: rebuild count tracking |
-| `src/route_observer.dart` | FlutterGuardRouteObserver: Navigator route tracking |
-
-## Pubspec Overrides
-melos-managed: flutterguard_core → path: ../flutterguard_core
-
-## Test
-- command: `flutter test` (not in melos scripts yet)
-- test file: `test/flutter_test.dart` (4 tests)
-
-## Why Frozen
-Only meaningful when core runtime tracing is also active. Static analysis approach (Path A) does not require Flutter runtime instrumentation.
-
-## Notes
-- Has Flutter SDK constraint (>=3.19.0) — testing requires Flutter SDK to be installed
-- Not included in `melos run test` (which uses `dart test`); run `flutter test` manually
diff --git a/archive/flutterguard_flutter/lib/flutterguard_flutter.dart b/archive/flutterguard_flutter/lib/flutterguard_flutter.dart
deleted file mode 100644
index d430129..0000000
--- a/archive/flutterguard_flutter/lib/flutterguard_flutter.dart
+++ /dev/null
@@ -1,7 +0,0 @@
-library flutterguard_flutter;
-
-export 'package:flutterguard_core/flutterguard_core.dart' hide FlutterGuard;
-
-export 'src/flutter_guard.dart';
-export 'src/route_observer.dart';
-export 'src/guard_boundary.dart';
diff --git a/archive/flutterguard_flutter/lib/src/flutter_guard.dart b/archive/flutterguard_flutter/lib/src/flutter_guard.dart
deleted file mode 100644
index f20bbf4..0000000
--- a/archive/flutterguard_flutter/lib/src/flutter_guard.dart
+++ /dev/null
@@ -1,122 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter/material.dart';
-import 'package:flutter/scheduler.dart';
-import 'package:flutterguard_core/flutterguard_core.dart' as core;
-
-import 'route_observer.dart';
-
-class FlutterGuard {
- static final FlutterGuardRouteObserver routeObserver =
- FlutterGuardRouteObserver();
-
- static void run({
- required Widget app,
- core.FlutterGuardConfig config = const core.FlutterGuardConfig(),
- }) {
- core.FlutterGuard.configure(config);
-
- final previousOnError = FlutterError.onError;
- if (config.collectErrors) {
- FlutterError.onError = (details) {
- core.FlutterGuard.recordError(core.ErrorTrace(
- flowId: core.FlutterGuard.currentTraceId,
- errorType: details.exception.runtimeType.toString(),
- message: details.exception.toString(),
- stackTrace: details.stack?.toString(),
- time: DateTime.now(),
- ));
- previousOnError?.call(details);
- };
- }
-
- if (config.collectFrames) {
- SchedulerBinding.instance.addPostFrameCallback((_) {
- _recordFrameStats(config);
- });
- }
-
- runZonedGuarded(
- () {
- WidgetsFlutterBinding.ensureInitialized();
- runApp(app);
- },
- (error, stack) {
- if (config.collectErrors) {
- core.FlutterGuard.recordError(core.ErrorTrace(
- flowId: core.FlutterGuard.currentTraceId,
- errorType: error.runtimeType.toString(),
- message: error.toString(),
- stackTrace: stack.toString(),
- time: DateTime.now(),
- ));
- }
- },
- );
- }
-
- static void _recordFrameStats(core.FlutterGuardConfig config) {
- final binding = SchedulerBinding.instance;
- try {
- binding.addTimingsCallback((timings) {
- for (final timing in timings) {
- final totalSpan = timing.totalSpan.inMicroseconds ~/ 1000;
- final buildDuration = timing.buildDuration.inMicroseconds ~/ 1000;
- final rasterDuration = timing.rasterDuration.inMicroseconds ~/ 1000;
- final janky = totalSpan > config.jankFrameMs;
-
- core.FlutterGuard.recordFrame(core.FrameTrace(
- flowId: core.FlutterGuard.currentTraceId,
- totalSpanMs: totalSpan,
- buildDurationMs: buildDuration,
- rasterDurationMs: rasterDuration,
- janky: janky,
- time: DateTime.now(),
- ));
- }
- });
- } on NoSuchMethodError {
- // addTimingsCallback not available on this Flutter version
- }
- }
-
- static String? get currentTraceId => core.FlutterGuard.currentTraceId;
-
- static Future action(
- String name,
- FutureOr Function() body, {
- Map tags = const {},
- }) =>
- core.FlutterGuard.action(name, body, tags: tags);
-
- static Future span(
- String name,
- FutureOr Function() body, {
- Map tags = const {},
- }) =>
- core.FlutterGuard.span(name, body, tags: tags);
-
- static void recordNetwork(core.NetworkTrace trace) =>
- core.FlutterGuard.recordNetwork(trace);
-
- static void recordRoute(core.RouteTrace trace) =>
- core.FlutterGuard.recordRoute(trace);
-
- static void recordError(core.ErrorTrace trace) =>
- core.FlutterGuard.recordError(trace);
-
- static void recordFrame(core.FrameTrace trace) =>
- core.FlutterGuard.recordFrame(trace);
-
- static void recordBuild(String boundaryName) =>
- core.FlutterGuard.recordBuild(boundaryName);
-
- static String exportJson() => core.FlutterGuard.exportJson();
-
- static String exportMarkdown() => core.FlutterGuard.exportMarkdown();
-
- static void reset() => core.FlutterGuard.reset();
-
- static void configure(core.FlutterGuardConfig config) =>
- core.FlutterGuard.configure(config);
-}
diff --git a/archive/flutterguard_flutter/lib/src/guard_boundary.dart b/archive/flutterguard_flutter/lib/src/guard_boundary.dart
deleted file mode 100644
index 5a70c86..0000000
--- a/archive/flutterguard_flutter/lib/src/guard_boundary.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutterguard_core/flutterguard_core.dart';
-
-class GuardBoundary extends StatelessWidget {
- final String name;
- final Widget child;
-
- const GuardBoundary({
- super.key,
- required this.name,
- required this.child,
- });
-
- @override
- Widget build(BuildContext context) {
- FlutterGuard.recordBuild(name);
- return child;
- }
-}
diff --git a/archive/flutterguard_flutter/lib/src/route_observer.dart b/archive/flutterguard_flutter/lib/src/route_observer.dart
deleted file mode 100644
index d091707..0000000
--- a/archive/flutterguard_flutter/lib/src/route_observer.dart
+++ /dev/null
@@ -1,56 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutterguard_core/flutterguard_core.dart';
-
-class FlutterGuardRouteObserver extends NavigatorObserver {
- @override
- void didPush(Route route, Route? previousRoute) {
- super.didPush(route, previousRoute);
- FlutterGuard.recordRoute(RouteTrace(
- flowId: FlutterGuard.currentTraceId,
- type: 'push',
- from: previousRoute != null ? _routeName(previousRoute) : null,
- to: _routeName(route),
- time: DateTime.now(),
- ));
- }
-
- @override
- void didPop(Route route, Route? previousRoute) {
- super.didPop(route, previousRoute);
- FlutterGuard.recordRoute(RouteTrace(
- flowId: FlutterGuard.currentTraceId,
- type: 'pop',
- from: _routeName(route),
- to: previousRoute != null ? _routeName(previousRoute) : null,
- time: DateTime.now(),
- ));
- }
-
- @override
- void didReplace({Route? newRoute, Route? oldRoute}) {
- super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
- FlutterGuard.recordRoute(RouteTrace(
- flowId: FlutterGuard.currentTraceId,
- type: 'replace',
- from: oldRoute != null ? _routeName(oldRoute) : null,
- to: newRoute != null ? _routeName(newRoute) : null,
- time: DateTime.now(),
- ));
- }
-
- @override
- void didRemove(Route route, Route? previousRoute) {
- super.didRemove(route, previousRoute);
- FlutterGuard.recordRoute(RouteTrace(
- flowId: FlutterGuard.currentTraceId,
- type: 'remove',
- from: _routeName(route),
- to: previousRoute != null ? _routeName(previousRoute) : null,
- time: DateTime.now(),
- ));
- }
-
- String _routeName(Route route) {
- return route.settings.name ?? route.runtimeType.toString();
- }
-}
diff --git a/archive/flutterguard_flutter/pubspec.yaml b/archive/flutterguard_flutter/pubspec.yaml
deleted file mode 100644
index f8893b4..0000000
--- a/archive/flutterguard_flutter/pubspec.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-name: flutterguard_flutter
-description: Flutter runtime integration for FlutterGuard - error hooks, route observer, frame metrics, and GuardBoundary.
-version: 0.1.0
-publish_to: none
-
-environment:
- sdk: ">=3.3.0 <4.0.0"
- flutter: ">=3.19.0"
-
-dependencies:
- flutter:
- sdk: flutter
- flutterguard_core:
- path: ../flutterguard_core
-
-dev_dependencies:
- flutter_test:
- sdk: flutter
- flutter_lints: ^3.0.2
diff --git a/archive/flutterguard_flutter/test/flutter_test.dart b/archive/flutterguard_flutter/test/flutter_test.dart
deleted file mode 100644
index 1d40a3d..0000000
--- a/archive/flutterguard_flutter/test/flutter_test.dart
+++ /dev/null
@@ -1,84 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:flutterguard_flutter/flutterguard_flutter.dart';
-
-void main() {
- setUp(() {
- FlutterGuard.reset();
- FlutterGuard.configure(const FlutterGuardConfig(maxTraces: 100));
- });
-
- tearDown(() {
- FlutterGuard.reset();
- });
-
- testWidgets('GuardBoundary records build count', (tester) async {
- await FlutterGuard.action('test_boundary', () async {
- await tester.pumpWidget(
- const MaterialApp(
- home: Scaffold(
- body: GuardBoundary(
- name: 'TestPage',
- child: Text('Hello'),
- ),
- ),
- ),
- );
-
- await tester.pumpWidget(
- const MaterialApp(
- home: Scaffold(
- body: GuardBoundary(
- name: 'TestPage',
- child: Text('Hello Again'),
- ),
- ),
- ),
- );
- });
-
- final json = FlutterGuard.exportJson();
- expect(json, contains('TestPage'));
- });
-
- testWidgets('route observer records push and pop', (tester) async {
- final observer = FlutterGuard.routeObserver;
-
- await tester.pumpWidget(
- MaterialApp(
- navigatorObservers: [observer],
- home: const Scaffold(body: Text('Home')),
- ),
- );
-
- await FlutterGuard.action('route_flow', () async {
- await tester.tap(find.text('Home'));
- });
- });
-
- test('error hook preserves previous handler', () async {
- FlutterError.onError = (details) {};
-
- try {
- await FlutterGuard.action('error_flow', () async {
- throw Exception('guarded error');
- });
- } catch (_) {}
-
- expect(true, isTrue);
- });
-
- test('frame aspect can attach to active flow', () {
- FlutterGuard.recordFrame(FrameTrace(
- flowId: 'test_frame_flow',
- totalSpanMs: 32,
- buildDurationMs: 20,
- rasterDurationMs: 12,
- janky: true,
- time: DateTime.now(),
- ));
-
- final json = FlutterGuard.exportJson();
- expect(json, contains('test_frame_flow'));
- });
-}
diff --git a/archive/usage_demo/README.md b/archive/usage_demo/README.md
deleted file mode 100644
index 7a5661b..0000000
--- a/archive/usage_demo/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Usage Demo
-
-Demonstrates the core `FlutterGuard` tracing API without Flutter dependencies.
-
-```bash
-# From the repo root
-melos bootstrap
-
-# Run the demo
-dart run examples/usage_demo/bin/trace_demo.dart
-```
diff --git a/archive/usage_demo/analysis_options.yaml b/archive/usage_demo/analysis_options.yaml
deleted file mode 100644
index 4d8bd09..0000000
--- a/archive/usage_demo/analysis_options.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-analyzer:
- errors:
- avoid_print: ignore
diff --git a/archive/usage_demo/bin/trace_demo.dart b/archive/usage_demo/bin/trace_demo.dart
deleted file mode 100644
index 01788da..0000000
--- a/archive/usage_demo/bin/trace_demo.dart
+++ /dev/null
@@ -1,52 +0,0 @@
-import 'package:flutterguard_core/flutterguard_core.dart';
-
-Future main() async {
- FlutterGuard.configure(const FlutterGuardConfig(enabled: true));
-
- print('=== FlutterGuard Tracing Demo ===\n');
-
- await FlutterGuard.action('fetch_user_data', () async {
- FlutterGuard.span('validate_token', () async {
- await Future.delayed(const Duration(milliseconds: 30));
- });
-
- FlutterGuard.span('query_database', () async {
- await Future.delayed(const Duration(milliseconds: 120));
- });
-
- FlutterGuard.recordNetwork(NetworkTrace(
- method: 'GET',
- path: '/api/users/42',
- statusCode: 200,
- durationMs: 85,
- success: true,
- ));
-
- FlutterGuard.span('format_response', () async {
- await Future.delayed(const Duration(milliseconds: 15));
- });
- });
-
- await FlutterGuard.action('process_payment', () async {
- FlutterGuard.recordNetwork(NetworkTrace(
- method: 'POST',
- path: '/api/payments',
- statusCode: 201,
- durationMs: 320,
- success: true,
- ));
- }, tags: {'amount': '29.99', 'currency': 'USD'});
-
- try {
- await FlutterGuard.action('failing_operation', () async {
- await FlutterGuard.span('risky_call', () async {
- await Future.delayed(const Duration(milliseconds: 10));
- throw Exception('Service unavailable');
- });
- });
- } catch (e) {
- // Error is captured in the trace; demos don't crash.
- }
-
- print(FlutterGuard.exportMarkdown());
-}
diff --git a/archive/usage_demo/pubspec.yaml b/archive/usage_demo/pubspec.yaml
deleted file mode 100644
index 17a5b41..0000000
--- a/archive/usage_demo/pubspec.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-name: usage_demo
-description: Demonstrates the FlutterGuard tracing API.
-publish_to: none
-
-environment:
- sdk: ">=3.3.0 <4.0.0"
-
-dependencies:
- flutterguard_core:
- path: ../../packages/flutterguard_core
diff --git a/bin/AGENTS.md b/bin/AGENTS.md
new file mode 100644
index 0000000..8e7587e
--- /dev/null
+++ b/bin/AGENTS.md
@@ -0,0 +1,7 @@
+# Executable layer
+
+`flutterguard.dart` may parse global arguments, route the four command families,
+print usage/version, and map format errors to exit code 2.
+
+Keep business logic in `lib/src/cli` or the scan kernel. Do not add rule wiring,
+configuration defaults, report construction, or a second versioned API here.
diff --git a/bin/flutterguard.dart b/bin/flutterguard.dart
new file mode 100644
index 0000000..678c719
--- /dev/null
+++ b/bin/flutterguard.dart
@@ -0,0 +1,111 @@
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:flutterguard_cli/src/cli/baseline_commands.dart';
+import 'package:flutterguard_cli/src/cli/cli_parsers.dart';
+import 'package:flutterguard_cli/src/cli/config_commands.dart';
+import 'package:flutterguard_cli/src/cli/rule_commands.dart';
+import 'package:flutterguard_cli/src/cli/scan_command.dart';
+
+const _version = '0.7.0';
+
+void main(List args) {
+ final parsers = CliParsers();
+ try {
+ final result = parsers.root.parse(args);
+ if (result['version'] == true) {
+ stdout.writeln('flutterguard $_version');
+ return;
+ }
+ if (args.isEmpty || result['help'] == true || result.command == null) {
+ _usage(parsers.root);
+ return;
+ }
+
+ final command = result.command!;
+ switch (command.name) {
+ case 'scan':
+ if (_help(command, parsers.scan, 'flutterguard scan [path]')) return;
+ ScanCommand.run(command, configPath: _explicitConfigPath(command));
+ return;
+ case 'baseline':
+ final leaf = command.command;
+ if (command['help'] == true || leaf == null) {
+ _subcommandUsage(
+ 'flutterguard baseline create [path]',
+ parsers.baseline,
+ );
+ return;
+ }
+ if (_help(
+ leaf,
+ parsers.baselineCreate,
+ 'flutterguard baseline create [path]',
+ )) {
+ return;
+ }
+ BaselineCommands.create(leaf, configPath: _explicitConfigPath(leaf));
+ return;
+ case 'config':
+ final leaf = command.command;
+ if (command['help'] == true || leaf == null) {
+ _subcommandUsage(
+ 'flutterguard config [path]',
+ parsers.config,
+ );
+ return;
+ }
+ if (leaf.name == 'init') {
+ if (_help(
+ leaf,
+ parsers.configInit,
+ 'flutterguard config init [path]',
+ )) {
+ return;
+ }
+ ConfigCommands.init(leaf);
+ return;
+ }
+ if (_help(
+ leaf,
+ parsers.configCheck,
+ 'flutterguard config check [path]',
+ )) {
+ return;
+ }
+ ConfigCommands.check(leaf, configPath: _explicitConfigPath(leaf));
+ return;
+ case 'rules':
+ if (_help(command, parsers.rules, 'flutterguard rules [rule-id]')) {
+ return;
+ }
+ RuleCommands.run(command);
+ return;
+ }
+ } on FormatException catch (error) {
+ stderr.writeln('Error: ${error.message}');
+ exitCode = 2;
+ }
+}
+
+bool _help(ArgResults args, ArgParser parser, String usage) {
+ if (args['help'] != true) return false;
+ _subcommandUsage(usage, parser);
+ return true;
+}
+
+String? _explicitConfigPath(ArgResults args) =>
+ args.wasParsed('config') ? args['config'] as String : null;
+
+void _usage(ArgParser parser) {
+ stdout.writeln('FlutterGuard — IoT Flutter static analysis CLI');
+ stdout.writeln('Usage: flutterguard [options]');
+ stdout.writeln();
+ stdout.writeln('Commands: scan, baseline, config, rules');
+ stdout.writeln(parser.usage);
+}
+
+void _subcommandUsage(String usage, ArgParser parser) {
+ stdout.writeln('Usage: $usage');
+ stdout.writeln(parser.usage);
+}
diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md
new file mode 100644
index 0000000..ec48e9f
--- /dev/null
+++ b/doc/ARCHITECTURE.md
@@ -0,0 +1,109 @@
+# FlutterGuard Architecture
+
+## Shape
+
+FlutterGuard is one publishable Dart package and one executable. The repository
+root is the package root.
+
+```text
+bin/flutterguard.dart parse and route four command families
+lib/src/cli/ command parsers and I/O adapters
+lib/src/scanner.dart scan orchestration
+lib/src/config_loader.dart YAML to compact generic rule settings
+lib/src/source_workspace.dart per-scan source/AST cache and diagnostics
+lib/src/import_graph.dart project-local resolved import graph
+lib/src/boundary_engine.dart shared dependency-boundary evaluator
+lib/src/rules/registry.dart only rule metadata/default/execution registry
+lib/src/rules/rule.dart RuleDefinition and RuleRegistration
+lib/src/rules/*.dart detectors
+lib/src/report_generator.dart table and JSON output
+lib/src/sarif_report.dart SARIF output
+test/ external contract and detector tests
+example/ CI scan target
+```
+
+There is no workspace orchestrator, runtime instrumentation package, dynamic
+plugin loader, generated registry, or public Dart scanner API.
+
+## Scan flow
+
+```text
+CLI
+ └─ FlutterGuardScanner.scan
+ ├─ ProjectResolver
+ ├─ ScanConfig.fromFile
+ ├─ FileCollector
+ ├─ ScanContext
+ │ ├─ SourceWorkspace
+ │ └─ changed/all/target file sets
+ ├─ RuleRegistry.analyze
+ │ ├─ one shared ImportGraph when needed
+ │ └─ explicit RuleRegistration executors
+ ├─ SuppressionFilter
+ ├─ optional Baseline
+ └─ table / JSON / SARIF report
+```
+
+`ScanContext` is the only scanner-to-rule scope carrier. `SourceWorkspace`
+reads and parses each source file at most once. Architecture checks share one
+`ImportGraph`; layer and module rules share `BoundaryRule` and
+`DependencyBoundaryEngine`.
+
+## Rule model
+
+`RuleRegistry.registrations` is the single source of truth. Every registration
+contains:
+
+- one `RuleDefinition`: ID, domain, default severity, documentation, options;
+- one executor receiving `ScanContext`, effective `RuleConfig`, and an optional
+ shared import graph.
+
+Configuration defaults and `flutterguard rules` output are derived from these
+definitions. Do not add another catalog, metadata registry, reflection layer,
+or barrel export.
+
+All rules receive common `enabled` and `severity` settings. Special settings
+belong in `RuleDefinition.defaultOptions`; the current example is `requireTls`.
+
+## Detection ownership
+
+- `lifecycle_resource_not_disposed` owns resource close/cancel/dispose checks.
+- `ble_scanning` owns scan timeout findings, not generic disconnect pairing.
+- `iot_security` owns credentials and insecure transport findings.
+- Dependency version and broker placement policy stay with ecosystem tools and
+ application configuration rather than FlutterGuard rules.
+- `BoundaryRule` owns both layer and module dependency policy.
+- Framework-specific state rules activate from source imports; there is no
+ global framework or confidence switch.
+
+This ownership prevents the same source pattern from being reported by several
+rule families.
+
+## External boundaries
+
+The supported integration boundary is the CLI plus JSON/SARIF. Code under
+`lib/src` is package-private and may change without a Dart API migration.
+
+Stable capabilities are:
+
+- positional project path and explicit `--config`;
+- changed-only scanning using a verified Git base;
+- inline suppression and baseline filtering;
+- severity-based CI exit codes;
+- JSON schema versioning and SARIF 2.1.0.
+
+Scoring, priority, confidence, compatibility aliases, profile presets, install
+diagnostics, issue export, and baseline management subcommands are intentionally
+outside the architecture.
+
+## Change rules
+
+When adding a detector:
+
+1. implement or extend one detector file;
+2. add one `RuleRegistration` with a `RuleDefinition`;
+3. add positive, negative, disabled, and output-contract tests where relevant;
+4. update the external spec only when the CLI/config/report contract changes.
+
+Before delivery run `dart analyze`, `dart test`, a demo scan, and
+`dart pub publish --dry-run` for release-affecting changes.
diff --git a/doc/EVOLUTION_ROADMAP.md b/doc/EVOLUTION_ROADMAP.md
new file mode 100644
index 0000000..33ed26c
--- /dev/null
+++ b/doc/EVOLUTION_ROADMAP.md
@@ -0,0 +1,215 @@
+# FlutterGuard 演进路线图
+
+本文档描述 FlutterGuard v0.7 之后的架构演进方向,按优先级分为四个阶段。
+当前收敛重构的进度跟踪请参考仓库根目录下的 `EVOLUTION_PLAN.md`。
+
+---
+
+## 架构速览
+
+```
+bin/flutterguard.dart 唯一入口,路由四个命令族
+lib/src/scanner.dart 扫描编配
+lib/src/scan_context.dart 不可变 scan 作用域载体
+lib/src/source_workspace.dart 单次读取/解析 AST 缓存
+lib/src/import_graph.dart 共享项目导入图
+lib/src/boundary_engine.dart layer/module 共享边界引擎
+lib/src/config_loader.dart YAML → 泛型 RuleConfig
+lib/src/report_generator.dart 表格/JSON 输出
+lib/src/sarif_report.dart SARIF 2.1.0 输出
+lib/src/rules/registry.dart 规则元数据/默认值/执行的唯一数据源
+lib/src/rules/*.dart 16 个检测器实现
+```
+
+---
+
+## 第一阶段:收尾当前收敛重构(短期,已计划) ✅ 已完成 (2026-07-17)
+
+参考 `EVOLUTION_PLAN.md` 中定义的 3 批提交:
+
+### 1.1 根包与内核收敛
+
+```
+refactor: flatten and simplify the flutterguard CLI
+```
+
+- 提交根 `pubspec.yaml`、`analysis_options.yaml`、`.gitignore`、`.pubignore`
+- 删除 `melos.yaml`,删除旧 `packages/flutterguard_cli/**`
+- 新增 `bin/**`、`lib/**`、`test/**`、`example/**`
+
+### 1.2 CI 与发布脚本收敛
+
+```
+ci: align workflows with the root Dart package
+```
+
+- 对齐 `.github/workflows/flutterguard.yml`、`release.yml`
+- 更新 `scripts/package_release.sh`、`scripts/package_release.ps1`
+- 删除废弃编译/扫描脚本
+
+### 1.3 文档与检查点
+
+```
+docs: document the simplified v0.7 architecture
+```
+
+- 定稿 `README.md`、`CHANGELOG.md`、`AGENTS.md`
+- 定稿 `doc/ARCHITECTURE.md`、`doc/FLUTTERGUARD_SPEC.md`
+- 提交本文件 `doc/EVOLUTION_ROADMAP.md`
+
+---
+
+## 第二阶段:检测质量提升(短期,已计划)
+
+### 2.1 OverlayEntry 生命周期检测 ✅ 已完成 (2026-07-21)
+
+- ~~检测 `OverlayEntry.remove()` 和 `OverlayEntry.dispose()` 的调用遗漏~~ (已添加 `OverlayEntry` → `remove` 到 `lifecycle_resource.dart` 的 `_resourceTypes`)
+- ~~归属到 `lifecycle_resource.dart`~~
+
+### 2.2 AST 化字符串检测 ✅ 已完成 (2026-07-21)
+
+**问题**:`iot_security.dart` 和 `ble_scanning.dart` 当前使用正则和字符串匹配:
+
+| 文件 | 方法 | 脆弱点 |
+|---|---|---|
+| `iot_security.dart` | `_secretPattern` 正则 | 误匹配注释/字符串常量中的模式 |
+| `iot_security.dart` | `_cleartextMqttPatterns` 正则 | URL 格式变更需同步 |
+| `ble_scanning.dart` | `body.contains('timeout')` | 方法名重命名后漏检 |
+
+**改进**:升级为 `analyzer` 库的 AST 节点遍历,通过类型信息和解析引用语义进行检测:
+
+1. 密钥检测 → 遍历 `VariableDeclaration` / `AssignmentExpression`,检查赋值右侧是否为明文密钥模式,排除注释和文档字符串
+2. 明文 MQTT/HTTP 检测 → 解析 URI 字符串常量的 scheme,利用 `Uri.parse`
+3. BLE timeout 检测 → 遍历 `MethodInvocation`,查找 `FlutterBluePlus.startScan` 或 `ScanMode` 相关调用及其 `timeout` 命名参数
+
+### 2.3 新规则质量门槛 ✅ 已完成 (2026-07-21)
+
+每条规则已覆盖五种测试场景:
+
+| 场景 | 含义 | 覆盖情况 |
+|---|---|---|
+| positive | 预期触发发现的代码,确认检测生效 | `iot_security_issue.dart`, `ble_scanning_issue.dart`, `lifecycle_issue.dart` |
+| negative | 不会触发的合法代码,确认无误报 | `iot_security_negative.dart`, `ble_scanning_negative.dart`, `lifecycle_negative.dart` |
+| disabled | `enabled: false` 后不产出发现 | rules_test.dart 中 `_disabled` 测试 |
+| suppression | 行内注释 `// flutterguard:ignore` 可抑制 | `iot_security_suppression.dart`, `ble_scanning_suppression.dart` |
+| report-contract | 验证 JSON 输出中 `ruleId`/`severity`/`domain`/`message` 正确 | `iot_security` report contract test |
+
+---
+
+## 第三阶段:架构内聚深化(中期)
+
+### 3.1 大文件拆分
+
+当前三个文件包含多条规则,与 `ble_scanning.dart` / `iot_security.dart` 等"一文件一规则"的惯例不一致:
+
+| 当前文件 | 行数 | 应拆分为 |
+|---|---|---|
+| `generic_state_management.dart` | 449 | `side_effect_in_build.dart`、`state_manager_created_in_build.dart`、`mutable_state_exposed.dart`、`state_layer_ui_dependency.dart` |
+| `provider_state_management.dart` | 326 | `provider_value_lifecycle_misuse.dart`、`notify_listeners_in_loop.dart` |
+| `riverpod_state_management.dart` | 276 | `riverpod_read_used_for_render.dart`、`riverpod_watch_in_callback.dart` |
+
+拆分后共享逻辑保留在 `state_management_utils.dart` 中。
+
+### 3.2 消除工具函数重复
+
+| 重复项 | 位置 | 解决方案 |
+|---|---|---|
+| `sourceLine` vs `lineNumberForOffset` | `state_management_utils.dart:97` / `source_workspace.dart:10` | 统一使用 `SourceWorkspace` 的 `lineNumberForOffset`,移除 `state_management_utils.dart` 中的版本 |
+| `_FirstOrNull` 扩展 | `provider_state_management.dart` | Dart 3.x 已内置 `firstOrNull`,直接移除自定义扩展 |
+| BLE 资源类型列表 | `lifecycle_resource.dart`、`ble_scanning.dart`、`generic_state_management.dart` | 抽取共享类型常量到 `flutter_resource_types.dart` 或统一通过 AST 类型判断 |
+
+### 3.3 `notify_listeners_in_loop` 的 `_provablyShortFor` AST 化
+
+当前使用正则判断 for 循环是否"显然很短":
+
+```dart
+RegExp(r'= 0; [A-Za-z_$][\w$]* < 1;')
+```
+
+对格式化差异和变量命名敏感。应改为:
+
+- 获取 `ForStatement` 的 `forLoopParts`,解析初始化表达式和条件表达式
+- 若初始化将循环变量设为常量 0,且条件为 `变量 < 1`,则判定为短循环
+- 排除循环体内包含 `await` 或嵌套循环的情况
+
+### 3.4 共享类型注册表
+
+建立 `lib/src/rules/flutter_resource_types.dart`,集中声明需要追踪生命周期的 Flutter 资源类型:
+
+```dart
+// 示例结构
+const _disposableTypes = {
+ 'StreamSubscription', 'Timer', 'AnimationController',
+ 'TextEditingController', 'ScrollController',
+ 'BluetoothDevice', 'BluetoothCharacteristic',
+ // ...
+};
+```
+
+供 `lifecycle_resource.dart`、`ble_scanning.dart`、`generic_state_management.dart` 统一引用。
+
+---
+
+## 第四阶段:能力扩展(中长期)
+
+### 4.1 IoT 专项规则扩展
+
+| 规则方向 | 检测内容 | 复杂度 |
+|---|---|---|
+| Wi-Fi 配置安全 | 检测代码中硬编码的 Wi-Fi SSID/密码,建议使用设备 provisioning API | 中 |
+| 固件 OTA 校验 | 检测 `http.get(url)` 下载固件但未验证签名/哈希的模式 | 高 |
+| MQTT QoS 检测 | 检测 QoS 0 发布关键消息(命令/配置)的场景 | 中 |
+| 蓝牙配对模式 | 检测 `createBond` / `JustWorks` 配对且无用户确认 | 中 |
+
+### 4.2 状态管理规则扩展
+
+| 规则方向 | 检测内容 | 复杂度 |
+|---|---|---|
+| Cubit 泄漏 | 检测 `BlocProvider` 创建但未在 dispose 中 `close()` 的 Cubit | 中 |
+| GetX 滥用 | 检测 `Get.to` 在 build 中调用、`Get.find` 在 initState 外使用 | 中 |
+| MobX reaction 泄漏 | 检测未 dispose 的 `ReactionDisposer` | 中 |
+| ValueNotifier dispose | 检测 `ValueNotifier` / `ChangeNotifier` 未 dispose | 低 |
+
+### 4.3 测试体系增强
+
+- 核心算法补充单元测试:
+ - `TarjanSccFinder._stronglyConnectedComponents`(`state_dependency_cycle.dart`)
+ - `_reconstructCycle` BFS 最短环路径重建
+ - `_shortestCycle` 全节点对最短环
+ - `ImportGraph.build` 多文件解析图构建
+- 添加规则级别的 golden-file 测试,验证 JSON/SARIF 报告输出结构
+
+### 4.4 跨文件数据流分析
+
+当前所有规则均为单文件分析。对 `iot_security` 规则引入有限的跨文件追踪:
+
+- 追踪 `static const` 密钥常量从一个文件被导出/导入到使用点的路径
+- 检测通过 `export` 间接暴露的密钥
+- 限制追踪深度为 1 层导入(通过 `ImportGraph` 即可实现,无需完整的数据流引擎)
+
+---
+
+## 架构约束(不可逾越的红线)
+
+以下设计决策在 v0.7 收敛中已确认,后续演进不得逆转:
+
+| 约束 | 说明 |
+|---|---|
+| 不恢复 Melos | 仓库根目录就是唯一的 Dart 包 |
+| 不恢复 runtime SDK | 没有运行时 instrumentation、APM、云服务 |
+| 不暴露公共 API | `lib/src` 是私有实现,集成边界仅 CLI + JSON/SARIF |
+| 不添加冗余元数据 | 无 score/priority/confidence/兼容别名 |
+| 不添加通用风格规则 | 无 generic-size、missing-const、格式化类规则 |
+| 单一注册点 | `RuleRegistry.registrations` 是唯一的规则注册源 |
+| 资源复用 | `SourceWorkspace` 和 `ImportGraph` 扫描生命周期内各构建一次 |
+| 每个 finding 有唯一 owner | 同一代码模式不会被多个规则重复报告 |
+| 框架自动检测 | 框架规则通过 import 检测激活,无全局配置开关 |
+
+---
+
+## 相关文档
+
+- 仓库根 `EVOLUTION_PLAN.md` — 当前收敛重构的续跑检查点和提交拆分
+- `doc/ARCHITECTURE.md` — 内部架构边界和 scan 流程
+- `doc/FLUTTERGUARD_SPEC.md` — 外部契约(CLI / JSON / SARIF)
+- 根 `AGENTS.md` — 仓库级约束和命令入口
diff --git a/doc/FLUTTERGUARD_SPEC.md b/doc/FLUTTERGUARD_SPEC.md
new file mode 100644
index 0000000..e575da4
--- /dev/null
+++ b/doc/FLUTTERGUARD_SPEC.md
@@ -0,0 +1,218 @@
+# FlutterGuard External Contract
+
+Version: 0.7 / JSON schema 2.0.0
+
+This document defines supported user-facing behavior. Internal classes and file
+layout are documented in `ARCHITECTURE.md` and are not compatibility contracts.
+
+## Identity and scope
+
+FlutterGuard is a local static analysis CLI for IoT and smart-home Flutter
+projects. It detects architecture boundary violations, lifecycle/resource
+risks, IoT transport/security problems, and state-management maintainability
+problems.
+
+It is not a runtime SDK, observability agent, crash reporter, hosted service,
+network proxy, widget library, or general formatting/style linter.
+
+## Commands
+
+```text
+flutterguard scan [path] [options]
+flutterguard baseline create [path] [options]
+flutterguard config init [path] [options]
+flutterguard config check [path] [options]
+flutterguard rules [rule-id] [--format table|json]
+```
+
+Global options are `--help` and `--version`.
+
+### Scan options
+
+| Option | Contract |
+|---|---|
+| `--config`, `-c` | Explicit config; missing explicit files are errors |
+| `--format`, `-f` | `table`, `json`, or `sarif`; default `table` |
+| `--output`, `-o` | Report directory; default `.flutterguard` |
+| `--verbose`, `-v` | Print diagnostics, detail, and evidence |
+| `--no-color` | Disable ANSI colors |
+| `--fail-on` | `none`, `high`, `medium`, or `low` |
+| `--changed-only` | Restrict source rules to Git-changed Dart files |
+| `--base` | Verified Git base ref; default `main` |
+| `--baseline` | Hide findings present in a baseline file |
+
+### Exit codes
+
+| Code | Meaning |
+|---|---|
+| `0` | Command succeeded and the severity gate passed |
+| `1` | Severity gate or config check failed |
+| `2` | Invalid arguments, project, config, baseline, or Git base |
+
+## Project and files
+
+Project discovery walks upward from the requested path and recognizes
+`flutterguard.yaml`, `pubspec.yaml`, or `lib/`. Default source selection is:
+
+```yaml
+include: [lib/**]
+exclude:
+ - lib/generated/**
+ - lib/**.g.dart
+ - lib/**.freezed.dart
+ - lib/**.mocks.dart
+```
+
+An ordinary full scan with zero matched Dart files is an error. A valid
+changed-only scan with zero changed Dart files succeeds and writes an empty
+requested report.
+
+## Configuration
+
+Top-level keys are `include`, `exclude`, `rules`, and `architecture`.
+
+```yaml
+rules:
+ :
+ enabled: true
+ severity: high
+ :
+
+architecture:
+ detect_cycles: false
+ layers: []
+ modules: []
+```
+
+Rule severity is `low`, `medium`, or `high`. Unknown rule IDs and unknown
+rule-specific options are errors in `config check`. Run `config init` to
+generate current definitions and defaults.
+
+A boundary entry is:
+
+```yaml
+- name: presentation
+ path: lib/presentation/**
+ allowed_deps: [domain, core]
+```
+
+Layer and module enforcement is inactive when its boundary list is empty.
+Circular import detection requires `architecture.detect_cycles: true` and is
+skipped in changed-only mode.
+
+## Rule inventory
+
+The registry contains 16 IDs:
+
+```text
+ble_scanning
+bloc_equatable_props_incomplete
+circular_dependency
+iot_security
+layer_violation
+lifecycle_resource_not_disposed
+module_violation
+mutable_state_exposed
+notify_listeners_in_loop
+provider_value_lifecycle_misuse
+riverpod_read_used_for_render
+riverpod_watch_in_callback
+side_effect_in_build
+state_dependency_cycle
+state_layer_ui_dependency
+state_manager_created_in_build
+```
+
+`rules [rule-id]` is the authoritative description of purpose, default
+severity, framework, options, example, and remediation.
+
+## Findings
+
+The internal finding model exposes one canonical JSON representation:
+
+```json
+{
+ "ruleId": "iot_security",
+ "title": "IoT 安全风险",
+ "file": "/project/lib/device.dart",
+ "line": 10,
+ "severity": "high",
+ "domain": "architecture",
+ "message": "...",
+ "detail": "...",
+ "suggestion": "...",
+ "metadata": {},
+ "framework": "generic",
+ "evidence": []
+}
+```
+
+There are no duplicate `id`/`ruleId` or `level`/`severity` fields, and no
+priority, score, or confidence fields.
+
+## JSON report
+
+JSON reports use schema version `2.0.0`:
+
+```json
+{
+ "schemaVersion": "2.0.0",
+ "projectPath": "/project",
+ "scanMode": "full",
+ "summary": {
+ "total": 1,
+ "high": 1,
+ "medium": 0,
+ "low": 0,
+ "suppressed": 0,
+ "suppressedByBaseline": 0,
+ "diagnostics": 0,
+ "byDomain": {}
+ },
+ "issues": [],
+ "diagnostics": []
+}
+```
+
+SARIF reports conform to SARIF 2.1.0 and include registry definitions and
+finding locations.
+
+## Suppression and baseline
+
+Supported comments are same-line and immediately preceding-line forms:
+
+```dart
+// flutterguard: ignore iot_security
+final endpoint = localEndpoint;
+```
+
+Multiple IDs may be comma-separated; `ignore all` suppresses all findings at
+the target line.
+
+`baseline create` stores stable fingerprints derived from rule ID, normalized
+path, line, and message. A scan with `--baseline` hides matching findings after
+inline suppression.
+
+## Changed-only behavior
+
+Changed-only mode verifies `^{commit}`, collects tracked changes and
+untracked files, and filters them to configured Dart sources. Unchanged project
+files remain available for import and state-graph resolution. Project-level
+pubspec checks run only when `pubspec.yaml` changed. State dependency cycles
+use the full graph and report cycles touching a changed source.
+
+## Release gates
+
+A release is valid only when all of the following pass from the repository
+root:
+
+```bash
+dart pub get
+dart analyze
+dart test
+dart run bin/flutterguard.dart scan example --format json --no-color
+dart pub publish --dry-run
+```
+
+Native release artifacts are produced by `scripts/package_release.sh` and
+`scripts/package_release.ps1` on their respective operating systems.
diff --git a/example/README.md b/example/README.md
new file mode 100644
index 0000000..fda363e
--- /dev/null
+++ b/example/README.md
@@ -0,0 +1,11 @@
+# Scan example
+
+This small Dart target is scanned by CI with the current checkout.
+
+```bash
+dart run bin/flutterguard.dart scan example --no-color
+dart run bin/flutterguard.dart scan example --format json --output .flutterguard/demo
+```
+
+Its configuration demonstrates the IoT lifecycle/security options without
+depending on Flutter or the FlutterGuard package.
diff --git a/example/flutterguard.yaml b/example/flutterguard.yaml
new file mode 100644
index 0000000..4a498fd
--- /dev/null
+++ b/example/flutterguard.yaml
@@ -0,0 +1,19 @@
+include:
+ - lib/**
+
+rules:
+ lifecycle_resource_not_disposed:
+ enabled: true
+ severity: medium
+ ble_scanning:
+ enabled: true
+ severity: medium
+ iot_security:
+ enabled: true
+ severity: high
+ requireTls: true
+
+architecture:
+ layers: []
+ modules: []
+ detect_cycles: false
diff --git a/examples/scan_demo/lib/main.dart b/example/lib/main.dart
similarity index 100%
rename from examples/scan_demo/lib/main.dart
rename to example/lib/main.dart
diff --git a/examples/scan_demo/lib/services/user_service.dart b/example/lib/services/user_service.dart
similarity index 100%
rename from examples/scan_demo/lib/services/user_service.dart
rename to example/lib/services/user_service.dart
diff --git a/examples/scan_demo/lib/widgets/profile_card.dart b/example/lib/widgets/profile_card.dart
similarity index 100%
rename from examples/scan_demo/lib/widgets/profile_card.dart
rename to example/lib/widgets/profile_card.dart
diff --git a/examples/scan_demo/pubspec.yaml b/example/pubspec.yaml
similarity index 100%
rename from examples/scan_demo/pubspec.yaml
rename to example/pubspec.yaml
diff --git a/examples/scan_demo/README.md b/examples/scan_demo/README.md
deleted file mode 100644
index d386cec..0000000
--- a/examples/scan_demo/README.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# Scan Demo
-
-A deliberately imperfect project to demonstrate `flutterguard scan`.
-
-```bash
-# From the repo root, run scan with low thresholds to trigger issues
-./flutterguard scan -p examples/scan_demo
-
-# With verbose output
-./flutterguard scan -p examples/scan_demo --verbose
-
-# JSON output
-./flutterguard scan -p examples/scan_demo --format json
-```
-
-The `flutterguard.yaml` in this directory uses low thresholds (maxLines: 80/30/20) to demonstrate issue detection even in small projects.
diff --git a/examples/scan_demo/flutterguard.yaml b/examples/scan_demo/flutterguard.yaml
deleted file mode 100644
index 0242a36..0000000
--- a/examples/scan_demo/flutterguard.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-include:
- - lib/**
-
-rules:
- large_file:
- enabled: true
- maxLines: 80
- large_class:
- enabled: true
- maxLines: 30
- large_build_method:
- enabled: true
- maxLines: 20
diff --git a/flutterguard.yaml b/flutterguard.yaml
index 0aa610b..8fc2034 100644
--- a/flutterguard.yaml
+++ b/flutterguard.yaml
@@ -7,40 +7,7 @@ exclude:
- lib/**.freezed.dart
- lib/**.mocks.dart
-rules:
- large_file:
- enabled: true
- maxLines: 500
- large_class:
- enabled: true
- maxLines: 300
- large_build_method:
- enabled: true
- maxLines: 80
- lifecycle_resource:
- enabled: true
- missing_const_constructor:
- enabled: true
-
architecture:
- # layers:
- # - name: presentation
- # path: lib/presentation/**
- # allowed_deps: [domain, core]
- # - name: domain
- # path: lib/domain/**
- # allowed_deps: [core]
- # - name: data
- # path: lib/data/**
- # allowed_deps: [domain, core]
- # - name: core
- # path: lib/core/**
- # allowed_deps: []
- # modules:
- # - name: device_mqtt
- # path: lib/device/mqtt/**
- # allowed_deps: [domain, core]
- # - name: device_ble
- # path: lib/device/ble/**
- # allowed_deps: [domain, core]
- # detect_cycles: true
+ layers: []
+ modules: []
+ detect_cycles: false
diff --git a/lib/AGENTS.md b/lib/AGENTS.md
new file mode 100644
index 0000000..387ed75
--- /dev/null
+++ b/lib/AGENTS.md
@@ -0,0 +1,7 @@
+# Library boundary
+
+`flutterguard_cli.dart` intentionally exports no scanner API. FlutterGuard is
+an executable package; integrations use the CLI plus JSON or SARIF.
+
+All implementation belongs under `src/`. Do not export internal rule,
+configuration, AST, or report types from the package barrel.
diff --git a/lib/flutterguard_cli.dart b/lib/flutterguard_cli.dart
new file mode 100644
index 0000000..1b53eb7
--- /dev/null
+++ b/lib/flutterguard_cli.dart
@@ -0,0 +1,5 @@
+/// FlutterGuard is distributed as the `flutterguard` executable.
+///
+/// The scanner implementation under `lib/src` is intentionally not a public
+/// Dart API. Integrations should invoke the CLI and consume JSON or SARIF.
+library;
diff --git a/lib/src/AGENTS.md b/lib/src/AGENTS.md
new file mode 100644
index 0000000..943ea3f
--- /dev/null
+++ b/lib/src/AGENTS.md
@@ -0,0 +1,18 @@
+# Scan kernel
+
+This directory owns reusable implementation behind the executable.
+
+- `scanner.dart`: orchestration, suppression, baseline, report writes.
+- `scan_context.dart`: immutable scan scope.
+- `source_workspace.dart`: one-read/one-parse cache and diagnostics.
+- `import_graph.dart`: resolved project import graph.
+- `boundary_engine.dart`: boundary evaluation shared by layer/module rules.
+- `config_loader.dart`: compact YAML parser and generic `RuleConfig`.
+- `config_tools.dart`: generated starter config and config validation.
+- `report_generator.dart` / `sarif_report.dart`: output adapters.
+- `cli/`: argument parser and command I/O only.
+- `rules/`: definitions, registry, and detectors.
+
+Do not bypass `ScanContext`, parse files independently inside scanner wiring,
+duplicate config defaults, or create another registry. File/path behavior must
+remain cross-platform and changed-only imports must resolve against all files.
diff --git a/packages/flutterguard_cli/lib/src/baseline.dart b/lib/src/baseline.dart
similarity index 55%
rename from packages/flutterguard_cli/lib/src/baseline.dart
rename to lib/src/baseline.dart
index 4bc8823..0fd8ec6 100644
--- a/packages/flutterguard_cli/lib/src/baseline.dart
+++ b/lib/src/baseline.dart
@@ -13,8 +13,8 @@ class Baseline {
static String fingerprint(StaticIssue issue, String projectPath) {
final relativePath =
p.isWithin(projectPath, issue.file) || p.equals(projectPath, issue.file)
- ? p.relative(issue.file, from: projectPath)
- : issue.file;
+ ? p.relative(issue.file, from: projectPath)
+ : issue.file;
final normalizedPath = relativePath.replaceAll('\\', '/');
final source = [
issue.id,
@@ -53,67 +53,13 @@ class Baseline {
return Baseline(fingerprints.map((v) => v.toString()).toSet());
}
- static Map stats(String path) {
- final baseline = load(path);
- return {
- 'path': path,
- 'fingerprints': baseline.fingerprints.length,
- };
- }
-
- static List newFingerprints({
- required String projectPath,
- required Baseline baseline,
- required List issues,
- }) {
- final current = issues
- .map((issue) => fingerprint(issue, projectPath))
- .where((fingerprint) => !baseline.fingerprints.contains(fingerprint))
- .toSet()
- .toList()
- ..sort();
- return current;
- }
-
- static String encodeFingerprints({
- required String projectPath,
- required List fingerprints,
- }) {
- final sorted = fingerprints.toSet().toList()..sort();
- final payload = {
- 'version': '1.0.0',
- 'generatedAt': DateTime.now().toIso8601String(),
- 'projectPath': projectPath,
- 'issueCount': sorted.length,
- 'fingerprints': sorted,
- };
- return const JsonEncoder.withIndent(' ').convert(payload);
- }
-
- static String prune({
- required String projectPath,
- required Baseline baseline,
- required List issues,
- }) {
- final current = issues
- .map((issue) => fingerprint(issue, projectPath))
- .where(baseline.fingerprints.contains)
- .toList();
- return encodeFingerprints(
- projectPath: projectPath,
- fingerprints: current,
- );
- }
-
static String encode({
required String projectPath,
required List issues,
}) {
- final fingerprints = issues
- .map((issue) => fingerprint(issue, projectPath))
- .toSet()
- .toList()
- ..sort();
+ final fingerprints =
+ issues.map((issue) => fingerprint(issue, projectPath)).toSet().toList()
+ ..sort();
final payload = {
'version': '1.0.0',
diff --git a/packages/flutterguard_cli/lib/src/boundary_engine.dart b/lib/src/boundary_engine.dart
similarity index 84%
rename from packages/flutterguard_cli/lib/src/boundary_engine.dart
rename to lib/src/boundary_engine.dart
index 0737481..979aaea 100644
--- a/packages/flutterguard_cli/lib/src/boundary_engine.dart
+++ b/lib/src/boundary_engine.dart
@@ -49,11 +49,13 @@ class DependencyBoundaryEngine {
break;
}
} on Object catch (error) {
- workspace.addDiagnostic(ScanDiagnostic(
- stage: 'boundary_glob',
- file: file,
- message: 'Invalid boundary glob "${boundary.path}": $error',
- ));
+ workspace.addDiagnostic(
+ ScanDiagnostic(
+ stage: 'boundary_glob',
+ file: file,
+ message: 'Invalid boundary glob "${boundary.path}": $error',
+ ),
+ );
}
}
}
@@ -66,11 +68,9 @@ class DependencyBoundaryEngine {
final target = fileToBoundary[edge.target];
if (target == null || target.name == source.name) continue;
if (!source.allowedDeps.contains(target.name)) {
- violations.add(BoundaryViolation(
- edge: edge,
- source: source,
- target: target,
- ));
+ violations.add(
+ BoundaryViolation(edge: edge, source: source, target: target),
+ );
}
}
}
diff --git a/lib/src/cli/baseline_commands.dart b/lib/src/cli/baseline_commands.dart
new file mode 100644
index 0000000..5dc4dbd
--- /dev/null
+++ b/lib/src/cli/baseline_commands.dart
@@ -0,0 +1,44 @@
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:path/path.dart' as p;
+
+import '../baseline.dart';
+import '../scanner.dart';
+
+class BaselineCommands {
+ static void create(ArgResults args, {String? configPath}) {
+ final projectPath = args.rest.isNotEmpty ? args.rest.first : '.';
+ final output = args['output'] as String;
+ try {
+ final result = FlutterGuardScanner.scan(
+ projectPath: projectPath,
+ configPath: configPath,
+ applySuppression: false,
+ );
+ final outputPath = p.isAbsolute(output)
+ ? output
+ : p.join(result.projectPath, output);
+ Directory(p.dirname(outputPath)).createSync(recursive: true);
+ File(outputPath).writeAsStringSync(
+ Baseline.encode(
+ projectPath: result.projectPath,
+ issues: result.rawIssues,
+ ),
+ );
+ stdout.writeln(
+ 'Created FlutterGuard baseline: $outputPath '
+ '(${result.rawIssues.length} issues)',
+ );
+ } on ScanException catch (error) {
+ _fail(error.message);
+ } on FormatException catch (error) {
+ _fail(error.message);
+ }
+ }
+
+ static Never _fail(String message) {
+ stderr.writeln('Error: $message');
+ exit(2);
+ }
+}
diff --git a/lib/src/cli/cli_parsers.dart b/lib/src/cli/cli_parsers.dart
new file mode 100644
index 0000000..d438d91
--- /dev/null
+++ b/lib/src/cli/cli_parsers.dart
@@ -0,0 +1,77 @@
+import 'package:args/args.dart';
+
+class CliParsers {
+ late final ArgParser scan;
+ late final ArgParser baselineCreate;
+ late final ArgParser baseline;
+ late final ArgParser configInit;
+ late final ArgParser configCheck;
+ late final ArgParser config;
+ late final ArgParser rules;
+ late final ArgParser root;
+
+ CliParsers() {
+ scan = ArgParser()
+ ..addOption('config', abbr: 'c', help: 'Explicit config file')
+ ..addOption(
+ 'format',
+ abbr: 'f',
+ defaultsTo: 'table',
+ allowed: ['table', 'json', 'sarif'],
+ )
+ ..addOption('output', abbr: 'o', defaultsTo: '.flutterguard')
+ ..addFlag('verbose', abbr: 'v', negatable: false)
+ ..addFlag('no-color', negatable: false)
+ ..addFlag('changed-only', negatable: false)
+ ..addOption('base', defaultsTo: 'main')
+ ..addOption(
+ 'fail-on',
+ defaultsTo: 'none',
+ allowed: ['none', 'high', 'medium', 'low'],
+ )
+ ..addOption('baseline')
+ ..addFlag('help', abbr: 'h', negatable: false);
+
+ baselineCreate = ArgParser()
+ ..addOption('config', abbr: 'c')
+ ..addOption(
+ 'output',
+ abbr: 'o',
+ defaultsTo: '.flutterguard/baseline.json',
+ )
+ ..addFlag('help', abbr: 'h', negatable: false);
+ baseline = ArgParser()
+ ..addCommand('create', baselineCreate)
+ ..addFlag('help', abbr: 'h', negatable: false);
+
+ configInit = ArgParser()
+ ..addOption('config', abbr: 'c', defaultsTo: 'flutterguard.yaml')
+ ..addFlag('with-architecture', negatable: false)
+ ..addFlag('force', negatable: false)
+ ..addFlag('help', abbr: 'h', negatable: false);
+ configCheck = ArgParser()
+ ..addOption('config', abbr: 'c')
+ ..addFlag('help', abbr: 'h', negatable: false);
+ config = ArgParser()
+ ..addCommand('init', configInit)
+ ..addCommand('check', configCheck)
+ ..addFlag('help', abbr: 'h', negatable: false);
+
+ rules = ArgParser()
+ ..addOption(
+ 'format',
+ abbr: 'f',
+ defaultsTo: 'table',
+ allowed: ['table', 'json'],
+ )
+ ..addFlag('help', abbr: 'h', negatable: false);
+
+ root = ArgParser()
+ ..addCommand('scan', scan)
+ ..addCommand('baseline', baseline)
+ ..addCommand('config', config)
+ ..addCommand('rules', rules)
+ ..addFlag('help', abbr: 'h', negatable: false)
+ ..addFlag('version', abbr: 'V', negatable: false);
+ }
+}
diff --git a/lib/src/cli/config_commands.dart b/lib/src/cli/config_commands.dart
new file mode 100644
index 0000000..496b88d
--- /dev/null
+++ b/lib/src/cli/config_commands.dart
@@ -0,0 +1,44 @@
+import 'dart:io';
+
+import 'package:args/args.dart';
+
+import '../config_tools.dart';
+
+class ConfigCommands {
+ static void init(ArgResults args) {
+ try {
+ final projectPath = args.rest.isNotEmpty ? args.rest.first : '.';
+ final outputPath = ConfigTools.writeInitConfig(
+ projectPath: projectPath,
+ configPath: args['config'] as String,
+ withArchitecture: args['with-architecture'] as bool,
+ force: args['force'] as bool,
+ );
+ stdout.writeln('Created FlutterGuard config: $outputPath');
+ stdout.writeln('Next: flutterguard config check $projectPath');
+ } on StateError catch (error) {
+ _fail(error.message);
+ }
+ }
+
+ static void check(ArgResults args, {String? configPath}) {
+ try {
+ final projectPath = args.rest.isNotEmpty ? args.rest.first : '.';
+ final result = ConfigTools.doctor(
+ projectPath: projectPath,
+ configPath: configPath,
+ );
+ stdout.write(ConfigTools.formatDoctorResult(result));
+ if (result.hasErrors) exit(1);
+ } on StateError catch (error) {
+ _fail(error.message);
+ } on FormatException catch (error) {
+ _fail(error.message);
+ }
+ }
+
+ static Never _fail(String message) {
+ stderr.writeln('Error: $message');
+ exit(2);
+ }
+}
diff --git a/lib/src/cli/rule_commands.dart b/lib/src/cli/rule_commands.dart
new file mode 100644
index 0000000..fcb41c3
--- /dev/null
+++ b/lib/src/cli/rule_commands.dart
@@ -0,0 +1,62 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:args/args.dart';
+
+import '../rules/registry.dart';
+import '../rules/rule.dart';
+
+class RuleCommands {
+ static void run(ArgResults args) {
+ if (args.rest.isNotEmpty) {
+ _describe(args.rest.first, json: args['format'] == 'json');
+ return;
+ }
+ final rules = RuleRegistry.all()..sort((a, b) => a.id.compareTo(b.id));
+ if (args['format'] == 'json') {
+ stdout.writeln(
+ const JsonEncoder.withIndent(
+ ' ',
+ ).convert(rules.map((rule) => rule.toJson()).toList()),
+ );
+ return;
+ }
+ stdout.writeln('Available rules (${rules.length}):');
+ for (final rule in rules) {
+ stdout.writeln(
+ ' ${rule.id.padRight(36)} '
+ '${rule.domain.name.padRight(14)} '
+ '${rule.defaultSeverity.name.padRight(8)} ${rule.name}',
+ );
+ }
+ stdout.writeln();
+ stdout.writeln('Run flutterguard rules for details.');
+ }
+
+ static void _describe(String id, {required bool json}) {
+ final rule = RuleRegistry.find(id);
+ if (rule == null) {
+ stderr.writeln('Error: unknown rule "$id".');
+ exit(2);
+ }
+ if (json) {
+ stdout.writeln(const JsonEncoder.withIndent(' ').convert(rule.toJson()));
+ return;
+ }
+ _writeDefinition(rule);
+ }
+
+ static void _writeDefinition(RuleDefinition rule) {
+ stdout.writeln('Rule: ${rule.id}');
+ stdout.writeln('Name: ${rule.name}');
+ stdout.writeln('Domain: ${rule.domain.name}');
+ stdout.writeln('Severity: ${rule.defaultSeverity.name}');
+ stdout.writeln('Framework: ${rule.framework}');
+ stdout.writeln();
+ stdout.writeln('Purpose: ${rule.purpose}');
+ stdout.writeln('Risk: ${rule.riskReason}');
+ stdout.writeln('Example: ${rule.badExample}');
+ stdout.writeln('Fix: ${rule.fixSuggestion}');
+ stdout.writeln('Config: ${rule.configKeys.join(', ')}');
+ }
+}
diff --git a/packages/flutterguard_cli/lib/src/cli/scan_command.dart b/lib/src/cli/scan_command.dart
similarity index 80%
rename from packages/flutterguard_cli/lib/src/cli/scan_command.dart
rename to lib/src/cli/scan_command.dart
index 620e792..9efcb83 100644
--- a/packages/flutterguard_cli/lib/src/cli/scan_command.dart
+++ b/lib/src/cli/scan_command.dart
@@ -11,12 +11,12 @@ class ScanCommand {
final verbose = args['verbose'] as bool;
final noColor = args['no-color'] as bool;
final failOn = args['fail-on'] as String;
- final minScore = _parseMinScore(args['min-score'] as String?);
+ final projectPath = args.rest.isNotEmpty ? args.rest.first : '.';
late final ScanResult result;
try {
result = FlutterGuardScanner.scan(
- projectPath: args['path'] as String,
+ projectPath: projectPath,
configPath: configPath,
outputDir: args['output'] as String,
writeJson: format == 'json',
@@ -77,23 +77,5 @@ class ScanCommand {
);
exit(1);
}
-
- if (minScore != null && result.score < minScore) {
- stderr.writeln(
- 'CI gate failed: Score ${result.score} is below minimum $minScore.',
- );
- exit(1);
- }
- }
-
- static int? _parseMinScore(String? value) {
- if (value == null) return null;
- final score = int.tryParse(value);
- if (score == null || score < 0 || score > 100) {
- throw const FormatException(
- 'Expected --min-score to be an integer 0-100.',
- );
- }
- return score;
}
}
diff --git a/lib/src/config_loader.dart b/lib/src/config_loader.dart
new file mode 100644
index 0000000..55e6fb5
--- /dev/null
+++ b/lib/src/config_loader.dart
@@ -0,0 +1,229 @@
+// ignore_for_file: avoid_dynamic_calls
+
+import 'dart:io';
+
+import 'package:yaml/yaml.dart';
+
+import 'static_issue.dart';
+
+typedef BoundaryConfig = ({String name, String path, List allowedDeps});
+
+typedef ArchitectureConfig = ({
+ List layers,
+ List modules,
+ bool detectCycles,
+});
+
+class RuleConfig {
+ final bool enabled;
+ final RiskLevel severity;
+ final Map options;
+
+ const RuleConfig({
+ required this.enabled,
+ required this.severity,
+ this.options = const {},
+ });
+
+ bool boolOption(String key) {
+ final value = options[key];
+ if (value is bool) return value;
+ throw FormatException('rules option "$key" must be a boolean.');
+ }
+}
+
+class ScanConfig {
+ final List include;
+ final List exclude;
+ final Map<
+ String,
+ ({bool enabled, RiskLevel? severity, Map options})
+ >
+ _rules;
+ final ArchitectureConfig architecture;
+
+ const ScanConfig({
+ required this.include,
+ required this.exclude,
+ required Map<
+ String,
+ ({bool enabled, RiskLevel? severity, Map options})
+ >
+ rules,
+ required this.architecture,
+ }) : _rules = rules;
+
+ Set get configuredRuleIds => _rules.keys.toSet();
+
+ Map configuredOptions(String id) =>
+ Map.unmodifiable(_rules[id]?.options ?? const {});
+
+ RuleConfig rule(
+ String id, {
+ required RiskLevel defaultSeverity,
+ Map defaultOptions = const {},
+ }) {
+ final configured = _rules[id];
+ return RuleConfig(
+ enabled: configured?.enabled ?? true,
+ severity: configured?.severity ?? defaultSeverity,
+ options: {...defaultOptions, ...?configured?.options},
+ );
+ }
+
+ static const _knownTopLevelKeys = {
+ 'include',
+ 'exclude',
+ 'rules',
+ 'architecture',
+ };
+ static const _knownArchitectureKeys = {'layers', 'modules', 'detect_cycles'};
+ static const _knownBoundaryKeys = {'name', 'path', 'allowed_deps'};
+
+ factory ScanConfig.fromFile(String path, {bool requireFile = false}) {
+ final file = File(path);
+ if (!file.existsSync()) {
+ if (requireFile) {
+ throw FormatException('Config file "$path" does not exist.');
+ }
+ return ScanConfig.defaults();
+ }
+
+ final parsed = loadYaml(file.readAsStringSync());
+ if (parsed == null) return ScanConfig.defaults();
+ if (parsed is! YamlMap) {
+ throw FormatException('Config file "$path" must contain a YAML map.');
+ }
+
+ _warnUnknownKeys(parsed, _knownTopLevelKeys, 'config');
+ final architecture = _optionalMap(parsed['architecture'], 'architecture');
+ _warnUnknownKeys(architecture, _knownArchitectureKeys, 'architecture');
+
+ return ScanConfig(
+ include: _stringList(parsed['include']) ?? const ['lib/**'],
+ exclude:
+ _stringList(parsed['exclude']) ??
+ const [
+ 'lib/generated/**',
+ 'lib/**.g.dart',
+ 'lib/**.freezed.dart',
+ 'lib/**.mocks.dart',
+ ],
+ rules: _parseRules(_optionalMap(parsed['rules'], 'rules')),
+ architecture: (
+ layers: _parseBoundaries(architecture['layers'], 'layers'),
+ modules: _parseBoundaries(architecture['modules'], 'modules'),
+ detectCycles: _boolValue(architecture, 'detect_cycles', false),
+ ),
+ );
+ }
+
+ factory ScanConfig.defaults() => const ScanConfig(
+ include: ['lib/**'],
+ exclude: [
+ 'lib/generated/**',
+ 'lib/**.g.dart',
+ 'lib/**.freezed.dart',
+ 'lib/**.mocks.dart',
+ ],
+ rules: {},
+ architecture: (layers: [], modules: [], detectCycles: false),
+ );
+
+ static Map<
+ String,
+ ({bool enabled, RiskLevel? severity, Map options})
+ >
+ _parseRules(YamlMap rules) {
+ final result =
+ <
+ String,
+ ({bool enabled, RiskLevel? severity, Map options})
+ >{};
+ for (final entry in rules.entries) {
+ final id = entry.key.toString();
+ final map = _optionalMap(entry.value, 'rules.$id');
+ final options = {};
+ for (final option in map.entries) {
+ final key = option.key.toString();
+ if (key == 'enabled' || key == 'severity') continue;
+ final value = option.value;
+ if (value is bool || value is int || value is String) {
+ options[key] = value;
+ } else {
+ throw FormatException('rules.$id.$key must be a scalar value.');
+ }
+ }
+ result[id] = (
+ enabled: _boolValue(map, 'enabled', true),
+ severity: _optionalRiskLevel(map, 'severity'),
+ options: options,
+ );
+ }
+ return result;
+ }
+
+ static List _parseBoundaries(Object? value, String path) {
+ if (value == null) return const [];
+ if (value is! YamlList) {
+ throw FormatException('architecture.$path must be a list.');
+ }
+ return [
+ for (final item in value) _parseBoundary(item, 'architecture.$path'),
+ ];
+ }
+
+ static BoundaryConfig _parseBoundary(Object? value, String path) {
+ if (value is! YamlMap) {
+ throw FormatException('Each $path entry must be a map.');
+ }
+ _warnUnknownKeys(value, _knownBoundaryKeys, path);
+ return (
+ name: _requiredString(value, 'name', path),
+ path: _requiredString(value, 'path', path),
+ allowedDeps: _stringList(value['allowed_deps']) ?? const [],
+ );
+ }
+
+ static YamlMap _optionalMap(Object? value, String path) {
+ if (value == null) return YamlMap();
+ if (value is YamlMap) return value;
+ throw FormatException('$path must be a map.');
+ }
+
+ static List? _stringList(Object? value) {
+ if (value == null) return null;
+ if (value is! YamlList) throw const FormatException('Expected a list.');
+ return value.map((item) => item.toString()).toList();
+ }
+
+ static bool _boolValue(YamlMap map, String key, bool fallback) {
+ final value = map[key];
+ if (value == null) return fallback;
+ if (value is bool) return value;
+ throw FormatException('$key must be a boolean.');
+ }
+
+ static RiskLevel? _optionalRiskLevel(YamlMap map, String key) {
+ final value = map[key];
+ if (value == null) return null;
+ for (final severity in RiskLevel.values) {
+ if (severity.name == value) return severity;
+ }
+ throw FormatException('$key must be one of: low, medium, high.');
+ }
+
+ static String _requiredString(YamlMap map, String key, String path) {
+ final value = map[key];
+ if (value is String && value.trim().isNotEmpty) return value;
+ throw FormatException('$path.$key must be a non-empty string.');
+ }
+
+ static void _warnUnknownKeys(YamlMap map, Set known, String context) {
+ for (final key in map.keys.map((key) => key.toString())) {
+ if (!known.contains(key)) {
+ stderr.writeln('Warning: unknown $context key "$key".');
+ }
+ }
+ }
+}
diff --git a/lib/src/config_tools.dart b/lib/src/config_tools.dart
new file mode 100644
index 0000000..5b24221
--- /dev/null
+++ b/lib/src/config_tools.dart
@@ -0,0 +1,258 @@
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import 'config_loader.dart';
+import 'file_collector.dart';
+import 'path_utils.dart';
+import 'project_resolver.dart';
+import 'rules/registry.dart';
+
+enum DoctorSeverity { info, warning, error }
+
+class DoctorMessage {
+ final DoctorSeverity severity;
+ final String message;
+
+ const DoctorMessage(this.severity, this.message);
+}
+
+class DoctorResult {
+ final String projectPath;
+ final String configPath;
+ final bool configExists;
+ final int fileCount;
+ final List messages;
+
+ const DoctorResult({
+ required this.projectPath,
+ required this.configPath,
+ required this.configExists,
+ required this.fileCount,
+ required this.messages,
+ });
+
+ bool get hasErrors =>
+ messages.any((message) => message.severity == DoctorSeverity.error);
+}
+
+class ConfigTools {
+ static String initTemplate({required bool withArchitecture}) {
+ final buffer = StringBuffer()
+ ..writeln('include:')
+ ..writeln(' - lib/**')
+ ..writeln()
+ ..writeln('exclude:')
+ ..writeln(' - lib/generated/**')
+ ..writeln(' - lib/**.g.dart')
+ ..writeln(' - lib/**.freezed.dart')
+ ..writeln(' - lib/**.mocks.dart')
+ ..writeln()
+ ..writeln('rules:');
+ for (final rule in RuleRegistry.all()) {
+ buffer
+ ..writeln(' ${rule.id}:')
+ ..writeln(' enabled: true')
+ ..writeln(' severity: ${rule.defaultSeverity.name}');
+ for (final option in rule.defaultOptions.entries) {
+ buffer.writeln(' ${option.key}: ${option.value}');
+ }
+ }
+
+ buffer
+ ..writeln()
+ ..writeln('architecture:')
+ ..writeln(' detect_cycles: false');
+ if (!withArchitecture) {
+ buffer
+ ..writeln(' layers: []')
+ ..writeln(' modules: []');
+ return buffer.toString();
+ }
+
+ buffer
+ ..writeln(' layers:')
+ ..writeln(' - name: presentation')
+ ..writeln(' path: lib/presentation/**')
+ ..writeln(' allowed_deps: [domain, core]')
+ ..writeln(' - name: domain')
+ ..writeln(' path: lib/domain/**')
+ ..writeln(' allowed_deps: [core]')
+ ..writeln(' - name: core')
+ ..writeln(' path: lib/core/**')
+ ..writeln(' allowed_deps: []')
+ ..writeln(' modules: []');
+ return buffer.toString();
+ }
+
+ static String writeInitConfig({
+ required String projectPath,
+ required String configPath,
+ required bool withArchitecture,
+ required bool force,
+ }) {
+ final resolvedProjectPath = ProjectResolver.resolveProjectPath(projectPath);
+ final outputPath = p.isAbsolute(configPath)
+ ? configPath
+ : p.join(resolvedProjectPath, configPath);
+ final file = File(outputPath);
+ if (file.existsSync() && !force) {
+ throw StateError(
+ 'Config file already exists at "$outputPath". Use --force to overwrite.',
+ );
+ }
+ file.parent.createSync(recursive: true);
+ file.writeAsStringSync(initTemplate(withArchitecture: withArchitecture));
+ return outputPath;
+ }
+
+ static String resolveConfigPathForProject({
+ required String projectPath,
+ String? configPath,
+ }) => ProjectResolver.resolveConfigPath(
+ projectPath: projectPath,
+ explicitConfig: configPath,
+ );
+
+ static DoctorResult doctor({
+ required String projectPath,
+ String? configPath,
+ }) {
+ final resolvedProjectPath = ProjectResolver.resolveProjectPath(projectPath);
+ if (!Directory(resolvedProjectPath).existsSync()) {
+ throw StateError('Project path "$resolvedProjectPath" does not exist.');
+ }
+ final resolvedConfigPath = resolveConfigPathForProject(
+ projectPath: resolvedProjectPath,
+ configPath: configPath,
+ );
+ final exists = File(resolvedConfigPath).existsSync();
+ final config = ScanConfig.fromFile(
+ resolvedConfigPath,
+ requireFile: configPath != null,
+ );
+ final files = FileCollector.collect(resolvedProjectPath, config);
+ final messages = [];
+
+ if (!exists) {
+ messages.add(
+ const DoctorMessage(
+ DoctorSeverity.info,
+ 'No config file found. Built-in defaults are being used.',
+ ),
+ );
+ }
+ if (files.isEmpty) {
+ messages.add(
+ const DoctorMessage(
+ DoctorSeverity.error,
+ 'No Dart files matched include/exclude patterns.',
+ ),
+ );
+ }
+
+ final definitions = {for (final rule in RuleRegistry.all()) rule.id: rule};
+ final knownRules = definitions.keys.toSet();
+ for (final id in config.configuredRuleIds.difference(knownRules)) {
+ messages.add(DoctorMessage(DoctorSeverity.error, 'Unknown rule "$id".'));
+ }
+ for (final id in config.configuredRuleIds.intersection(knownRules)) {
+ final knownOptions = definitions[id]!.defaultOptions.keys.toSet();
+ for (final option
+ in config
+ .configuredOptions(id)
+ .keys
+ .toSet()
+ .difference(knownOptions)) {
+ messages.add(
+ DoctorMessage(
+ DoctorSeverity.error,
+ 'Unknown option "rules.$id.$option".',
+ ),
+ );
+ }
+ }
+ _checkBoundaries(
+ kind: 'layer',
+ boundaries: config.architecture.layers,
+ files: files,
+ projectPath: resolvedProjectPath,
+ messages: messages,
+ );
+ _checkBoundaries(
+ kind: 'module',
+ boundaries: config.architecture.modules,
+ files: files,
+ projectPath: resolvedProjectPath,
+ messages: messages,
+ );
+
+ return DoctorResult(
+ projectPath: resolvedProjectPath,
+ configPath: resolvedConfigPath,
+ configExists: exists,
+ fileCount: files.length,
+ messages: messages,
+ );
+ }
+
+ static String formatDoctorResult(DoctorResult result) {
+ final buffer = StringBuffer()
+ ..writeln('FlutterGuard config check')
+ ..writeln('Project: ${result.projectPath}')
+ ..writeln('Config: ${result.configPath}')
+ ..writeln('Config exists: ${result.configExists}')
+ ..writeln('Matched Dart files: ${result.fileCount}');
+ if (result.messages.isEmpty) {
+ buffer.writeln('Status: OK');
+ return buffer.toString();
+ }
+ buffer.writeln();
+ for (final message in result.messages) {
+ buffer.writeln(
+ '${message.severity.name.toUpperCase()}: ${message.message}',
+ );
+ }
+ return buffer.toString();
+ }
+
+ static void _checkBoundaries({
+ required String kind,
+ required List boundaries,
+ required List files,
+ required String projectPath,
+ required List messages,
+ }) {
+ final names = boundaries.map((boundary) => boundary.name).toSet();
+ if (names.length != boundaries.length) {
+ messages.add(
+ DoctorMessage(
+ DoctorSeverity.error,
+ 'Duplicate architecture $kind names found.',
+ ),
+ );
+ }
+ for (final boundary in boundaries) {
+ final unknown = boundary.allowedDeps.where((dep) => !names.contains(dep));
+ for (final dependency in unknown) {
+ messages.add(
+ DoctorMessage(
+ DoctorSeverity.error,
+ 'Architecture $kind "${boundary.name}" allows unknown dependency "$dependency".',
+ ),
+ );
+ }
+ final matches = files.any(
+ (file) => matchesProjectGlob(file, boundary.path, projectPath),
+ );
+ if (!matches) {
+ messages.add(
+ DoctorMessage(
+ DoctorSeverity.warning,
+ 'Architecture $kind "${boundary.name}" path "${boundary.path}" matched no files.',
+ ),
+ );
+ }
+ }
+ }
+}
diff --git a/packages/flutterguard_cli/lib/src/file_collector.dart b/lib/src/file_collector.dart
similarity index 78%
rename from packages/flutterguard_cli/lib/src/file_collector.dart
rename to lib/src/file_collector.dart
index bbf439a..fb849b8 100644
--- a/packages/flutterguard_cli/lib/src/file_collector.dart
+++ b/lib/src/file_collector.dart
@@ -54,8 +54,9 @@ class FileCollector {
final gitRoot = (rootResult.stdout as String).trim();
final canonicalGitRoot = Directory(gitRoot).resolveSymbolicLinksSync();
- final canonicalProjectPath =
- Directory(projectPath).resolveSymbolicLinksSync();
+ final canonicalProjectPath = Directory(
+ projectPath,
+ ).resolveSymbolicLinksSync();
if (base.startsWith('-')) {
throw ChangedFilesException('Invalid Git base "$base".');
}
@@ -67,26 +68,22 @@ class FileCollector {
);
if (refResult.exitCode != 0) {
throw ChangedFilesException(
- _gitFailureMessage('Invalid Git base "$base"', refResult.stderr),
+ _gitFailureMessage(
+ 'Invalid Git base "$base"',
+ refResult.stderr.toString(),
+ ),
);
}
final verifiedRef = (refResult.stdout as String).trim();
final result = Process.runSync(
'git',
- [
- 'diff',
- '--name-only',
- '--diff-filter=ACMR',
- '-z',
- verifiedRef,
- '--',
- ],
+ ['diff', '--name-only', '--diff-filter=ACMR', '-z', verifiedRef, '--'],
workingDirectory: gitRoot,
stdoutEncoding: utf8,
);
if (result.exitCode != 0) {
throw ChangedFilesException(
- _gitFailureMessage('git diff', result.stderr),
+ _gitFailureMessage('git diff', result.stderr.toString()),
);
}
final untracked = Process.runSync(
@@ -97,29 +94,33 @@ class FileCollector {
);
if (untracked.exitCode != 0) {
throw ChangedFilesException(
- _gitFailureMessage('git ls-files', untracked.stderr),
+ _gitFailureMessage('git ls-files', untracked.stderr.toString()),
);
}
final changed = {};
for (final path in (result.stdout as String).split('\x00')) {
if (path.isNotEmpty) {
- changed.add(_projectAnchoredGitPath(
- projectPath: projectPath,
- canonicalProjectPath: canonicalProjectPath,
- canonicalGitRoot: canonicalGitRoot,
- gitRelativePath: path,
- ));
+ changed.add(
+ _projectAnchoredGitPath(
+ projectPath: projectPath,
+ canonicalProjectPath: canonicalProjectPath,
+ canonicalGitRoot: canonicalGitRoot,
+ gitRelativePath: path,
+ ),
+ );
}
}
for (final path in (untracked.stdout as String).split('\x00')) {
if (path.isNotEmpty) {
- changed.add(_projectAnchoredGitPath(
- projectPath: projectPath,
- canonicalProjectPath: canonicalProjectPath,
- canonicalGitRoot: canonicalGitRoot,
- gitRelativePath: path,
- ));
+ changed.add(
+ _projectAnchoredGitPath(
+ projectPath: projectPath,
+ canonicalProjectPath: canonicalProjectPath,
+ canonicalGitRoot: canonicalGitRoot,
+ gitRelativePath: path,
+ ),
+ );
}
}
return changed;
diff --git a/packages/flutterguard_cli/lib/src/import_graph.dart b/lib/src/import_graph.dart
similarity index 88%
rename from packages/flutterguard_cli/lib/src/import_graph.dart
rename to lib/src/import_graph.dart
index 640a374..855cd2b 100644
--- a/packages/flutterguard_cli/lib/src/import_graph.dart
+++ b/lib/src/import_graph.dart
@@ -2,7 +2,6 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'import_utils.dart';
import 'path_utils.dart';
-import 'source_utils.dart';
import 'source_workspace.dart';
class ImportEdge {
@@ -53,12 +52,14 @@ class ImportGraph {
projectPath: projectPath,
);
if (target == null || target == sourcePath) continue;
- edges.add(ImportEdge(
- source: sourcePath,
- target: target,
- uri: uri,
- line: lineNumberForOffset(source.lineInfo, directive.uri.offset),
- ));
+ edges.add(
+ ImportEdge(
+ source: sourcePath,
+ target: target,
+ uri: uri,
+ line: lineNumberForOffset(source.lineInfo, directive.uri.offset),
+ ),
+ );
}
outgoing[sourcePath] = edges;
}
diff --git a/packages/flutterguard_cli/lib/src/import_utils.dart b/lib/src/import_utils.dart
similarity index 95%
rename from packages/flutterguard_cli/lib/src/import_utils.dart
rename to lib/src/import_utils.dart
index d0cf539..999fba9 100644
--- a/packages/flutterguard_cli/lib/src/import_utils.dart
+++ b/lib/src/import_utils.dart
@@ -16,8 +16,10 @@ String? resolveImport(
};
if (importStr.startsWith('package:')) {
- final packageRelative =
- importStr.replaceFirst(RegExp(r'^package:[^/]+/'), '');
+ final packageRelative = importStr.replaceFirst(
+ RegExp(r'^package:[^/]+/'),
+ '',
+ );
return _resolvePackageImport(
packageRelative,
normalizedFiles,
diff --git a/packages/flutterguard_cli/lib/src/path_utils.dart b/lib/src/path_utils.dart
similarity index 86%
rename from packages/flutterguard_cli/lib/src/path_utils.dart
rename to lib/src/path_utils.dart
index 473ffc6..622ddb9 100644
--- a/packages/flutterguard_cli/lib/src/path_utils.dart
+++ b/lib/src/path_utils.dart
@@ -7,11 +7,7 @@ p.Context projectPathContext(String projectPath, {p.Context? context}) {
return p.Context(style: context.style, current: absoluteRoot);
}
-String normalizePath(
- String path, {
- p.Context? context,
- String? basePath,
-}) {
+String normalizePath(String path, {p.Context? context, String? basePath}) {
context ??= p.context;
if (basePath != null) {
context = projectPathContext(basePath, context: context);
@@ -29,8 +25,10 @@ bool matchesProjectGlob(
final projectContext = projectPathContext(projectPath, context: context);
final normalizedFile = normalizePath(filePath, context: projectContext);
final normalizedPattern = pattern.replaceAll('\\', '/');
- return Glob(normalizedPattern, context: projectContext)
- .matches(normalizedFile);
+ return Glob(
+ normalizedPattern,
+ context: projectContext,
+ ).matches(normalizedFile);
}
String projectRelativePath(
diff --git a/packages/flutterguard_cli/lib/src/project_resolver.dart b/lib/src/project_resolver.dart
similarity index 94%
rename from packages/flutterguard_cli/lib/src/project_resolver.dart
rename to lib/src/project_resolver.dart
index 3b0388c..29c7954 100644
--- a/packages/flutterguard_cli/lib/src/project_resolver.dart
+++ b/lib/src/project_resolver.dart
@@ -5,10 +5,7 @@ import 'package:path/path.dart' as p;
class ProjectResolver {
static const defaultConfigPath = 'flutterguard.yaml';
- static const _discoveryMarkers = [
- defaultConfigPath,
- 'pubspec.yaml',
- ];
+ static const _discoveryMarkers = [defaultConfigPath, 'pubspec.yaml'];
static String resolveProjectPath(String? explicitPath) {
if (explicitPath != null && explicitPath != '.') {
diff --git a/lib/src/report_generator.dart b/lib/src/report_generator.dart
new file mode 100644
index 0000000..5c0cee6
--- /dev/null
+++ b/lib/src/report_generator.dart
@@ -0,0 +1,179 @@
+import 'dart:convert';
+
+import 'package:path/path.dart' as p;
+
+import 'source_workspace.dart';
+import 'static_issue.dart';
+
+class _Ansi {
+ static const reset = '\x1B[0m';
+ static const red = '\x1B[31m';
+ static const yellow = '\x1B[33m';
+ static const gray = '\x1B[90m';
+ static const bold = '\x1B[1m';
+ static const dim = '\x1B[2m';
+ static const green = '\x1B[32m';
+}
+
+const _domainLabels = {
+ IssueDomain.architecture: '架构与安全',
+ IssueDomain.performance: '生命周期与性能',
+ IssueDomain.standards: '工程标准',
+};
+
+const _levelLabels = {
+ RiskLevel.high: 'HIGH',
+ RiskLevel.medium: 'MEDIUM',
+ RiskLevel.low: 'LOW',
+};
+
+class ReportGenerator {
+ static String generateJson({
+ required String projectPath,
+ required List issues,
+ required String scanMode,
+ int suppressedCount = 0,
+ int suppressedByBaselineCount = 0,
+ List diagnostics = const [],
+ }) {
+ final payload = {
+ 'schemaVersion': '2.0.0',
+ 'projectPath': projectPath,
+ 'scanMode': scanMode,
+ 'summary': {
+ 'total': issues.length,
+ 'high': issues.where((issue) => issue.level == RiskLevel.high).length,
+ 'medium': issues
+ .where((issue) => issue.level == RiskLevel.medium)
+ .length,
+ 'low': issues.where((issue) => issue.level == RiskLevel.low).length,
+ 'suppressed': suppressedCount,
+ 'suppressedByBaseline': suppressedByBaselineCount,
+ 'diagnostics': diagnostics.length,
+ 'byDomain': _summaryByDomain(issues),
+ },
+ 'issues': issues.map((issue) => issue.toJson()).toList(),
+ 'diagnostics': diagnostics.map((item) => item.toJson()).toList(),
+ };
+ return const JsonEncoder.withIndent(' ').convert(payload);
+ }
+
+ static String generateStdout({
+ required String projectPath,
+ required List issues,
+ int? scannedFileCount,
+ bool verbose = false,
+ bool noColor = false,
+ }) {
+ final buffer = StringBuffer();
+ final bold = noColor ? '' : _Ansi.bold;
+ final reset = noColor ? '' : _Ansi.reset;
+ buffer
+ ..writeln('$bold FlutterGuard Report$reset — ${p.basename(projectPath)}')
+ ..writeln(
+ ' Files: ${scannedFileCount ?? issues.map((issue) => issue.file).toSet().length} '
+ 'Issues: ${issues.length}',
+ );
+ if (issues.isEmpty) {
+ buffer.writeln(' No issues found.');
+ return buffer.toString();
+ }
+
+ for (final domain in IssueDomain.values) {
+ final domainIssues = issues
+ .where((issue) => issue.domain == domain)
+ .toList();
+ if (domainIssues.isEmpty) continue;
+ buffer
+ ..writeln()
+ ..writeln('${_domainLabels[domain]} (${domainIssues.length})');
+ domainIssues.sort(
+ (a, b) => _severityOrder(a).compareTo(_severityOrder(b)),
+ );
+ for (final issue in domainIssues) {
+ _writeIssue(
+ buffer,
+ issue,
+ projectPath,
+ verbose: verbose,
+ noColor: noColor,
+ );
+ }
+ }
+ return buffer.toString();
+ }
+
+ static bool shouldFail(List issues, String failOn) {
+ final threshold = switch (failOn) {
+ 'high' => 2,
+ 'medium' => 1,
+ 'low' => 0,
+ _ => 3,
+ };
+ return issues.any((issue) => issue.level.index >= threshold);
+ }
+
+ static void _writeIssue(
+ StringBuffer buffer,
+ StaticIssue issue,
+ String projectPath, {
+ required bool verbose,
+ required bool noColor,
+ }) {
+ final color = noColor
+ ? ''
+ : switch (issue.level) {
+ RiskLevel.high => _Ansi.red,
+ RiskLevel.medium => _Ansi.yellow,
+ RiskLevel.low => _Ansi.gray,
+ };
+ final reset = noColor ? '' : _Ansi.reset;
+ final bold = noColor ? '' : _Ansi.bold;
+ final dim = noColor ? '' : _Ansi.dim;
+ final green = noColor ? '' : _Ansi.green;
+ final path = p.isWithin(projectPath, issue.file)
+ ? p.relative(issue.file, from: projectPath)
+ : issue.file;
+ final line = issue.line == null ? '' : ':${issue.line}';
+ buffer
+ ..writeln(' $color${_levelLabels[issue.level]}$reset ${issue.id}')
+ ..writeln(' $bold${issue.title}$reset')
+ ..writeln(' $path$line — ${issue.message}');
+ if (verbose && issue.detail.isNotEmpty) {
+ for (final detail in issue.detail.split('\n')) {
+ buffer.writeln(' $dim$detail$reset');
+ }
+ }
+ if (verbose) {
+ for (final evidence in issue.evidence.take(5)) {
+ buffer.writeln(' $dim evidence: $evidence$reset');
+ }
+ }
+ buffer.writeln(' $green fix: ${issue.suggestion}$reset');
+ }
+
+ static int _severityOrder(StaticIssue issue) => switch (issue.level) {
+ RiskLevel.high => 0,
+ RiskLevel.medium => 1,
+ RiskLevel.low => 2,
+ };
+
+ static Map> _summaryByDomain(
+ List issues,
+ ) {
+ final result = >{};
+ for (final domain in IssueDomain.values) {
+ final values = issues.where((issue) => issue.domain == domain).toList();
+ if (values.isEmpty) continue;
+ result[domain.name] = {
+ 'high': values.where((issue) => issue.level == RiskLevel.high).length,
+ 'medium': values
+ .where((issue) => issue.level == RiskLevel.medium)
+ .length,
+ 'low': values.where((issue) => issue.level == RiskLevel.low).length,
+ 'total': values.length,
+ };
+ }
+ return result;
+ }
+}
diff --git a/lib/src/rules/AGENTS.md b/lib/src/rules/AGENTS.md
new file mode 100644
index 0000000..44c1e5f
--- /dev/null
+++ b/lib/src/rules/AGENTS.md
@@ -0,0 +1,20 @@
+# Rule layer
+
+Every detector returns `List`. Scanner execution is explicit in
+`RuleRegistry.registrations`; metadata and defaults live in the adjacent
+`RuleDefinition`.
+
+Ownership:
+
+- `boundary_rule.dart`: both layer and module dependency enforcement.
+- `lifecycle_resource.dart`: resource cancel/close/dispose/disconnect checks.
+- `ble_scanning.dart`: BLE scan timeout only.
+- `iot_security.dart`: credentials and insecure transport.
+- state files: generic, Riverpod, Bloc, Provider, and dependency-cycle checks.
+
+Use the supplied `SourceWorkspace`; do not read or parse files again. Respect
+`RuleConfig.enabled` and emit `config.severity`. Add special scalar defaults to
+`RuleDefinition.defaultOptions`.
+
+Do not add `describe` metadata to another catalog, restore direct public rule
+exports, or let two rule families own the same finding.
diff --git a/lib/src/rules/ble_scanning.dart b/lib/src/rules/ble_scanning.dart
new file mode 100644
index 0000000..4792210
--- /dev/null
+++ b/lib/src/rules/ble_scanning.dart
@@ -0,0 +1,178 @@
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/source/line_info.dart';
+
+import '../config_loader.dart';
+import 'rule.dart';
+import '../source_workspace.dart';
+import '../static_issue.dart';
+
+const _bleTypeNames = {'Ble', 'BluetoothDevice', 'Bluetooth'};
+
+bool _isBleType(String typeName) =>
+ _bleTypeNames.any((t) => typeName.contains(t));
+
+class BleScanningRule {
+ final RuleConfig config;
+
+ const BleScanningRule(this.config);
+
+ List analyze(List files, {SourceWorkspace? workspace}) {
+ if (!config.enabled) return [];
+
+ final issues = [];
+ final sources = workspace ?? SourceWorkspace();
+
+ for (final file in files) {
+ final source = sources.source(file);
+ if (source == null) continue;
+ issues.addAll(_checkFile(file, source.unit, source.lineInfo));
+ }
+
+ return issues;
+ }
+
+ List _checkFile(
+ String file,
+ CompilationUnit unit,
+ LineInfo lineInfo,
+ ) {
+ final issues = [];
+
+ for (final cls in unit.declarations.whereType()) {
+ final hasBleField = cls.members.whereType().any((f) {
+ final type = f.fields.type?.toSource() ?? '';
+ return _isBleType(type);
+ });
+
+ final hasBleRef = cls.members.whereType().any((m) {
+ final visitor = _BleTypeUsageVisitor();
+ m.accept(visitor);
+ return visitor.usesBleType;
+ });
+
+ if (!hasBleField && !hasBleRef) continue;
+
+ for (final method in cls.members.whereType()) {
+ if (!_isStartScan(method.name.lexeme)) continue;
+ _checkScanTimeout(file, method, lineInfo, issues);
+ }
+ }
+
+ return issues;
+ }
+
+ bool _isStartScan(String name) =>
+ name == 'startScan' || name.endsWith('StartScan');
+
+ void _checkScanTimeout(
+ String file,
+ MethodDeclaration method,
+ LineInfo lineInfo,
+ List issues,
+ ) {
+ if (_hasTimeoutParameter(method)) return;
+ if (_hasTimeoutInBody(method)) return;
+
+ final line = lineNumberForOffset(lineInfo, method.name.offset);
+ issues.add(
+ StaticIssue(
+ id: 'ble_scanning',
+ title: 'BLE 扫描缺少超时配置',
+ file: file,
+ line: line,
+ level: config.severity,
+ domain: IssueDomain.architecture,
+ message: '${method.name.lexeme}() 调用未配置超时参数',
+ detail:
+ '方法: ${method.name.lexeme}\n'
+ 'BLE 扫描应设置超时以限制扫描时间,避免过度耗电',
+ suggestion: '为 startScan() 添加明确的 timeout 或 duration 参数',
+ metadata: {'check': 'scan_without_timeout'},
+ ),
+ );
+ }
+
+ bool _hasTimeoutParameter(MethodDeclaration method) {
+ for (final param in method.parameters?.parameters ?? []) {
+ if (param is DefaultFormalParameter) {
+ final name = param.name?.lexeme ?? '';
+ if (_isTimeoutName(name)) return true;
+ }
+ }
+ return false;
+ }
+
+ bool _hasTimeoutInBody(MethodDeclaration method) {
+ final visitor = _TimeoutArgumentVisitor();
+ method.body.accept(visitor);
+ return visitor.hasTimeout;
+ }
+
+ bool _isTimeoutName(String name) {
+ const names = {
+ 'timeout',
+ 'duration',
+ 'maxScanDuration',
+ 'scanDuration',
+ 'scanTimeout',
+ };
+ return names.any(
+ (n) => name.toLowerCase().replaceAll('_', '') == n.toLowerCase(),
+ );
+ }
+
+ static RuleDefinition describe() => const RuleDefinition(
+ id: 'ble_scanning',
+ name: 'BLE 扫描管理异常',
+ domain: IssueDomain.architecture,
+ defaultSeverity: RiskLevel.medium,
+ purpose: '检测 BLE 扫描是否配置明确超时',
+ riskReason: '无限扫描会持续占用无线资源并消耗电量',
+ badExample: 'startScan() 没有 timeout 或 duration 限制',
+ fixSuggestion: '为 startScan 添加明确超时,并由资源生命周期规则检查释放',
+ );
+}
+
+class _BleTypeUsageVisitor extends RecursiveAstVisitor {
+ bool usesBleType = false;
+
+ @override
+ void visitNamedType(NamedType node) {
+ if (_isBleType(node.toSource())) {
+ usesBleType = true;
+ }
+ super.visitNamedType(node);
+ }
+
+ @override
+ void visitSimpleIdentifier(SimpleIdentifier node) {
+ if (_isBleType(node.name)) {
+ usesBleType = true;
+ }
+ super.visitSimpleIdentifier(node);
+ }
+}
+
+class _TimeoutArgumentVisitor extends RecursiveAstVisitor {
+ bool hasTimeout = false;
+
+ static const _timeoutArgNames = {
+ 'timeout',
+ 'duration',
+ 'maxScanDuration',
+ 'scanDuration',
+ 'scanTimeout',
+ };
+
+ @override
+ void visitNamedExpression(NamedExpression node) {
+ final label = node.name.label.name;
+ if (_timeoutArgNames.any(
+ (n) => label.toLowerCase().replaceAll('_', '') == n.toLowerCase(),
+ )) {
+ hasTimeout = true;
+ }
+ super.visitNamedExpression(node);
+ }
+}
diff --git a/lib/src/rules/bloc_state_management.dart b/lib/src/rules/bloc_state_management.dart
new file mode 100644
index 0000000..9ec79e3
--- /dev/null
+++ b/lib/src/rules/bloc_state_management.dart
@@ -0,0 +1,103 @@
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+
+import '../config_loader.dart';
+import 'rule.dart';
+import '../source_workspace.dart';
+import '../static_issue.dart';
+import 'state_management_utils.dart';
+
+class BlocEquatablePropsIncompleteRule {
+ final RuleConfig config;
+
+ const BlocEquatablePropsIncompleteRule(this.config);
+
+ List analyze(List files, {SourceWorkspace? workspace}) {
+ if (!stateRuleEnabled(config)) return [];
+ final sources = workspace ?? SourceWorkspace();
+ final issues = [];
+ for (final file in files) {
+ final source = sources.source(file);
+ if (source == null) continue;
+ if (!hasEquatableImport(source.unit)) {
+ continue;
+ }
+ if (!frameworkAllowed(source.unit, StateManagementFramework.bloc)) {
+ continue;
+ }
+ for (final cls
+ in source.unit.declarations.whereType()) {
+ if (!_inheritsEquatable(cls)) continue;
+ final fields = {};
+ for (final field in cls.members.whereType()) {
+ if (field.isStatic || !field.fields.isFinal) continue;
+ for (final variable in field.fields.variables) {
+ final name = variable.name.lexeme;
+ fields.add(name);
+ }
+ }
+ if (fields.isEmpty) continue;
+ final props = cls.members.whereType().where(
+ (method) => method.isGetter && method.name.lexeme == 'props',
+ );
+ final referenced = {};
+ for (final getter in props) {
+ final visitor = _IdentifierCollector();
+ getter.body.accept(visitor);
+ referenced.addAll(visitor.names);
+ }
+ final missing = fields.difference(referenced).toList()..sort();
+ if (missing.isEmpty) continue;
+ issues.add(
+ StaticIssue(
+ id: 'bloc_equatable_props_incomplete',
+ title: 'Equatable props 不完整',
+ file: file,
+ line: sourceLine(source, cls),
+ level: config.severity,
+ domain: IssueDomain.standards,
+ message: '${cls.name.lexeme}.props 缺少字段: ${missing.join(', ')}',
+ suggestion: '将所有参与值相等判断的 final instance 字段加入 props',
+ framework: StateManagementFramework.bloc,
+ evidence: missing
+ .map((field) => 'missing: $field')
+ .take(5)
+ .toList(),
+ metadata: {'className': cls.name.lexeme, 'missingFields': missing},
+ ),
+ );
+ }
+ }
+ return issues;
+ }
+
+ static bool _inheritsEquatable(ClassDeclaration cls) {
+ final superclass = cls.extendsClause?.superclass.toSource() ?? '';
+ final mixins =
+ cls.withClause?.mixinTypes.map((type) => type.toSource()).toList() ??
+ const [];
+ return superclass == 'Equatable' || mixins.contains('EquatableMixin');
+ }
+
+ static RuleDefinition describe() => const RuleDefinition(
+ id: 'bloc_equatable_props_incomplete',
+ name: 'Equatable props 不完整',
+ domain: IssueDomain.standards,
+ defaultSeverity: RiskLevel.medium,
+ purpose: '比较 Equatable 类的 final instance 字段与 props 字段引用',
+ riskReason: '遗漏字段会让不同状态被误判为相等,阻止 Bloc UI 更新',
+ badExample: 'final a; final b; List