From d0950ef450f6d0d39364ffbb258c0043648ea0df Mon Sep 17 00:00:00 2001 From: lizy Date: Sun, 12 Jul 2026 20:54:47 +0800 Subject: [PATCH] refactor: converge app architecture and agent docs --- AGENTS.md | 17 +- AI_ANALYSIS.md | 6 +- AI_ANALYSIS_SCHEMA.json | 13 +- AI_PROJECT_CONTEXT.md | 219 +++++++------ REFACTOR_PLAN.md | 259 +++++++--------- lib/AI_ANALYSIS.md | 5 +- lib/AI_MODULE_INDEX.md | 289 ++++++++++++++---- lib/app/AI_ANALYSIS.md | 15 +- lib/app/app_bootstrap.dart | 25 ++ lib/app/category_navigation.dart | 36 +++ lib/app/category_window_app.dart | 78 +++++ lib/app/module_home_page.dart | 149 +++++++++ lib/app/router/AI_ANALYSIS.md | 7 +- lib/app/router/app_route_table.dart | 179 +---------- lib/main.dart | 37 +-- lib/module_registry/AI_ANALYSIS.md | 14 +- .../module_catalog_utils.dart} | 14 +- lib/modules/AI_ANALYSIS.md | 4 +- lib/modules/async/AI_ANALYSIS.md | 4 +- .../async/isolate_basic/AI_ANALYSIS.md | 4 +- .../async/isolate_task_manager/AI_ANALYSIS.md | 4 +- .../async/stream_subscription/AI_ANALYSIS.md | 4 +- lib/modules/basic/AI_ANALYSIS.md | 4 +- .../basic/debounce_throttle/AI_ANALYSIS.md | 4 +- lib/modules/basic/microtask/AI_ANALYSIS.md | 4 +- lib/modules/basic/tree_state/AI_ANALYSIS.md | 4 +- lib/modules/platform/AI_ANALYSIS.md | 4 +- .../platform/dio_interceptor/AI_ANALYSIS.md | 4 +- .../platform/usb_detector/AI_ANALYSIS.md | 4 +- lib/modules/popup_table/AI_ANALYSIS.md | 4 +- .../overlay_follow_compare/AI_ANALYSIS.md | 4 +- .../popup_list_interaction/AI_ANALYSIS.md | 4 +- .../popup_table/popup_widgets/AI_ANALYSIS.md | 4 +- .../popup_table/scroll_table/AI_ANALYSIS.md | 4 +- lib/modules/state/AI_ANALYSIS.md | 4 +- lib/modules/state/flutter_ioc/AI_ANALYSIS.md | 4 +- .../state/status_management/AI_ANALYSIS.md | 4 +- lib/modules/ui/AI_ANALYSIS.md | 4 +- lib/modules/ui/adsorption_line/AI_ANALYSIS.md | 4 +- .../ui/download_animation/AI_ANALYSIS.md | 4 +- .../ui/gcode_visualizer/AI_ANALYSIS.md | 4 +- lib/shared/AI_ANALYSIS.md | 6 +- lib/shared/multi_window/AI_ANALYSIS.md | 12 +- .../multi_window/category_window_app.dart | 142 --------- lib/shared/platform/AI_ANALYSIS.md | 4 +- test/shared/module_catalog_utils_test.dart | 59 ++++ tool/generate_agent_indexes.js | 209 +++++++++++-- tool/generate_harness_ai_analysis.sh | 1 + tool/validate_agent_docs.js | 115 +++++++ 49 files changed, 1243 insertions(+), 763 deletions(-) create mode 100644 lib/app/app_bootstrap.dart create mode 100644 lib/app/category_navigation.dart create mode 100644 lib/app/category_window_app.dart create mode 100644 lib/app/module_home_page.dart rename lib/{shared/multi_window/multi_window_route_filter.dart => module_registry/module_catalog_utils.dart} (67%) delete mode 100644 lib/shared/multi_window/category_window_app.dart create mode 100644 test/shared/module_catalog_utils_test.dart create mode 100644 tool/validate_agent_docs.js diff --git a/AGENTS.md b/AGENTS.md index 9d9f6e5..6b09b0a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,9 +5,12 @@ ## 前置阅读 执行任何修改前,agent 必须读取: -1. `AI_PROJECT_CONTEXT.md` - 项目整体上下文 -2. `REFACTOR_PLAN.md` - 整改计划与优先级 -3. 目标模块的 `AI_ANALYSIS.md` - 模块结构与修改建议 +1. `AI_ANALYSIS_SCHEMA.json` - agent 文档 schema +2. `AI_PROJECT_CONTEXT.md` - 机器可解析项目契约 +3. `REFACTOR_PLAN.md` - 机器可解析任务队列 +4. 目标模块的 `AI_ANALYSIS.md` - 机器可解析模块契约 + +以上 agent 文档均为 JSON,禁止加入 Markdown、自然语言段落或手工文件清单。 ## 新增模块规则 @@ -16,7 +19,7 @@ | 必需项 | 说明 | |--------|------| | `module_entry.dart` | 导出 `*Entry` Widget,作为模块入口 | -| `AI_ANALYSIS.md` | 模块分析文档:功能、文件结构、数据流、关键类、修改建议 | +| `AI_ANALYSIS.md` | 模块机器契约:route、category、status、entrypoints、owns、depends、analysis_parent、validation | | 路由注册 | 在 `lib/router/app_route_table.dart` 的 `_modules` 中注册 | | 模块元数据 | `ModuleEntry` 必须填写 `category`、`difficulty`、`concepts`、`estimatedMinutes`、`status`、`subtitle` | | 教学页面 | 至少 1 个页面使用外部 `flutter_study_learning` 包中的教学模板组件(`LearningScaffold` 等) | @@ -24,13 +27,15 @@ ## 修改模块规则 1. 修改前先读取该模块的 `AI_ANALYSIS.md` -2. 修改后同步更新 `AI_ANALYSIS.md` 中的文件结构和关键类信息 -3. 如果修改了路由注册,同步更新元数据字段 +2. 修改模块、依赖、路由或层级时,更新 `tool/generate_agent_indexes.js` 中的生成源 +3. 执行 `bash tool/generate_harness_ai_analysis.sh` 重新生成并校验 agent 文档 +4. 如果修改了路由注册,同步更新元数据字段 ## 验收规则 每次代码修改后 **必须** 执行: ```bash +bash tool/generate_harness_ai_analysis.sh dart format . flutter analyze dart run flutterguard_cli:flutterguard scan --path . --fail-on high diff --git a/AI_ANALYSIS.md b/AI_ANALYSIS.md index 2454def..49750d5 100644 --- a/AI_ANALYSIS.md +++ b/AI_ANALYSIS.md @@ -10,6 +10,7 @@ }, "entrypoints": [ "lib/main.dart", + "lib/app/app_bootstrap.dart", "lib/app/app.dart", "lib/app/router/app_route_table.dart" ], @@ -38,12 +39,13 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, "validation": [ + "bash tool/generate_harness_ai_analysis.sh", "dart format .", "flutter analyze", "dart run flutterguard_cli:flutterguard scan --path . --fail-on high" diff --git a/AI_ANALYSIS_SCHEMA.json b/AI_ANALYSIS_SCHEMA.json index f78705c..91c3ea0 100644 --- a/AI_ANALYSIS_SCHEMA.json +++ b/AI_ANALYSIS_SCHEMA.json @@ -1,8 +1,15 @@ { - "schema": "vibecoding.harness.ai_analysis_schema.v1", + "schema": "flutter_study.agent_docs.schema.v2", "syntax": "json_config", "prose": "forbidden", "markdown": "forbidden", + "generated_by": "tool/generate_agent_indexes.js", + "documents": { + "project_context": "AI_PROJECT_CONTEXT.md", + "refactor_plan": "REFACTOR_PLAN.md", + "module_index": "lib/AI_MODULE_INDEX.md", + "analysis_glob": "**/AI_ANALYSIS.md" + }, "levels": { "workspace": [ "AI_ANALYSIS.md" @@ -51,8 +58,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness" + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract" }, "module_contract_policy": { "keep_for_module_rule": true, diff --git a/AI_PROJECT_CONTEXT.md b/AI_PROJECT_CONTEXT.md index 582b2e8..3d14bf1 100644 --- a/AI_PROJECT_CONTEXT.md +++ b/AI_PROJECT_CONTEXT.md @@ -1,92 +1,127 @@ -# AI 项目上下文 - -> 此文件为 AI 编程助手提供项目整体上下文,修改代码前请先阅读。 - -## 项目概述 - -- **名称**: main_app -- **类型**: Flutter 学习项目集合,单应用多模块架构 -- **支持平台**: macOS, Windows, iOS, Android -- **技术栈**: Flutter 3.x / Dart 3.x - -## 架构模式 - -- **路由**: go_router 统一管理,首页为模块列表,点击后 push 到对应模块 -- **状态管理**: 项目实验多种方案(Provider, Riverpod, Bloc, ChangeNotifier, 自研 IoC) -- **模块组织**: 每个功能模块位于 `lib/modules///` 下,通过 `module_entry.dart` 暴露入口 Widget - -## 关键约定 - -1. **模块入口**: 每个模块必须有 `module_entry.dart`,导出名为 `*Entry` 的 Widget -2. **路由注册**: 新模块需在 `lib/app/router/app_route_table.dart` 的 `_modules` 列表中注册 -3. **依赖**: 所有依赖在根 `pubspec.yaml` 中声明,模块间不共享独立依赖 -4. **命名**: 模块目录使用 snake_case,路由路径使用 kebab-case - -## 目录结构 - -``` -lib/ -├── main.dart # 应用入口,ProviderScope 包裹 -├── app/ -│ ├── app.dart # MaterialApp.router 配置 -│ └── router/ # go_router 路由配置 -│ ├── app_router.dart -│ ├── app_route_table.dart # 路由表 + 模块列表 + 首页 UI -│ └── AI_ANALYSIS.md -├── module_registry/ # 模块元数据定义 -│ ├── module_entry.dart -│ └── module_category.dart -├── shared/ # 共享能力 -│ ├── multi_window/ # 多窗口能力封装 -│ └── platform/ # 平台通道与系统能力封装 -├── modules/ # 学习模块分区 -│ ├── basic/ # 基础机制 -│ ├── async/ # 异步并发 -│ ├── state/ # 状态管理 -│ ├── ui/ # UI 与动效 -│ ├── popup_table/ # 弹窗与列表 -│ └── platform/ # 网络与平台 -``` - -教学模板组件由外部包 `flutter_study_learning` 提供(`LearningScaffold`、`LearningObjectives`、`ConceptChips`、`CodeSnippetCard`、`CommonPitfalls`、`ExerciseCard`、`StateLogView`)。 - -## 模块内部结构(推荐) - -``` -modules/// -├── module_entry.dart # 模块入口 Widget -├── module_routes.dart # 子路由定义(有子路由时才需要) -├── AI_ANALYSIS.md # 模块分析文档 -├── presentation/ # UI 层(pages/ + widgets/) -├── application/ # 应用层(state/ + services/) -├── domain/ # 领域层(models/) -└── data/ # 数据层(api/ + parser/ + mock/) -``` - -简单模块可以保留精简版: -``` -modules/basic/debounce_throttle/ -├── module_entry.dart -├── module_root.dart -├── AI_ANALYSIS.md -└── utils/ -``` - -## 添加新模块步骤 - -1. 在 `lib/modules//` 下创建模块目录 `lib/modules//my_module/` -2. 创建 `module_entry.dart`,导出 `MyModuleEntry` Widget -3. 在 `lib/app/router/app_route_table.dart` 中: - - import 模块入口 - - 在 `_modules` 列表添加 `ModuleEntry` -4. 如需子路由,在模块内定义 `List` 并在 `ModuleEntry.routes` 中传入 - -## 常用命令 - -```bash -flutter pub get # 安装依赖 -flutter run -d macos # 运行 macOS 版本 -flutter analyze # 代码检查 -dart format . # 格式化 -flutter build macos # 构建 macOS 版本 -``` +{ + "schema": "flutter_study.agent_docs.project_context.v1", + "consumer": "coding_agent", + "package": { + "name": "main_app", + "type": "flutter_modular_learning_app", + "sdk": [ + "flutter_3", + "dart_3" + ] + }, + "platform": { + "current_hosts": [ + "macos", + "windows" + ], + "next_host": "android", + "target_hosts": [ + "android", + "ios", + "macos", + "windows" + ] + }, + "entrypoints": { + "process": "lib/main.dart", + "bootstrap": "lib/app/app_bootstrap.dart", + "app": "lib/app/app.dart", + "router": "lib/app/router/app_router.dart", + "route_table": "lib/app/router/app_route_table.dart" + }, + "layers": [ + { + "id": "app", + "path": "lib/app", + "owns": [ + "host_bootstrap", + "app_shell", + "navigation_policy", + "route_composition" + ], + "may_depend_on": [ + "module_registry", + "shared", + "modules" + ] + }, + { + "id": "module_registry", + "path": "lib/module_registry", + "owns": [ + "module_metadata", + "catalog_operations" + ], + "may_depend_on": [ + "flutter", + "go_router" + ] + }, + { + "id": "shared", + "path": "lib/shared", + "owns": [ + "business_neutral_capabilities", + "platform_boundaries" + ], + "forbidden_dependencies": [ + "app", + "modules" + ] + }, + { + "id": "modules", + "path": "lib/modules/{category}/{module}", + "owns": [ + "learning_ui", + "module_state", + "module_domain", + "module_data" + ], + "forbidden_dependencies": [ + "other_modules" + ] + } + ], + "module_contract": { + "required_files": [ + "module_entry.dart", + "AI_ANALYSIS.md" + ], + "required_registration": "lib/app/router/app_route_table.dart", + "required_metadata": [ + "category", + "difficulty", + "concepts", + "estimatedMinutes", + "status", + "subtitle" + ], + "required_learning_dependency": "flutter_study_learning", + "route_path_style": "kebab_case", + "directory_style": "snake_case" + }, + "platform_rules": { + "router_platform_api": "forbidden", + "module_host_navigation": "forbidden", + "desktop_window_policy": "lib/app/category_navigation.dart", + "platform_capability_contract": "business_neutral_interface" + }, + "change_protocol": { + "pre_read": [ + "AI_PROJECT_CONTEXT.md", + "REFACTOR_PLAN.md", + "{target}/AI_ANALYSIS.md" + ], + "update_source": [ + "tool/generate_agent_indexes.js" + ], + "generate": "bash tool/generate_harness_ai_analysis.sh", + "validate": [ + "bash tool/generate_harness_ai_analysis.sh", + "dart format .", + "flutter analyze", + "dart run flutterguard_cli:flutterguard scan --path . --fail-on high" + ] + } +} diff --git a/REFACTOR_PLAN.md b/REFACTOR_PLAN.md index 1f0ba41..73c11cb 100644 --- a/REFACTOR_PLAN.md +++ b/REFACTOR_PLAN.md @@ -1,158 +1,103 @@ -# 整改计划 - -> 项目重构与代码质量提升计划。优先级从高到低排列。 - -## Phase 1 — 目录结构重构 ✅ - -**目标**: 从扁平 `lib//` 改为 `app/ + module_registry/ + shared/ + modules//` 层级。 - -**已完成**: -- [x] `app.dart` → `app/app.dart` -- [x] `router/` → `app/router/` -- [x] 新建 `module_registry/`,拆分 ModuleEntry 模型和枚举 -- [x] 按 basic/async/state/ui/platform 分类迁移所有 15 个模块 -- [x] 清理目录名(去掉 `_demo`/`_test` 后缀) -- [x] 更新所有 import 路径和文档 - -## Phase 2 — 共享能力归拢 ⏳ - -**目标**: 将已经暴露出跨模块价值的能力从具体学习模块中抽出,先归拢到 `lib/shared/`,等复用边界稳定后再考虑独立 Flutter plugin/package。 - -### 2.1 文件选择能力抽离 - -**背景**: `gcode_visualizer` 已接入 macOS 原生 `NSOpenPanel`,当前实现位于模块内,具备跨模块复用价值,但尚未证明需要独立发布。 - -**目标结构**: - -``` -lib/shared/platform/file_picker/ -├── file_picker_service.dart # 业务无关接口: PickedFile / FilePickerService -├── method_channel_file_picker.dart # MethodChannel 实现 -└── AI_ANALYSIS.md # shared 能力分析文档 -``` - -**Dart API 草案**: - -```dart -class PickedFile { - const PickedFile({ - required this.path, - this.name, - }); - - final String path; - final String? name; +{ + "schema": "flutter_study.agent_docs.refactor_plan.v1", + "objective": "android_readiness_after_architecture_convergence", + "active_phase": "mobile_preintegration", + "completed_milestones": [ + "directory_layers", + "shared_package_extraction", + "module_analysis_coverage", + "app_navigation_boundary", + "host_bootstrap_boundary" + ], + "work_queue": [ + { + "id": "module_platform_contract", + "priority": 1, + "status": "pending", + "changes": [ + "ModuleEntry.platform_support", + "ModuleHomePage.availability_state" + ], + "acceptance": [ + "catalog_platform_metadata_complete", + "unsupported_module_state_visible" + ] + }, + { + "id": "platform_plugin_audit", + "priority": 2, + "status": "pending", + "targets": [ + "desktop_multi_window", + "file_picker_bridge", + "usb_serial", + "device_info_plus" + ], + "acceptance": [ + "android_support_matrix", + "unsupported_fallbacks" + ] + }, + { + "id": "usb_platform_boundary", + "priority": 3, + "status": "pending", + "targets": [ + "lib/modules/platform/usb_detector" + ], + "acceptance": [ + "no_windows_hardcode", + "android_system_info", + "error_branch_test" + ] + }, + { + "id": "mobile_layout_baseline", + "priority": 4, + "status": "pending", + "viewport_width_dp": 360, + "targets": [ + "module_home", + "category_home", + "ready_modules", + "recommended_modules" + ], + "acceptance": [ + "no_overflow", + "safe_area", + "keyboard_avoidance", + "touch_targets" + ] + }, + { + "id": "android_host", + "priority": 5, + "status": "blocked_by_dependencies", + "depends_on": [ + "module_platform_contract", + "platform_plugin_audit", + "mobile_layout_baseline" + ], + "acceptance": [ + "android_directory", + "manifest_capabilities", + "debug_apk", + "emulator_smoke" + ] + } + ], + "quality_gate": [ + "node tool/validate_agent_docs.js", + "dart format .", + "flutter analyze:no_error", + "flutterguard:no_high", + "logic_change:targeted_test", + "teaching_ui_change:visual_evidence" + ], + "deferred_queue": [ + "popup_widgets_decomposition", + "widget_test_coverage", + "flutterguard_med_reduction", + "recommended_module_visual_evidence" + ] } - -abstract class FilePickerService { - Future pickFile({ - List allowedExtensions = const [], - String? title, - String? message, - }); -} -``` - -**macOS 原生侧目标**: -- Channel 从 `flutter_study/gcode_file_picker` 调整为 `file_picker_bridge/file_picker` -- `pickFile` 支持 `allowedExtensions`、`title`、`message` -- 保留 `com.apple.security.files.user-selected.read-only` -- G-code 模块只传 G-code 扩展名,不再持有平台通道细节 - -**实施步骤**: -- [x] 新增 `lib/shared/platform/file_picker/` 共享能力目录 -- [x] 将 `GcodeFilePicker` 改造成通用 `FilePickerService` -- [x] 调整 macOS `AppDelegate.swift` MethodChannel 名称和参数协议 -- [x] `gcode_visualizer` 只依赖 shared file picker 接口 -- [x] 删除模块内 `services/gcode_file_picker.dart` -- [x] 补充 shared 能力 `AI_ANALYSIS.md` -- [x] 为 MethodChannel service 增加可 mock 的单元测试 - -**暂不独立成插件包的原因**: -- 当前只有 macOS 实现,API 仍小 -- 只有 `gcode_visualizer` 一个模块实际使用 -- 独立 plugin 会引入额外的 package、registrant、Pods/Gradle 维护成本 - -**升级为独立 plugin/package 的触发条件**: -- 2 个以上模块稳定复用 -- 需要 Windows/iOS/Android 文件选择实现 -- 需要被其它工程复用 -- 需要独立版本、测试、发布节奏 - -### 2.2 shared 能力治理 - -- [x] 教学模板组件已从 `lib/shared/learning/` 抽出为外部 `flutter_study_learning` 包 -- [x] `file_picker` 已从 `lib/shared/platform/` 抽出为外部 `file_picker_bridge` 包 -- [x] 为 `lib/shared/platform/` 补充平台能力说明 `AI_ANALYSIS.md` -- [ ] 共享能力必须提供业务无关接口,模块只能传入业务参数 -- [ ] 共享能力新增后必须至少被 1 个模块接入验证 -- [ ] 若 shared 能力 3 个月内仍只有 1 个模块使用,保留在 shared,但不升级为独立插件 - -## Phase 3 — 模块内部规范化 ⏳ - -**目标**: 逐步统一模块内部分层(presentation/application/domain/data)。 - -- [ ] `popup_widgets` — 拆分 895 行 `module_root.dart` 为独立页面 -- [ ] `debounce_throttle` — 迁移为 `module_entry + module_root + utils` 精简模式 -- [ ] `status_management` — 保持 app/features/shared 分层,添加测试 -- [x] 所有模块补齐 `AI_ANALYSIS.md`(如有缺失) -- [x] `gcode_visualizer` — 文件选择器抽到 `shared/platform/file_picker` 后更新模块分析文档 - -## Phase 4 — 测试与质量 🔲 - -**目标**: 提升测试覆盖率和代码质量。 - -- [ ] 为无测试模块补充基础 Widget 测试 -- [ ] 引入 lint 规则增强(如 `prefer_const_constructors`) -- [ ] `scroll_table` 和 `usb_detector` — 从 `ModuleStatus.pending` 提升到 `ready` -- [ ] 建立 shared 能力测试样例: MethodChannel mock、错误分支、取消选择分支 -- [ ] FlutterGuard MED 项分批治理,优先处理 shared 与推荐模块 - -## Phase 5 — 教学体验 🔲 - -**目标**: 更多模块使用教学模板(`LearningScaffold`)。 - -- [x] `adsorption_line` — 改造为教学页面 -- [x] `stream_subscription` — 改造为教学页面 -- [x] `download_animation` — 改造为教学页面 - -## Phase 6 — 项目整体归拢 🔲 - -**目标**: 从“模块集合”收敛为“可维护的学习平台”,统一入口、共享能力、模块质量和验收标准。 - -### 6.1 目录归拢 - -- [ ] `app/`: 只放应用壳、路由、主题入口,不放模块业务 -- [ ] `module_registry/`: 只放模块元数据模型和枚举 -- [ ] `shared/learning/`: 只放教学模板组件 -- [ ] `shared/platform/`: 只放平台通道、系统能力、设备能力等业务无关服务 -- [ ] `modules///`: 只放模块内业务、页面、状态、解析器和模块文档 - -### 6.2 模块质量分级 - -- [ ] `ModuleStatus.pending`: 有明显结构、文档、体验或质量缺口 -- [ ] `ModuleStatus.ready`: 可独立学习,元数据完整,分析文档完整,基础验收通过 -- [ ] `ModuleStatus.recommended`: 使用教学模板,交互完整,有测试覆盖,FlutterGuard 无新增 high - -### 6.3 每轮重构验收 - -每次涉及代码调整后继续执行: - -```bash -dart format . -flutter analyze -dart run flutterguard_cli:flutterguard scan --path . --fail-on high -``` - -按变更类型补充: -- 逻辑/解析器: 跑对应 `flutter test` -- macOS 原生改动: 跑 `flutter build macos` -- UI 教学页: 截图或人工验收说明 - -### 6.4 近期优先级 - -1. 修正并稳定 `AI_ANALYSIS.md` 层级生成/维护规则,避免根文档再次退化为全量文件清单 -2. `popup_widgets` 拆分 900+ 行 `module_root.dart`,降低 FlutterGuard MED/LOW 噪音 -3. 为已使用 `LearningScaffold` 的 ready 模块补齐 Widget smoke test,满足后续推荐条件 -4. 建立 shared 能力测试模板,覆盖多窗口、平台桥接、取消/错误分支 -5. 对推荐模块逐步补齐截图或人工验收记录 diff --git a/lib/AI_ANALYSIS.md b/lib/AI_ANALYSIS.md index 6c73b1b..d0356bd 100644 --- a/lib/AI_ANALYSIS.md +++ b/lib/AI_ANALYSIS.md @@ -10,6 +10,7 @@ }, "entrypoints": [ "main.dart", + "app/app_bootstrap.dart", "app/app.dart", "app/router/app_route_table.dart" ], @@ -34,8 +35,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/AI_MODULE_INDEX.md b/lib/AI_MODULE_INDEX.md index ce549eb..33e79a5 100644 --- a/lib/AI_MODULE_INDEX.md +++ b/lib/AI_MODULE_INDEX.md @@ -1,58 +1,231 @@ -# AI 模块索引 - -> 此文件描述 lib/ 下所有模块的结构,AI 修改模块代码前请查阅对应模块的 AI_ANALYSIS.md。 - -## 模块列表 - -| 模块 | 路径 | 路由路径 | 状态管理 | 复杂度 | AI 分析文件 | -|------|------|---------|---------|--------|------------| -| adsorption_line | `modules/ui/adsorption_line` | /adsorption-line | ChangeNotifier + Provider | 高 | `modules/ui/adsorption_line/AI_ANALYSIS.md` | -| debounce_throttle | `modules/basic/debounce_throttle` | /debounce-throttle | StatefulWidget | 低 | `modules/basic/debounce_throttle/AI_ANALYSIS.md` | -| download_animation | `modules/ui/download_animation` | /download-animation | StatefulWidget | 中 | `modules/ui/download_animation/AI_ANALYSIS.md` | -| flutter_ioc | `modules/state/flutter_ioc` | /flutter-ioc | 自研 IoC + Provider | 中 | `modules/state/flutter_ioc/AI_ANALYSIS.md` | -| gcode_visualizer | `modules/ui/gcode_visualizer` | /gcode-visualizer | ChangeNotifier + AnimationController | 高 | `modules/ui/gcode_visualizer/AI_ANALYSIS.md` | -| dio_interceptor | `modules/platform/dio_interceptor` | /dio-interceptor | 无(Dio 拦截器) | 中 | `modules/platform/dio_interceptor/AI_ANALYSIS.md` | -| isolate_task_manager | `modules/async/isolate_task_manager` | /isolate-stream | StatefulWidget | 中 | `modules/async/isolate_task_manager/AI_ANALYSIS.md` | -| isolate_basic | `modules/async/isolate_basic` | /isolate-basic | StatefulWidget | 低 | `modules/async/isolate_basic/AI_ANALYSIS.md` | -| microtask | `modules/basic/microtask` | /microtask | StatefulWidget | 低 | `modules/basic/microtask/AI_ANALYSIS.md` | -| popup_widgets | `modules/popup_table/popup_widgets` | /popup-widgets | StatefulWidget | 高 | `modules/popup_table/popup_widgets/AI_ANALYSIS.md` | -| popup_list_interaction | `modules/popup_table/popup_list_interaction` | /popup-list-interaction | StatefulWidget | 低 | `modules/popup_table/popup_list_interaction/AI_ANALYSIS.md` | -| scroll_table | `modules/popup_table/scroll_table` | /scroll-table | 无 | 低 | `modules/popup_table/scroll_table/AI_ANALYSIS.md` | -| overlay_follow_compare | `modules/popup_table/overlay_follow_compare` | /overlay-compare | StatefulWidget | 中 | `modules/popup_table/overlay_follow_compare/AI_ANALYSIS.md` | -| status_management | `modules/state/status_management` | /status-management | Provider/Riverpod/Bloc | 高 | `modules/state/status_management/AI_ANALYSIS.md` | -| stream_subscription | `modules/async/stream_subscription` | /stream-subscription | StreamController | 中 | `modules/async/stream_subscription/AI_ANALYSIS.md` | -| tree_state | `modules/basic/tree_state` | /tree-state | StatefulWidget | 低 | `modules/basic/tree_state/AI_ANALYSIS.md` | -| usb_detector | `modules/platform/usb_detector` | /usb-detector | StreamController | 中 | `modules/platform/usb_detector/AI_ANALYSIS.md` | - -## 模块模式分类 - -### 模式 A: 简单入口(module_entry -> module_root) -- debounce_throttle -- download_animation -- flutter_ioc -- isolate_basic -- isolate_task_manager -- overlay_follow_compare -- popup_list_interaction -- popup_widgets -- scroll_table -- usb_detector - -### 模式 B: 页面路由型(module_entry -> module_routes -> pages) -- tree_state -- microtask -- stream_subscription -- dio_interceptor -- status_management - -### 模式 C: 功能分区型(module_entry 直接装配 pages/state/widgets/services) -- adsorption_line(models/state/services/widgets) -- gcode_visualizer(models/parser/services/state/widgets/pages) - -## 层级维护规则 - -- 根级 `AI_ANALYSIS.md` 只记录工作区层级、模块总数、外部包边界和下一步队列。 -- `lib/app/**/AI_ANALYSIS.md` 只记录应用壳和路由聚合,不展开模块内部细节。 -- `lib/shared/**/AI_ANALYSIS.md` 只记录业务无关共享能力、平台边界和可复用 API。 -- `lib/modules/**/AI_ANALYSIS.md` 只记录单模块结构、数据流、关键类、教学组件和变更备注。 -- 新增或迁移模块时,同步更新本索引、模块自身 `AI_ANALYSIS.md` 和 `lib/app/router/app_route_table.dart`。 +{ + "schema": "flutter_study.agent_docs.module_index.v1", + "registry": "lib/app/router/app_route_table.dart", + "count": 17, + "modules": [ + { + "id": "tree_state", + "category": "basic", + "path": "lib/modules/basic/tree_state", + "route": "/tree-state", + "status": "recommended", + "depends": [ + "flutter_study_learning", + "module_registry", + "go_router" + ], + "analysis": "lib/modules/basic/tree_state/AI_ANALYSIS.md" + }, + { + "id": "microtask", + "category": "basic", + "path": "lib/modules/basic/microtask", + "route": "/microtask", + "status": "recommended", + "depends": [ + "flutter_study_learning", + "module_registry", + "go_router" + ], + "analysis": "lib/modules/basic/microtask/AI_ANALYSIS.md" + }, + { + "id": "debounce_throttle", + "category": "basic", + "path": "lib/modules/basic/debounce_throttle", + "route": "/debounce-throttle", + "status": "ready", + "depends": [ + "flutter_study_learning", + "module_registry" + ], + "analysis": "lib/modules/basic/debounce_throttle/AI_ANALYSIS.md" + }, + { + "id": "stream_subscription", + "category": "async", + "path": "lib/modules/async/stream_subscription", + "route": "/stream-subscription", + "status": "recommended", + "depends": [ + "flutter_study_learning", + "module_registry", + "go_router" + ], + "analysis": "lib/modules/async/stream_subscription/AI_ANALYSIS.md" + }, + { + "id": "isolate_basic", + "category": "async", + "path": "lib/modules/async/isolate_basic", + "route": "/isolate-basic", + "status": "ready", + "depends": [ + "flutter_study_learning", + "module_registry", + "go_router" + ], + "analysis": "lib/modules/async/isolate_basic/AI_ANALYSIS.md" + }, + { + "id": "isolate_task_manager", + "category": "async", + "path": "lib/modules/async/isolate_task_manager", + "route": "/isolate-stream", + "status": "ready", + "depends": [ + "flutter_study_learning", + "module_registry" + ], + "analysis": "lib/modules/async/isolate_task_manager/AI_ANALYSIS.md" + }, + { + "id": "status_management", + "category": "state", + "path": "lib/modules/state/status_management", + "route": "/status-management", + "status": "recommended", + "depends": [ + "flutter_study_learning", + "provider", + "flutter_riverpod", + "flutter_bloc", + "module_registry", + "go_router" + ], + "analysis": "lib/modules/state/status_management/AI_ANALYSIS.md" + }, + { + "id": "flutter_ioc", + "category": "state", + "path": "lib/modules/state/flutter_ioc", + "route": "/flutter-ioc", + "status": "ready", + "depends": [ + "flutter_study_learning", + "flutter_ioc_core", + "provider", + "module_registry" + ], + "analysis": "lib/modules/state/flutter_ioc/AI_ANALYSIS.md" + }, + { + "id": "gcode_visualizer", + "category": "ui", + "path": "lib/modules/ui/gcode_visualizer", + "route": "/gcode-visualizer", + "status": "ready", + "depends": [ + "flutter_study_learning", + "gcode_core", + "file_picker_bridge", + "module_registry" + ], + "analysis": "lib/modules/ui/gcode_visualizer/AI_ANALYSIS.md" + }, + { + "id": "adsorption_line", + "category": "ui", + "path": "lib/modules/ui/adsorption_line", + "route": "/adsorption-line", + "status": "ready", + "depends": [ + "flutter_study_learning", + "provider", + "module_registry" + ], + "analysis": "lib/modules/ui/adsorption_line/AI_ANALYSIS.md" + }, + { + "id": "download_animation", + "category": "ui", + "path": "lib/modules/ui/download_animation", + "route": "/download-animation", + "status": "ready", + "depends": [ + "flutter_study_learning", + "module_registry", + "go_router" + ], + "analysis": "lib/modules/ui/download_animation/AI_ANALYSIS.md" + }, + { + "id": "popup_widgets", + "category": "popup_table", + "path": "lib/modules/popup_table/popup_widgets", + "route": "/popup-widgets", + "status": "ready", + "depends": [ + "flutter_study_learning", + "module_registry" + ], + "analysis": "lib/modules/popup_table/popup_widgets/AI_ANALYSIS.md" + }, + { + "id": "popup_list_interaction", + "category": "popup_table", + "path": "lib/modules/popup_table/popup_list_interaction", + "route": "/popup-list-interaction", + "status": "ready", + "depends": [ + "flutter_study_learning", + "module_registry", + "go_router" + ], + "analysis": "lib/modules/popup_table/popup_list_interaction/AI_ANALYSIS.md" + }, + { + "id": "scroll_table", + "category": "popup_table", + "path": "lib/modules/popup_table/scroll_table", + "route": "/scroll-table", + "status": "ready", + "depends": [ + "flutter_study_learning", + "two_dimensional_scrollables", + "module_registry" + ], + "analysis": "lib/modules/popup_table/scroll_table/AI_ANALYSIS.md" + }, + { + "id": "overlay_follow_compare", + "category": "popup_table", + "path": "lib/modules/popup_table/overlay_follow_compare", + "route": "/overlay-compare", + "status": "ready", + "depends": [ + "flutter_study_learning", + "module_registry" + ], + "analysis": "lib/modules/popup_table/overlay_follow_compare/AI_ANALYSIS.md" + }, + { + "id": "dio_interceptor", + "category": "platform", + "path": "lib/modules/platform/dio_interceptor", + "route": "/dio-interceptor", + "status": "ready", + "depends": [ + "flutter_study_learning", + "dio", + "module_registry", + "go_router" + ], + "analysis": "lib/modules/platform/dio_interceptor/AI_ANALYSIS.md" + }, + { + "id": "usb_detector", + "category": "platform", + "path": "lib/modules/platform/usb_detector", + "route": "/usb-detector", + "status": "ready", + "depends": [ + "flutter_study_learning", + "usb_serial", + "device_info_plus", + "module_registry" + ], + "analysis": "lib/modules/platform/usb_detector/AI_ANALYSIS.md" + } + ] +} diff --git a/lib/app/AI_ANALYSIS.md b/lib/app/AI_ANALYSIS.md index fc31e42..8ca5e14 100644 --- a/lib/app/AI_ANALYSIS.md +++ b/lib/app/AI_ANALYSIS.md @@ -10,16 +10,25 @@ }, "entrypoints": [ "app.dart", + "app_bootstrap.dart", + "module_home_page.dart", + "category_navigation.dart", + "category_window_app.dart", "router/app_router.dart", "router/app_route_table.dart" ], "owns": [ + "host_bootstrap", "material_app_router", - "router" + "router", + "module_home", + "adaptive_category_navigation", + "desktop_category_window_shell" ], "depends": [ "go_router", "module_registry", + "shared/multi_window", "modules" ], "children": [ @@ -29,8 +38,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/app/app_bootstrap.dart b/lib/app/app_bootstrap.dart new file mode 100644 index 0000000..f8f0c2c --- /dev/null +++ b/lib/app/app_bootstrap.dart @@ -0,0 +1,25 @@ +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../shared/multi_window/multi_window_manager.dart'; +import 'app.dart'; +import 'category_window_app.dart'; + +/// Resolves the host-specific application shell before mounting Flutter. +Future bootstrapFlutterStudyApp() async { + WidgetsFlutterBinding.ensureInitialized(); + + Widget root = const App(); + if (MultiWindowManager.isSupported) { + final windowController = await WindowController.fromCurrentEngine(); + final arguments = MultiWindowManager.parseArguments( + windowController.arguments, + ); + if (arguments.type == WindowType.category && arguments.category != null) { + root = CategoryWindowApp(category: arguments.category!); + } + } + + runApp(ProviderScope(child: root)); +} diff --git a/lib/app/category_navigation.dart b/lib/app/category_navigation.dart new file mode 100644 index 0000000..6c8272f --- /dev/null +++ b/lib/app/category_navigation.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +import '../module_registry/module_catalog_utils.dart'; +import '../module_registry/module_category.dart'; +import '../module_registry/module_entry.dart'; +import '../shared/multi_window/multi_window_manager.dart'; +import 'category_window_app.dart'; + +/// Selects the platform-appropriate way to open a module category. +/// +/// Desktop hosts may create a separate window. Mobile and other hosts keep the +/// same content inside the current navigation stack. +class CategoryNavigation { + const CategoryNavigation._(); + + static bool get opensInNewWindow => MultiWindowManager.isSupported; + + static Future open( + BuildContext context, { + required ModuleCategory category, + required List modules, + }) async { + if (opensInNewWindow) { + await MultiWindowManager.instance.createCategoryWindow(category); + return; + } + + if (!context.mounted) return; + final filtered = filterModulesByCategory(modules, category); + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => CategoryHomePage(category: category, modules: filtered), + ), + ); + } +} diff --git a/lib/app/category_window_app.dart b/lib/app/category_window_app.dart new file mode 100644 index 0000000..8007f05 --- /dev/null +++ b/lib/app/category_window_app.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../module_registry/module_catalog_utils.dart'; +import '../module_registry/module_category.dart'; +import '../module_registry/module_entry.dart'; +import 'module_home_page.dart'; +import 'router/app_route_table.dart'; + +class CategoryWindowApp extends StatelessWidget { + const CategoryWindowApp({super.key, required this.category}); + + final ModuleCategory category; + + static GoRouter createRouter(ModuleCategory category) { + final modules = filterModulesByCategory(AppRouteTable.modules, category); + final childRoutes = buildCategoryRoutes(modules); + + return GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (context, state) => + CategoryHomePage(category: category, modules: modules), + routes: childRoutes, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: CategoryWindowApp.createRouter(category), + title: category.label, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + ); + } +} + +class CategoryHomePage extends StatelessWidget { + const CategoryHomePage({ + super.key, + required this.category, + required this.modules, + }); + + final ModuleCategory category; + final List modules; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(category.label), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + Navigator.of(context).maybePop(); + } + }, + ), + ), + body: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: modules.length, + itemBuilder: (context, index) => ModuleListTile(module: modules[index]), + ), + ); + } +} diff --git a/lib/app/module_home_page.dart b/lib/app/module_home_page.dart new file mode 100644 index 0000000..1bb10fa --- /dev/null +++ b/lib/app/module_home_page.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../module_registry/module_category.dart'; +import '../module_registry/module_entry.dart'; +import 'category_navigation.dart'; + +class ModuleHomePage extends StatelessWidget { + const ModuleHomePage({super.key, required this.modules}); + + final List modules; + + @override + Widget build(BuildContext context) { + const categories = ModuleCategory.values; + + return Scaffold( + appBar: AppBar(title: const Text('Flutter 学习实验室')), + body: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + final categoryModules = modules + .where((module) => module.category == category) + .toList(); + + if (categoryModules.isEmpty) return const SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 8, 8), + child: Row( + children: [ + Expanded( + child: Text( + category.label, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + IconButton( + icon: Icon( + CategoryNavigation.opensInNewWindow + ? Icons.open_in_new + : Icons.chevron_right, + size: 20, + ), + tooltip: '打开分类', + onPressed: () => CategoryNavigation.open( + context, + category: category, + modules: modules, + ), + ), + ], + ), + ), + ...categoryModules.map( + (module) => ModuleListTile(module: module), + ), + const Divider(height: 1), + ], + ); + }, + ), + ); + } +} + +class ModuleListTile extends StatelessWidget { + const ModuleListTile({super.key, required this.module}); + + final ModuleEntry module; + + Color _difficultyColor(Difficulty difficulty) { + return switch (difficulty) { + Difficulty.beginner => Colors.green, + Difficulty.intermediate => Colors.orange, + Difficulty.advanced => Colors.red, + }; + } + + @override + Widget build(BuildContext context) { + final difficultyColor = _difficultyColor(module.difficulty); + + return ListTile( + title: Row( + children: [ + Expanded(child: Text(module.title)), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: difficultyColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + module.difficulty.label, + style: TextStyle( + fontSize: 11, + color: difficultyColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(module.subtitle, style: const TextStyle(fontSize: 12)), + const SizedBox(height: 6), + Wrap( + spacing: 4, + runSpacing: 4, + children: module.concepts + .map( + (concept) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(4), + ), + child: Text(concept, style: const TextStyle(fontSize: 10)), + ), + ) + .toList(), + ), + const SizedBox(height: 4), + Text( + '预计 ${module.estimatedMinutes} 分钟 · ${module.status.label}', + style: TextStyle(fontSize: 11, color: Colors.grey.shade600), + ), + ], + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => context.push(module.path), + ); + } +} diff --git a/lib/app/router/AI_ANALYSIS.md b/lib/app/router/AI_ANALYSIS.md index 1714943..7f7606d 100644 --- a/lib/app/router/AI_ANALYSIS.md +++ b/lib/app/router/AI_ANALYSIS.md @@ -15,9 +15,10 @@ "owns": [ "go_router_root", "module_route_aggregation", - "module_home_index" + "module_catalog_composition" ], "depends": [ + "app/module_home_page", "module_registry", "modules" ], @@ -26,8 +27,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/app/router/app_route_table.dart b/lib/app/router/app_route_table.dart index 32e113c..2227c51 100644 --- a/lib/app/router/app_route_table.dart +++ b/lib/app/router/app_route_table.dart @@ -1,11 +1,8 @@ -import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import '../module_home_page.dart'; import '../../module_registry/module_category.dart'; import '../../module_registry/module_entry.dart'; -import '../../shared/multi_window/category_window_app.dart'; -import '../../shared/multi_window/multi_window_manager.dart'; -import '../../shared/multi_window/multi_window_route_filter.dart'; import '../../modules/basic/debounce_throttle/module_entry.dart'; import '../../modules/basic/microtask/module_entry.dart'; import '../../modules/basic/microtask/module_routes.dart'; @@ -38,16 +35,16 @@ import '../../modules/platform/usb_detector/module_entry.dart'; // ==================== 状态管理子路由(模块内部已定义映射) ==================== -List _buildStatusManageRoutes() => - StatusManagementRoutes.routes.entries - .map( - (entry) => GoRoute( - path: - entry.key.startsWith('/') ? entry.key.substring(1) : entry.key, - builder: (context, state) => entry.value(context), - ), - ) - .toList(); +List _buildStatusManageRoutes() => StatusManagementRoutes + .routes + .entries + .map( + (entry) => GoRoute( + path: entry.key.startsWith('/') ? entry.key.substring(1) : entry.key, + builder: (context, state) => entry.value(context), + ), + ) + .toList(); // ==================== 模块注册 ==================== @@ -233,7 +230,7 @@ final List _modules = [ 'LayerLink', 'CompositedTransformFollower', 'markNeedsBuild', - 'ScrollController' + 'ScrollController', ], estimatedMinutes: 30, status: ModuleStatus.ready, @@ -266,24 +263,6 @@ final List _modules = [ ), ]; -// ==================== 路由聚合 ==================== - -Future _openCategoryWindow( - BuildContext context, - ModuleCategory category, -) async { - final filtered = filterModulesByCategory(_modules, category); - if (MultiWindowManager.isSupported) { - await MultiWindowManager.instance.createCategoryWindow(category); - } else { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => CategoryHomePage(category: category, modules: filtered), - ), - ); - } -} - final List _routes = [ GoRoute( path: '/', @@ -301,137 +280,3 @@ class AppRouteTable { static List get routes => _routes; static List get modules => _modules; } - -// ==================== 首页 ==================== - -class ModuleHomePage extends StatelessWidget { - const ModuleHomePage({super.key, required this.modules}); - - final List modules; - - @override - Widget build(BuildContext context) { - const categories = ModuleCategory.values; - - return Scaffold( - appBar: AppBar( - title: const Text('Flutter 学习实验室'), - ), - body: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: categories.length, - itemBuilder: (context, index) { - final category = categories[index]; - final categoryModules = - modules.where((m) => m.category == category).toList(); - - if (categoryModules.isEmpty) return const SizedBox.shrink(); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 8, 8), - child: Row( - children: [ - Expanded( - child: Text( - category.label, - style: - Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - IconButton( - icon: const Icon(Icons.open_in_new, size: 20), - tooltip: '在新窗口打开', - onPressed: () { - _openCategoryWindow(context, category); - }, - ), - ], - ), - ), - ...categoryModules.map((module) => ModuleCard(module: module)), - const Divider(height: 1), - ], - ); - }, - ), - ); - } -} - -class ModuleCard extends StatelessWidget { - const ModuleCard({super.key, required this.module}); - - final ModuleEntry module; - - Color _difficultyColor(Difficulty d) { - return switch (d) { - Difficulty.beginner => Colors.green, - Difficulty.intermediate => Colors.orange, - Difficulty.advanced => Colors.red, - }; - } - - @override - Widget build(BuildContext context) { - return ListTile( - title: Row( - children: [ - Expanded(child: Text(module.title)), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: - _difficultyColor(module.difficulty).withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - module.difficulty.label, - style: TextStyle( - fontSize: 11, - color: _difficultyColor(module.difficulty), - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(module.subtitle, style: const TextStyle(fontSize: 12)), - const SizedBox(height: 6), - Wrap( - spacing: 4, - runSpacing: 4, - children: module.concepts - .map( - (c) => Container( - padding: - const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.grey.shade200, - borderRadius: BorderRadius.circular(4), - ), - child: Text(c, style: const TextStyle(fontSize: 10)), - ), - ) - .toList(), - ), - const SizedBox(height: 4), - Text( - '预计 ${module.estimatedMinutes} 分钟 · ${module.status.label}', - style: TextStyle(fontSize: 11, color: Colors.grey.shade600), - ), - ], - ), - trailing: const Icon(Icons.chevron_right), - onTap: () => context.push(module.path), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index 3ab8e8d..8e7ac24 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,36 +1,3 @@ -import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'app/app_bootstrap.dart'; -import 'app/app.dart'; -import 'shared/multi_window/category_window_app.dart'; -import 'shared/multi_window/multi_window_manager.dart'; - -void main(List args) async { - WidgetsFlutterBinding.ensureInitialized(); - - if (MultiWindowManager.isSupported) { - final wc = await WindowController.fromCurrentEngine(); - final windowArgs = MultiWindowManager.parseArguments(wc.arguments); - - if (windowArgs.type == WindowType.category && windowArgs.category != null) { - runApp( - ProviderScope( - child: CategoryWindowApp(category: windowArgs.category!), - ), - ); - return; - } - } - - runApp(const ProviderScope(child: MainApp())); -} - -class MainApp extends StatelessWidget { - const MainApp({super.key}); - - @override - Widget build(BuildContext context) { - return const App(); - } -} +Future main() => bootstrapFlutterStudyApp(); diff --git a/lib/module_registry/AI_ANALYSIS.md b/lib/module_registry/AI_ANALYSIS.md index c0f53f3..916c8db 100644 --- a/lib/module_registry/AI_ANALYSIS.md +++ b/lib/module_registry/AI_ANALYSIS.md @@ -10,24 +10,28 @@ }, "entrypoints": [ "module_entry.dart", - "module_category.dart" + "module_category.dart", + "module_catalog_utils.dart" ], "owns": [ "module_entry_model", "module_category_enum", "difficulty_enum", - "module_status_enum" + "module_status_enum", + "module_catalog_filtering", + "category_route_rebasing" ], "depends": [ - "flutter_material" + "flutter_material", + "go_router" ], "children": [], "contracts": { "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/shared/multi_window/multi_window_route_filter.dart b/lib/module_registry/module_catalog_utils.dart similarity index 67% rename from lib/shared/multi_window/multi_window_route_filter.dart rename to lib/module_registry/module_catalog_utils.dart index 7942b77..f1addf1 100644 --- a/lib/shared/multi_window/multi_window_route_filter.dart +++ b/lib/module_registry/module_catalog_utils.dart @@ -1,13 +1,13 @@ import 'package:go_router/go_router.dart'; -import '../../module_registry/module_category.dart'; -import '../../module_registry/module_entry.dart'; +import 'module_category.dart'; +import 'module_entry.dart'; List filterModulesByCategory( List allModules, ModuleCategory category, ) { - return allModules.where((m) => m.category == category).toList(); + return allModules.where((module) => module.category == category).toList(); } List buildCategoryRoutes(List modules) { @@ -26,12 +26,12 @@ String _stripLeadingSlash(String path) { } List _rebasedRoutes(List routes) { - return routes.map((r) { - final strippedPath = r.path.startsWith('/') ? r.path.substring(1) : r.path; + return routes.map((route) { + final strippedPath = _stripLeadingSlash(route.path); return GoRoute( path: strippedPath, - builder: r.builder, - routes: r.routes, + builder: route.builder, + routes: route.routes, ); }).toList(); } diff --git a/lib/modules/AI_ANALYSIS.md b/lib/modules/AI_ANALYSIS.md index a2fae24..e25ba75 100644 --- a/lib/modules/AI_ANALYSIS.md +++ b/lib/modules/AI_ANALYSIS.md @@ -36,8 +36,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/async/AI_ANALYSIS.md b/lib/modules/async/AI_ANALYSIS.md index 334c059..1391044 100644 --- a/lib/modules/async/AI_ANALYSIS.md +++ b/lib/modules/async/AI_ANALYSIS.md @@ -29,8 +29,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/async/isolate_basic/AI_ANALYSIS.md b/lib/modules/async/isolate_basic/AI_ANALYSIS.md index 9453ebe..7ce7a82 100644 --- a/lib/modules/async/isolate_basic/AI_ANALYSIS.md +++ b/lib/modules/async/isolate_basic/AI_ANALYSIS.md @@ -31,8 +31,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/async/isolate_task_manager/AI_ANALYSIS.md b/lib/modules/async/isolate_task_manager/AI_ANALYSIS.md index 4a12fec..4ac72d2 100644 --- a/lib/modules/async/isolate_task_manager/AI_ANALYSIS.md +++ b/lib/modules/async/isolate_task_manager/AI_ANALYSIS.md @@ -29,8 +29,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/async/stream_subscription/AI_ANALYSIS.md b/lib/modules/async/stream_subscription/AI_ANALYSIS.md index d4b7747..c82383f 100644 --- a/lib/modules/async/stream_subscription/AI_ANALYSIS.md +++ b/lib/modules/async/stream_subscription/AI_ANALYSIS.md @@ -31,8 +31,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/basic/AI_ANALYSIS.md b/lib/modules/basic/AI_ANALYSIS.md index 4a3048d..6b03944 100644 --- a/lib/modules/basic/AI_ANALYSIS.md +++ b/lib/modules/basic/AI_ANALYSIS.md @@ -29,8 +29,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/basic/debounce_throttle/AI_ANALYSIS.md b/lib/modules/basic/debounce_throttle/AI_ANALYSIS.md index 6da03d8..88d2723 100644 --- a/lib/modules/basic/debounce_throttle/AI_ANALYSIS.md +++ b/lib/modules/basic/debounce_throttle/AI_ANALYSIS.md @@ -29,8 +29,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/basic/microtask/AI_ANALYSIS.md b/lib/modules/basic/microtask/AI_ANALYSIS.md index cc0bf38..257bd8f 100644 --- a/lib/modules/basic/microtask/AI_ANALYSIS.md +++ b/lib/modules/basic/microtask/AI_ANALYSIS.md @@ -32,8 +32,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/basic/tree_state/AI_ANALYSIS.md b/lib/modules/basic/tree_state/AI_ANALYSIS.md index ad0e9e3..4f4cfb9 100644 --- a/lib/modules/basic/tree_state/AI_ANALYSIS.md +++ b/lib/modules/basic/tree_state/AI_ANALYSIS.md @@ -31,8 +31,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/platform/AI_ANALYSIS.md b/lib/modules/platform/AI_ANALYSIS.md index dd561a9..853f3d8 100644 --- a/lib/modules/platform/AI_ANALYSIS.md +++ b/lib/modules/platform/AI_ANALYSIS.md @@ -29,8 +29,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/platform/dio_interceptor/AI_ANALYSIS.md b/lib/modules/platform/dio_interceptor/AI_ANALYSIS.md index 9321306..0c7ae94 100644 --- a/lib/modules/platform/dio_interceptor/AI_ANALYSIS.md +++ b/lib/modules/platform/dio_interceptor/AI_ANALYSIS.md @@ -32,8 +32,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/platform/usb_detector/AI_ANALYSIS.md b/lib/modules/platform/usb_detector/AI_ANALYSIS.md index 1cee5c5..eba8aa4 100644 --- a/lib/modules/platform/usb_detector/AI_ANALYSIS.md +++ b/lib/modules/platform/usb_detector/AI_ANALYSIS.md @@ -31,8 +31,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/popup_table/AI_ANALYSIS.md b/lib/modules/popup_table/AI_ANALYSIS.md index acc16dc..f6e4dba 100644 --- a/lib/modules/popup_table/AI_ANALYSIS.md +++ b/lib/modules/popup_table/AI_ANALYSIS.md @@ -32,8 +32,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/popup_table/overlay_follow_compare/AI_ANALYSIS.md b/lib/modules/popup_table/overlay_follow_compare/AI_ANALYSIS.md index a5b7cf0..37321d5 100644 --- a/lib/modules/popup_table/overlay_follow_compare/AI_ANALYSIS.md +++ b/lib/modules/popup_table/overlay_follow_compare/AI_ANALYSIS.md @@ -30,8 +30,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/popup_table/popup_list_interaction/AI_ANALYSIS.md b/lib/modules/popup_table/popup_list_interaction/AI_ANALYSIS.md index f48f268..8367145 100644 --- a/lib/modules/popup_table/popup_list_interaction/AI_ANALYSIS.md +++ b/lib/modules/popup_table/popup_list_interaction/AI_ANALYSIS.md @@ -32,8 +32,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/popup_table/popup_widgets/AI_ANALYSIS.md b/lib/modules/popup_table/popup_widgets/AI_ANALYSIS.md index 45c55e4..bcc4681 100644 --- a/lib/modules/popup_table/popup_widgets/AI_ANALYSIS.md +++ b/lib/modules/popup_table/popup_widgets/AI_ANALYSIS.md @@ -30,8 +30,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/popup_table/scroll_table/AI_ANALYSIS.md b/lib/modules/popup_table/scroll_table/AI_ANALYSIS.md index f0a06a4..5e49cbe 100644 --- a/lib/modules/popup_table/scroll_table/AI_ANALYSIS.md +++ b/lib/modules/popup_table/scroll_table/AI_ANALYSIS.md @@ -31,8 +31,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/state/AI_ANALYSIS.md b/lib/modules/state/AI_ANALYSIS.md index c92a46b..2b1ae20 100644 --- a/lib/modules/state/AI_ANALYSIS.md +++ b/lib/modules/state/AI_ANALYSIS.md @@ -29,8 +29,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/state/flutter_ioc/AI_ANALYSIS.md b/lib/modules/state/flutter_ioc/AI_ANALYSIS.md index 11fea91..4607156 100644 --- a/lib/modules/state/flutter_ioc/AI_ANALYSIS.md +++ b/lib/modules/state/flutter_ioc/AI_ANALYSIS.md @@ -31,8 +31,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/state/status_management/AI_ANALYSIS.md b/lib/modules/state/status_management/AI_ANALYSIS.md index 08f6dde..d01c9cc 100644 --- a/lib/modules/state/status_management/AI_ANALYSIS.md +++ b/lib/modules/state/status_management/AI_ANALYSIS.md @@ -35,8 +35,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/ui/AI_ANALYSIS.md b/lib/modules/ui/AI_ANALYSIS.md index bb359bd..af68442 100644 --- a/lib/modules/ui/AI_ANALYSIS.md +++ b/lib/modules/ui/AI_ANALYSIS.md @@ -31,8 +31,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/ui/adsorption_line/AI_ANALYSIS.md b/lib/modules/ui/adsorption_line/AI_ANALYSIS.md index 4a914b5..8af98c2 100644 --- a/lib/modules/ui/adsorption_line/AI_ANALYSIS.md +++ b/lib/modules/ui/adsorption_line/AI_ANALYSIS.md @@ -32,8 +32,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/ui/download_animation/AI_ANALYSIS.md b/lib/modules/ui/download_animation/AI_ANALYSIS.md index 10307f7..b4fee72 100644 --- a/lib/modules/ui/download_animation/AI_ANALYSIS.md +++ b/lib/modules/ui/download_animation/AI_ANALYSIS.md @@ -32,8 +32,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/modules/ui/gcode_visualizer/AI_ANALYSIS.md b/lib/modules/ui/gcode_visualizer/AI_ANALYSIS.md index 2708d41..5b2af7d 100644 --- a/lib/modules/ui/gcode_visualizer/AI_ANALYSIS.md +++ b/lib/modules/ui/gcode_visualizer/AI_ANALYSIS.md @@ -33,8 +33,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/shared/AI_ANALYSIS.md b/lib/shared/AI_ANALYSIS.md index ed4e06f..46c152c 100644 --- a/lib/shared/AI_ANALYSIS.md +++ b/lib/shared/AI_ANALYSIS.md @@ -14,7 +14,7 @@ ], "owns": [ "business_free_capabilities", - "desktop_windowing", + "desktop_window_lifecycle", "platform_boundaries" ], "depends": [ @@ -29,8 +29,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/shared/multi_window/AI_ANALYSIS.md b/lib/shared/multi_window/AI_ANALYSIS.md index 6b80216..6679372 100644 --- a/lib/shared/multi_window/AI_ANALYSIS.md +++ b/lib/shared/multi_window/AI_ANALYSIS.md @@ -9,18 +9,14 @@ "status": "active" }, "entrypoints": [ - "multi_window_manager.dart", - "category_window_app.dart", - "multi_window_route_filter.dart" + "multi_window_manager.dart" ], "owns": [ "desktop_window_lifecycle", - "category_window_router", - "module_route_filter" + "desktop_window_arguments" ], "depends": [ "desktop_multi_window", - "go_router", "module_registry" ], "children": [], @@ -28,8 +24,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/lib/shared/multi_window/category_window_app.dart b/lib/shared/multi_window/category_window_app.dart deleted file mode 100644 index c38cf2f..0000000 --- a/lib/shared/multi_window/category_window_app.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -import '../../app/router/app_route_table.dart'; -import '../../module_registry/module_category.dart'; -import '../../module_registry/module_entry.dart'; -import 'multi_window_route_filter.dart'; - -class CategoryWindowApp extends StatelessWidget { - const CategoryWindowApp({super.key, required this.category}); - - final ModuleCategory category; - - static GoRouter createRouter(ModuleCategory category) { - final modules = filterModulesByCategory(AppRouteTable.modules, category); - final childRoutes = buildCategoryRoutes(modules); - - return GoRouter( - initialLocation: '/', - routes: [ - GoRoute( - path: '/', - builder: (context, state) => - CategoryHomePage(category: category, modules: modules), - routes: childRoutes, - ), - ], - ); - } - - @override - Widget build(BuildContext context) { - return MaterialApp.router( - routerConfig: CategoryWindowApp.createRouter(category), - title: category.label, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), - useMaterial3: true, - ), - ); - } -} - -class CategoryHomePage extends StatelessWidget { - const CategoryHomePage({ - super.key, - required this.category, - required this.modules, - }); - - final ModuleCategory category; - final List modules; - - Color _difficultyColor(Difficulty d) { - return switch (d) { - Difficulty.beginner => Colors.green, - Difficulty.intermediate => Colors.orange, - Difficulty.advanced => Colors.red, - }; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(category.label), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - if (context.canPop()) { - context.pop(); - } else { - Navigator.of(context).maybePop(); - } - }, - ), - ), - body: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: modules.length, - itemBuilder: (context, index) { - final module = modules[index]; - return ListTile( - title: Row( - children: [ - Expanded(child: Text(module.title)), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: _difficultyColor(module.difficulty) - .withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - module.difficulty.label, - style: TextStyle( - fontSize: 11, - color: _difficultyColor(module.difficulty), - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(module.subtitle, style: const TextStyle(fontSize: 12)), - const SizedBox(height: 6), - Wrap( - spacing: 4, - runSpacing: 4, - children: module.concepts - .map( - (c) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.grey.shade200, - borderRadius: BorderRadius.circular(4), - ), - child: Text(c, style: const TextStyle(fontSize: 10)), - ), - ) - .toList(), - ), - const SizedBox(height: 4), - Text( - '预计 ${module.estimatedMinutes} 分钟 · ${module.status.label}', - style: TextStyle(fontSize: 11, color: Colors.grey.shade600), - ), - ], - ), - trailing: const Icon(Icons.chevron_right), - onTap: () => context.push(module.path), - ); - }, - ), - ); - } -} diff --git a/lib/shared/platform/AI_ANALYSIS.md b/lib/shared/platform/AI_ANALYSIS.md index e7e836a..39dad22 100644 --- a/lib/shared/platform/AI_ANALYSIS.md +++ b/lib/shared/platform/AI_ANALYSIS.md @@ -24,8 +24,8 @@ "no_natural_language": true, "index_only": true, "max_index_depth": 2, - "doc_consumer": "vibecoding", - "doc_mode": "harness", + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", "update_required_on_file_change": true, "import_direction_enforced": true }, diff --git a/test/shared/module_catalog_utils_test.dart b/test/shared/module_catalog_utils_test.dart new file mode 100644 index 0000000..a34728e --- /dev/null +++ b/test/shared/module_catalog_utils_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:main_app/module_registry/module_catalog_utils.dart'; +import 'package:main_app/module_registry/module_category.dart'; +import 'package:main_app/module_registry/module_entry.dart'; + +void main() { + ModuleEntry createModule({ + required String path, + required ModuleCategory category, + List routes = const [], + }) { + return ModuleEntry( + title: path, + path: path, + subtitle: 'test', + category: category, + difficulty: Difficulty.beginner, + concepts: const ['test'], + estimatedMinutes: 1, + status: ModuleStatus.ready, + builder: (_) => const SizedBox.shrink(), + routes: routes, + ); + } + + test('filters modules without changing catalog order', () { + final modules = [ + createModule(path: '/basic-a', category: ModuleCategory.basic), + createModule(path: '/ui-a', category: ModuleCategory.ui), + createModule(path: '/basic-b', category: ModuleCategory.basic), + ]; + + final filtered = filterModulesByCategory(modules, ModuleCategory.basic); + + expect(filtered.map((module) => module.path), ['/basic-a', '/basic-b']); + }); + + test('rebases module and child paths for a category window', () { + final modules = [ + createModule( + path: '/basic-a', + category: ModuleCategory.basic, + routes: [ + GoRoute( + path: '/details', + builder: (_, __) => const SizedBox.shrink(), + ), + ], + ), + ]; + + final routes = buildCategoryRoutes(modules); + + expect(routes.single.path, 'basic-a'); + expect((routes.single.routes.single as GoRoute).path, 'details'); + }); +} diff --git a/tool/generate_agent_indexes.js b/tool/generate_agent_indexes.js index bacfb63..9f9a1d9 100644 --- a/tool/generate_agent_indexes.js +++ b/tool/generate_agent_indexes.js @@ -7,8 +7,8 @@ const contracts = { no_natural_language: true, index_only: true, max_index_depth: 2, - doc_consumer: 'vibecoding', - doc_mode: 'harness', + doc_consumer: 'coding_agent', + doc_mode: 'machine_contract', update_required_on_file_change: true, import_direction_enforced: true, }; @@ -84,10 +84,17 @@ function writeIndex({ function writeSchema() { writeJson('AI_ANALYSIS_SCHEMA.json', { - schema: 'vibecoding.harness.ai_analysis_schema.v1', + schema: 'flutter_study.agent_docs.schema.v2', syntax: 'json_config', prose: 'forbidden', markdown: 'forbidden', + generated_by: 'tool/generate_agent_indexes.js', + documents: { + project_context: 'AI_PROJECT_CONTEXT.md', + refactor_plan: 'REFACTOR_PLAN.md', + module_index: 'lib/AI_MODULE_INDEX.md', + analysis_glob: '**/AI_ANALYSIS.md', + }, levels: { workspace: ['AI_ANALYSIS.md'], section: [ @@ -116,8 +123,8 @@ function writeSchema() { no_natural_language: true, index_only: true, max_index_depth: 2, - doc_consumer: 'vibecoding', - doc_mode: 'harness', + doc_consumer: 'coding_agent', + doc_mode: 'machine_contract', }, module_contract_policy: { keep_for_module_rule: true, @@ -127,22 +134,181 @@ function writeSchema() { }); } +function writeProjectContext() { + writeJson('AI_PROJECT_CONTEXT.md', { + schema: 'flutter_study.agent_docs.project_context.v1', + consumer: 'coding_agent', + package: { + name: 'main_app', + type: 'flutter_modular_learning_app', + sdk: ['flutter_3', 'dart_3'], + }, + platform: { + current_hosts: ['macos', 'windows'], + next_host: 'android', + target_hosts: ['android', 'ios', 'macos', 'windows'], + }, + entrypoints: { + process: 'lib/main.dart', + bootstrap: 'lib/app/app_bootstrap.dart', + app: 'lib/app/app.dart', + router: 'lib/app/router/app_router.dart', + route_table: 'lib/app/router/app_route_table.dart', + }, + layers: [ + { + id: 'app', + path: 'lib/app', + owns: ['host_bootstrap', 'app_shell', 'navigation_policy', 'route_composition'], + may_depend_on: ['module_registry', 'shared', 'modules'], + }, + { + id: 'module_registry', + path: 'lib/module_registry', + owns: ['module_metadata', 'catalog_operations'], + may_depend_on: ['flutter', 'go_router'], + }, + { + id: 'shared', + path: 'lib/shared', + owns: ['business_neutral_capabilities', 'platform_boundaries'], + forbidden_dependencies: ['app', 'modules'], + }, + { + id: 'modules', + path: 'lib/modules/{category}/{module}', + owns: ['learning_ui', 'module_state', 'module_domain', 'module_data'], + forbidden_dependencies: ['other_modules'], + }, + ], + module_contract: { + required_files: ['module_entry.dart', 'AI_ANALYSIS.md'], + required_registration: 'lib/app/router/app_route_table.dart', + required_metadata: ['category', 'difficulty', 'concepts', 'estimatedMinutes', 'status', 'subtitle'], + required_learning_dependency: 'flutter_study_learning', + route_path_style: 'kebab_case', + directory_style: 'snake_case', + }, + platform_rules: { + router_platform_api: 'forbidden', + module_host_navigation: 'forbidden', + desktop_window_policy: 'lib/app/category_navigation.dart', + platform_capability_contract: 'business_neutral_interface', + }, + change_protocol: { + pre_read: ['AI_PROJECT_CONTEXT.md', 'REFACTOR_PLAN.md', '{target}/AI_ANALYSIS.md'], + update_source: ['tool/generate_agent_indexes.js'], + generate: 'bash tool/generate_harness_ai_analysis.sh', + validate: [ + 'bash tool/generate_harness_ai_analysis.sh', + 'dart format .', + 'flutter analyze', + 'dart run flutterguard_cli:flutterguard scan --path . --fail-on high', + ], + }, + }); +} + +function writeRefactorPlan() { + writeJson('REFACTOR_PLAN.md', { + schema: 'flutter_study.agent_docs.refactor_plan.v1', + objective: 'android_readiness_after_architecture_convergence', + active_phase: 'mobile_preintegration', + completed_milestones: [ + 'directory_layers', + 'shared_package_extraction', + 'module_analysis_coverage', + 'app_navigation_boundary', + 'host_bootstrap_boundary', + ], + work_queue: [ + { + id: 'module_platform_contract', + priority: 1, + status: 'pending', + changes: ['ModuleEntry.platform_support', 'ModuleHomePage.availability_state'], + acceptance: ['catalog_platform_metadata_complete', 'unsupported_module_state_visible'], + }, + { + id: 'platform_plugin_audit', + priority: 2, + status: 'pending', + targets: ['desktop_multi_window', 'file_picker_bridge', 'usb_serial', 'device_info_plus'], + acceptance: ['android_support_matrix', 'unsupported_fallbacks'], + }, + { + id: 'usb_platform_boundary', + priority: 3, + status: 'pending', + targets: ['lib/modules/platform/usb_detector'], + acceptance: ['no_windows_hardcode', 'android_system_info', 'error_branch_test'], + }, + { + id: 'mobile_layout_baseline', + priority: 4, + status: 'pending', + viewport_width_dp: 360, + targets: ['module_home', 'category_home', 'ready_modules', 'recommended_modules'], + acceptance: ['no_overflow', 'safe_area', 'keyboard_avoidance', 'touch_targets'], + }, + { + id: 'android_host', + priority: 5, + status: 'blocked_by_dependencies', + depends_on: ['module_platform_contract', 'platform_plugin_audit', 'mobile_layout_baseline'], + acceptance: ['android_directory', 'manifest_capabilities', 'debug_apk', 'emulator_smoke'], + }, + ], + quality_gate: [ + 'node tool/validate_agent_docs.js', + 'dart format .', + 'flutter analyze:no_error', + 'flutterguard:no_high', + 'logic_change:targeted_test', + 'teaching_ui_change:visual_evidence', + ], + deferred_queue: [ + 'popup_widgets_decomposition', + 'widget_test_coverage', + 'flutterguard_med_reduction', + 'recommended_module_visual_evidence', + ], + }); +} + +function writeModuleIndex() { + writeJson('lib/AI_MODULE_INDEX.md', { + schema: 'flutter_study.agent_docs.module_index.v1', + registry: 'lib/app/router/app_route_table.dart', + count: modules.length, + modules: modules.map(([category, module, route, status, depends]) => ({ + id: module, + category, + path: `lib/modules/${category}/${module}`, + route, + status, + depends, + analysis: `lib/modules/${category}/${module}/AI_ANALYSIS.md`, + })), + }); +} + function writeRootIndexes() { writeIndex({ rel: 'AI_ANALYSIS.md', id: 'flutter_study.root', kind: 'workspace_index', - entrypoints: ['lib/main.dart', 'lib/app/app.dart', 'lib/app/router/app_route_table.dart'], + entrypoints: ['lib/main.dart', 'lib/app/app_bootstrap.dart', 'lib/app/app.dart', 'lib/app/router/app_route_table.dart'], owns: ['app_shell', 'module_registry', 'shared_capabilities', 'learning_modules', 'host_integrations'], depends: ['../gcode_core', '../flutter_study_learning', '../file_picker_bridge', '../flutter_ioc_core', '../flutterguard/packages/flutterguard_cli'], children: ['lib/AI_ANALYSIS.md', 'lib/app/AI_ANALYSIS.md', 'lib/module_registry/AI_ANALYSIS.md', 'lib/shared/AI_ANALYSIS.md', 'lib/modules/AI_ANALYSIS.md'], - validation: ['dart format .', 'flutter analyze', 'dart run flutterguard_cli:flutterguard scan --path . --fail-on high'], + validation: ['bash tool/generate_harness_ai_analysis.sh', 'dart format .', 'flutter analyze', 'dart run flutterguard_cli:flutterguard scan --path . --fail-on high'], }); writeIndex({ rel: 'lib/AI_ANALYSIS.md', id: 'main_app.lib', kind: 'source_index', - entrypoints: ['main.dart', 'app/app.dart', 'app/router/app_route_table.dart'], + entrypoints: ['main.dart', 'app/app_bootstrap.dart', 'app/app.dart', 'app/router/app_route_table.dart'], owns: ['app', 'module_registry', 'shared', 'modules'], depends: ['flutter_sdk', 'go_router', 'flutter_riverpod'], children: ['app/AI_ANALYSIS.md', 'module_registry/AI_ANALYSIS.md', 'shared/AI_ANALYSIS.md', 'modules/AI_ANALYSIS.md'], @@ -154,9 +320,9 @@ function writeLayerIndexes() { rel: 'lib/app/AI_ANALYSIS.md', id: 'main_app.app', kind: 'app_index', - entrypoints: ['app.dart', 'router/app_router.dart', 'router/app_route_table.dart'], - owns: ['material_app_router', 'router'], - depends: ['go_router', 'module_registry', 'modules'], + entrypoints: ['app.dart', 'app_bootstrap.dart', 'module_home_page.dart', 'category_navigation.dart', 'category_window_app.dart', 'router/app_router.dart', 'router/app_route_table.dart'], + owns: ['host_bootstrap', 'material_app_router', 'router', 'module_home', 'adaptive_category_navigation', 'desktop_category_window_shell'], + depends: ['go_router', 'module_registry', 'shared/multi_window', 'modules'], children: ['router/AI_ANALYSIS.md'], }); writeIndex({ @@ -164,23 +330,23 @@ function writeLayerIndexes() { id: 'main_app.app.router', kind: 'router_index', entrypoints: ['app_router.dart', 'app_route_table.dart'], - owns: ['go_router_root', 'module_route_aggregation', 'module_home_index'], - depends: ['module_registry', 'modules'], + owns: ['go_router_root', 'module_route_aggregation', 'module_catalog_composition'], + depends: ['app/module_home_page', 'module_registry', 'modules'], }); writeIndex({ rel: 'lib/module_registry/AI_ANALYSIS.md', id: 'main_app.module_registry', kind: 'registry_index', - entrypoints: ['module_entry.dart', 'module_category.dart'], - owns: ['module_entry_model', 'module_category_enum', 'difficulty_enum', 'module_status_enum'], - depends: ['flutter_material'], + entrypoints: ['module_entry.dart', 'module_category.dart', 'module_catalog_utils.dart'], + owns: ['module_entry_model', 'module_category_enum', 'difficulty_enum', 'module_status_enum', 'module_catalog_filtering', 'category_route_rebasing'], + depends: ['flutter_material', 'go_router'], }); writeIndex({ rel: 'lib/shared/AI_ANALYSIS.md', id: 'main_app.shared', kind: 'shared_index', entrypoints: ['multi_window', 'platform'], - owns: ['business_free_capabilities', 'desktop_windowing', 'platform_boundaries'], + owns: ['business_free_capabilities', 'desktop_window_lifecycle', 'platform_boundaries'], depends: ['desktop_multi_window', '../file_picker_bridge'], children: ['multi_window/AI_ANALYSIS.md', 'platform/AI_ANALYSIS.md'], }); @@ -188,9 +354,9 @@ function writeLayerIndexes() { rel: 'lib/shared/multi_window/AI_ANALYSIS.md', id: 'main_app.shared.multi_window', kind: 'shared_capability_index', - entrypoints: ['multi_window_manager.dart', 'category_window_app.dart', 'multi_window_route_filter.dart'], - owns: ['desktop_window_lifecycle', 'category_window_router', 'module_route_filter'], - depends: ['desktop_multi_window', 'go_router', 'module_registry'], + entrypoints: ['multi_window_manager.dart'], + owns: ['desktop_window_lifecycle', 'desktop_window_arguments'], + depends: ['desktop_multi_window', 'module_registry'], }); writeIndex({ rel: 'lib/shared/platform/AI_ANALYSIS.md', @@ -261,6 +427,9 @@ function writeModuleContracts() { } writeSchema(); +writeProjectContext(); +writeRefactorPlan(); +writeModuleIndex(); writeRootIndexes(); writeLayerIndexes(); writeModuleIndexes(); diff --git a/tool/generate_harness_ai_analysis.sh b/tool/generate_harness_ai_analysis.sh index f4950f9..82c57b7 100755 --- a/tool/generate_harness_ai_analysis.sh +++ b/tool/generate_harness_ai_analysis.sh @@ -3,3 +3,4 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" node "$ROOT/tool/generate_agent_indexes.js" +node "$ROOT/tool/validate_agent_docs.js" diff --git a/tool/validate_agent_docs.js b/tool/validate_agent_docs.js new file mode 100644 index 0000000..4973dfc --- /dev/null +++ b/tool/validate_agent_docs.js @@ -0,0 +1,115 @@ +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); +const failures = []; +const documents = new Map(); + +function readJson(rel) { + try { + const source = fs.readFileSync(path.join(root, rel), 'utf8'); + if (/[^\x00-\x7F]/.test(source)) failures.push(`${rel}:non_ascii_content`); + const document = JSON.parse(source); + documents.set(rel, document); + return document; + } catch (error) { + failures.push(`${rel}:invalid_json:${error.message}`); + return null; + } +} + +function collectAnalysisFiles(dir) { + const result = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name.startsWith('.') || entry.name === 'build') continue; + const absolute = path.join(dir, entry.name); + if (entry.isDirectory()) { + result.push(...collectAnalysisFiles(absolute)); + } else if (entry.name === 'AI_ANALYSIS.md') { + result.push(path.relative(root, absolute)); + } + } + return result; +} + +const machineDocuments = [ + 'AI_ANALYSIS_SCHEMA.json', + 'AI_PROJECT_CONTEXT.md', + 'REFACTOR_PLAN.md', + 'lib/AI_MODULE_INDEX.md', + ...collectAnalysisFiles(root), +]; + +const requiredAnalysisKeys = [ + 'schema', + 'mode', + 'node', + 'entrypoints', + 'owns', + 'depends', + 'children', + 'contracts', + 'validation', +]; + +for (const rel of [...new Set(machineDocuments)].sort()) { + const document = readJson(rel); + if (!document) continue; + if (path.basename(rel) !== 'AI_ANALYSIS.md') continue; + + for (const key of requiredAnalysisKeys) { + if (!(key in document)) failures.push(`${rel}:missing_key:${key}`); + } + if (document.contracts?.no_natural_language !== true) { + failures.push(`${rel}:contract:no_natural_language`); + } + if (document.contracts?.doc_consumer !== 'coding_agent') { + failures.push(`${rel}:contract:doc_consumer`); + } + if (document.contracts?.doc_mode !== 'machine_contract') { + failures.push(`${rel}:contract:doc_mode`); + } + + for (const child of document.children ?? []) { + const childPath = path.normalize(path.join(path.dirname(rel), child)); + if (!fs.existsSync(path.join(root, childPath))) { + failures.push(`${rel}:missing_child:${child}`); + } + } +} + +const moduleIndex = documents.get('lib/AI_MODULE_INDEX.md'); +if (moduleIndex) { + if (moduleIndex.count !== moduleIndex.modules?.length) { + failures.push('lib/AI_MODULE_INDEX.md:count_mismatch'); + } + const ids = new Set(); + const routes = new Set(); + for (const module of moduleIndex.modules ?? []) { + if (ids.has(module.id)) failures.push(`lib/AI_MODULE_INDEX.md:duplicate_id:${module.id}`); + if (routes.has(module.route)) failures.push(`lib/AI_MODULE_INDEX.md:duplicate_route:${module.route}`); + ids.add(module.id); + routes.add(module.route); + + const contract = documents.get(module.analysis); + if (!contract) { + failures.push(`lib/AI_MODULE_INDEX.md:missing_analysis:${module.analysis}`); + continue; + } + for (const key of ['route', 'category']) { + if (contract[key] !== module[key]) { + failures.push(`${module.analysis}:index_mismatch:${key}`); + } + } + if (contract.node?.status !== module.status) { + failures.push(`${module.analysis}:index_mismatch:status`); + } + } +} + +if (failures.length > 0) { + process.stderr.write(`${failures.join('\n')}\n`); + process.exit(1); +} + +process.stdout.write(`agent_docs_valid:${new Set(machineDocuments).size}\n`);