Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/agents/trellis-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ After finding issues:
FMP is a Flutter app: "lint and typecheck" is `flutter analyze`. Read the task's `package` from `task.json` first: `legacy` verifies against `lib/AGENTS.md`, `app` against `app/AGENTS.md`. Run, in order:

1. Codegen when a model or `*.i18n.json` changed, or `*.g.dart` is missing: `dart run build_runner build`, `dart run slang`. Stale codegen fails as a missing getter that looks like a source bug.
2. Format, then analyze. `legacy`: `dart format lib test tool`, then `flutter analyze`, at the repo root. `app`: inside `app/`, `dart format --output=none --set-exit-if-changed .`, then `flutter analyze`.
2. Format, then analyze. `legacy`: `dart format lib test tool`, then `flutter analyze`, at the repo root. `app`: inside `app/`, `dart format --output=none --set-exit-if-changed .`, then `dart analyze --fatal-infos` (the only one that shows the `fmp_lints` / `riverpod_lint` plugin diagnostics) and `flutter analyze`.
3. The tests for every changed area: the matching rows of the package's `AGENTS.md` § Verification / § 驗證, plus the Quality Check section of each touched `.trellis/spec/<package>/<layer>/index.md`.

If anything fails, fix it and re-run.
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/trellis-implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Read the task's prd.md, design.md if present, and implement.md if present:
FMP is a Flutter app: "lint and typecheck" is `flutter analyze`. Read the task's `package` from `task.json` first: `legacy` verifies against `lib/AGENTS.md` § Verification, `app` against `app/AGENTS.md` § 驗證.

1. Codegen when a model or `*.i18n.json` changed, or `*.g.dart` is missing: `dart run build_runner build`, `dart run slang`.
2. Format, then analyze. `legacy`: `dart format lib test tool`, then `flutter analyze`, at the repo root. `app`: inside `app/`, `dart format --output=none --set-exit-if-changed .`, then `flutter analyze`.
2. Format, then analyze. `legacy`: `dart format lib test tool`, then `flutter analyze`, at the repo root. `app`: inside `app/`, `dart format --output=none --set-exit-if-changed .`, then `dart analyze --fatal-infos` (the only one that shows the `fmp_lints` / `riverpod_lint` plugin diagnostics) and `flutter analyze`.
3. The tests named by the matching rows of the package's § Verification / § 驗證, plus the tests you wrote.

If the change is user-visible, say so in the report: on-device verification is the main session's job (`verify-legacy-on-device` skill for `legacy`, `verify-on-device` for `app`).
Expand Down
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ jobs:
needs: changes
if: needs.changes.outputs.app == 'true' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 15
# 兩次 dart analyze(含第一次編譯插件)比 PR 2 多幾分鐘。
timeout-minutes: 20
defaults:
run:
working-directory: app
Expand All @@ -226,9 +227,27 @@ jobs:
- name: Check formatting
run: dart format --output=none --set-exit-if-changed .

# fmp_lints 與 riverpod_lint 是 analyzer 插件,只有 dart analyze 看得到
# 它們的診斷(flutter/flutter#187999)。第一次會解析並編譯插件,要連 pub。
- name: Plugin lints (dart analyze)
run: dart analyze --fatal-infos

# 暫放違反每條 fmp 規則的檔案,斷言 dart analyze 失敗且報出每條規則名:
# 插件沒載入或規則沒開時,上一步會照樣綠。
- name: Lint wiring sentinel
run: dart run tool/lint_sentinel.dart

# 留著:Flutter 專屬的診斷只在這裡。它不顯示插件診斷,見上。
- name: Static analysis
run: flutter analyze

# 第二次以 Windows 路徑重跑,驗證規則的允許清單不受分隔符影響。
- name: Lint rule tests
working-directory: app/packages/fmp_lints
run: |
dart test
TEST_ANALYZER_WINDOWS_PATHS=true dart test

# 不加參數:dart_test.yaml 讓 live 預設跳過(ADR 0015 §決定 3)。
# 身分測試要 cmake,ubuntu runner 內建。
- name: Unit and widget tests
Expand Down
51 changes: 51 additions & 0 deletions .trellis/spec/app/lints/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Lint 規則(`app/packages/fmp_lints/`)

改或加 `fmp_` 規則時適用。每條規則守什麼、允許清單在哪,見 `app/AGENTS.md` § Lint;
為什麼是這些規則,見 ADR 0015 §決定 2。這裡只寫怎麼寫。

## 一條規則的形狀

- 一條規則一個檔案:`lib/src/rules/<規則名去掉 fmp_>.dart`,裡面是 `AnalysisRule` 子類別加
`SimpleAstVisitor`。
- `LintCode` 放成 `static const`(唯一實例,`// ignore:` 才對得上),名稱 `fmp_…`,
`severity: DiagnosticSeverity.WARNING`。訊息用英文,比照 log 字串。
- 允許清單、套件清單寫成同一個檔案頂端的具名常數,路徑一律相對 package 根、以 `/`
分隔、不帶結尾斜線(`lib/ui/toast`)。
- 判斷檔案位置只用 `PackagePath.of(context)`(`lib/src/package_path.dart`)與它的
`isIn`/`isInLib`/`isInTest`,不自己處理分隔符。
- 名稱比對帶 import 前綴的寫法(`m.ScaffoldMessenger`)用 `lib/src/ast_names.dart` 的
`isNamedReference`。
- 在 `lib/main.dart` 的 `fmpRules()` 登記,並在 `app/analysis_options.yaml` 的
`diagnostics:` 開啟;`test/plugin_test.dart` 會比對兩邊。

## 測試(雙向變異)

每條規則一個 `test/rules/<規則>_test.dart`,繼承 `test/support/rule_test_base.dart` 的
`FmpRuleTest`:

```dart
Future<void> test_rawValuesInUi() => assertLints('lib/ui/search/page.dart', '''
const a = EdgeInsets.all([!8!]);
''');
```

- `[!…!]` 標出預期的診斷範圍;沒有標記就是斷言不報。只比對受測規則的診斷,未解析的
名稱等編譯錯誤不參與。
- 至少一個「報」的案例,和至少一個相鄰但不該報的案例:允許目錄內、改名、改格式,或
在註解與字串裡提到同樣的字。
- 依賴解析結果的寫法(`Dio()` 要解析成建構子呼叫)在 `addStubPackages` 裡用
`newPackage(...)` 造最小的假套件。假宣告要照真套件的形狀:Flutter 的 `debugPrint` 是
函式型別的頂層變數,呼叫解析成 `FunctionExpressionInvocation` 而不是
`MethodInvocation`;名稱沒解析到時兩者都是 `MethodInvocation`,測試會綠、實際卻漏報。
- 本機在 Windows 上跑的是 Windows 路徑;CI 另外以 `TEST_ANALYZER_WINDOWS_PATHS=true` 再跑一次。

## 哨兵

在 `app/tool/lint_sentinel.dart` 的 `_violations` 加一行違反新規則的程式碼。沒加的話,
哨兵會因為「開啟的規則沒被報」而失敗。

## Quality Check

- `packages/fmp_lints/` 內 `dart test` 全綠。
- `app/` 內 `dart analyze --fatal-infos` 乾淨,`dart run tool/lint_sentinel.dart` 通過。
- `app/AGENTS.md` § Lint 的表格有新規則的一列。
3 changes: 2 additions & 1 deletion .trellis/tasks/09-28-m1-skeleton-tracer/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ app/
lib/
main.dart # 由 flavor 決定身分;組 ProviderScope(retry 關閉)
app/ # MaterialApp、路由、外殼、ToastHost
core/ # logging/、redaction/、errors/、network/、settings/
core/ # logging/、redaction/、errors/、network/、endpoints.dart;不 import data/ 以上
settings/ # 各組設定的 Notifier(讀 data/ 的 repository)
data/ # drift database、tables、repositories
domain/ # TrackKey 等純型別
platform/ # 每能力一目錄:<能力>.dart+<能力>_<平台>.dart
Expand Down
7 changes: 6 additions & 1 deletion .trellis/tasks/09-28-m1-skeleton-tracer/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@

### 6. 設定與 log

- [ ] 設定的 Notifier 模式(外觀組)。
- [ ] 設定的 Notifier 模式(外觀組),放在 `lib/settings/`。
- [ ] 第一次用 Riverpod:`main.dart` 包 `ProviderScope`,並在 `app/analysis_options.yaml` 重新開啟 `riverpod_lint` 的 `missing_provider_scope`(PR 3 暫時關閉)。
- [ ] log 門面、遮蔽函式;log 檔:JSON Lines、2MB×3、在 `logs/`。
- 測試:遮蔽,含 stackTrace(ADR 0011 §如何確認);檔案輪替;壞行略過。

Expand Down Expand Up @@ -201,6 +202,10 @@
- [ ] 更新 `milestones.md` 的 M1 狀態與勾選;開 Linux 平台任務。
- [ ] 本任務 `finish`、`archive`。

## 待升級

- `analysis_server_plugin`、`analyzer`、`analyzer_testing` 停在 0.3.18/13.3.0/0.3.2:Flutter 3.47.5 的 `flutter_test` 釘 `test_api 0.7.12`,把 analyzer 限制在 14 以下(PR 3 發現)。Flutter 放寬後三個一起升到最新。

## 風險與回滾點

| 風險 | 處理 |
Expand Down
3 changes: 2 additions & 1 deletion .trellis/tasks/09-28-m1-skeleton-tracer/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"subtasks": [],
"children": [
"09-29-split-agent-instructions",
"09-29-app-skeleton"
"09-29-app-skeleton",
"09-29-fmp-lints"
],
"parent": "09-26-fmp-rewrite",
"relatedFiles": [],
Expand Down
4 changes: 4 additions & 0 deletions .trellis/tasks/archive/2026-09/09-29-fmp-lints/check.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"file": ".trellis/tasks/09-28-m1-skeleton-tracer/design.md", "reason": "Directory layout the allow-lists refer to"}
{"file": ".trellis/tasks/09-28-m1-skeleton-tracer/research/m1-tooling-facts.md", "reason": "analysis_server_plugin, analyzer_testing and riverpod_lint facts"}
{"file": "docs/adr/0015-testing-gates-and-dev-environment.md", "reason": "Lint rule table and gates"}
{"file": ".trellis/spec/app/testing/index.md", "reason": "Existing app test conventions"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"file": ".trellis/tasks/09-28-m1-skeleton-tracer/design.md", "reason": "Directory layout the allow-lists refer to"}
{"file": ".trellis/tasks/09-28-m1-skeleton-tracer/research/m1-tooling-facts.md", "reason": "analysis_server_plugin, analyzer_testing and riverpod_lint facts"}
{"file": "docs/adr/0015-testing-gates-and-dev-environment.md", "reason": "Lint rule table and gates"}
{"file": ".trellis/spec/app/testing/index.md", "reason": "Existing app test conventions"}
91 changes: 91 additions & 0 deletions .trellis/tasks/archive/2026-09/09-29-fmp-lints/prd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# fmp_lints 與接線哨兵(M1 PR 3)

父任務:`../09-28-m1-skeleton-tracer`(design §2、§3「M1 的 lint」;implement「3.」)。

依據:
- ADR 0015 §決定 2(規則表、`analyzer_testing`、接線哨兵);
- 各 ADR 的「如何確認」:0008、0009、0010、0016、0018、0020、0021、0022 的 `fmp_layer_imports`;0023 `fmp_toast_entry`;0024 `fmp_design_tokens`。

## 做什麼

1. **套件**:
- `app/packages/fmp_lints/`,用官方 `analysis_server_plugin`(目前 stable,版本以 pub.dev 為準,`research/m1-tooling-facts.md` 記 0.3.23)。
- 加進 `app/pubspec.yaml` 的 `workspace:`。
- `app/analysis_options.yaml` 以頂層 `plugins:` 接上 `fmp_lints`,並在 `diagnostics:` 逐條開啟。
- 寫法先以 context7 或官方文件查證,把來源記在 `research/notes.md`。
2. **規則**:12 條。
- ADR 0015 的十條核心規則,加上 `fmp_toast_entry`、`fmp_design_tokens`;`fmp_periodic_timer_owner` 留到 M2。
- 允許清單用 `app/lib/` 的相對路徑,照父任務 design §2 的目錄:

| 規則 | M1 的允許清單 |
|---|---|
| `fmp_layer_imports` | 依賴方向表見下 |
| `fmp_no_empty_catch` | 無豁免;catch 本體沒有陳述式就違規,變數名 `_` 不豁免 |
| `fmp_log_facade` | `print`、`debugPrint`、`dart:developer` 的 `log`、`package:talker*` 只准在 `lib/core/logging/` |
| `fmp_source_id_literal` | 官方插件 id 字串(M1 只有 `bilibili`;清單集中在規則內一處,M3 加 `youtube`、`netease`)只准在 `lib/legacy_import/` 與 `test/` |
| `fmp_url_literal` | `http://`、`https://` 字面值只准在一個端點檔 `lib/core/endpoints.dart`(本 PR 不建,第一個需要網址的 PR 建) |
| `fmp_no_for_testing` | `lib/` 不得宣告名稱以 `ForTesting` 結尾的成員 |
| `fmp_http_client_owner` | `Dio(` 只准在 `lib/core/network/` |
| `fmp_test_waits` | `test/` 內直接呼叫 `pumpEventQueue` 只准在 `test/support/pump_until.dart` |
| `fmp_ignore_reason` | `// ignore: fmp_…` 與 `// ignore_for_file: fmp_…` 同一行必須寫理由(規則名之後有 ` — ` 或 ` - ` 加文字) |
| `fmp_platform_checks` | `Platform.isXxx`、`Platform.operatingSystem`、`defaultTargetPlatform`、`TargetPlatform` 只准在 `lib/platform/` |
| `fmp_toast_entry` | `SnackBar(`、`ScaffoldMessenger.of`/`.maybeOf`、`showSnackBar`、`clearSnackBars` 只准在 `lib/ui/toast/` |
| `fmp_design_tokens` | `lib/ui/`(`lib/ui/theme/` 除外):`EdgeInsets.*`、`EdgeInsetsDirectional.*`、`SizedBox` 的寬高與 `SizedBox.fromSize` 的 `Size(`、`BorderRadius.circular`/`.all`、`BorderRadiusDirectional.*`、`Radius.circular`/`.elliptical`(含巢狀)、`fontSize:` 不得用數字字面值(`0` 除外);`Color(…)`、`Color.fromARGB`/`fromRGBO`/`from` 不得有數字字面值(含 `0`);不得寫 `Colors.*` |

**`fmp_layer_imports` 的依賴方向表**:
- `app/` 不得 import 根目錄舊專案,也就是以相對路徑跳出 `app/`,或 `package:fmp/` 指向舊專案。`app/` 的 package 名也是 `fmp`,所以只能用路徑判斷。
- `lib/legacy_import/` 不被其他目錄 import;`package:isar_community*` 只准在 `lib/legacy_import/`。
- `package:drift*`、`package:sqlite3*` 只准在 `lib/data/`。
- 平台套件只准在 `lib/platform/`:
- `path_provider`、`window_manager`、`tray_manager`、`hotkey_manager`、`launch_at_startup`;
- `desktop_multi_window`、`flutter_overlay_window`;
- `permission_handler`、`smtc_windows`、`audio_service`、`audio_service_mpris`;
- `connectivity_plus`、`file_picker`、`package_info_plus`;
- `flutter_inappwebview`、`flutter_secure_storage`。

清單集中一處,之後的 ADR 再加。
- `package:just_audio*`、`package:media_kit*` 只准在 `lib/playback/backends/`。
- `package:dio*` 只准在 `lib/core/network/`。
- `package:flutter_js*` 只准在 `lib/plugins/runtime/`。
- `package:background_downloader*` 只准在 `lib/downloads/`(M6 才有,先列入)。
- `lib/core/` 與 `lib/domain/` 不 import `lib/ui/`、`lib/playback/`、`lib/plugins/`、`lib/data/`、`lib/settings/`。設定的 Notifier 讀資料層,所以放在 `lib/settings/`,不在 `core/`(本 PR 同步修正父任務 design §2)。
- `lib/data/` 不 import `lib/ui/`。
- ADR 0018 的細部規則(結束原因型別、串流窄介面)在 PR 10 加。
3. **`material_ui` 的閘門**:
- PR 2 的 `test/static_rules/material_import_static_rule_test.dart` 改成規則 `fmp_material_import`:`lib/` 不得 import `package:flutter/material.dart` 與 `package:flutter/cupertino.dart`。
- 刪掉那個測試。
- 在 ADR 0015 的「後續 ADR 新增的規則」補一句,說明它守的是 ADR 0024 §決定 1 的 import 路徑。
4. **`riverpod_lint`**:
- 若已支援新插件系統(研究說 3.1.9 依賴 `analysis_server_plugin ^0.3.0`),一起接上 `plugins:`;
- 不行就記在 notes,留到第一個用 Riverpod 的 PR。
5. **測試**:
- 每條規則用 `analyzer_testing` 做雙向變異:
- 至少一個違規案例會報;
- 至少一個相鄰、無關的寫法不報:允許目錄內、重新命名、改格式、註解或字串裡提到規則字樣。
- 在 `app/packages/fmp_lints/` 內 `dart test`。
6. **接線哨兵**:一支可在 CI 與本機跑的 Dart 腳本 `app/tool/lint_sentinel.dart`。
- 在 `app/lib/` 暫放一個違反每條規則的檔案,跑 `dart analyze --fatal-infos`;
- 斷言失敗,而且輸出含每一條規則名;
- 最後刪掉暫放檔,失敗時也要刪。
7. **CI**:`app` job 加三步:
- `dart analyze --fatal-infos`(在 `app/`);
- 哨兵;
- `fmp_lints` 的測試。

`flutter analyze` 留著,因為它有 Flutter 專屬的診斷。在註解裡寫明 flutter/flutter#187999。
8. **文件**:
- `app/AGENTS.md` 加 lint 段,列每條規則名、它守什麼、允許清單在哪裡改(ADR 0015 §如何確認:列出的每條規則寫對應規則名);
- 原本由 static-rule 測試守的 `material_ui` 那句改指規則名;
- 建 `.trellis/spec/app/lints/index.md`(繁中),寫新規則怎麼加(雙向變異、哨兵、AGENTS.md);
- 不建 `spec/app/index.md`。
- `trellis-check.md`、`trellis-implement.md` 的 `app` 分支加 `dart analyze --fatal-infos`。

## 驗收

- [ ] `app/`:`dart analyze --fatal-infos` 零問題;`flutter analyze`、`flutter test` 通過。
- [ ] `app/packages/fmp_lints/`:`dart test` 全綠,12+1 條規則各有報與不報的案例。
- [ ] 哨兵在本機會紅,輸出含 13 個規則名;暫放檔在結束後不存在。
- [ ] 實測(§7):
- 同一個違規檔,`dart analyze` 報出插件診斷,`flutter analyze` 看不到;
- 把兩者的輸出寫進 PR 描述。
- [ ] CI 的 `app` job 綠。
Loading
Loading