diff --git a/.hermes/plans/2026-07-22_171303-agent-driven-foundation.md b/.hermes/plans/2026-07-22_171303-agent-driven-foundation.md new file mode 100644 index 0000000..6e995d8 --- /dev/null +++ b/.hermes/plans/2026-07-22_171303-agent-driven-foundation.md @@ -0,0 +1,434 @@ +# Agent-Driven Development Foundation Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** 将 `flutter_study` 从“有部分 Agent 文档”升级为可复现、可验证、可审计、可由 Agent 独立闭环执行的工程。 + +**Architecture:** 保留现有 `AGENTS.md + JSON 机器契约 + 分层 AI_ANALYSIS.md` 体系,将环境、命令、质量门禁、任务输入和变更输出全部标准化。优先解决单仓无法自举和质量门禁不可运行,再补 CI、文档漂移检测、任务模板和平台验收。 + +**Tech Stack:** Flutter 3.44.6、Dart 3.12.2、Node.js 20.20.2、GitHub Actions、FlutterGuard、JSON Schema、Shell/Node.js automation。 + +--- + +## 1. 当前基线与关键结论 + +### 已具备 + +- `AGENTS.md` 已定义修改协议、新模块规则、验收规则和禁止事项。 +- `AI_ANALYSIS_SCHEMA.json`、`AI_PROJECT_CONTEXT.md`、`REFACTOR_PLAN.md` 已采用 JSON 机器契约。 +- 根、分层、分类和模块级共有 32 个 `AI_ANALYSIS.md`;`node tool/validate_agent_docs.js` 当前通过,输出 `agent_docs_valid:36`。 +- `tool/generate_agent_indexes.js` 可生成 Agent 索引,`tool/validate_agent_docs.js` 可做基础一致性校验。 +- `.githooks/pre-commit` 已存在且 `core.hooksPath=.githooks` 已启用。 +- 本机 Flutter Doctor 全绿:Flutter 3.44.6、Dart 3.12.2、Android SDK 36.1、JDK 21(Android Studio 内置)、Xcode 26.6、CocoaPods 1.16.2。 + +### 阻塞问题 + +1. **单仓无法自举。** `pubspec.yaml` 依赖 5 个仓库外相对路径;其中 `flutterguard_cli` 仍指向已不存在的 `../flutterguard/packages/flutterguard_cli`,实际包已在 `../flutterguard` 根目录。 +2. **质量门禁当前不可执行。** `flutter analyze`、`flutter test`、FlutterGuard 都在依赖解析阶段因上述路径失败。 +3. **格式基线未收敛。** `dart format` 检测到 138 个 Dart 文件中的 85 个会变化。 +4. **无 CI。** 仓库没有 `.github/workflows/`,规则仅依赖开发者本机钩子,Agent 可在未启用钩子的环境中绕过。 +5. **工具链未精确锁定。** README 只写 Flutter 3.x / Dart 3.x;项目没有 `.fvmrc`、`.nvmrc` 或统一版本检查脚本。 +6. **文档存在漂移。** `README.md` 引用了不存在的 `PLUGIN_DECOMPOSITION_PLAN.md`;`GCODE_VISUALIZER_EVOLUTION_PLAN.md` 仍使用迁移前的 `lib/gcode_visualizer/...` 路径,并记录了不可作为当前事实使用的历史测试结果。 +7. **生成源是手写清单。** `tool/generate_agent_indexes.js` 的模块、状态、依赖和分类清单与路由表重复维护,存在双源漂移。 +8. **预提交门禁不完整。** 当前只运行 FlutterGuard,没有生成文档校验、格式化、分析和测试。 +9. **FlutterGuard 配置重复。** `flutterguard.yaml` 中 `shared_purity` boundary 定义了两次。 +10. **缺少 Agent 任务/交付协议。** 没有统一任务输入模板、变更报告模板、风险等级、允许修改范围和可机读验收结果。 + +--- + +## 2. 基础文档最小集合 + +文档应分为“人类入口”“Agent 契约”“决策记录”三层,避免同一事实重复维护。 + +### 人类入口 + +- `README.md`:项目定位、快速开始、支持平台、唯一权威命令入口。 +- `CONTRIBUTING.md`:分支策略、提交规范、PR 流程、Definition of Done。 +- `docs/DEVELOPMENT.md`:环境安装、自举、常用命令、故障排查。 +- `docs/TESTING.md`:单元/Widget/Golden/集成/平台测试分层及何时必须补测试。 +- `docs/RELEASE.md`:版本号、构建产物、平台发布与回滚。 +- `SECURITY.md`:密钥处理、漏洞上报、依赖和平台权限审计。 + +### Agent 权威契约 + +- `AGENTS.md`:只保留全局行为规则、读取顺序、修改边界和统一门禁入口。 +- `AI_PROJECT_CONTEXT.md`:架构边界、平台矩阵、入口和依赖方向。 +- `REFACTOR_PLAN.md`:当前阶段、原子任务队列、依赖、状态和验收条件。 +- `AI_ANALYSIS_SCHEMA.json`:所有机器文档的 schema 与约束。 +- `lib/AI_MODULE_INDEX.md`:生成的模块索引,不手工编辑。 +- `**/AI_ANALYSIS.md`:局部契约,只描述 ownership、依赖、入口、状态和验证。 +- `docs/agent/TASK_SCHEMA.json`:Agent 任务输入格式。 +- `docs/agent/CHANGE_REPORT_SCHEMA.json`:Agent 完成报告格式。 +- `docs/agent/COMMANDS.json`:命令名、用途、是否修改文件、超时、产物和成功条件。 + +建议的任务字段: + +- `id` +- `goal` +- `scope.allow` +- `scope.deny` +- `pre_read` +- `dependencies` +- `acceptance` +- `validation` +- `risk` +- `manual_review` +- `status` + +建议的变更报告字段: + +- `task_id` +- `changed_files` +- `tests_added` +- `commands` +- `results` +- `generated_artifacts` +- `known_risks` +- `manual_verification` +- `followups` + +### 决策记录 + +- `docs/adr/README.md`:ADR 索引与状态定义。 +- `docs/adr/0001-repository-layout.md`:单仓/多仓决策。 +- `docs/adr/0002-agent-contract-source-of-truth.md`:哪些文档生成、哪些手写。 +- `docs/adr/0003-platform-capability-boundary.md`:平台 API 和 fallback 规则。 +- `docs/adr/0004-quality-gate.md`:必过检查、允许例外和例外期限。 + +ADR 只记录不可从代码直接推导的“为什么”,不要复制目录和 API 清单。 + +--- + +## 3. 推荐环境形态 + +### 首选:单仓工作区 + +将当前同级包迁入当前仓库: + +- `packages/gcode_core` +- `packages/flutter_study_learning` +- `packages/file_picker_bridge` +- `packages/flutter_ioc_core` +- `tools/flutterguard_cli`,或改用已发布且锁版本的 FlutterGuard + +同步调整: + +- `pubspec.yaml` +- `pubspec.lock` +- `tool/migrate_sibling_packages.sh`(迁移完成后删除或归档) +- `AI_PROJECT_CONTEXT.md` 的生成源 +- 所有依赖路径和相关 AI 契约 + +理由:Agent 在一个 clone 中即可获取源码、运行测试和提交原子变更;不会依赖开发机目录布局。 + +如果必须维持多仓,则将 path dependencies 改为带固定 tag/commit 的 Git dependencies,并提供 `tool/bootstrap.sh` 克隆脚本;不建议继续使用未声明的 `../` 相对目录约定。 + +### 工具链锁定 + +新增: + +- `.fvmrc`:精确锁定 Flutter `3.44.6`。 +- `.nvmrc`:锁定 Node.js `20.20.2`,供 Agent 文档生成器使用。 +- `tool/check_environment.sh`:检查 Flutter、Dart、Node、Java、Xcode/CocoaPods、Android SDK 和必要平台目录。 +- `tool/bootstrap.sh`:安装/校验工具、获取所有包依赖、启用 hooks、执行最小 smoke check。 + +不要用 Docker 作为唯一开发环境:Flutter 的 iOS/macOS 构建仍要求 macOS/Xcode。Docker 可作为纯 Dart、Web 和静态检查的补充环境。 + +--- + +## 4. 分阶段实施 + +### Task 1: 修复依赖布局并恢复可执行基线 + +**Objective:** 任意新 clone 不依赖未声明的父目录即可完成依赖解析。 + +**Files:** + +- Modify: `pubspec.yaml` +- Modify: `pubspec.lock` +- Create/Move: `packages/*` 或创建外部依赖 bootstrap 配置 +- Modify: `tool/generate_agent_indexes.js` +- Modify: `AI_PROJECT_CONTEXT.md`(通过生成器) +- Modify/Delete: `tool/migrate_sibling_packages.sh` + +**Steps:** + +1. 为“单仓工作区”与“固定 Git 依赖”做一次 ADR 决策,默认选择单仓。 +2. 先修正 FlutterGuard 的失效路径,不覆盖当前未提交的 `pubspec.yaml`/`pubspec.lock` 修改。 +3. 迁入四个共享包和 FlutterGuard,逐包执行原有测试。 +4. 执行 `flutter pub get`,确认不再访问仓库外路径。 +5. 执行 `flutter analyze`、`flutter test`、FlutterGuard,记录真实基线。 +6. 单独提交依赖布局变更,避免与格式化混在一起。 + +**Acceptance:** + +- 新 clone 只需仓库内容和标准 SDK 即可 `flutter pub get`。 +- 不存在 `path: ../...`。 +- 所有包均有明确 owner、测试入口和 Agent 契约。 + +### Task 2: 建立唯一命令入口 + +**Objective:** 人和 Agent 使用同一组脚本,不再从多份文档拼接命令。 + +**Files:** + +- Create: `tool/bootstrap.sh` +- Create: `tool/check_environment.sh` +- Create: `tool/quality_gate.sh` +- Create: `tool/test_all.sh` +- Create: `docs/agent/COMMANDS.json` +- Modify: `AGENTS.md` +- Modify: `README.md` + +**Steps:** + +1. `check_environment.sh` 输出版本、缺失组件和明确修复建议。 +2. `bootstrap.sh` 校验环境、执行依赖获取、设置 hooks,不修改业务代码。 +3. `test_all.sh` 遍历主应用与 workspace packages,逐包执行测试并保留失败码。 +4. `quality_gate.sh` 按固定顺序执行:生成文档、检测生成差异、格式化、分析、测试、FlutterGuard。 +5. 命令输出至少包含稳定的阶段标识和最终 exit code,便于 Agent 解析。 +6. `AGENTS.md` 的验收规则改为唯一命令 `bash tool/quality_gate.sh`,详细步骤只在 `COMMANDS.json` 维护。 + +**Acceptance:** + +- Agent 只需运行 bootstrap 与 quality gate 两个入口。 +- 任一步失败都返回非零状态并指明失败阶段。 +- 本地与 CI 调用完全相同的脚本。 + +### Task 3: 收敛格式和静态检查基线 + +**Objective:** 消除 85 个待格式化文件,使格式门禁具有信号价值。 + +**Files:** + +- Modify: 当前 formatter 报告的 Dart 文件 +- Modify: `analysis_options.yaml` +- Modify: `flutterguard.yaml` + +**Steps:** + +1. 在独立提交中执行 `dart format .`,不混入逻辑改动。 +2. 删除 `flutterguard.yaml` 重复的 `shared_purity` 配置。 +3. 评估并启用更严格但可渐进落地的 analyzer 规则;不要一次引入大量无关告警。 +4. 运行全量质量门禁并建立当前 FlutterGuard medium 级基线。 + +**Acceptance:** + +- 再次运行 formatter 不产生 diff。 +- `flutter analyze` 无 error。 +- FlutterGuard 无 high。 + +### Task 4: 强化机器文档生成与漂移检测 + +**Objective:** 模块清单、路由和契约不再靠重复手工维护。 + +**Files:** + +- Modify: `tool/generate_agent_indexes.js` +- Modify: `tool/validate_agent_docs.js` +- Modify: `tool/generate_harness_ai_analysis.sh` +- Create: `tool/check_generated_docs.sh` +- Modify: `lib/app/router/app_route_table.dart` 或增加可解析的 registry source + +**Steps:** + +1. 明确单一源:推荐由结构化 registry manifest 生成 Dart 路由元数据和 AI 模块索引。 +2. 验证器补充:模块目录注册覆盖、`module_entry.dart` 存在、元数据完整、route 唯一、目录/route 命名风格、教学模板依赖、父子索引一致。 +3. 生成后执行 `git diff --exit-code`,CI 中发现未提交生成物即失败。 +4. 验证所有 `.md` 机器文档确实为合法 JSON,并与 schema 版本一致。 +5. 给生成器和验证器添加 Node 单元测试或 fixture 测试。 + +**Acceptance:** + +- 新增未注册模块会失败。 +- 修改路由而不更新契约会失败。 +- 手工修改生成文件会失败。 +- 生成器连续运行两次结果完全一致。 + +### Task 5: 清理和补齐基础文档 + +**Objective:** 消除失效链接、历史事实冒充当前事实和多源命令说明。 + +**Files:** + +- Modify: `README.md` +- Create: `CONTRIBUTING.md` +- Create: `docs/DEVELOPMENT.md` +- Create: `docs/TESTING.md` +- Create: `docs/RELEASE.md` +- Create: `SECURITY.md` +- Move/Rewrite: `GCODE_VISUALIZER_EVOLUTION_PLAN.md` +- Create: `docs/adr/*.md` + +**Steps:** + +1. 删除或补回 README 中不存在的 `PLUGIN_DECOMPOSITION_PLAN.md` 引用。 +2. 将 G-code 演进计划中的旧路径更新到 `lib/modules/ui/gcode_visualizer` 和 `packages/gcode_core`;历史测试数字标记时间与 commit,或移出权威文档。 +3. README 只保留最短上手路径并链接详细文档。 +4. 在 `CONTRIBUTING.md` 定义 Definition of Done:契约更新、测试、质量门禁、UI 证据、平台影响说明。 +5. 文档增加 owner、last_verified 或 generated 标识,避免 Agent 不知道是否可编辑。 + +**Acceptance:** + +- 仓库内文档链接检查通过。 +- README 不引用不存在文件或迁移前路径。 +- 命令、版本和平台支持状态只有一个权威来源。 + +### Task 6: 建立 CI 强制门禁 + +**Objective:** 无论提交来自人还是 Agent,远端都执行同一质量门禁。 + +**Files:** + +- Create: `.github/workflows/ci.yml` +- Create: `.github/workflows/platform-smoke.yml` +- Create: `.github/dependabot.yml` +- Create: `.github/CODEOWNERS` +- Create: `.github/pull_request_template.md` +- Create: `.github/ISSUE_TEMPLATE/agent-task.yml` + +**Steps:** + +1. Linux job:文档校验、格式、analyze、全部纯 Dart/Flutter tests、FlutterGuard SARIF。 +2. macOS job:macOS build、iOS simulator smoke(阶段开启)。 +3. Windows job:Windows build 和关键插件 smoke。 +4. Android job:debug APK 与 emulator smoke;在 `REFACTOR_PLAN.md` 阻塞任务完成后设为 required。 +5. 上传 test、coverage、SARIF 和截图产物;失败时保留日志。 +6. PR 模板强制填写任务 ID、修改范围、测试结果、平台影响、截图/人工验收和剩余风险。 +7. 配置 protected branch required checks;本地 pre-commit 仅作快速反馈,CI 才是最终权威。 + +**Acceptance:** + +- 未格式化、文档漂移、analyze error、test failure、FlutterGuard high 均阻止合并。 +- Agent PR 能从 CI 产物还原验证过程。 + +### Task 7: 定义 Agent 任务与交付协议 + +**Objective:** Agent 获得边界明确、可验证、可恢复的原子任务。 + +**Files:** + +- Create: `docs/agent/TASK_SCHEMA.json` +- Create: `docs/agent/CHANGE_REPORT_SCHEMA.json` +- Create: `docs/agent/examples/*.json` +- Modify: `REFACTOR_PLAN.md` generator source +- Modify: `AGENTS.md` + +**Steps:** + +1. 每个任务声明 allow/deny paths,避免越界重构。 +2. 每个验收条件映射到真实命令或人工证据,不接受“应该可用”。 +3. 高风险任务声明 `manual_review=true`:依赖升级、平台权限、原生配置、安全、数据迁移、发布。 +4. Agent 输出结构化变更报告,并由 CI 校验 schema。 +5. 任务状态只在质量门禁通过和人工条件满足后变为 completed。 + +**Acceptance:** + +- 任一任务可在新会话中仅凭任务 JSON 和仓库契约执行。 +- 完成报告能回答改了什么、如何验证、还有什么风险。 + +### Task 8: 补齐测试与平台证据 + +**Objective:** 让“通过”不仅代表静态检查通过,还代表主要行为和目标平台可用。 + +**Files:** + +- Modify/Create: `test/**` +- Create: `integration_test/**` +- Create: `tool/capture_learning_pages.sh` 或等价 screenshot harness +- Modify: `REFACTOR_PLAN.md` generator source + +**Steps:** + +1. 为 registry/route contract、模块 entry smoke、平台 fallback 增加测试。 +2. 每个 module 至少有入口 smoke test;逻辑模块覆盖正常、边界和错误分支。 +3. 教学 UI 建立 360dp 窄屏、桌面宽屏和 text scale 基线。 +4. 对 recommended 模块保存可审计截图或 golden;避免只写“人工看过”。 +5. 对 Android/iOS/macOS/Windows 建立支持矩阵,并将 unsupported 状态呈现在模块元数据和 UI。 + +**Acceptance:** + +- 新模块没有 smoke test 或教学模板验证时 CI 失败。 +- 平台特定模块有明确支持/降级行为和测试证据。 + +--- + +## 5. 建议执行顺序 + +### P0:先恢复工程可运行 + +1. Task 1 依赖布局。 +2. Task 2 唯一命令入口。 +3. Task 3 格式与静态基线。 + +### P1:让规则不可绕过 + +4. Task 4 文档生成与漂移检测。 +5. Task 6 CI 强制门禁。 +6. Task 5 文档清理与 ADR。 + +### P2:提高 Agent 自主闭环能力 + +7. Task 7 任务/报告 schema。 +8. Task 8 测试、平台和视觉证据。 + +--- + +## 6. 验证命令 + +实施完成后的目标入口: + +```bash +bash tool/check_environment.sh +bash tool/bootstrap.sh +bash tool/quality_gate.sh +``` + +`tool/quality_gate.sh` 内部至少执行: + +```bash +bash tool/generate_harness_ai_analysis.sh +git diff --exit-code -- AI_ANALYSIS_SCHEMA.json AI_PROJECT_CONTEXT.md REFACTOR_PLAN.md lib/**/AI_ANALYSIS.md lib/AI_MODULE_INDEX.md +dart format . +git diff --exit-code -- '*.dart' +flutter analyze +bash tool/test_all.sh +dart run flutterguard_cli:flutterguard scan --path . --fail-on high +``` + +平台阶段再执行: + +```bash +flutter build macos +flutter build windows +flutter build apk --debug +flutter test integration_test +``` + +Windows build 必须在 Windows runner 执行;Android/iOS smoke 必须使用对应 emulator/simulator。 + +--- + +## 7. 风险与边界 + +- 当前 `pubspec.yaml` 和 `pubspec.lock` 有用户未提交修改;实施时必须先确认并保留,不能用 checkout 覆盖。 +- 单仓迁移会影响多个原独立仓库的历史、发布方式和 CI,应先做 ADR,再移动文件。 +- formatter 会改动 85 个文件,必须独立提交,避免掩盖逻辑 diff。 +- CI 平台矩阵成本较高;先建立 Linux 核心门禁,再逐步把 macOS/Windows/Android 设为 required。 +- 不应将所有知识写进 `AGENTS.md`。全局规则放 AGENTS,机器事实放 JSON 契约,决策原因放 ADR,操作步骤放 DEVELOPMENT/TESTING。 +- 不建议允许 Agent 自动提交、推送、合并或发布;这些应作为显式授权动作,并由 branch protection/环境审批控制。 + +--- + +## 8. 完成定义 + +项目达到“Agent 开发驱动”基础标准时,应同时满足: + +- 新机器单次 clone 后可通过脚本完成自举。 +- Agent 无需猜测工具版本、依赖目录、入口、架构边界和验证命令。 +- 所有权威机器文档可校验,生成物不可漂移。 +- 本地与 CI 使用同一质量门禁。 +- 任务输入和完成报告可机读、可追踪。 +- 每个代码变更都有自动测试或明确人工证据。 +- 平台支持状态显式,不支持的平台有可见 fallback。 +- 任何规则失败都会阻止合并,而不是只写在文档里。 diff --git a/.hermes/plans/2026-07-22_174807-shared-dependency-migration-codex-tasks.md b/.hermes/plans/2026-07-22_174807-shared-dependency-migration-codex-tasks.md new file mode 100644 index 0000000..ab327e6 --- /dev/null +++ b/.hermes/plans/2026-07-22_174807-shared-dependency-migration-codex-tasks.md @@ -0,0 +1,559 @@ +# Shared Dependency Migration — Codex Task Queue + +Decision already accepted for planning purposes: + +- Internal shared packages move under `packages/`. +- Dart Pub Workspace manages internal packages. +- Repository-external `../` path dependencies are forbidden. +- `flutterguard_cli` becomes an immutable Git dependency. +- Sibling repositories are read-only migration sources. + +Execution rules: + +- Execute tasks in the listed order. +- Use a fresh Codex context for every task. +- Read `AGENTS.md` before every modifying task. +- Preserve all pre-existing working-tree changes. +- Never use `git reset`, `git checkout`, `git stash`, history rewriting, commit, push, merge, or publish. +- If a required file is not in `allowed_files`, stop and report `blocked` instead of extending scope. +- Do not combine migration with formatting or package-internal refactoring. + +## Ordered Queue + +| Order | Task ID | Risk | Depends on | +|---|---|---|---| +| 1 | MIG-P0-001 | Low | none | +| 2 | MIG-P0-002 | Low | MIG-P0-001 | +| 3 | MIG-P0-003 | Low | MIG-P0-002 | +| 4 | MIG-P0-004 | Low | MIG-P0-002 | +| 5 | MIG-P0-005 | Low | MIG-P0-002 | +| 6 | MIG-P0-006 | Low | MIG-P0-002 | +| 7 | MIG-P0-007 | Medium | MIG-P0-003..006 | +| 8 | MIG-P0-008 | Medium | MIG-P0-001 | +| 9 | MIG-P0-009 | Low | MIG-P0-007, MIG-P0-008 | +| 10 | MIG-P0-010 | Medium | MIG-P0-007, MIG-P0-009 | +| 11 | MIG-P0-011 | Low | MIG-P0-007..010 | +| 12 | MIG-P0-012 | Low, read-only | MIG-P0-011 | + +--- + +```yaml +task_id: MIG-P0-001 +objective: Build a read-only inventory of the current dependency graph and migration inputs. +background: > + The governance decision is already made. This task does not compare alternatives and + does not modify the repository. It identifies every current path dependency, package + manifest, package type, direct dependency, test command, source location, and path + reference that later migration tasks must update. +allowed_files: [] +forbidden_changes: + - Do not modify any file in the current repository. + - Do not modify sibling repositories. + - Do not run pub get, formatter, code generation, or any command that can update lockfiles. + - Do not reassess or replace the accepted governance decision. +implementation_steps: + - Read AGENTS.md, AI_PROJECT_CONTEXT.md, REFACTOR_PLAN.md, pubspec.yaml, and pubspec.lock. + - Inspect the pubspec.yaml and public entrypoint of gcode_core. + - Inspect the pubspec.yaml and public entrypoint of flutter_study_learning. + - Inspect the pubspec.yaml and public entrypoint of file_picker_bridge. + - Inspect the pubspec.yaml and public entrypoint of flutter_ioc_core. + - Inspect the pubspec.yaml, executable entrypoint, current Git remote, tags, and HEAD commit of flutterguard_cli. + - Enumerate all tracked pubspec files and record every path dependency containing ../. + - Search tracked scripts, Agent contracts, and human documents for references to the five sibling paths. + - Classify each package as pure Dart, Flutter package, Flutter plugin/bridge, or CLI according to its actual manifest and source. + - Record the package-specific dependency-fetch, analyze, and test commands. + - Return the inventory in the task execution report; create no report file in the repository. +acceptance_criteria: + - All five current sibling dependencies appear in the inventory. + - Every tracked external ../ path dependency is listed with its declaring file. + - Each package has a recorded source path, target path, package type, public entrypoint, and test command. + - flutterguard_cli has a recorded immutable tag candidate or exact commit candidate. + - Initial and final git status are identical. +validation_commands: + - git status --short --branch + - git ls-files '*pubspec.yaml' + - git diff --exit-code + - git status --short +``` + +--- + +```yaml +task_id: MIG-P0-002 +objective: Create the empty canonical packages/ container without changing dependency resolution. +background: > + Internal packages will be imported by later isolated tasks. Git does not retain empty + directories, so this task creates only a temporary tracked marker. It must not copy + package source or edit any manifest. +allowed_files: + - packages/.gitkeep +forbidden_changes: + - Do not modify pubspec.yaml or pubspec.lock. + - Do not create package subdirectories yet. + - Do not modify lib/, test/, tool/, Agent documents, platform projects, or sibling repositories. + - Do not add workspace configuration. +implementation_steps: + - Read AGENTS.md and confirm packages/ does not already contain user-owned content. + - Create packages/ if absent. + - Add only packages/.gitkeep. + - Confirm the new container does not alter dependency resolution or tracked source. +acceptance_criteria: + - packages/ exists. + - packages/.gitkeep is the only changed file created by this task. + - Root pubspec.yaml and pubspec.lock are unchanged. +validation_commands: + - git status --short + - test -d packages + - test -f packages/.gitkeep + - git diff --check +``` + +--- + +```yaml +task_id: MIG-P0-003 +objective: Import gcode_core into packages/gcode_core without changing its behavior or manifest semantics. +background: > + ../gcode_core is a read-only migration source. The package currently has Flutter in + its manifest, so this task must treat it according to its actual source rather than + assuming it is pure Dart. Workspace activation and root dependency changes happen later. +allowed_files: + - packages/gcode_core/** +forbidden_changes: + - Do not modify ../gcode_core/**. + - Do not modify root pubspec.yaml or pubspec.lock. + - Do not modify package APIs, imports, SDK constraints, dependencies, tests, or formatting. + - Do not copy .git/, .dart_tool/, build/, IDE state, logs, credentials, or generated caches. + - Do not copy a legacy AI_ANALYSIS.md that violates the current machine-document schema; Agent context is handled by MIG-P0-010. +implementation_steps: + - Read AGENTS.md and inspect the source package manifest, public entrypoint, source, tests, README, license, and gitignore. + - Confirm packages/gcode_core does not contain user-owned files. + - Copy the package into packages/gcode_core while applying the exclusion rules. + - Preserve file contents and executable bits; do not reformat. + - Compare source and destination manifests, public entrypoints, lib/, and test/ trees. + - Fetch package dependencies from the destination. + - Run the destination package's actual analyze and test commands. +acceptance_criteria: + - packages/gcode_core contains its manifest, public API, source, tests, and applicable documentation/license. + - Source and destination functional files are identical before workspace-specific edits. + - No excluded cache, repository metadata, credential, or legacy incompatible Agent document is copied. + - Destination dependency resolution, analysis, and tests succeed. + - The sibling source repository is unchanged. +validation_commands: + - flutter pub get + - flutter analyze + - flutter test + - diff -qr ../gcode_core/lib packages/gcode_core/lib + - diff -qr ../gcode_core/test packages/gcode_core/test + - git diff --check + - git status --short +``` + +--- + +```yaml +task_id: MIG-P0-004 +objective: Import flutter_study_learning into packages/flutter_study_learning without changing behavior or manifest semantics. +background: > + This package provides shared Flutter teaching widgets and is an internal application-owned + package. This task copies only this package; workspace and root dependency activation are deferred. +allowed_files: + - packages/flutter_study_learning/** +forbidden_changes: + - Do not modify ../flutter_study_learning/**. + - Do not modify root pubspec.yaml or pubspec.lock. + - Do not modify APIs, imports, SDK constraints, dependencies, tests, or formatting. + - Do not copy .git/, .dart_tool/, build/, IDE state, logs, credentials, or generated caches. + - Do not copy a legacy incompatible AI_ANALYSIS.md. +implementation_steps: + - Read AGENTS.md and inspect the source manifest, public entrypoint, lib/, test/, README, license, and gitignore. + - Confirm the destination does not contain user-owned files. + - Copy the package to packages/flutter_study_learning with exclusions. + - Preserve file contents and executable bits; do not reformat. + - Compare functional source and test trees. + - Fetch dependencies and run destination package analysis/tests. +acceptance_criteria: + - The destination contains the package manifest, public entrypoint, source, tests, and applicable documentation/license. + - Functional files match the sibling source before workspace-specific edits. + - Destination analysis/tests succeed. + - No excluded files or incompatible Agent document is copied. + - The sibling source repository is unchanged. +validation_commands: + - flutter pub get + - flutter analyze + - flutter test + - diff -qr ../flutter_study_learning/lib packages/flutter_study_learning/lib + - if both test directories exist, diff -qr ../flutter_study_learning/test packages/flutter_study_learning/test + - git diff --check + - git status --short +``` + +--- + +```yaml +task_id: MIG-P0-005 +objective: Import file_picker_bridge into packages/file_picker_bridge without changing behavior or platform ownership. +background: > + file_picker_bridge currently supplies a Flutter-side bridge while host platform + registration may remain in the main application. This task is source relocation only; + it must not convert the package into a full plugin or change platform support claims. +allowed_files: + - packages/file_picker_bridge/** +forbidden_changes: + - Do not modify ../file_picker_bridge/**. + - Do not modify root pubspec.yaml or pubspec.lock. + - Do not modify macos/, windows/, Android/iOS files, channel names, APIs, tests, or formatting. + - Do not create plugin scaffolding. + - Do not copy .git/, .dart_tool/, build/, IDE state, logs, credentials, generated caches, or a legacy incompatible AI_ANALYSIS.md. +implementation_steps: + - Read AGENTS.md and inspect the source manifest, public entrypoint, channels, lib/, test/, README, license, and gitignore. + - Confirm the destination does not contain user-owned files. + - Copy the package to packages/file_picker_bridge with exclusions. + - Preserve file contents and executable bits; do not reformat. + - Compare functional source and tests. + - Fetch dependencies and run destination package analysis/tests. +acceptance_criteria: + - The destination contains the package manifest, public entrypoint, source, tests, and applicable documentation/license. + - Existing MethodChannel/API identifiers are unchanged. + - No plugin conversion or platform-host modification occurs. + - Destination analysis/tests succeed. + - The sibling source repository is unchanged. +validation_commands: + - flutter pub get + - flutter analyze + - flutter test + - diff -qr ../file_picker_bridge/lib packages/file_picker_bridge/lib + - diff -qr ../file_picker_bridge/test packages/file_picker_bridge/test + - git diff --check + - git status --short +``` + +--- + +```yaml +task_id: MIG-P0-006 +objective: Import flutter_ioc_core into packages/flutter_ioc_core without changing its pure Dart behavior. +background: > + flutter_ioc_core is an application-owned pure Dart package. This task relocates it + unchanged and verifies it independently before workspace activation. +allowed_files: + - packages/flutter_ioc_core/** +forbidden_changes: + - Do not modify ../flutter_ioc_core/**. + - Do not modify root pubspec.yaml or pubspec.lock. + - Do not add Flutter dependencies. + - Do not modify APIs, SDK constraints, dependencies, tests, or formatting. + - Do not copy .git/, .dart_tool/, build/, IDE state, logs, credentials, generated caches, or a legacy incompatible AI_ANALYSIS.md. +implementation_steps: + - Read AGENTS.md and inspect the source manifest, public entrypoint, lib/, test/, README, license, and gitignore. + - Confirm the destination does not contain user-owned files. + - Copy the package to packages/flutter_ioc_core with exclusions. + - Preserve file contents and executable bits; do not reformat. + - Compare functional source and tests. + - Fetch dependencies and run destination package analysis/tests using Dart commands. +acceptance_criteria: + - The destination contains the manifest, public entrypoint, source, tests, and applicable documentation/license. + - The package remains pure Dart. + - Functional files match the sibling source before workspace-specific edits. + - Destination Dart analysis/tests succeed. + - The sibling source repository is unchanged. +validation_commands: + - dart pub get + - dart analyze + - dart test + - diff -qr ../flutter_ioc_core/lib packages/flutter_ioc_core/lib + - if both test directories exist, diff -qr ../flutter_ioc_core/test packages/flutter_ioc_core/test + - git diff --check + - git status --short +``` + +--- + +```yaml +task_id: MIG-P0-007 +objective: Activate Dart Pub Workspace and switch the four internal dependencies to repository-local packages/ paths. +background: > + All four packages have already been copied and independently verified. This task makes + packages/ canonical by configuring the native workspace, adjusting only manifests, + regenerating the lockfile, and removing the temporary directory marker. +allowed_files: + - pubspec.yaml + - pubspec.lock + - packages/.gitkeep + - packages/gcode_core/pubspec.yaml + - packages/flutter_study_learning/pubspec.yaml + - packages/file_picker_bridge/pubspec.yaml + - packages/flutter_ioc_core/pubspec.yaml +forbidden_changes: + - Do not modify Dart source, tests, public APIs, package names, or package versions. + - Do not modify flutterguard_cli dependency in this task. + - Do not modify tool scripts, Agent documents, human documents, platform projects, or sibling repositories. + - Do not upgrade unrelated hosted dependencies. +implementation_steps: + - Read AGENTS.md and preserve all pre-existing root manifest/lockfile changes. + - Confirm the active Dart SDK supports native Pub Workspace. + - Add the four packages to the root workspace declaration using supported syntax. + - Add the required workspace-resolution declaration to each member manifest. + - Raise member SDK lower bounds only when required by the selected native workspace format; do not otherwise widen or upgrade constraints. + - Change root internal path dependencies from ../name to packages/name. + - Leave flutterguard_cli unchanged for MIG-P0-008. + - Remove packages/.gitkeep after real package directories are tracked. + - Run root dependency resolution and inspect lockfile changes for unrelated upgrades. + - Run dependency resolution, analysis, and tests through the workspace and per package as needed. +acceptance_criteria: + - Root manifest declares all four internal workspace members. + - Each member participates in workspace resolution. + - All four root internal dependencies resolve from packages/ and contain no ../ path. + - flutterguard_cli remains unchanged for the next task. + - Root and member dependency resolution succeeds. + - Root and package analysis/tests succeed, or any proven pre-existing failure is reported without being modified. + - No source or test file changes. +validation_commands: + - flutter pub get + - dart pub workspace list or the supported native workspace listing command + - flutter analyze + - flutter test + - run flutter analyze/flutter test in each Flutter member when not covered by workspace tooling + - run dart analyze/dart test in flutter_ioc_core + - inspect git diff for pubspec.yaml, pubspec.lock, and member manifests only + - git diff --check + - git status --short +``` + +--- + +```yaml +task_id: MIG-P0-008 +objective: Replace the flutterguard_cli external path dependency with an immutable Git tag or commit. +background: > + flutterguard_cli remains independently governed. The current local package version is + 0.7.1 and declares a public repository. This task must select an existing immutable tag + that resolves to the intended source; if no suitable tag exists, it must pin an exact + commit SHA. A branch name is forbidden. +allowed_files: + - pubspec.yaml + - pubspec.lock +forbidden_changes: + - Do not modify internal package dependencies or workspace declarations. + - Do not modify ../flutterguard/** or any remote repository. + - Do not create or push a Git tag. + - Do not use main, master, dev, HEAD, or another moving branch as ref. + - Do not upgrade unrelated dependencies. + - Do not modify source, tests, scripts, Agent documents, or platform files. +implementation_steps: + - Read AGENTS.md and capture the existing root manifest/lockfile diff. + - Verify the repository URL declared by flutterguard_cli. + - Fetch or inspect remote tags without modifying the remote. + - Prefer an existing release tag corresponding to the intended 0.7.1 source. + - Verify the selected tag resolves to an exact commit and contains package name flutterguard_cli plus bin/flutterguard.dart. + - If no valid immutable tag exists, select the exact verified commit SHA. + - Replace only the flutterguard_cli path dependency with the immutable Git source/ref. + - Run root dependency resolution and inspect the lockfile's resolved Git revision. + - Run FlutterGuard help and scan commands. +acceptance_criteria: + - flutterguard_cli is a Git dependency pinned to an immutable tag or commit. + - No moving branch ref is used. + - pubspec.lock records the resolved immutable revision. + - dart run can resolve and execute FlutterGuard. + - No unrelated dependency declaration changes. + - ../flutterguard remains unchanged. +validation_commands: + - git ls-remote --tags https://github.com/lizy-coding/flutterguard.git + - flutter pub get + - dart run flutterguard_cli:flutterguard --help + - dart run flutterguard_cli:flutterguard scan --path . --fail-on high + - inspect git diff for pubspec.yaml and pubspec.lock only + - git diff --check + - git status --short +``` + +--- + +```yaml +task_id: MIG-P0-009 +objective: Update migration and maintenance scripts so they no longer depend on or recreate the legacy sibling layout. +background: > + The canonical internal package location is now packages/. Existing one-time migration + tooling may contain ../ paths, destructive copy operations, or instructions that could + overwrite the new canonical packages. This task updates or retires only those scripts. +allowed_files: + - tool/migrate_sibling_packages.sh + - tool/generate_harness_ai_analysis.sh + - tool/validate_agent_docs.js +forbidden_changes: + - Do not modify tool/generate_agent_indexes.js in this task; Agent context paths are handled by MIG-P0-010. + - Do not modify package source, manifests, lockfile, application source, tests, Agent documents, human documents, hooks, or platform projects. + - Do not execute the legacy migration script against sibling repositories. +implementation_steps: + - Read AGENTS.md and inspect every current tool script for ../ package paths. + - Identify scripts that are one-time migration utilities versus active validation tools. + - Retire tool/migrate_sibling_packages.sh safely by deleting it or replacing it with a non-destructive failure message, according to repository convention. + - Remove stale package-path assumptions from the other allowed scripts only if they exist. + - Preserve current document generation and validation behavior. + - Do not add package orchestration or P1 quality-gate functionality. +acceptance_criteria: + - No active allowed script reads, writes, deletes, or recreates the five legacy sibling dependency paths. + - The retired migration script cannot overwrite packages/ or sibling repositories. + - Agent document validation still runs successfully against the current pre-context-update state, or any expected temporary failure is explicitly tied to MIG-P0-010. + - Shell scripts pass syntax validation and Node scripts parse. +validation_commands: + - bash -n tool/migrate_sibling_packages.sh if the file remains + - bash -n tool/generate_harness_ai_analysis.sh + - node --check tool/validate_agent_docs.js + - search tool/ for the five legacy ../ dependency paths + - git diff --check + - git status --short +``` + +--- + +```yaml +task_id: MIG-P0-010 +objective: Update generated Agent Context so repository ownership and dependency paths match the new workspace layout. +background: > + Agent machine documents are generated JSON contracts despite .md extensions. The generator + remains the authoritative edit point. This task updates dependency ownership, paths, package + nodes, and validation coverage, then regenerates all affected contracts deterministically. +allowed_files: + - tool/generate_agent_indexes.js + - tool/validate_agent_docs.js + - AI_ANALYSIS_SCHEMA.json + - AI_PROJECT_CONTEXT.md + - REFACTOR_PLAN.md + - AI_ANALYSIS.md + - lib/AI_ANALYSIS.md + - lib/AI_MODULE_INDEX.md + - lib/**/AI_ANALYSIS.md + - packages/gcode_core/AI_ANALYSIS.md + - packages/flutter_study_learning/AI_ANALYSIS.md + - packages/file_picker_bridge/AI_ANALYSIS.md + - packages/flutter_ioc_core/AI_ANALYSIS.md +forbidden_changes: + - Do not modify source code, tests, manifests, lockfile, README, DEVELOPMENT docs, hooks, or platform projects. + - Do not hand-edit generated machine documents without making the corresponding generator-source change. + - Do not add natural-language Markdown to machine-contract files. + - Do not describe gcode_core as pure Dart if its actual manifest still depends on Flutter. + - Do not claim file_picker_bridge is a complete plugin unless its actual package structure proves it. +implementation_steps: + - Read AGENTS.md, schema, project context, refactor plan, root index, package manifests, and accepted repository-layout ADR. + - Replace generated dependency references from sibling paths to packages/ paths. + - Represent flutterguard_cli as an immutable external Git tool dependency rather than an internal sibling package. + - Add machine-contract nodes for the four internal workspace packages with accurate package types, ownership, entrypoints, dependencies, and validation commands. + - Extend schema/validator coverage only as needed for package-level contracts. + - Keep module route and status data unchanged. + - Run the generator and validator. + - Run the generator a second time and verify deterministic zero diff. + - Search generated contracts for all five forbidden sibling paths. +acceptance_criteria: + - All generated Agent contracts use packages/ for internal shared packages. + - flutterguard_cli is represented as an external immutable tool dependency. + - Each internal package has a valid machine contract. + - Contracts match actual package manifests and do not repeat stale boundary claims. + - No forbidden ../ dependency path remains in generated Agent documents. + - Generator output is deterministic. + - Agent document validation passes. +validation_commands: + - bash tool/generate_harness_ai_analysis.sh + - node tool/validate_agent_docs.js + - run generator a second time + - git diff --exit-code after the second generator run, relative to first generated state + - search AI_ANALYSIS_SCHEMA.json, AI_PROJECT_CONTEXT.md, REFACTOR_PLAN.md, AI_ANALYSIS.md, lib/, and packages/ Agent contracts for legacy sibling paths + - git diff --check + - git status --short +``` + +--- + +```yaml +task_id: MIG-P0-011 +objective: Update human-facing path references to the canonical packages/ layout and immutable FlutterGuard source policy. +background: > + Machine Agent Context is already updated. This low-risk documentation task changes only + human-facing path and setup references made stale by the migration. It must not broaden + platform support claims or rewrite unrelated roadmap content. +allowed_files: + - README.md + - GCODE_VISUALIZER_EVOLUTION_PLAN.md + - docs/DEVELOPMENT.md + - docs/adr/0001-repository-layout.md +forbidden_changes: + - Do not modify machine Agent documents, generator source, manifests, lockfile, source code, tests, scripts, hooks, or platform projects. + - Do not change the accepted decision in ADR 0001; only mark implementation status and correct paths. + - Do not claim a platform or package boundary that has not been validated. +implementation_steps: + - Read AGENTS.md, the updated machine context, accepted ADR, root manifest, and package manifests. + - Replace the four internal sibling paths with packages/ paths. + - Replace the local FlutterGuard path description with the immutable Git dependency policy. + - Update setup and package-layout examples. + - Correct G-code paths while preserving the document's feature scope. + - Mark ADR implementation status without altering its accepted decision. + - Verify every referenced local path exists. +acceptance_criteria: + - No modified human document instructs users to create the old sibling layout. + - All four internal package paths resolve inside packages/. + - FlutterGuard is described as an immutable external dependency. + - All local document links and paths exist. + - No source or machine-contract file changes. +validation_commands: + - search modified documents for ../gcode_core, ../flutter_study_learning, ../file_picker_bridge, ../flutter_ioc_core, and ../flutterguard + - verify every backticked local path in modified documents exists + - git diff --check + - git status --short +``` + +--- + +```yaml +task_id: MIG-P0-012 +objective: Perform a read-only end-to-end verification of the completed shared dependency migration. +background: > + This task validates the migration and creates no fixes. Any failure must be returned as + a blocker with evidence. Verification must distinguish pre-existing working-tree changes + from changes created by validation commands. +allowed_files: [] +forbidden_changes: + - Do not modify any tracked or untracked project file. + - Do not run repository-wide formatter in place. + - Do not regenerate Agent documents; validate the committed/generated state as found. + - Do not repair failures. + - Do not modify sibling or remote repositories. +implementation_steps: + - Capture initial git status and diff. + - Verify root workspace membership and package resolution. + - Verify all declared local path dependencies resolve inside the repository. + - Verify no tracked pubspec uses a ../ path dependency. + - Verify flutterguard_cli uses an immutable Git tag or commit and lockfile contains the resolved revision. + - Run Agent document validation without regeneration. + - Run root analysis and tests. + - Run each internal package's analysis/tests. + - Run FlutterGuard help and high-severity scan. + - Verify tool scripts contain no active legacy sibling path operations. + - Verify human and machine documents contain no stale sibling dependency paths. + - Compare final status/diff with initial state and report any validation-created mutation. +acceptance_criteria: + - Root dependency resolution succeeds from the repository checkout. + - The four internal packages are workspace members under packages/. + - No tracked pubspec contains an external ../ path dependency. + - flutterguard_cli resolves from an immutable Git revision. + - Root analyze and tests pass. + - Every internal package analyze/test command passes. + - FlutterGuard scan passes with no high-severity issue. + - Agent document validation passes. + - No active script or current document depends on the old sibling layout. + - Verification creates no repository change. +validation_commands: + - git status --short --branch + - flutter pub get + - dart pub workspace list or the supported native workspace listing command + - search all tracked pubspec.yaml files for path values containing ../ + - node tool/validate_agent_docs.js + - flutter analyze + - flutter test + - run flutter analyze/flutter test for each Flutter workspace package + - run dart analyze/dart test for flutter_ioc_core + - dart run flutterguard_cli:flutterguard --help + - dart run flutterguard_cli:flutterguard scan --path . --fail-on high + - search active scripts and current documents for the five legacy sibling paths + - git diff --check + - git status --short +``` 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..dc5193f 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" ], @@ -21,31 +22,36 @@ "host_integrations" ], "depends": [ - "../gcode_core", - "../flutter_study_learning", - "../file_picker_bridge", - "../flutter_ioc_core", - "../flutterguard/packages/flutterguard_cli" + "packages/gcode_core", + "packages/flutter_study_learning", + "packages/file_picker_bridge", + "packages/flutter_ioc_core", + "git:https://github.com/lizy-coding/flutterguard.git#9f9be84a73dc4b99a956a8529b8c334849566b03" ], "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" + "lib/modules/AI_ANALYSIS.md", + "packages/gcode_core/AI_ANALYSIS.md", + "packages/flutter_study_learning/AI_ANALYSIS.md", + "packages/file_picker_bridge/AI_ANALYSIS.md", + "packages/flutter_ioc_core/AI_ANALYSIS.md" ], "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 }, "validation": [ + "bash tool/generate_harness_ai_analysis.sh", "dart format .", "flutter analyze", - "dart run flutterguard_cli:flutterguard scan --path . --fail-on high" + "dart run flutterguard_cli:flutterguard scan . --fail-on high" ] } diff --git a/AI_ANALYSIS_SCHEMA.json b/AI_ANALYSIS_SCHEMA.json index f78705c..75227e2 100644 --- a/AI_ANALYSIS_SCHEMA.json +++ b/AI_ANALYSIS_SCHEMA.json @@ -1,12 +1,25 @@ { - "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" ], + "package_contract": [ + "packages/gcode_core/AI_ANALYSIS.md", + "packages/flutter_study_learning/AI_ANALYSIS.md", + "packages/file_picker_bridge/AI_ANALYSIS.md", + "packages/flutter_ioc_core/AI_ANALYSIS.md" + ], "section": [ "lib/AI_ANALYSIS.md", "lib/app/AI_ANALYSIS.md", @@ -51,8 +64,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, @@ -68,5 +81,21 @@ "long_file_inventory", "natural_language_notes" ] + }, + "package_contract_policy": { + "required_for_workspace_member": true, + "content": [ + "package_type", + "workspace", + "entrypoints", + "owns", + "depends", + "validation", + "test_status" + ], + "avoid": [ + "platform_claims_not_proven_by_manifest", + "natural_language_notes" + ] } } diff --git a/AI_PROJECT_CONTEXT.md b/AI_PROJECT_CONTEXT.md index 582b2e8..a303c81 100644 --- a/AI_PROJECT_CONTEXT.md +++ b/AI_PROJECT_CONTEXT.md @@ -1,92 +1,175 @@ -# 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" + }, + "repository": { + "layout": "pub_workspace", + "workspace_root": ".", + "members": [ + "packages/gcode_core", + "packages/flutter_study_learning", + "packages/file_picker_bridge", + "packages/flutter_ioc_core" + ], + "resolution_status": "blocked", + "resolution_blocker": "test_analyzer_flutter_sdk_pin_conflict" + }, + "internal_packages": [ + { + "name": "gcode_core", + "type": "flutter_package", + "path": "packages/gcode_core", + "entrypoint": "lib/gcode_core.dart" + }, + { + "name": "flutter_study_learning", + "type": "flutter_package", + "path": "packages/flutter_study_learning", + "entrypoint": "lib/flutter_study_learning.dart" + }, + { + "name": "file_picker_bridge", + "type": "flutter_bridge_package", + "path": "packages/file_picker_bridge", + "entrypoint": "lib/file_picker_bridge.dart" + }, + { + "name": "flutter_ioc_core", + "type": "dart_package", + "path": "packages/flutter_ioc_core", + "entrypoint": "lib/flutter_ioc_core.dart" + } + ], + "external_tools": [ + { + "package": "flutterguard_cli", + "source": "git", + "url": "https://github.com/lizy-coding/flutterguard.git", + "ref": "9f9be84a73dc4b99a956a8529b8c334849566b03", + "immutable": true, + "lock_status": "pending_dependency_resolution" + } + ], + "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 . --fail-on high" + ] + } +} diff --git a/REFACTOR_PLAN.md b/REFACTOR_PLAN.md index 1f0ba41..4cb8d6f 100644 --- a/REFACTOR_PLAN.md +++ b/REFACTOR_PLAN.md @@ -1,158 +1,123 @@ -# 整改计划 - -> 项目重构与代码质量提升计划。优先级从高到低排列。 - -## 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", + "workspace_package_import" + ], + "dependency_migration": { + "layout": "pub_workspace", + "internal_packages": [ + "packages/gcode_core", + "packages/flutter_study_learning", + "packages/file_picker_bridge", + "packages/flutter_ioc_core" + ], + "workspace_resolution_status": "blocked", + "workspace_resolution_blocker": "test_analyzer_flutter_sdk_pin_conflict", + "external_tool": { + "package": "flutterguard_cli", + "source": "git", + "url": "https://github.com/lizy-coding/flutterguard.git", + "ref": "9f9be84a73dc4b99a956a8529b8c334849566b03", + "immutable": true, + "lock_status": "pending_dependency_resolution" + } + }, + "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..fe1bd85 100644 --- a/lib/shared/AI_ANALYSIS.md +++ b/lib/shared/AI_ANALYSIS.md @@ -14,12 +14,12 @@ ], "owns": [ "business_free_capabilities", - "desktop_windowing", + "desktop_window_lifecycle", "platform_boundaries" ], "depends": [ "desktop_multi_window", - "../file_picker_bridge" + "packages/file_picker_bridge" ], "children": [ "multi_window/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/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..c08b653 100644 --- a/lib/shared/platform/AI_ANALYSIS.md +++ b/lib/shared/platform/AI_ANALYSIS.md @@ -16,7 +16,7 @@ "host_channel_registry" ], "depends": [ - "../file_picker_bridge", + "packages/file_picker_bridge", "macos/Runner/AppDelegate.swift" ], "children": [], @@ -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/packages/file_picker_bridge/.gitignore b/packages/file_picker_bridge/.gitignore new file mode 100644 index 0000000..a68bac3 --- /dev/null +++ b/packages/file_picker_bridge/.gitignore @@ -0,0 +1,19 @@ +# Dart/Flutter generated files +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +build/ + +# IDE and editor files +.idea/ +*.iml +.vscode/ + +# OS metadata +.DS_Store +Thumbs.db + +# Logs and coverage +*.log +coverage/ diff --git a/packages/file_picker_bridge/AI_ANALYSIS.md b/packages/file_picker_bridge/AI_ANALYSIS.md new file mode 100644 index 0000000..b586232 --- /dev/null +++ b/packages/file_picker_bridge/AI_ANALYSIS.md @@ -0,0 +1,44 @@ +{ + "schema": "vibecoding.harness.ai_analysis.v2", + "mode": "package_contract", + "node": { + "id": "flutter_study.workspace.file_picker_bridge", + "kind": "flutter_bridge_package", + "package": "file_picker_bridge", + "path": "packages/file_picker_bridge", + "status": "active" + }, + "package_type": "flutter_bridge_package", + "workspace": { + "member": true, + "resolution": "workspace", + "resolution_status": "blocked", + "resolution_blocker": "test_analyzer_flutter_sdk_pin_conflict" + }, + "entrypoints": [ + "lib/file_picker_bridge.dart" + ], + "owns": [ + "file_picker_api", + "method_channel_client" + ], + "depends": [ + "flutter_sdk" + ], + "children": [], + "contracts": { + "no_natural_language": true, + "index_only": true, + "max_index_depth": 2, + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", + "update_required_on_file_change": true, + "import_direction_enforced": true + }, + "validation": [ + "flutter pub get", + "flutter analyze", + "flutter test" + ], + "test_status": "configured" +} diff --git a/packages/file_picker_bridge/README.md b/packages/file_picker_bridge/README.md new file mode 100644 index 0000000..deb291a --- /dev/null +++ b/packages/file_picker_bridge/README.md @@ -0,0 +1,11 @@ +# file_picker_bridge + +Platform file picker bridge API for adapting macOS and Windows file manager integrations. + +Host applications register platform implementations through the `file_picker_bridge/file_picker` MethodChannel. This package owns the Dart API and mock-friendly channel client used to bridge Flutter code with native macOS and Windows file selection behavior. + +## Roadmap + +- Add and maintain macOS and Windows host file picking support while keeping the Dart API stable. +- Normalize returned file metadata across macOS and Windows. +- Keep MethodChannel tests mock-friendly so platform behavior can be verified without native UI. diff --git a/packages/file_picker_bridge/lib/file_picker_bridge.dart b/packages/file_picker_bridge/lib/file_picker_bridge.dart new file mode 100644 index 0000000..ad89817 --- /dev/null +++ b/packages/file_picker_bridge/lib/file_picker_bridge.dart @@ -0,0 +1,4 @@ +library file_picker_bridge; + +export 'src/file_picker_service.dart'; +export 'src/method_channel_file_picker.dart'; diff --git a/packages/file_picker_bridge/lib/src/file_picker_service.dart b/packages/file_picker_bridge/lib/src/file_picker_service.dart new file mode 100644 index 0000000..684a9a8 --- /dev/null +++ b/packages/file_picker_bridge/lib/src/file_picker_service.dart @@ -0,0 +1,17 @@ +class PickedFile { + const PickedFile({ + required this.path, + this.name, + }); + + final String path; + final String? name; +} + +abstract class FilePickerService { + Future pickFile({ + List allowedExtensions = const [], + String? title, + String? message, + }); +} diff --git a/packages/file_picker_bridge/lib/src/method_channel_file_picker.dart b/packages/file_picker_bridge/lib/src/method_channel_file_picker.dart new file mode 100644 index 0000000..3db9280 --- /dev/null +++ b/packages/file_picker_bridge/lib/src/method_channel_file_picker.dart @@ -0,0 +1,41 @@ +import 'package:flutter/services.dart'; + +import 'file_picker_service.dart'; + +class MethodChannelFilePicker implements FilePickerService { + const MethodChannelFilePicker({ + MethodChannel channel = _defaultChannel, + }) : _channel = channel; + + static const MethodChannel _defaultChannel = MethodChannel( + 'file_picker_bridge/file_picker', + ); + + final MethodChannel _channel; + + @override + Future pickFile({ + List allowedExtensions = const [], + String? title, + String? message, + }) async { + final result = await _channel.invokeMapMethod( + 'pickFile', + { + 'allowedExtensions': allowedExtensions, + 'title': title, + 'message': message, + }, + ); + + final path = result?['path'] as String?; + if (path == null || path.isEmpty) { + return null; + } + + return PickedFile( + path: path, + name: result?['name'] as String?, + ); + } +} diff --git a/packages/file_picker_bridge/pubspec.yaml b/packages/file_picker_bridge/pubspec.yaml new file mode 100644 index 0000000..9e23daa --- /dev/null +++ b/packages/file_picker_bridge/pubspec.yaml @@ -0,0 +1,21 @@ +name: file_picker_bridge +description: File picker bridge for adapting macOS and Windows platform file managers. +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: '>=3.6.0 <4.0.0' + +resolution: workspace + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + +flutter: + uses-material-design: true diff --git a/packages/file_picker_bridge/test/method_channel_file_picker_test.dart b/packages/file_picker_bridge/test/method_channel_file_picker_test.dart new file mode 100644 index 0000000..e8ab88e --- /dev/null +++ b/packages/file_picker_bridge/test/method_channel_file_picker_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:file_picker_bridge/file_picker_bridge.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('MethodChannelFilePicker', () { + const channel = MethodChannel('test/file_picker'); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('returns picked file from platform response', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (call) async { + expect(call.method, 'pickFile'); + expect(call.arguments, { + 'allowedExtensions': ['nc', 'gcode'], + 'title': '选择文件', + 'message': '导入刀路', + }); + + return { + 'path': '/tmp/sample.nc', + 'name': 'sample.nc', + }; + }); + + const picker = MethodChannelFilePicker(channel: channel); + final file = await picker.pickFile( + allowedExtensions: ['nc', 'gcode'], + title: '选择文件', + message: '导入刀路', + ); + + expect(file?.path, '/tmp/sample.nc'); + expect(file?.name, 'sample.nc'); + }); + + test('returns null when user cancels', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (call) async => null); + + const picker = MethodChannelFilePicker(channel: channel); + + expect(await picker.pickFile(), isNull); + }); + }); +} diff --git a/packages/flutter_ioc_core/AI_ANALYSIS.md b/packages/flutter_ioc_core/AI_ANALYSIS.md new file mode 100644 index 0000000..d916afa --- /dev/null +++ b/packages/flutter_ioc_core/AI_ANALYSIS.md @@ -0,0 +1,42 @@ +{ + "schema": "vibecoding.harness.ai_analysis.v2", + "mode": "package_contract", + "node": { + "id": "flutter_study.workspace.flutter_ioc_core", + "kind": "dart_package", + "package": "flutter_ioc_core", + "path": "packages/flutter_ioc_core", + "status": "active" + }, + "package_type": "dart_package", + "workspace": { + "member": true, + "resolution": "workspace", + "resolution_status": "blocked", + "resolution_blocker": "test_analyzer_flutter_sdk_pin_conflict" + }, + "entrypoints": [ + "lib/flutter_ioc_core.dart" + ], + "owns": [ + "ioc_container", + "registration_lifetimes", + "scoped_resolution" + ], + "depends": [], + "children": [], + "contracts": { + "no_natural_language": true, + "index_only": true, + "max_index_depth": 2, + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", + "update_required_on_file_change": true, + "import_direction_enforced": true + }, + "validation": [ + "dart pub get", + "dart analyze" + ], + "test_status": "missing_test_directory" +} diff --git a/packages/flutter_ioc_core/README.md b/packages/flutter_ioc_core/README.md new file mode 100644 index 0000000..75b1ff3 --- /dev/null +++ b/packages/flutter_ioc_core/README.md @@ -0,0 +1,3 @@ +# flutter_ioc_core + +Pure Dart IoC container extracted from the Flutter IoC teaching module. diff --git a/packages/flutter_ioc_core/lib/flutter_ioc_core.dart b/packages/flutter_ioc_core/lib/flutter_ioc_core.dart new file mode 100644 index 0000000..5e9ba69 --- /dev/null +++ b/packages/flutter_ioc_core/lib/flutter_ioc_core.dart @@ -0,0 +1,4 @@ +library flutter_ioc_core; + +export 'src/container.dart'; +export 'src/types.dart'; diff --git a/packages/flutter_ioc_core/lib/src/container.dart b/packages/flutter_ioc_core/lib/src/container.dart new file mode 100644 index 0000000..6a0f430 --- /dev/null +++ b/packages/flutter_ioc_core/lib/src/container.dart @@ -0,0 +1,256 @@ +import 'dart:async'; + +import 'types.dart'; + +class _ContainerKey { + _ContainerKey(this.type, this.name); + final Type type; + final String? name; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _ContainerKey && + runtimeType == other.runtimeType && + type == other.type && + name == other.name; + + @override + int get hashCode => type.hashCode ^ name.hashCode; + + @override + String toString() => '${type.toString()}${name != null ? '#$name' : ''}'; +} + +class _Registration { + _Registration({ + required this.key, + required this.factory, + required this.lifetime, + required this.condition, + required this.propertyInjectors, + }); + + final _ContainerKey key; + final Factory factory; + final Lifetime lifetime; + final Condition condition; + final List> propertyInjectors; + dynamic instance; +} + +/// Concrete IoC container with lifecycle management, conditional selection, and scopes. +class Container implements IoCContainer { + Container({Map environment = const {}, Container? parent}) + : _environment = Map.from(environment), + _parent = parent; + + final Map> _registrations = {}; + final Map<_ContainerKey, dynamic> _scopedInstances = + {}; // Scope-specific cache. + final List<_ContainerKey> _resolutionPath = + []; // Tracks resolution chain to detect cycles. + final Map _environment; + final Container? _parent; + + @override + void registerSingleton( + Factory factory, { + String? name, + Condition? condition, + List>? propertyInjectors, + }) { + _register( + factory: factory, + lifetime: Lifetime.singleton, + name: name, + condition: condition, + propertyInjectors: propertyInjectors, + ); + } + + @override + void registerTransient( + Factory factory, { + String? name, + Condition? condition, + List>? propertyInjectors, + }) { + _register( + factory: factory, + lifetime: Lifetime.transient, + name: name, + condition: condition, + propertyInjectors: propertyInjectors, + ); + } + + @override + void registerScoped( + Factory factory, { + String? name, + Condition? condition, + List>? propertyInjectors, + }) { + _register( + factory: factory, + lifetime: Lifetime.scoped, + name: name, + condition: condition, + propertyInjectors: propertyInjectors, + ); + } + + void _register({ + required Factory factory, + required Lifetime lifetime, + String? name, + Condition? condition, + List>? propertyInjectors, + }) { + final registration = _Registration( + key: _ContainerKey(T, name), + factory: factory, + lifetime: lifetime, + condition: condition ?? (_) => true, + propertyInjectors: + propertyInjectors?.cast>() ?? const [], + ); + _registrations.putIfAbsent(T, () => []).add(registration); + } + + @override + T resolve({String? name}) { + final result = _resolveInternal( + name: name, scope: this, allowAsyncFactories: false); + if (result is Future) { + throw ContainerException( + 'Async factory registered for $T; call resolveAsync<$T>() instead.'); + } + return result as T; + } + + @override + Future resolveAsync({String? name}) async { + final result = await _resolveInternal( + name: name, + scope: this, + allowAsyncFactories: true, + ); + if (result is Future) { + return await result; + } + return result as T; + } + + dynamic _resolveInternal( + {String? name, + required Container scope, + required bool allowAsyncFactories}) { + final token = _ContainerKey(T, name); + if (_resolutionPath.contains(token)) { + final chain = + [..._resolutionPath, token].map((e) => e.toString()).join(' -> '); + throw ContainerException('Circular dependency detected: $chain'); + } + + final registration = _findRegistration(name: name, scope: scope); + _resolutionPath.add(token); + try { + switch (registration.lifetime) { + case Lifetime.singleton: + return registration.instance ??= + _createInstance(registration, scope, allowAsyncFactories); + case Lifetime.transient: + return _createInstance(registration, scope, allowAsyncFactories); + case Lifetime.scoped: + return scope._scopedInstances.putIfAbsent( + registration.key, + () => _createInstance(registration, scope, allowAsyncFactories), + ); + } + } finally { + _resolutionPath.removeLast(); + } + } + + dynamic _createInstance( + _Registration registration, + Container scope, + bool allowAsyncFactories, + ) { + final created = registration.factory(scope); + if (created is Future && !allowAsyncFactories) { + throw ContainerException( + 'Async factory registered for $T; call resolveAsync<$T>() instead.'); + } + + if (created is Future) { + return created.then((value) { + _injectProperties(registration, value, scope); + return value; + }); + } + + _injectProperties(registration, created, scope); + return created; + } + + void _injectProperties( + _Registration registration, dynamic instance, Container scope) { + for (final injector in registration.propertyInjectors) { + injector(instance, scope); + } + } + + _Registration _findRegistration({String? name, required Container scope}) { + final registrations = _collectRegistrations(T); + final matching = registrations + .where((r) => r.key.name == name && r.condition(scope)) + .toList(); + + if (matching.isEmpty) { + final availableNames = registrations + .where((r) => r.key.name != null) + .map((r) => r.key.name) + .toSet() + .join(', '); + throw ContainerException( + 'No registration found for $T ${name != null ? 'with name $name ' : ''}' + '${availableNames.isNotEmpty ? '(available names: $availableNames)' : ''}'); + } + return matching.first; + } + + List<_Registration> _collectRegistrations(Type type) { + final current = _registrations[type] ?? const <_Registration>[]; + if (_parent == null) { + return current; + } + return [...current, ..._parent!._collectRegistrations(type)]; + } + + @override + IoCContainer createScope( + {Map environmentOverrides = const {}}) { + final env = {..._environment, ...environmentOverrides}; + return Container(environment: env, parent: this); + } + + @override + bool flag(String key, {bool defaultValue = false}) { + final value = env(key); + if (value is bool) return value; + return defaultValue; + } + + @override + Object? env(String key) => _environment[key] ?? _parent?.env(key); + + @override + void autoRegister(List registrars) { + for (final registrar in registrars) { + registrar.register(this); + } + } +} diff --git a/packages/flutter_ioc_core/lib/src/types.dart b/packages/flutter_ioc_core/lib/src/types.dart new file mode 100644 index 0000000..ac93a74 --- /dev/null +++ b/packages/flutter_ioc_core/lib/src/types.dart @@ -0,0 +1,65 @@ +import 'dart:async'; + +/// Lifecycle options for registered services. +enum Lifetime { singleton, transient, scoped } + +/// Factory that creates instances and can resolve other dependencies. +typedef Factory = FutureOr Function(ContainerResolver resolver); + +/// Predicate to decide whether a registration should be used. +typedef Condition = bool Function(ContainerResolver resolver); + +/// Hook to inject dependencies into already created instances. +typedef PropertyInjector = void Function( + T instance, ContainerResolver resolver); + +/// Registers a bundle of services. +abstract class AutoRegistrar { + void register(IoCContainer container); +} + +/// Minimal resolver interface exposed to factories and property injectors. +abstract class ContainerResolver { + T resolve({String? name}); + Future resolveAsync({String? name}); + bool flag(String key, {bool defaultValue = false}); + Object? env(String key); +} + +/// Public IoC container interface. +abstract class IoCContainer implements ContainerResolver { + void registerSingleton( + Factory factory, { + String? name, + Condition? condition, + List>? propertyInjectors, + }); + + void registerTransient( + Factory factory, { + String? name, + Condition? condition, + List>? propertyInjectors, + }); + + void registerScoped( + Factory factory, { + String? name, + Condition? condition, + List>? propertyInjectors, + }); + + void autoRegister(List registrars); + + IoCContainer createScope( + {Map environmentOverrides = const {}}); +} + +/// Base error type for the container. +class ContainerException implements Exception { + ContainerException(this.message); + final String message; + + @override + String toString() => 'ContainerException: $message'; +} diff --git a/packages/flutter_ioc_core/pubspec.yaml b/packages/flutter_ioc_core/pubspec.yaml new file mode 100644 index 0000000..080c0a7 --- /dev/null +++ b/packages/flutter_ioc_core/pubspec.yaml @@ -0,0 +1,12 @@ +name: flutter_ioc_core +description: Pure Dart IoC container core extracted from Flutter study. +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: '>=3.6.0 <4.0.0' + +resolution: workspace + +dev_dependencies: + lints: ^4.0.0 diff --git a/packages/flutter_study_learning/AI_ANALYSIS.md b/packages/flutter_study_learning/AI_ANALYSIS.md new file mode 100644 index 0000000..6a6a011 --- /dev/null +++ b/packages/flutter_study_learning/AI_ANALYSIS.md @@ -0,0 +1,43 @@ +{ + "schema": "vibecoding.harness.ai_analysis.v2", + "mode": "package_contract", + "node": { + "id": "flutter_study.workspace.flutter_study_learning", + "kind": "flutter_package", + "package": "flutter_study_learning", + "path": "packages/flutter_study_learning", + "status": "active" + }, + "package_type": "flutter_package", + "workspace": { + "member": true, + "resolution": "workspace", + "resolution_status": "blocked", + "resolution_blocker": "test_analyzer_flutter_sdk_pin_conflict" + }, + "entrypoints": [ + "lib/flutter_study_learning.dart" + ], + "owns": [ + "learning_scaffold_widgets", + "teaching_ui_components" + ], + "depends": [ + "flutter_sdk" + ], + "children": [], + "contracts": { + "no_natural_language": true, + "index_only": true, + "max_index_depth": 2, + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", + "update_required_on_file_change": true, + "import_direction_enforced": true + }, + "validation": [ + "flutter pub get", + "flutter analyze" + ], + "test_status": "missing_test_directory" +} diff --git a/packages/flutter_study_learning/README.md b/packages/flutter_study_learning/README.md new file mode 100644 index 0000000..68bae4d --- /dev/null +++ b/packages/flutter_study_learning/README.md @@ -0,0 +1,10 @@ +# flutter_study_learning + +Shared teaching page widgets for Flutter study modules. + +## Scope + +- `LearningScaffold` +- Learning objectives, concept chips, code snippets, state logs, pitfalls, and exercise cards + +This package has no module-specific business logic. diff --git a/packages/flutter_study_learning/lib/flutter_study_learning.dart b/packages/flutter_study_learning/lib/flutter_study_learning.dart new file mode 100644 index 0000000..09df57d --- /dev/null +++ b/packages/flutter_study_learning/lib/flutter_study_learning.dart @@ -0,0 +1,3 @@ +library flutter_study_learning; + +export 'src/learning_scaffold.dart'; diff --git a/packages/flutter_study_learning/lib/src/learning_scaffold.dart b/packages/flutter_study_learning/lib/src/learning_scaffold.dart new file mode 100644 index 0000000..3875f0b --- /dev/null +++ b/packages/flutter_study_learning/lib/src/learning_scaffold.dart @@ -0,0 +1,301 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// 学习目标展示区块 +class LearningObjectives extends StatelessWidget { + const LearningObjectives({super.key, required this.objectives}); + + final List objectives; + + @override + Widget build(BuildContext context) { + return _Section( + title: '🎯 学习目标', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: objectives + .map( + (o) => Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('• ', style: TextStyle(fontSize: 16)), + Expanded(child: Text(o)), + ], + ), + ), + ) + .toList(), + ), + ); + } +} + +/// 核心概念标签组 +class ConceptChips extends StatelessWidget { + const ConceptChips({super.key, required this.concepts}); + + final List concepts; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: concepts + .map( + (c) => Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + c, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ), + ) + .toList(), + ), + ); + } +} + +/// 代码片段展示卡片 +class CodeSnippetCard extends StatelessWidget { + const CodeSnippetCard({ + super.key, + required this.title, + required this.code, + this.explanation, + }); + + final String title; + final String code; + final String? explanation; + + @override + Widget build(BuildContext context) { + return _Section( + title: '📝 $title', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(8), + ), + child: SelectableText( + code, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 13, + color: Colors.white, + height: 1.4, + ), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + IconButton( + icon: const Icon(Icons.copy, size: 18), + onPressed: () { + Clipboard.setData(ClipboardData(text: code)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('已复制到剪贴板'), + duration: Duration(seconds: 1), + ), + ); + }, + tooltip: '复制代码', + ), + if (explanation != null) + Expanded( + child: Text(explanation!, + style: const TextStyle(fontSize: 12))), + ], + ), + ], + ), + ); + } +} + +/// 状态变化/日志展示区 +class StateLogView extends StatelessWidget { + const StateLogView({super.key, required this.logs, this.maxLines = 8}); + + final List logs; + final int maxLines; + + @override + Widget build(BuildContext context) { + return _Section( + title: '📊 状态日志', + child: Container( + constraints: BoxConstraints(maxHeight: maxLines * 20), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: ListView.builder( + itemCount: logs.length, + itemBuilder: (context, index) { + return Text( + logs[index], + style: const TextStyle(fontFamily: 'monospace', fontSize: 12), + ); + }, + ), + ), + ); + } +} + +/// 常见误区提示 +class CommonPitfalls extends StatelessWidget { + const CommonPitfalls({super.key, required this.pitfalls}); + + final List pitfalls; + + @override + Widget build(BuildContext context) { + return _Section( + title: '⚠️ 常见误区', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: pitfalls + .map( + (p) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.warning_amber, + size: 18, color: Colors.orange), + const SizedBox(width: 8), + Expanded(child: Text(p)), + ], + ), + ), + ) + .toList(), + ), + ); + } +} + +/// 练习任务卡片 +class ExerciseCard extends StatelessWidget { + const ExerciseCard({super.key, required this.task, this.hint}); + + final String task; + final String? hint; + + @override + Widget build(BuildContext context) { + return _Section( + title: '💪 练习任务', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(task), + if (hint != null) ...[ + const SizedBox(height: 8), + Text( + '提示: $hint', + style: TextStyle(fontSize: 12, color: Colors.grey.shade600), + ), + ], + ], + ), + ); + } +} + +/// 教学区块容器 +class _Section extends StatelessWidget { + const _Section({required this.title, required this.child}); + + final String title; + final Widget child; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + child, + ], + ), + ); + } +} + +/// 标准教学页面脚手架 +class LearningScaffold extends StatelessWidget { + const LearningScaffold({ + super.key, + required this.title, + required this.sections, + this.interactiveDemo, + this.floatingActionButton, + }); + + final String title; + final List sections; + final Widget? interactiveDemo; + final Widget? floatingActionButton; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(title)), + body: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (interactiveDemo != null) ...[ + Padding( + padding: const EdgeInsets.all(16), + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: interactiveDemo), + ), + ), + const Divider(), + ], + ...sections, + ], + ), + ), + floatingActionButton: floatingActionButton, + ); + } +} diff --git a/packages/flutter_study_learning/pubspec.yaml b/packages/flutter_study_learning/pubspec.yaml new file mode 100644 index 0000000..012d995 --- /dev/null +++ b/packages/flutter_study_learning/pubspec.yaml @@ -0,0 +1,21 @@ +name: flutter_study_learning +description: Shared learning scaffold widgets for Flutter study modules. +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: '>=3.6.0 <4.0.0' + +resolution: workspace + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + +flutter: + uses-material-design: true diff --git a/packages/gcode_core/.gitignore b/packages/gcode_core/.gitignore new file mode 100644 index 0000000..d4cddb8 --- /dev/null +++ b/packages/gcode_core/.gitignore @@ -0,0 +1,41 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Dart/Pub related +.dart_tool/ +.pub-cache/ +.pub/ +/build/ +/coverage/ +/pubspec.lock + +# Generated code +*.g.dart +*.freezed.dart +*.mocks.dart +*.pb.dart +*.pbjson.dart +*.pbenum.dart +*.grpc.dart diff --git a/packages/gcode_core/AI_ANALYSIS.md b/packages/gcode_core/AI_ANALYSIS.md new file mode 100644 index 0000000..21a9026 --- /dev/null +++ b/packages/gcode_core/AI_ANALYSIS.md @@ -0,0 +1,46 @@ +{ + "schema": "vibecoding.harness.ai_analysis.v2", + "mode": "package_contract", + "node": { + "id": "flutter_study.workspace.gcode_core", + "kind": "flutter_package", + "package": "gcode_core", + "path": "packages/gcode_core", + "status": "active" + }, + "package_type": "flutter_package", + "workspace": { + "member": true, + "resolution": "workspace", + "resolution_status": "blocked", + "resolution_blocker": "test_analyzer_flutter_sdk_pin_conflict" + }, + "entrypoints": [ + "lib/gcode_core.dart" + ], + "owns": [ + "gcode_parsing", + "line_reading", + "toolpath_building", + "flutter_visualization_widgets" + ], + "depends": [ + "flutter_sdk" + ], + "children": [], + "contracts": { + "no_natural_language": true, + "index_only": true, + "max_index_depth": 2, + "doc_consumer": "coding_agent", + "doc_mode": "machine_contract", + "update_required_on_file_change": true, + "import_direction_enforced": true + }, + "validation": [ + "flutter pub get", + "flutter analyze", + "flutter test" + ], + "test_status": "configured" +} diff --git a/packages/gcode_core/PHASE_SUMMARY.md b/packages/gcode_core/PHASE_SUMMARY.md new file mode 100644 index 0000000..65c7773 --- /dev/null +++ b/packages/gcode_core/PHASE_SUMMARY.md @@ -0,0 +1,96 @@ +# Phase Summary — gcode_core + +> 2026-06-14 · 当前版本 0.1.0 · 基于 xgimi_gcode2d 对比分析后的首轮优化完成 + +## 当前架构 + +``` +lib/src/ +├── application/ ── 编排层 +│ └── gcode_readline_pipeline.dart Stream 流式管道 + isolate 后台解析 +├── core/ ── 核心抽象(本次新增) +│ ├── gcode_bounds.dart 包围盒,增量合并,供 painter 复用 +│ └── gcode_style.dart 绘制样式,预创建 Paint,light/dark 工厂 +├── data/readers/ ── 输入/IO层 +│ ├── gcode_line_reader.dart 抽象接口 Stream +│ ├── string_gcode_line_reader.dart 内存字符串读取 +│ └── file_gcode_line_reader.dart 流式文件读取(openRead + LineSplitter) +├── domain/ ── 领域类型 +│ ├── gcode_load_stage.dart idle/reading/parsing/ready/failed 枚举 +│ ├── gcode_line_record.dart 原始行元数据 +│ ├── gcode_load_snapshot.dart 不可变进度快照(含 bounds) +│ └── parsed_gcode_line.dart sealed class: command/error/skipped +├── models/ ── 数据模型 +│ ├── gcode_command.dart G0/G1 + 参数 + 注释 +│ ├── machine_position.dart X/Y/F 位置状态 +│ └── toolpath_segment.dart 起止点 + 类型(rapid/linear) +├── parser/ ── 解析 +│ ├── gcode_parser.dart 词法/语法解析,支持流式 parseRecord +│ └── gcode_parse_result.dart 批量解析结果 + 错误 DTO +├── services/ ── 业务逻辑 +│ └── toolpath_builder.dart 批量/增量 toolpath 构建 + bounds 增量跟踪 +└── widgets/ ── Flutter 控件 + ├── gcode_canvas.dart CustomPaint 可视化(支持 Bounds + Style) + ├── command_timeline.dart 指令/错误时间线列表 + └── playback_controls.dart 播放/暂停/进度/速度控件 +``` + +## 首轮优化完成项 (2026-06-14) + +### 1. GcodeBounds — 边界预计算 +- **问题**:`_ToolpathPainter.paint()` 每帧 O(n) 遍历 segments 计算 bounds +- **方案**:`IncrementalToolpathBuilder.accept()` 增量维护 `GcodeBounds`,经由 `GcodeLoadSnapshot.bounds` 透传至 painter +- **效果**:painter 直接接收预计算 bounds,删除内部 `_calculateBounds()` 遍历 + +### 2. GcodeStyle — 样式抽离 +- **问题**:painter 内每帧 `new Paint()` + 颜色硬编码 +- **方案**:`GcodeStyle` 类预创建所有 Paint(rapidMoveBg/rapidMove/linearMoveBg/linearMove/toolHead/toolHeadGlow/origin/originDot/grid),`GcodeStyle.light()` 工厂 +- **效果**:零帧内开销 + 外部可自定义配色 + +### 3. Isolate 后台流式解析 +- **问题**:大文件解析阻塞 UI 线程 +- **方案**:`loadFileInBackground(path)` / `loadStringInBackground(source)` 使用 `Isolate.spawn` + `SendPort`/`ReceivePort` 流式返回 `Stream` +- **效果**:与 `load()` 完全一致的 `await for` 消费方式,仅调用入口不同 + +### 不采纳的优化(已评估排除) + +| 项 | 排除原因 | +|---|---| +| SoA 数据模型 (Float32List) | gcode_core 面向中小规模 G-code,Dart 对象开销可忽略;SoA 增加维护负担 | +| Viewport/Transform 两层分离 | 当前无 pan/zoom 交互需求,引入两层会增加不必要的复杂度 | +| Picture 缓存 / Checkpoint 缓存 | 当前 segment 量级下收益有限,后续如需要可作为第二轮专项 | +| G25/G102/G103 指令支持 | 业务领域不同,gcode_core 聚焦 G0/G1 | +| SceneClassifier(矢量/光栅分类) | 仅处理矢量路径,无分类需求 | +| GCodeMemoryTrace 调试日志 | 面向生产环境,教学级项目暂不需要 | + +## 后续规划 + +### 优先级 A — 近期可做 + +| 任务 | 预估工作量 | 说明 | +|---|---|---| +| **Picture 缓存** | 中 | 已完成路径录制成 `ui.Picture`,播放时只画 tail,避免全量重绘 | +| **GcodeController** | 中 | ChangeNotifier 控制器,封装 play/pause/seek/speed 逻辑,替代示例 app 中手写 Timer | +| **错误统计增强** | 小 | `scannedLineCount` / `skippedLineCount` 透传至 snapshot | + +### 优先级 B — 按需启动 + +| 任务 | 预估工作量 | 说明 | +|---|---|---| +| **G2 圆弧支持** | 大 | 新增 `GcodeSegmentType.arc`,painter 实现弧线绘制 | +| **多层绘制** | 大 | 背景网格/辅助线/路径分层,独立 togglable | +| **撤销/重做** | 中 | 编辑场景下的状态回退能力 | + +### 优先级 C — 远期探索 + +| 任务 | 说明 | +|---|---| +| **SVG/Bitmap → G-code 生成** | 当前包只做解析+预览,生成是反向需求 | +| **3D 预览** | 需要整体架构升级 | + +## 测试覆盖 + +``` +flutter test → 22 tests passed (parser × 11, toolpath × 3, pipeline × 3, widget × 1) +flutter analyze → 0 issues +``` diff --git a/packages/gcode_core/README.md b/packages/gcode_core/README.md new file mode 100644 index 0000000..65480fe --- /dev/null +++ b/packages/gcode_core/README.md @@ -0,0 +1,65 @@ +# gcode_core + +![example](https://github.com/lizy-coding/gcode_core/blob/master/gcode_print.gif) + +G-code parsing and visualization package extracted from `flutter_study`. + +## Scope + +- Read G-code from strings or files line by line. +- Parse G0/G1 commands with X/Y/F parameters. +- Collect parse errors with line metadata. +- Build incremental or batch toolpath segments. +- Render toolpaths with Flutter `CustomPaint`. +- Render command timelines and playback controls for Flutter frontends. + +This package does not open system file pickers or own app-level playback state. + +## Test + +```bash +flutter test +``` + +## Example + +Run the Flutter example app: + +```bash +cd example +flutter run +``` + +The example demonstrates local file selection, streaming parse snapshots, +`GcodeCanvas` drawing, `CommandTimeline`, and `PlaybackControls`. + +Run the console example: + +```bash +dart run example/gcode_core_example.dart +``` + +Minimal usage: + +```dart +import 'package:gcode_core/gcode_core.dart'; + +Future main() async { + const source = ''' +G0 X0 Y0 +G1 X10 Y0 F1200 +G1 X10 Y10 +'''; + + final pipeline = GcodeReadlinePipeline(); + + await for (final snapshot + in pipeline.load(const StringGcodeLineReader(source))) { + if (snapshot.stage == GcodeLoadStage.ready) { + print(snapshot.commands.length); + print(snapshot.segments.length); + print(snapshot.errors.length); + } + } +} +``` diff --git a/packages/gcode_core/example/.gitignore b/packages/gcode_core/example/.gitignore new file mode 100644 index 0000000..6a40388 --- /dev/null +++ b/packages/gcode_core/example/.gitignore @@ -0,0 +1,54 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Generated code +*.g.dart +*.freezed.dart +*.mocks.dart +*.pb.dart +*.pbjson.dart +*.pbenum.dart +*.grpc.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/gcode_core/example/.metadata b/packages/gcode_core/example/.metadata new file mode 100644 index 0000000..c24b9a1 --- /dev/null +++ b/packages/gcode_core/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + - platform: macos + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/gcode_core/example/README.md b/packages/gcode_core/example/README.md new file mode 100644 index 0000000..f7139cd --- /dev/null +++ b/packages/gcode_core/example/README.md @@ -0,0 +1,38 @@ +# gcode_core example + +Flutter example for the `gcode_core` package. + +It demonstrates the full local workflow: + +- Pick a local `.gcode`, `.nc`, `.tap`, or `.txt` file. +- Read the file line by line with `FileGcodeLineReader`. +- Parse supported `G0/G1` commands with `GcodeReadlinePipeline`. +- Dynamically refresh parsed snapshots while reading. +- Draw toolpath segments with the package-provided `GcodeCanvas`. +- Show `G0` jump moves as red dashed lines and `G1` cutting moves as solid paths. +- Show commands and parse errors with `CommandTimeline`. +- Preview the generated path with `PlaybackControls`. + +The main integration points are: + +```dart +final pipeline = GcodeReadlinePipeline( + options: const GcodeReadlineOptions(snapshotBatchSize: 1), +); + +await for (final snapshot in pipeline.load(FileGcodeLineReader(file.path))) { + setState(() => _snapshot = snapshot); +} + +GcodeCanvas( + segments: snapshot.segments, + progress: playbackProgress, + errorCount: snapshot.errors.length, +); +``` + +Run it from this directory: + +```bash +flutter run +``` diff --git a/packages/gcode_core/example/analysis_options.yaml b/packages/gcode_core/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/packages/gcode_core/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/gcode_core/example/lib/main.dart b/packages/gcode_core/example/lib/main.dart new file mode 100644 index 0000000..1e12219 --- /dev/null +++ b/packages/gcode_core/example/lib/main.dart @@ -0,0 +1,485 @@ +import 'dart:async'; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; +import 'package:gcode_core/gcode_core.dart'; + +void main() { + runApp(const GcodeCoreExampleApp()); +} + +class GcodeCoreExampleApp extends StatelessWidget { + const GcodeCoreExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'G-code Core Example', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff2563eb)), + useMaterial3: true, + ), + home: const GcodeExamplePage(), + ); + } +} + +class GcodeExamplePage extends StatefulWidget { + const GcodeExamplePage({super.key}); + + @override + State createState() => _GcodeExamplePageState(); +} + +class _GcodeExamplePageState extends State { + static const _sampleSource = ''' +G0 X0 Y0 +G1 X30 Y0 F1200 +G1 X30 Y18 +G1 X12 Y18 +G0 X6 Y8 +G1 X22 Y8 +G2 X40 Y40 +'''; + + final _pipeline = GcodeReadlinePipeline( + options: const GcodeReadlineOptions(snapshotBatchSize: 1), + ); + + GcodeLoadSnapshot? _snapshot; + String _sourceName = '未选择文件'; + String _status = '请选择本地 G-code 文件,或加载内置示例。'; + bool _loading = false; + bool _isPlaying = false; + double _playbackProgress = 1; + double _speedMultiplier = 1; + Timer? _playbackTimer; + + @override + void dispose() { + _playbackTimer?.cancel(); + super.dispose(); + } + + Future _pickAndParseFile() async { + const typeGroup = XTypeGroup( + label: 'G-code', + extensions: ['gcode', 'nc', 'tap', 'txt'], + ); + + final file = await openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) return; + + await _parseReader(FileGcodeLineReader(file.path), sourceName: file.name); + } + + Future _loadSample() { + return _parseReader( + const StringGcodeLineReader(_sampleSource), + sourceName: '内置示例', + ); + } + + Future _parseReader( + GcodeLineReader reader, { + required String sourceName, + }) async { + _playbackTimer?.cancel(); + setState(() { + _loading = true; + _isPlaying = false; + _playbackProgress = 1; + _sourceName = sourceName; + _snapshot = null; + _status = '正在读取 $sourceName'; + }); + + await for (final snapshot in _pipeline.load(reader)) { + if (!mounted) return; + setState(() { + _snapshot = snapshot; + _status = snapshot.message; + _playbackProgress = 1; + }); + if (snapshot.stage == GcodeLoadStage.parsing) { + await Future.delayed(const Duration(milliseconds: 16)); + } + } + + if (!mounted) return; + setState(() => _loading = false); + } + + void _play() { + if ((_snapshot?.segments.isEmpty ?? true) || _loading) return; + + _playbackTimer?.cancel(); + setState(() => _isPlaying = true); + _playbackTimer = Timer.periodic(const Duration(milliseconds: 16), (_) { + if (!mounted) return; + final next = _playbackProgress + 0.004 * _speedMultiplier; + setState(() { + _playbackProgress = next.clamp(0, 1); + _isPlaying = _playbackProgress < 1; + }); + if (_playbackProgress >= 1) { + _playbackTimer?.cancel(); + } + }); + } + + void _pause() { + _playbackTimer?.cancel(); + setState(() => _isPlaying = false); + } + + void _resetPlayback() { + _playbackTimer?.cancel(); + setState(() { + _isPlaying = false; + _playbackProgress = 0; + }); + } + + void _seekPlayback(double value) { + setState(() => _playbackProgress = value); + } + + void _setSpeed(double value) { + setState(() => _speedMultiplier = value); + } + + int _currentCommandIndex(GcodeLoadSnapshot? snapshot) { + final commandCount = snapshot?.commands.length ?? 0; + if (commandCount == 0) return -1; + return (_playbackProgress * commandCount).ceil().clamp(1, commandCount) - 1; + } + + @override + Widget build(BuildContext context) { + final snapshot = _snapshot; + + return Scaffold( + appBar: AppBar( + title: const Text('G-code Core 绘制示例'), + actions: [ + TextButton.icon( + onPressed: _loading ? null : _loadSample, + icon: const Icon(Icons.data_object), + label: const Text('示例数据'), + ), + const SizedBox(width: 8), + FilledButton.icon( + onPressed: _loading ? null : _pickAndParseFile, + icon: const Icon(Icons.folder_open), + label: const Text('选择 G-code'), + ), + const SizedBox(width: 16), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _StatusBar( + sourceName: _sourceName, + status: _status, + loading: _loading, + ), + const SizedBox(height: 16), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + flex: 3, + child: _CanvasPanel( + snapshot: snapshot, + parsing: _loading, + progress: _playbackProgress, + isPlaying: _isPlaying, + speedMultiplier: _speedMultiplier, + onPlay: _play, + onPause: _pause, + onReset: _resetPlayback, + onSeek: _seekPlayback, + onSpeedChange: _setSpeed, + ), + ), + const SizedBox(width: 16), + SizedBox( + width: 360, + child: _ResultPanel( + snapshot: snapshot, + currentIndex: _currentCommandIndex(snapshot), + onCommandTap: (index) { + final total = snapshot?.commands.length ?? 0; + if (total == 0) return; + _pause(); + setState(() => _playbackProgress = (index + 1) / total); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _StatusBar extends StatelessWidget { + const _StatusBar({ + required this.sourceName, + required this.status, + required this.loading, + }); + + final String sourceName; + final String status; + final bool loading; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + if (loading) + const SizedBox.square( + dimension: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + const Icon(Icons.route), + const SizedBox(width: 12), + Expanded( + child: Text( + '$sourceName - $status', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } +} + +class _CanvasPanel extends StatelessWidget { + const _CanvasPanel({ + required this.snapshot, + required this.parsing, + required this.progress, + required this.isPlaying, + required this.speedMultiplier, + required this.onPlay, + required this.onPause, + required this.onReset, + required this.onSeek, + required this.onSpeedChange, + }); + + final GcodeLoadSnapshot? snapshot; + final bool parsing; + final double progress; + final bool isPlaying; + final double speedMultiplier; + final VoidCallback onPlay; + final VoidCallback onPause; + final VoidCallback onReset; + final ValueChanged onSeek; + final ValueChanged onSpeedChange; + + @override + Widget build(BuildContext context) { + final segments = snapshot?.segments ?? const []; + final errors = snapshot?.errors.length ?? 0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Stack( + children: [ + Positioned.fill( + child: GcodeCanvas( + segments: segments, + progress: parsing ? 1 : progress, + errorCount: errors, + bounds: snapshot?.bounds, + ), + ), + Positioned( + left: 12, + top: 12, + child: _CanvasLegend( + parsing: parsing, + segments: segments.length, + mainSegments: segments + .where( + (segment) => segment.type == GcodeSegmentType.linear, + ) + .length, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + PlaybackControls( + isPlaying: isPlaying, + progress: parsing ? 1 : progress, + speedMultiplier: speedMultiplier, + onPlay: onPlay, + onPause: onPause, + onReset: onReset, + onSeek: onSeek, + onSpeedChange: onSpeedChange, + ), + ], + ); + } +} + +class _CanvasLegend extends StatelessWidget { + const _CanvasLegend({ + required this.parsing, + required this.segments, + required this.mainSegments, + }); + + final bool parsing; + final int segments; + final int mainSegments; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.surface.withValues(alpha: 0.9), + border: Border.all(color: theme.colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: DefaultTextStyle( + style: theme.textTheme.labelMedium!, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(parsing ? '动态解析绘制中' : 'GcodeCanvas 绘制'), + const SizedBox(height: 4), + Text('主线段 G1: $mainSegments'), + Text('移动段 G0/G1: $segments'), + ], + ), + ), + ), + ); + } +} + +class _ResultPanel extends StatelessWidget { + const _ResultPanel({ + required this.snapshot, + required this.currentIndex, + required this.onCommandTap, + }); + + final GcodeLoadSnapshot? snapshot; + final int currentIndex; + final ValueChanged onCommandTap; + + @override + Widget build(BuildContext context) { + final current = snapshot; + + if (current == null) { + return const Center(child: Text('解析结果会显示在这里')); + } + + return ListView( + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _Metric(label: '行数', value: current.linesRead.toString()), + _Metric(label: '指令', value: current.commands.length.toString()), + _Metric(label: '轨迹', value: current.segments.length.toString()), + _Metric(label: '错误', value: current.errors.length.toString()), + ], + ), + const SizedBox(height: 16), + CommandTimeline( + commands: current.commands, + errors: current.errors, + currentIndex: currentIndex, + onTap: onCommandTap, + maxHeight: 360, + ), + const SizedBox(height: 16), + Text('解析错误', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + if (current.errors.isEmpty) + const Text('无') + else + for (final error in current.errors) + ListTile( + dense: true, + leading: const Icon(Icons.warning_amber), + title: Text('第 ${error.lineNumber} 行'), + subtitle: Text('${error.message}\n${error.rawLine}'), + ), + ], + ); + } +} + +class _Metric extends StatelessWidget { + const _Metric({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return SizedBox( + width: 78, + child: DecoratedBox( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: theme.textTheme.labelMedium), + const SizedBox(height: 4), + Text(value, style: theme.textTheme.titleLarge), + ], + ), + ), + ), + ); + } +} diff --git a/packages/gcode_core/example/macos/.gitignore b/packages/gcode_core/example/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/packages/gcode_core/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/gcode_core/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/gcode_core/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/packages/gcode_core/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/gcode_core/example/macos/Flutter/Flutter-Release.xcconfig b/packages/gcode_core/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/packages/gcode_core/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/gcode_core/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/gcode_core/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..14b5f7c --- /dev/null +++ b/packages/gcode_core/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import file_selector_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) +} diff --git a/packages/gcode_core/example/macos/Podfile b/packages/gcode_core/example/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/packages/gcode_core/example/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/gcode_core/example/macos/Runner.xcodeproj/project.pbxproj b/packages/gcode_core/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c59ba60 --- /dev/null +++ b/packages/gcode_core/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 110DD9541D412F57D0FC25B1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BFF0FA50648256D262FB427 /* Pods_Runner.framework */; }; + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + EE563B3C323D38A90A7815D1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15B5728AAD4B596071FFF779 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 02484B4ED94AC349495FF031 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 0FB2C87507FF7C43AE0AB5FE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 15B5728AAD4B596071FFF779 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 57F4617597A1FF85FCC8B1C5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 8BFF0FA50648256D262FB427 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + C969A4D026E4EABF8F0A69F8 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + CF5D4B7C8248EF71D487E239 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + D74F4B79EB67493B99E1AC43 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EE563B3C323D38A90A7815D1 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 110DD9541D412F57D0FC25B1 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 807FC030647A3AE78EB79582 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 807FC030647A3AE78EB79582 /* Pods */ = { + isa = PBXGroup; + children = ( + 0FB2C87507FF7C43AE0AB5FE /* Pods-Runner.debug.xcconfig */, + CF5D4B7C8248EF71D487E239 /* Pods-Runner.release.xcconfig */, + 57F4617597A1FF85FCC8B1C5 /* Pods-Runner.profile.xcconfig */, + C969A4D026E4EABF8F0A69F8 /* Pods-RunnerTests.debug.xcconfig */, + D74F4B79EB67493B99E1AC43 /* Pods-RunnerTests.release.xcconfig */, + 02484B4ED94AC349495FF031 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8BFF0FA50648256D262FB427 /* Pods_Runner.framework */, + 15B5728AAD4B596071FFF779 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + A007395C57EBD64C92800C10 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 940F9EE8C72997939F9C78C1 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 1BAF74549412E04249338CF8 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1BAF74549412E04249338CF8 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 940F9EE8C72997939F9C78C1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + A007395C57EBD64C92800C10 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C969A4D026E4EABF8F0A69F8 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D74F4B79EB67493B99E1AC43 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 02484B4ED94AC349495FF031 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/gcode_core/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/gcode_core/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/gcode_core/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/gcode_core/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/gcode_core/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..ac78810 --- /dev/null +++ b/packages/gcode_core/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/gcode_core/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/gcode_core/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/packages/gcode_core/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/gcode_core/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/gcode_core/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/gcode_core/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/gcode_core/example/macos/Runner/AppDelegate.swift b/packages/gcode_core/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/packages/gcode_core/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/gcode_core/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/gcode_core/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/gcode_core/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/gcode_core/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..f67a84b --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/packages/gcode_core/example/macos/Runner/Configs/Debug.xcconfig b/packages/gcode_core/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/gcode_core/example/macos/Runner/Configs/Release.xcconfig b/packages/gcode_core/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/gcode_core/example/macos/Runner/Configs/Warnings.xcconfig b/packages/gcode_core/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/gcode_core/example/macos/Runner/DebugProfile.entitlements b/packages/gcode_core/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/packages/gcode_core/example/macos/Runner/Info.plist b/packages/gcode_core/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/gcode_core/example/macos/Runner/MainFlutterWindow.swift b/packages/gcode_core/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/gcode_core/example/macos/Runner/Release.entitlements b/packages/gcode_core/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/packages/gcode_core/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/gcode_core/example/macos/RunnerTests/RunnerTests.swift b/packages/gcode_core/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/packages/gcode_core/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/gcode_core/example/pubspec.lock b/packages/gcode_core/example/pubspec.lock new file mode 100644 index 0000000..72801cb --- /dev/null +++ b/packages/gcode_core/example/pubspec.lock @@ -0,0 +1,337 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.5+2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.9" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.3" + file_selector: + dependency: "direct main" + description: + name: file_selector + sha256: bd15e43e9268db636b53eeaca9f56324d1622af30e5c34d6e267649758c84d9a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: "89243030ea4b3463fb402b44d5eeacc4ccb1c46a88870cb2a5080d693200c1ed" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.2+6" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.3+5" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: "73181fbc5257776d8ecaa6a94ab3c8e920ad143b9132a6d984a9271dfc6928d3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.5" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.3+5" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + gcode_core: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.1.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.18.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.flutter-io.cn" + source: hosted + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" +sdks: + dart: ">=3.11.5 <4.0.0" + flutter: ">=3.38.0" diff --git a/packages/gcode_core/example/pubspec.yaml b/packages/gcode_core/example/pubspec.yaml new file mode 100644 index 0000000..4f1bc4a --- /dev/null +++ b/packages/gcode_core/example/pubspec.yaml @@ -0,0 +1,93 @@ +name: example +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.11.5 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + + gcode_core: + path: ../ + file_selector: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/packages/gcode_core/example/test/widget_test.dart b/packages/gcode_core/example/test/widget_test.dart new file mode 100644 index 0000000..7c72bb2 --- /dev/null +++ b/packages/gcode_core/example/test/widget_test.dart @@ -0,0 +1,19 @@ +import 'package:gcode_core/gcode_core.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('GcodeReadlinePipeline smoke test', (WidgetTester tester) async { + final pipeline = GcodeReadlinePipeline( + options: const GcodeReadlineOptions(snapshotBatchSize: 2), + ); + + final snapshots = await pipeline + .load(const StringGcodeLineReader('G0 X0 Y0\nG1 X10 Y0\n')) + .toList(); + + expect(snapshots.isNotEmpty, true); + final last = snapshots.last; + expect(last.stage, GcodeLoadStage.ready); + expect(last.commands.length, 2); + }); +} diff --git a/packages/gcode_core/gcode_print.gif b/packages/gcode_core/gcode_print.gif new file mode 100644 index 0000000..29c3f7c Binary files /dev/null and b/packages/gcode_core/gcode_print.gif differ diff --git a/packages/gcode_core/lib/gcode_core.dart b/packages/gcode_core/lib/gcode_core.dart new file mode 100644 index 0000000..c100cd6 --- /dev/null +++ b/packages/gcode_core/lib/gcode_core.dart @@ -0,0 +1,21 @@ +library gcode_core; + +export 'src/application/gcode_readline_pipeline.dart'; +export 'src/core/gcode_bounds.dart'; +export 'src/core/gcode_style.dart'; +export 'src/data/readers/file_gcode_line_reader.dart'; +export 'src/data/readers/gcode_line_reader.dart'; +export 'src/data/readers/string_gcode_line_reader.dart'; +export 'src/domain/gcode_line_record.dart'; +export 'src/domain/gcode_load_snapshot.dart'; +export 'src/domain/gcode_load_stage.dart'; +export 'src/domain/parsed_gcode_line.dart'; +export 'src/models/gcode_command.dart'; +export 'src/models/machine_position.dart'; +export 'src/models/toolpath_segment.dart'; +export 'src/parser/gcode_parse_result.dart'; +export 'src/parser/gcode_parser.dart'; +export 'src/services/toolpath_builder.dart'; +export 'src/widgets/command_timeline.dart'; +export 'src/widgets/gcode_canvas.dart'; +export 'src/widgets/playback_controls.dart'; diff --git a/packages/gcode_core/lib/src/application/gcode_readline_pipeline.dart b/packages/gcode_core/lib/src/application/gcode_readline_pipeline.dart new file mode 100644 index 0000000..378415c --- /dev/null +++ b/packages/gcode_core/lib/src/application/gcode_readline_pipeline.dart @@ -0,0 +1,370 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; + +import '../data/readers/gcode_line_reader.dart'; +import '../domain/gcode_line_record.dart'; +import '../domain/gcode_load_snapshot.dart'; +import '../domain/gcode_load_stage.dart'; +import '../domain/parsed_gcode_line.dart'; +import '../models/gcode_command.dart'; +import '../models/toolpath_segment.dart'; +import '../parser/gcode_parse_result.dart'; +import '../parser/gcode_parser.dart'; +import '../services/toolpath_builder.dart'; + +class GcodeReadlineOptions { + const GcodeReadlineOptions({ + this.snapshotBatchSize = 200, + }); + + final int snapshotBatchSize; +} + +class GcodeReadlinePipeline { + GcodeReadlinePipeline({ + GcodeParser? parser, + IncrementalToolpathBuilder? toolpathBuilder, + this.options = const GcodeReadlineOptions(), + }) : _parser = parser ?? GcodeParser(), + _toolpathBuilder = toolpathBuilder ?? IncrementalToolpathBuilder(); + + final GcodeParser _parser; + final IncrementalToolpathBuilder _toolpathBuilder; + final GcodeReadlineOptions options; + + Stream load(GcodeLineReader reader) async* { + final commands = []; + final errors = []; + final segments = []; + var linesRead = 0; + var changedSinceSnapshot = 0; + + _toolpathBuilder.reset(); + + yield const GcodeLoadSnapshot( + stage: GcodeLoadStage.reading, + commands: [], + errors: [], + segments: [], + linesRead: 0, + message: '开始逐行读取', + ); + + try { + await for (final record in reader.readLines()) { + linesRead = record.lineNumber; + final parsed = _parser.parseRecord(record); + + switch (parsed.kind) { + case ParsedGcodeLineKind.command: + final command = parsed.command!; + commands.add(command); + final segment = _toolpathBuilder.accept(command); + if (segment != null) { + segments.add(segment); + } + case ParsedGcodeLineKind.error: + errors.add(parsed.error!); + case ParsedGcodeLineKind.skipped: + break; + } + + changedSinceSnapshot++; + if (changedSinceSnapshot >= options.snapshotBatchSize) { + changedSinceSnapshot = 0; + yield _snapshot( + stage: GcodeLoadStage.parsing, + commands: commands, + errors: errors, + segments: segments, + linesRead: linesRead, + message: '已读取 $linesRead 行', + ); + } + } + + yield _snapshot( + stage: GcodeLoadStage.ready, + commands: commands, + errors: errors, + segments: segments, + linesRead: linesRead, + message: '逐行读取完成: $linesRead 行, ' + '${commands.length} 条指令, ${errors.length} 个错误, ' + '${segments.length} 条轨迹段', + ); + } catch (error) { + yield _snapshot( + stage: GcodeLoadStage.failed, + commands: commands, + errors: errors, + segments: segments, + linesRead: linesRead, + message: '读取失败: $error', + ); + } + } + + Stream loadFileInBackground(String filePath) { + final receivePort = ReceivePort(); + + Isolate.spawn( + _isolateLoadFile, + (filePath, options, receivePort.sendPort), + ); + + return receivePort + .takeWhile((msg) => msg is! _IsolateDone) + .cast(); + } + + Stream loadStringInBackground(String source) { + final receivePort = ReceivePort(); + + Isolate.spawn( + _isolateLoadString, + (source, options, receivePort.sendPort), + ); + + return receivePort + .takeWhile((msg) => msg is! _IsolateDone) + .cast(); + } + + GcodeLoadSnapshot _snapshot({ + required GcodeLoadStage stage, + required List commands, + required List errors, + required List segments, + required int linesRead, + required String message, + }) { + final b = _toolpathBuilder.bounds; + return GcodeLoadSnapshot( + stage: stage, + commands: List.unmodifiable(commands), + errors: List.unmodifiable(errors), + segments: List.unmodifiable(segments), + linesRead: linesRead, + message: message, + bounds: + b.minX != 0 || b.maxX != 0 || b.minY != 0 || b.maxY != 0 ? b : null, + ); + } + + static void _isolateLoadFile( + (String, GcodeReadlineOptions, SendPort) args, + ) { + () async { + final (filePath, options, sendPort) = args; + final parser = GcodeParser(); + final builder = IncrementalToolpathBuilder(); + + final commands = []; + final errors = []; + final segments = []; + var linesRead = 0; + var changedSinceSnapshot = 0; + + try { + sendPort.send( + const GcodeLoadSnapshot( + stage: GcodeLoadStage.reading, + commands: [], + errors: [], + segments: [], + linesRead: 0, + message: '开始逐行读取', + ), + ); + + final file = File(filePath); + final stream = file + .openRead() + .transform(utf8.decoder) + .transform(const LineSplitter()); + + await for (final line in stream) { + linesRead++; + final record = GcodeLineRecord( + lineNumber: linesRead, + rawLine: line, + byteOffset: 0, + ); + + final parsed = parser.parseRecord(record); + + switch (parsed.kind) { + case ParsedGcodeLineKind.command: + final command = parsed.command!; + commands.add(command); + final segment = builder.accept(command); + if (segment != null) { + segments.add(segment); + } + case ParsedGcodeLineKind.error: + errors.add(parsed.error!); + case ParsedGcodeLineKind.skipped: + break; + } + + changedSinceSnapshot++; + if (changedSinceSnapshot >= options.snapshotBatchSize) { + changedSinceSnapshot = 0; + final b = builder.bounds; + sendPort.send( + GcodeLoadSnapshot( + stage: GcodeLoadStage.parsing, + commands: List.unmodifiable(commands), + errors: List.unmodifiable(errors), + segments: List.unmodifiable(segments), + linesRead: linesRead, + message: '已读取 $linesRead 行', + bounds: b.minX != 0 || b.maxX != 0 || b.minY != 0 || b.maxY != 0 + ? b + : null, + ), + ); + } + } + + final b = builder.bounds; + sendPort.send( + GcodeLoadSnapshot( + stage: GcodeLoadStage.ready, + commands: List.unmodifiable(commands), + errors: List.unmodifiable(errors), + segments: List.unmodifiable(segments), + linesRead: linesRead, + message: '逐行读取完成: $linesRead 行, ' + '${commands.length} 条指令, ${errors.length} 个错误, ' + '${segments.length} 条轨迹段', + bounds: b.minX != 0 || b.maxX != 0 || b.minY != 0 || b.maxY != 0 + ? b + : null, + ), + ); + } catch (error) { + sendPort.send( + GcodeLoadSnapshot( + stage: GcodeLoadStage.failed, + commands: List.unmodifiable(commands), + errors: List.unmodifiable(errors), + segments: List.unmodifiable(segments), + linesRead: linesRead, + message: '读取失败: $error', + ), + ); + } + + sendPort.send(const _IsolateDone()); + }(); + } + + static void _isolateLoadString( + (String, GcodeReadlineOptions, SendPort) args, + ) { + final (source, options, sendPort) = args; + final parser = GcodeParser(); + final builder = IncrementalToolpathBuilder(); + + final commands = []; + final errors = []; + final segments = []; + var linesRead = 0; + var changedSinceSnapshot = 0; + + try { + sendPort.send( + const GcodeLoadSnapshot( + stage: GcodeLoadStage.reading, + commands: [], + errors: [], + segments: [], + linesRead: 0, + message: '开始逐行读取', + ), + ); + + for (final line in const LineSplitter().convert(source)) { + linesRead++; + final record = GcodeLineRecord( + lineNumber: linesRead, + rawLine: line, + byteOffset: 0, + ); + + final parsed = parser.parseRecord(record); + + switch (parsed.kind) { + case ParsedGcodeLineKind.command: + final command = parsed.command!; + commands.add(command); + final segment = builder.accept(command); + if (segment != null) { + segments.add(segment); + } + case ParsedGcodeLineKind.error: + errors.add(parsed.error!); + case ParsedGcodeLineKind.skipped: + break; + } + + changedSinceSnapshot++; + if (changedSinceSnapshot >= options.snapshotBatchSize) { + changedSinceSnapshot = 0; + final b = builder.bounds; + sendPort.send( + GcodeLoadSnapshot( + stage: GcodeLoadStage.parsing, + commands: commands, + errors: errors, + segments: segments, + linesRead: linesRead, + message: '已读取 $linesRead 行', + bounds: b.minX != 0 || b.maxX != 0 || b.minY != 0 || b.maxY != 0 + ? b + : null, + ), + ); + } + } + + final b = builder.bounds; + sendPort.send( + GcodeLoadSnapshot( + stage: GcodeLoadStage.ready, + commands: commands, + errors: errors, + segments: segments, + linesRead: linesRead, + message: '逐行读取完成: $linesRead 行, ' + '${commands.length} 条指令, ${errors.length} 个错误, ' + '${segments.length} 条轨迹段', + bounds: b.minX != 0 || b.maxX != 0 || b.minY != 0 || b.maxY != 0 + ? b + : null, + ), + ); + } catch (error) { + sendPort.send( + GcodeLoadSnapshot( + stage: GcodeLoadStage.failed, + commands: commands, + errors: errors, + segments: segments, + linesRead: linesRead, + message: '读取失败: $error', + ), + ); + } + + sendPort.send(const _IsolateDone()); + } +} + +class _IsolateDone { + const _IsolateDone(); +} diff --git a/packages/gcode_core/lib/src/core/gcode_bounds.dart b/packages/gcode_core/lib/src/core/gcode_bounds.dart new file mode 100644 index 0000000..22c26a2 --- /dev/null +++ b/packages/gcode_core/lib/src/core/gcode_bounds.dart @@ -0,0 +1,24 @@ +class GcodeBounds { + const GcodeBounds({ + required this.minX, + required this.maxX, + required this.minY, + required this.maxY, + }); + + static const zero = GcodeBounds(minX: 0, maxX: 0, minY: 0, maxY: 0); + + final double minX; + final double maxX; + final double minY; + final double maxY; + + GcodeBounds expand(double x, double y) { + return GcodeBounds( + minX: x < minX ? x : minX, + maxX: x > maxX ? x : maxX, + minY: y < minY ? y : minY, + maxY: y > maxY ? y : maxY, + ); + } +} diff --git a/packages/gcode_core/lib/src/core/gcode_style.dart b/packages/gcode_core/lib/src/core/gcode_style.dart new file mode 100644 index 0000000..4cc5ec2 --- /dev/null +++ b/packages/gcode_core/lib/src/core/gcode_style.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +class GcodeStyle { + const GcodeStyle({ + required this.rapidMovePaint, + required this.rapidMoveBgPaint, + required this.linearMovePaint, + required this.linearMoveBgPaint, + required this.toolHeadPaint, + required this.toolHeadGlowPaint, + required this.originPaint, + required this.originDotPaint, + required this.gridPaint, + }); + + final Paint rapidMovePaint; + final Paint rapidMoveBgPaint; + final Paint linearMovePaint; + final Paint linearMoveBgPaint; + final Paint toolHeadPaint; + final Paint toolHeadGlowPaint; + final Paint originPaint; + final Paint originDotPaint; + final Paint gridPaint; + + factory GcodeStyle.light({ + Color rapidColor = Colors.red, + Color linearColor = Colors.green, + Color toolHeadColor = Colors.red, + Color originColor = const Color(0x99FF9800), + Color gridColor = const Color(0x26000000), + }) { + return GcodeStyle( + rapidMovePaint: Paint() + ..color = rapidColor + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round, + rapidMoveBgPaint: Paint() + ..color = rapidColor.withValues(alpha: 0.25) + ..strokeWidth = 1 + ..style = PaintingStyle.stroke, + linearMovePaint: Paint() + ..color = linearColor + ..strokeWidth = 2.5 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round, + linearMoveBgPaint: Paint() + ..color = linearColor.withValues(alpha: 0.15) + ..strokeWidth = 1 + ..style = PaintingStyle.stroke, + toolHeadPaint: Paint() + ..color = toolHeadColor + ..style = PaintingStyle.fill, + toolHeadGlowPaint: Paint() + ..color = toolHeadColor.withValues(alpha: 0.3) + ..style = PaintingStyle.fill, + originPaint: Paint() + ..color = originColor + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke, + originDotPaint: Paint() + ..color = originColor + ..style = PaintingStyle.fill, + gridPaint: Paint() + ..color = gridColor + ..strokeWidth = 0.5, + ); + } +} diff --git a/packages/gcode_core/lib/src/data/readers/file_gcode_line_reader.dart b/packages/gcode_core/lib/src/data/readers/file_gcode_line_reader.dart new file mode 100644 index 0000000..b90532b --- /dev/null +++ b/packages/gcode_core/lib/src/data/readers/file_gcode_line_reader.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; +import 'dart:io'; + +import '../../domain/gcode_line_record.dart'; +import 'gcode_line_reader.dart'; + +class FileGcodeLineReader implements GcodeLineReader { + const FileGcodeLineReader(this.path); + + final String path; + + static String normalizePath(String value) { + var normalized = value.trim(); + if (normalized.length >= 2) { + final first = normalized[0]; + final last = normalized[normalized.length - 1]; + if ((first == '"' && last == '"') || (first == "'" && last == "'")) { + normalized = normalized.substring(1, normalized.length - 1).trim(); + } + } + + if (normalized.startsWith('file://')) { + normalized = + Uri.parse(normalized).toFilePath(windows: Platform.isWindows); + } + + final home = Platform.environment['HOME'] ?? + Platform.environment['USERPROFILE'] ?? + ''; + if (home.isNotEmpty && normalized == '~') { + normalized = home; + } else if (home.isNotEmpty && normalized.startsWith('~/')) { + normalized = '$home/${normalized.substring(2)}'; + } + + if (!Platform.isWindows) { + normalized = normalized.replaceAllMapped( + RegExp(r'\\([ ()\[\]&;])'), + (match) => match.group(1)!, + ); + } + + return normalized; + } + + @override + Stream readLines() async* { + final normalizedPath = normalizePath(path); + final type = await FileSystemEntity.type(normalizedPath); + if (type == FileSystemEntityType.notFound) { + throw FileSystemException('文件不存在', normalizedPath); + } + if (type == FileSystemEntityType.directory) { + throw FileSystemException('路径是目录,不是 G-code 文件', normalizedPath); + } + + final file = File(normalizedPath); + var lineNumber = 0; + var byteOffset = 0; + + await for (final line in file + .openRead() + .transform(utf8.decoder) + .transform(const LineSplitter())) { + lineNumber++; + yield GcodeLineRecord( + lineNumber: lineNumber, + rawLine: line, + byteOffset: byteOffset, + ); + byteOffset += utf8.encode(line).length + 1; + } + } +} diff --git a/packages/gcode_core/lib/src/data/readers/gcode_line_reader.dart b/packages/gcode_core/lib/src/data/readers/gcode_line_reader.dart new file mode 100644 index 0000000..4691395 --- /dev/null +++ b/packages/gcode_core/lib/src/data/readers/gcode_line_reader.dart @@ -0,0 +1,5 @@ +import '../../domain/gcode_line_record.dart'; + +abstract interface class GcodeLineReader { + Stream readLines(); +} diff --git a/packages/gcode_core/lib/src/data/readers/string_gcode_line_reader.dart b/packages/gcode_core/lib/src/data/readers/string_gcode_line_reader.dart new file mode 100644 index 0000000..a122ef8 --- /dev/null +++ b/packages/gcode_core/lib/src/data/readers/string_gcode_line_reader.dart @@ -0,0 +1,39 @@ +import '../../domain/gcode_line_record.dart'; +import 'gcode_line_reader.dart'; + +class StringGcodeLineReader implements GcodeLineReader { + const StringGcodeLineReader(this.source); + + final String source; + + @override + Stream readLines() async* { + var lineNumber = 0; + var byteOffset = 0; + var start = 0; + + for (var i = 0; i < source.length; i++) { + final codeUnit = source.codeUnitAt(i); + if (codeUnit != 10) continue; + + lineNumber++; + final end = i > start && source.codeUnitAt(i - 1) == 13 ? i - 1 : i; + yield GcodeLineRecord( + lineNumber: lineNumber, + rawLine: source.substring(start, end), + byteOffset: byteOffset, + ); + byteOffset = i + 1; + start = i + 1; + } + + if (start < source.length || source.isEmpty) { + lineNumber++; + yield GcodeLineRecord( + lineNumber: lineNumber, + rawLine: source.substring(start), + byteOffset: byteOffset, + ); + } + } +} diff --git a/packages/gcode_core/lib/src/domain/gcode_line_record.dart b/packages/gcode_core/lib/src/domain/gcode_line_record.dart new file mode 100644 index 0000000..be8fa24 --- /dev/null +++ b/packages/gcode_core/lib/src/domain/gcode_line_record.dart @@ -0,0 +1,11 @@ +class GcodeLineRecord { + const GcodeLineRecord({ + required this.lineNumber, + required this.rawLine, + required this.byteOffset, + }); + + final int lineNumber; + final String rawLine; + final int byteOffset; +} diff --git a/packages/gcode_core/lib/src/domain/gcode_load_snapshot.dart b/packages/gcode_core/lib/src/domain/gcode_load_snapshot.dart new file mode 100644 index 0000000..2e5bc95 --- /dev/null +++ b/packages/gcode_core/lib/src/domain/gcode_load_snapshot.dart @@ -0,0 +1,41 @@ +import '../core/gcode_bounds.dart'; +import '../models/gcode_command.dart'; +import '../models/toolpath_segment.dart'; +import '../parser/gcode_parse_result.dart'; +import 'gcode_load_stage.dart'; + +class GcodeLoadSnapshot { + const GcodeLoadSnapshot({ + required this.stage, + required this.commands, + required this.errors, + required this.segments, + required this.linesRead, + this.message = '', + this.diagnosticMessage = '', + this.bounds, + }); + + factory GcodeLoadSnapshot.empty() { + return const GcodeLoadSnapshot( + stage: GcodeLoadStage.idle, + commands: [], + errors: [], + segments: [], + linesRead: 0, + ); + } + + final GcodeLoadStage stage; + final List commands; + final List errors; + final List segments; + final int linesRead; + final String message; + final String diagnosticMessage; + final GcodeBounds? bounds; + + GcodeParseResult toParseResult() { + return GcodeParseResult(commands: commands, errors: errors); + } +} diff --git a/packages/gcode_core/lib/src/domain/gcode_load_stage.dart b/packages/gcode_core/lib/src/domain/gcode_load_stage.dart new file mode 100644 index 0000000..dffe2da --- /dev/null +++ b/packages/gcode_core/lib/src/domain/gcode_load_stage.dart @@ -0,0 +1,7 @@ +enum GcodeLoadStage { + idle, + reading, + parsing, + ready, + failed, +} diff --git a/packages/gcode_core/lib/src/domain/parsed_gcode_line.dart b/packages/gcode_core/lib/src/domain/parsed_gcode_line.dart new file mode 100644 index 0000000..712a52b --- /dev/null +++ b/packages/gcode_core/lib/src/domain/parsed_gcode_line.dart @@ -0,0 +1,51 @@ +import '../models/gcode_command.dart'; +import '../parser/gcode_parse_result.dart'; +import 'gcode_line_record.dart'; + +enum ParsedGcodeLineKind { command, error, skipped } + +class ParsedGcodeLine { + const ParsedGcodeLine._({ + required this.kind, + required this.record, + this.command, + this.error, + }); + + factory ParsedGcodeLine.command( + GcodeLineRecord record, + GcodeCommand command, + ) { + return ParsedGcodeLine._( + kind: ParsedGcodeLineKind.command, + record: record, + command: command, + ); + } + + factory ParsedGcodeLine.error( + GcodeLineRecord record, + GcodeParseError error, + ) { + return ParsedGcodeLine._( + kind: ParsedGcodeLineKind.error, + record: record, + error: error, + ); + } + + factory ParsedGcodeLine.skipped(GcodeLineRecord record) { + return ParsedGcodeLine._( + kind: ParsedGcodeLineKind.skipped, + record: record, + ); + } + + final ParsedGcodeLineKind kind; + final GcodeLineRecord record; + final GcodeCommand? command; + final GcodeParseError? error; + + bool get hasCommand => kind == ParsedGcodeLineKind.command; + bool get hasError => kind == ParsedGcodeLineKind.error; +} diff --git a/packages/gcode_core/lib/src/models/gcode_command.dart b/packages/gcode_core/lib/src/models/gcode_command.dart new file mode 100644 index 0000000..46fe468 --- /dev/null +++ b/packages/gcode_core/lib/src/models/gcode_command.dart @@ -0,0 +1,31 @@ +import 'machine_position.dart'; + +enum GcodeSegmentType { rapid, linear } + +class GcodeCommand { + const GcodeCommand({ + required this.lineNumber, + required this.rawLine, + required this.code, + required this.params, + this.comment = '', + }); + + final int lineNumber; + final String rawLine; + final String code; + final Map params; + final String comment; + + double? get x => params['X']; + double? get y => params['Y']; + double? get feedRate => params['F']; + + MachinePosition toPosition(MachinePosition current) { + return MachinePosition( + x: x ?? current.x, + y: y ?? current.y, + feedRate: feedRate ?? current.feedRate, + ); + } +} diff --git a/packages/gcode_core/lib/src/models/machine_position.dart b/packages/gcode_core/lib/src/models/machine_position.dart new file mode 100644 index 0000000..057d958 --- /dev/null +++ b/packages/gcode_core/lib/src/models/machine_position.dart @@ -0,0 +1,24 @@ +class MachinePosition { + const MachinePosition({ + this.x = 0, + this.y = 0, + this.feedRate = 0, + }); + + final double x; + final double y; + final double feedRate; + + MachinePosition copyWith({double? x, double? y, double? feedRate}) { + return MachinePosition( + x: x ?? this.x, + y: y ?? this.y, + feedRate: feedRate ?? this.feedRate, + ); + } + + @override + String toString() { + return 'MachinePosition(x: $x, y: $y, F: $feedRate)'; + } +} diff --git a/packages/gcode_core/lib/src/models/toolpath_segment.dart b/packages/gcode_core/lib/src/models/toolpath_segment.dart new file mode 100644 index 0000000..00a2722 --- /dev/null +++ b/packages/gcode_core/lib/src/models/toolpath_segment.dart @@ -0,0 +1,16 @@ +import 'gcode_command.dart'; +import 'machine_position.dart'; + +class ToolpathSegment { + const ToolpathSegment({ + required this.start, + required this.end, + required this.command, + required this.type, + }); + + final MachinePosition start; + final MachinePosition end; + final GcodeCommand command; + final GcodeSegmentType type; +} diff --git a/packages/gcode_core/lib/src/parser/gcode_parse_result.dart b/packages/gcode_core/lib/src/parser/gcode_parse_result.dart new file mode 100644 index 0000000..87b3966 --- /dev/null +++ b/packages/gcode_core/lib/src/parser/gcode_parse_result.dart @@ -0,0 +1,28 @@ +import '../models/gcode_command.dart'; + +class GcodeParseError { + const GcodeParseError({ + required this.lineNumber, + required this.rawLine, + required this.message, + }); + + final int lineNumber; + final String rawLine; + final String message; + + @override + String toString() => 'Line $lineNumber: $message (raw: "$rawLine")'; +} + +class GcodeParseResult { + const GcodeParseResult({ + required this.commands, + required this.errors, + }); + + final List commands; + final List errors; + + bool get hasErrors => errors.isNotEmpty; +} diff --git a/packages/gcode_core/lib/src/parser/gcode_parser.dart b/packages/gcode_core/lib/src/parser/gcode_parser.dart new file mode 100644 index 0000000..52e0ada --- /dev/null +++ b/packages/gcode_core/lib/src/parser/gcode_parser.dart @@ -0,0 +1,205 @@ +import '../models/gcode_command.dart'; +import '../domain/gcode_line_record.dart'; +import '../domain/parsed_gcode_line.dart'; +import 'gcode_parse_result.dart'; + +class GcodeParser { + static const _supportedCodes = {'G0', 'G00', 'G1', 'G01', 'G90', 'G91'}; + static final _paramPattern = RegExp(r'^([A-Za-z])(-?(?:\d+\.?\d*|\.\d+))$'); + + GcodeParseResult parse(String source) { + final commands = []; + final errors = []; + final lines = source.split('\n'); + + for (var i = 0; i < lines.length; i++) { + final lineNumber = i + 1; + final rawLine = lines[i].trim(); + + if (rawLine.isEmpty) continue; + + final result = parseLine(rawLine, lineNumber); + + result.when( + command: (cmd) => commands.add(cmd), + error: (err) => errors.add(err), + skipped: () {}, + ); + } + + return GcodeParseResult(commands: commands, errors: errors); + } + + ParsedGcodeLine parseRecord(GcodeLineRecord record) { + final result = parseLine(record.rawLine.trim(), record.lineNumber); + return result.when( + command: (cmd) => ParsedGcodeLine.command(record, cmd), + error: (err) => ParsedGcodeLine.error(record, err), + skipped: () => ParsedGcodeLine.skipped(record), + ); + } + + LineParseResult parseLine(String rawLine, int lineNumber) { + var line = rawLine; + + line = _removeParenthesesComments(line); + + final comment = _extractSemicolonComment(line); + line = comment != null + ? line.substring(0, line.indexOf(';')).trim() + : line.trim(); + + if (line.isEmpty) { + return LineParseResult.skipped(); + } + + final tokens = _tokenize(line); + if (tokens.isEmpty) { + return LineParseResult.skipped(); + } + + final commandCode = tokens[0].toUpperCase(); + + final normalized = _normalizeCode(commandCode); + if (!_supportedCodes.contains(normalized)) { + return LineParseResult.error( + GcodeParseError( + lineNumber: lineNumber, + rawLine: rawLine, + message: 'Unsupported code: $commandCode', + ), + ); + } + + final params = {}; + for (var i = 1; i < tokens.length; i++) { + final token = tokens[i]; + final match = _paramPattern.firstMatch(token); + if (match == null) { + return LineParseResult.error( + GcodeParseError( + lineNumber: lineNumber, + rawLine: rawLine, + message: 'Malformed parameter: $token', + ), + ); + } + final key = match.group(1)!.toUpperCase(); + final valueStr = match.group(2); + if (valueStr == null) { + return LineParseResult.error( + GcodeParseError( + lineNumber: lineNumber, + rawLine: rawLine, + message: 'Missing numeric value in: $token', + ), + ); + } + final value = double.tryParse(valueStr); + if (value == null) { + return LineParseResult.error( + GcodeParseError( + lineNumber: lineNumber, + rawLine: rawLine, + message: 'Invalid numeric value: $token', + ), + ); + } + params[key] = value; + } + + return LineParseResult.command( + GcodeCommand( + lineNumber: lineNumber, + rawLine: rawLine, + code: normalized, + params: params, + comment: comment ?? '', + ), + ); + } + + String _removeParenthesesComments(String line) { + final result = StringBuffer(); + var inComment = false; + for (var i = 0; i < line.length; i++) { + final ch = line[i]; + if (ch == '(') { + inComment = true; + } else if (ch == ')') { + inComment = false; + } else if (!inComment) { + result.write(ch); + } + } + return result.toString(); + } + + String? _extractSemicolonComment(String line) { + final index = line.indexOf(';'); + if (index == -1) return null; + return line.substring(index + 1).trim(); + } + + List _tokenize(String line) { + final tokens = []; + final buffer = StringBuffer(); + for (var i = 0; i < line.length; i++) { + final ch = line[i]; + if (ch == ' ' || ch == '\t') { + if (buffer.isNotEmpty) { + tokens.add(buffer.toString()); + buffer.clear(); + } + } else { + buffer.write(ch); + } + } + if (buffer.isNotEmpty) { + tokens.add(buffer.toString()); + } + return tokens; + } + + String _normalizeCode(String code) { + return switch (code) { + 'G0' || 'G00' => 'G0', + 'G1' || 'G01' => 'G1', + _ => code, + }; + } +} + +sealed class LineParseResult { + const LineParseResult(); + + factory LineParseResult.command(GcodeCommand cmd) => _CommandResult(cmd); + factory LineParseResult.error(GcodeParseError err) => _ErrorResult(err); + factory LineParseResult.skipped() => const _SkippedResult(); + + T when({ + required T Function(GcodeCommand) command, + required T Function(GcodeParseError) error, + required T Function() skipped, + }) { + return switch (this) { + _CommandResult(:final cmd) => command(cmd), + _ErrorResult(:final err) => error(err), + _SkippedResult() => skipped(), + }; + } +} + +class _CommandResult extends LineParseResult { + const _CommandResult(this.cmd); + final GcodeCommand cmd; +} + +class _ErrorResult extends LineParseResult { + const _ErrorResult(this.err); + final GcodeParseError err; +} + +class _SkippedResult extends LineParseResult { + const _SkippedResult(); +} diff --git a/packages/gcode_core/lib/src/services/toolpath_builder.dart b/packages/gcode_core/lib/src/services/toolpath_builder.dart new file mode 100644 index 0000000..ad3fc96 --- /dev/null +++ b/packages/gcode_core/lib/src/services/toolpath_builder.dart @@ -0,0 +1,107 @@ +import '../core/gcode_bounds.dart'; +import '../models/gcode_command.dart'; +import '../models/machine_position.dart'; +import '../models/toolpath_segment.dart'; + +enum CoordinateMode { absolute, relative } + +MachinePosition _applyCommand( + GcodeCommand cmd, MachinePosition current, CoordinateMode mode) { + if (mode == CoordinateMode.absolute) { + return cmd.toPosition(current); + } + return MachinePosition( + x: cmd.x != null ? current.x + cmd.x! : current.x, + y: cmd.y != null ? current.y + cmd.y! : current.y, + feedRate: cmd.feedRate ?? current.feedRate, + ); +} + +class ToolpathBuilder { + static List build(List commands) { + final segments = []; + var current = const MachinePosition(); + var mode = CoordinateMode.absolute; + + for (final cmd in commands) { + if (cmd.code == 'G90') { + mode = CoordinateMode.absolute; + continue; + } + if (cmd.code == 'G91') { + mode = CoordinateMode.relative; + continue; + } + + final next = _applyCommand(cmd, current, mode); + + if (next.x != current.x || next.y != current.y) { + final type = + cmd.code == 'G0' ? GcodeSegmentType.rapid : GcodeSegmentType.linear; + + segments.add( + ToolpathSegment( + start: current, + end: next, + command: cmd, + type: type, + ), + ); + } + + current = next; + } + + return segments; + } +} + +class IncrementalToolpathBuilder { + MachinePosition _current = const MachinePosition(); + GcodeBounds _bounds = GcodeBounds.zero; + CoordinateMode _mode = CoordinateMode.absolute; + + MachinePosition get current => _current; + + GcodeBounds get bounds => _bounds; + + CoordinateMode get coordinateMode => _mode; + + ToolpathSegment? accept(GcodeCommand command) { + if (command.code == 'G90') { + _mode = CoordinateMode.absolute; + return null; + } + if (command.code == 'G91') { + _mode = CoordinateMode.relative; + return null; + } + + final next = _applyCommand(command, _current, _mode); + + if (next.x == _current.x && next.y == _current.y) { + _current = next; + return null; + } + + _bounds = _bounds.expand(_current.x, _current.y).expand(next.x, next.y); + + final segment = ToolpathSegment( + start: _current, + end: next, + command: command, + type: command.code == 'G0' + ? GcodeSegmentType.rapid + : GcodeSegmentType.linear, + ); + + _current = next; + return segment; + } + + void reset() { + _current = const MachinePosition(); + _bounds = GcodeBounds.zero; + _mode = CoordinateMode.absolute; + } +} diff --git a/packages/gcode_core/lib/src/widgets/command_timeline.dart b/packages/gcode_core/lib/src/widgets/command_timeline.dart new file mode 100644 index 0000000..9b8f78b --- /dev/null +++ b/packages/gcode_core/lib/src/widgets/command_timeline.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; + +import '../models/gcode_command.dart'; +import '../parser/gcode_parse_result.dart'; + +class CommandTimeline extends StatelessWidget { + const CommandTimeline({ + super.key, + required this.commands, + required this.errors, + this.currentIndex = -1, + this.onTap, + this.maxHeight, + }); + + final List commands; + final List errors; + final int currentIndex; + final ValueChanged? onTap; + final double? maxHeight; + + @override + Widget build(BuildContext context) { + final items = _buildTimelineItems(); + + return Container( + constraints: + maxHeight != null ? BoxConstraints(maxHeight: maxHeight!) : null, + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), + child: Row( + children: [ + Text( + '指令列表 (${commands.length})', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + if (errors.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 8), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${errors.length} 错误', + style: const TextStyle( + fontSize: 11, + color: Colors.red, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ), + const Divider(height: 1), + Flexible( + child: ListView.builder( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + final cmd = item.command; + final error = item.error; + final commandIndex = cmd == null ? -1 : commands.indexOf(cmd); + final isCurrent = + commandIndex >= 0 && commandIndex == currentIndex; + final hasError = error != null; + final code = cmd?.code; + + return InkWell( + onTap: onTap != null && commandIndex >= 0 + ? () => onTap!(commandIndex) + : null, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isCurrent + ? Theme.of(context).colorScheme.primaryContainer + : hasError + ? Colors.red.withValues(alpha: 0.05) + : null, + ), + child: Row( + children: [ + SizedBox( + width: 32, + child: Text( + '${item.lineNumber}', + style: TextStyle( + fontSize: 11, + color: hasError + ? Colors.red.shade500 + : Colors.grey.shade500, + fontFamily: 'monospace', + ), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: hasError + ? Colors.red.withValues(alpha: 0.15) + : code == 'G0' + ? Colors.blue.withValues(alpha: 0.15) + : Colors.green.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(3), + ), + child: Text( + hasError ? 'ERR' : code ?? '', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: hasError + ? Colors.red + : code == 'G0' + ? Colors.blue + : Colors.green, + fontFamily: 'monospace', + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.rawLine, + style: TextStyle( + fontSize: 12, + fontFamily: 'monospace', + color: hasError + ? Colors.red.shade700 + : isCurrent + ? null + : Colors.grey.shade700, + ), + ), + if (hasError) + Text( + error.message, + style: TextStyle( + fontSize: 11, + color: Colors.red.shade600, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + List<_TimelineItem> _buildTimelineItems() { + final items = <_TimelineItem>[ + for (final command in commands) _TimelineItem.command(command), + for (final error in errors) _TimelineItem.error(error), + ]; + items.sort((a, b) => a.lineNumber.compareTo(b.lineNumber)); + return items; + } +} + +class _TimelineItem { + const _TimelineItem._({ + required this.lineNumber, + required this.rawLine, + this.command, + this.error, + }); + + factory _TimelineItem.command(GcodeCommand command) => _TimelineItem._( + lineNumber: command.lineNumber, + rawLine: command.rawLine, + command: command, + ); + + factory _TimelineItem.error(GcodeParseError error) => _TimelineItem._( + lineNumber: error.lineNumber, + rawLine: error.rawLine, + error: error, + ); + + final int lineNumber; + final String rawLine; + final GcodeCommand? command; + final GcodeParseError? error; +} diff --git a/packages/gcode_core/lib/src/widgets/gcode_canvas.dart b/packages/gcode_core/lib/src/widgets/gcode_canvas.dart new file mode 100644 index 0000000..e5451e5 --- /dev/null +++ b/packages/gcode_core/lib/src/widgets/gcode_canvas.dart @@ -0,0 +1,502 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../core/gcode_bounds.dart'; +import '../core/gcode_style.dart'; +import '../models/gcode_command.dart'; +import '../models/toolpath_segment.dart'; + +class GcodeCanvas extends StatelessWidget { + const GcodeCanvas({ + super.key, + required this.segments, + required this.progress, + this.errorCount = 0, + this.commandCount = 0, + this.bounds, + this.style, + this.showLegend = true, + }); + + final List segments; + final double progress; + final int errorCount; + final int commandCount; + final GcodeBounds? bounds; + final GcodeStyle? style; + final bool showLegend; + + @override + Widget build(BuildContext context) { + final effectiveStyle = style ?? GcodeStyle.light(); + + return Container( + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: LayoutBuilder( + builder: (context, constraints) { + Widget content; + + if (segments.isEmpty && commandCount == 0) { + content = _buildEmptyState(); + } else if (segments.isEmpty && commandCount > 0) { + content = _buildNoMovementState(commandCount); + } else if (segments.isNotEmpty && errorCount > 0) { + content = _buildPartialErrorState(constraints, effectiveStyle); + } else { + content = Stack( + children: [ + CustomPaint( + size: Size(constraints.maxWidth, constraints.maxHeight), + painter: _ToolpathPainter( + segments: segments, + progress: progress, + bounds: bounds, + style: effectiveStyle, + ), + ), + if (showLegend) + Positioned( + left: 8, + bottom: 8, + child: _CanvasLegend(style: effectiveStyle), + ), + ], + ); + } + + return content; + }, + ), + ); + } + + Widget _buildEmptyState() { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.draw_outlined, size: 48, color: Colors.grey), + SizedBox(height: 12), + Text( + '输入并解析 G-code 后显示轨迹', + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + ], + ), + ); + } + + Widget _buildNoMovementState(int cmds) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.info_outline, size: 48, color: Colors.orange.shade300), + const SizedBox(height: 12), + Text( + '已解析 $cmds 条指令,但未产生运动轨迹', + style: const TextStyle(fontSize: 14, color: Colors.grey), + ), + const SizedBox(height: 4), + Text( + '提示:G1 F1200 仅设置进给率,需配合 X/Y 坐标', + style: TextStyle(fontSize: 12, color: Colors.grey.shade500), + ), + ], + ), + ); + } + + Widget _buildPartialErrorState(BoxConstraints constraints, GcodeStyle style) { + return Stack( + children: [ + CustomPaint( + size: Size(constraints.maxWidth, constraints.maxHeight), + painter: _ToolpathPainter( + segments: segments, + progress: progress, + bounds: bounds, + style: style, + ), + ), + if (showLegend) + Positioned( + left: 8, + bottom: 8, + child: _CanvasLegend(style: style), + ), + Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '$errorCount 个解析错误', + style: const TextStyle( + fontSize: 11, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ); + } +} + +class _CanvasLegend extends StatelessWidget { + const _CanvasLegend({required this.style}); + + final GcodeStyle style; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.85), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _legendRow(style.rapidMovePaint, 'G0 快速移动', isDashed: true), + const SizedBox(height: 4), + _legendRow(style.linearMovePaint, 'G1 线性移动'), + const SizedBox(height: 4), + _legendRow(style.toolHeadPaint, '当前刀头', isCircle: true), + const SizedBox(height: 4), + _legendRow( + Paint() + ..color = const Color(0x99FF9800) + ..strokeWidth = 1.5, + '原点', + isCross: true), + ], + ), + ); + } + + Widget _legendRow(Paint paint, String label, + {bool isDashed = false, bool isCircle = false, bool isCross = false}) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 24, + height: 12, + child: CustomPaint( + painter: _LegendIconPainter( + iconPaint: paint, + isDashed: isDashed, + isCircle: isCircle, + isCross: isCross, + ), + ), + ), + const SizedBox(width: 6), + Text(label, style: const TextStyle(fontSize: 11)), + ], + ); + } +} + +class _LegendIconPainter extends CustomPainter { + _LegendIconPainter({ + required Paint iconPaint, + this.isDashed = false, + this.isCircle = false, + this.isCross = false, + }) : _iconPaint = iconPaint; + + final Paint _iconPaint; + final bool isDashed; + final bool isCircle; + final bool isCross; + + @override + void paint(Canvas canvas, Size size) { + if (isCircle) { + canvas.drawCircle(Offset(size.width / 2, size.height / 2), 4, _iconPaint); + } else if (isCross) { + final cx = size.width / 2; + final cy = size.height / 2; + canvas.drawLine(Offset(cx - 4, cy), Offset(cx + 4, cy), _iconPaint); + canvas.drawLine(Offset(cx, cy - 4), Offset(cx, cy + 4), _iconPaint); + } else if (isDashed) { + const dash = 4.0; + const gap = 3.0; + var dx = 0.0; + while (dx < size.width) { + final end = (dx + dash).clamp(0.0, size.width); + canvas.drawLine(Offset(dx, size.height / 2), + Offset(end, size.height / 2), _iconPaint); + dx = end + gap; + } + } else { + canvas.drawLine( + Offset(0, size.height / 2), + Offset(size.width, size.height / 2), + _iconPaint, + ); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class _ToolpathPainter extends CustomPainter { + _ToolpathPainter({ + required this.segments, + required this.progress, + required this.bounds, + required this.style, + }); + + final List segments; + final double progress; + final GcodeBounds? bounds; + final GcodeStyle style; + + static const _padding = 30.0; + static const _gridSpacing = 20.0; + + @override + void paint(Canvas canvas, Size size) { + if (segments.isEmpty) return; + + final b = bounds ?? _computeBounds(); + final machineRangeX = max(b.maxX - b.minX, 1.0); + final machineRangeY = max(b.maxY - b.minY, 1.0); + final scaleX = (size.width - _padding * 2) / machineRangeX; + final scaleY = (size.height - _padding * 2) / machineRangeY; + final scale = min(scaleX, scaleY); + + final offsetX = + _padding + (size.width - _padding * 2 - machineRangeX * scale) / 2; + final offsetY = + _padding + (size.height - _padding * 2 - machineRangeY * scale) / 2; + + _drawGrid(canvas, size, b, scale, offsetX, offsetY); + _drawFullPath(canvas, b, scale, offsetX, offsetY); + _drawAnimatedPath(canvas, b, scale, offsetX, offsetY); + _drawToolHead(canvas, b, scale, offsetX, offsetY); + _drawOrigin(canvas, b, scale, offsetX, offsetY); + } + + void _drawGrid( + Canvas canvas, + Size size, + GcodeBounds bounds, + double scale, + double offsetX, + double offsetY, + ) { + final gridStep = _gridSpacing / scale; + + var x = (bounds.minX / gridStep).floor() * gridStep; + while (x <= bounds.maxX) { + final sx = offsetX + (x - bounds.minX) * scale; + canvas.drawLine( + Offset(sx, offsetY), + Offset(sx, offsetY + (bounds.maxY - bounds.minY) * scale), + style.gridPaint, + ); + x += gridStep; + } + + var y = (bounds.minY / gridStep).floor() * gridStep; + while (y <= bounds.maxY) { + final sy = offsetY + (bounds.maxY - y - bounds.minY) * scale; + canvas.drawLine( + Offset(offsetX, sy), + Offset(offsetX + (bounds.maxX - bounds.minX) * scale, sy), + style.gridPaint, + ); + y += gridStep; + } + } + + void _drawFullPath( + Canvas canvas, + GcodeBounds bounds, + double scale, + double offsetX, + double offsetY, + ) { + for (final seg in segments) { + final sx = offsetX + (seg.start.x - bounds.minX) * scale; + final sy = offsetY + (bounds.maxY - seg.start.y - bounds.minY) * scale; + final ex = offsetX + (seg.end.x - bounds.minX) * scale; + final ey = offsetY + (bounds.maxY - seg.end.y - bounds.minY) * scale; + + if (seg.type == GcodeSegmentType.rapid) { + _drawDashedLine( + canvas, + Offset(sx, sy), + Offset(ex, ey), + style.rapidMoveBgPaint, + ); + } else { + canvas.drawLine( + Offset(sx, sy), + Offset(ex, ey), + style.linearMoveBgPaint, + ); + } + } + } + + void _drawAnimatedPath( + Canvas canvas, + GcodeBounds bounds, + double scale, + double offsetX, + double offsetY, + ) { + if (progress <= 0 || segments.isEmpty) return; + + final totalSegments = segments.length; + final currentSegFloat = progress * totalSegments; + final currentSegIndex = currentSegFloat.floor().clamp(0, totalSegments - 1); + final localProgress = (currentSegFloat - currentSegIndex).clamp(0.0, 1.0); + + for (var i = 0; i <= currentSegIndex && i < totalSegments; i++) { + final seg = segments[i]; + final isCurrent = i == currentSegIndex; + + var endX = seg.end.x; + var endY = seg.end.y; + + if (isCurrent) { + endX = seg.start.x + (seg.end.x - seg.start.x) * localProgress; + endY = seg.start.y + (seg.end.y - seg.start.y) * localProgress; + } + + final sx = offsetX + (seg.start.x - bounds.minX) * scale; + final sy = offsetY + (bounds.maxY - seg.start.y - bounds.minY) * scale; + final ex = offsetX + (endX - bounds.minX) * scale; + final ey = offsetY + (bounds.maxY - endY - bounds.minY) * scale; + + if (seg.type == GcodeSegmentType.rapid) { + _drawDashedLine( + canvas, + Offset(sx, sy), + Offset(ex, ey), + style.rapidMovePaint, + ); + } else { + canvas.drawLine( + Offset(sx, sy), + Offset(ex, ey), + style.linearMovePaint, + ); + } + } + } + + void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) { + const dashLength = 7.0; + const gapLength = 5.0; + final delta = end - start; + final distance = delta.distance; + if (distance == 0) return; + + final direction = delta / distance; + var current = 0.0; + while (current < distance) { + final next = min(current + dashLength, distance); + canvas.drawLine( + start + direction * current, + start + direction * next, + paint, + ); + current = next + gapLength; + } + } + + void _drawToolHead( + Canvas canvas, + GcodeBounds bounds, + double scale, + double offsetX, + double offsetY, + ) { + if (progress <= 0 || segments.isEmpty) return; + + final totalSegments = segments.length; + final currentSegFloat = progress * totalSegments; + final currentSegIndex = currentSegFloat.floor().clamp(0, totalSegments - 1); + final localProgress = (currentSegFloat - currentSegIndex).clamp(0.0, 1.0); + final seg = segments[currentSegIndex]; + + final toolX = seg.start.x + (seg.end.x - seg.start.x) * localProgress; + final toolY = seg.start.y + (seg.end.y - seg.start.y) * localProgress; + + final sx = offsetX + (toolX - bounds.minX) * scale; + final sy = offsetY + (bounds.maxY - toolY - bounds.minY) * scale; + + canvas.drawCircle(Offset(sx, sy), 10, style.toolHeadGlowPaint); + canvas.drawCircle(Offset(sx, sy), 5, style.toolHeadPaint); + } + + void _drawOrigin( + Canvas canvas, + GcodeBounds bounds, + double scale, + double offsetX, + double offsetY, + ) { + final ox = offsetX + (0 - bounds.minX) * scale; + final oy = offsetY + (bounds.maxY - 0 - bounds.minY) * scale; + + const size = 6; + canvas.drawLine( + Offset(ox - size, oy), Offset(ox + size, oy), style.originPaint); + canvas.drawLine( + Offset(ox, oy - size), Offset(ox, oy + size), style.originPaint); + canvas.drawCircle(Offset(ox, oy), 2, style.originDotPaint); + } + + GcodeBounds _computeBounds() { + var minX = double.infinity; + var maxX = double.negativeInfinity; + var minY = double.infinity; + var maxY = double.negativeInfinity; + + for (final seg in segments) { + minX = min(minX, min(seg.start.x, seg.end.x)); + maxX = max(maxX, max(seg.start.x, seg.end.x)); + minY = min(minY, min(seg.start.y, seg.end.y)); + maxY = max(maxY, max(seg.start.y, seg.end.y)); + } + + return GcodeBounds( + minX: minX, + maxX: maxX, + minY: minY, + maxY: maxY, + ); + } + + @override + bool shouldRepaint(covariant _ToolpathPainter oldDelegate) { + return oldDelegate.progress != progress || + oldDelegate.segments != segments || + oldDelegate.bounds != bounds || + oldDelegate.style != style; + } +} diff --git a/packages/gcode_core/lib/src/widgets/playback_controls.dart b/packages/gcode_core/lib/src/widgets/playback_controls.dart new file mode 100644 index 0000000..5597d2d --- /dev/null +++ b/packages/gcode_core/lib/src/widgets/playback_controls.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; + +class PlaybackControls extends StatelessWidget { + const PlaybackControls({ + super.key, + required this.isPlaying, + required this.progress, + required this.speedMultiplier, + required this.onPlay, + required this.onPause, + required this.onReset, + required this.onSeek, + required this.onSpeedChange, + }); + + final bool isPlaying; + final double progress; + final double speedMultiplier; + final VoidCallback onPlay; + final VoidCallback onPause; + final VoidCallback onReset; + final ValueChanged onSeek; + final ValueChanged onSpeedChange; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + children: [ + Row( + children: [ + IconButton.filled( + onPressed: isPlaying ? onPause : onPlay, + icon: + Icon(isPlaying ? Icons.pause : Icons.play_arrow, size: 20), + style: IconButton.styleFrom( + minimumSize: const Size(36, 36), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + IconButton.filledTonal( + onPressed: onReset, + icon: const Icon(Icons.stop, size: 18), + style: IconButton.styleFrom( + minimumSize: const Size(36, 36), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + const SizedBox(width: 8), + Expanded( + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 4, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6), + overlayShape: RoundSliderOverlayShape(overlayRadius: 12), + ), + child: Slider( + value: progress, + onChanged: onSeek, + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 50, + child: Text( + '${(progress * 100).toStringAsFixed(0)}%', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), + ), + ), + ], + ), + Row( + children: [ + const Text('速度', style: TextStyle(fontSize: 12)), + const SizedBox(width: 8), + Expanded( + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 2, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 5), + overlayShape: RoundSliderOverlayShape(overlayRadius: 10), + ), + child: Slider( + value: speedMultiplier, + min: 0.25, + max: 4.0, + divisions: 15, + label: '${speedMultiplier.toStringAsFixed(1)}x', + onChanged: onSpeedChange, + ), + ), + ), + SizedBox( + width: 40, + child: Text( + '${speedMultiplier.toStringAsFixed(1)}x', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/packages/gcode_core/pubspec.yaml b/packages/gcode_core/pubspec.yaml new file mode 100644 index 0000000..3ac411b --- /dev/null +++ b/packages/gcode_core/pubspec.yaml @@ -0,0 +1,20 @@ +name: gcode_core +description: G-code parsing, line reading, toolpath building, and Flutter visualization widgets. +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: '>=3.6.0 <4.0.0' + +resolution: workspace + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + lints: ^4.0.0 + +flutter: diff --git a/packages/gcode_core/test/application/gcode_readline_pipeline_test.dart b/packages/gcode_core/test/application/gcode_readline_pipeline_test.dart new file mode 100644 index 0000000..c27e9b4 --- /dev/null +++ b/packages/gcode_core/test/application/gcode_readline_pipeline_test.dart @@ -0,0 +1,80 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:gcode_core/gcode_core.dart'; + +void main() { + group('GcodeReadlinePipeline', () { + test('reads string source line by line and builds segments', () async { + const source = ''' +G0 X0 Y0 +G1 X10 Y0 +G2 X10 Y10 +G1 X10 Y10 +'''; + + final pipeline = GcodeReadlinePipeline( + options: const GcodeReadlineOptions(snapshotBatchSize: 2), + ); + final snapshots = + await pipeline.load(const StringGcodeLineReader(source)).toList(); + + expect(snapshots.first.stage, GcodeLoadStage.reading); + expect(snapshots.last.stage, GcodeLoadStage.ready); + expect(snapshots.last.linesRead, 4); + expect(snapshots.last.commands, hasLength(3)); + expect(snapshots.last.errors, hasLength(1)); + expect(snapshots.last.segments, hasLength(2)); + expect(snapshots.last.segments.last.end.x, 10); + expect(snapshots.last.segments.last.end.y, 10); + }); + + test('reads gcode from file path', () async { + final file = File('${Directory.systemTemp.path}/gcode_readline_test.nc'); + await file.writeAsString('G0 X0 Y0\nG1 X5 Y5\n'); + addTearDown(() { + if (file.existsSync()) { + file.deleteSync(); + } + }); + + final pipeline = GcodeReadlinePipeline(); + final snapshots = + await pipeline.load(FileGcodeLineReader(file.path)).toList(); + + expect(snapshots.last.stage, GcodeLoadStage.ready); + expect(snapshots.last.linesRead, 2); + expect(snapshots.last.commands, hasLength(2)); + expect(snapshots.last.segments, hasLength(1)); + expect(snapshots.last.segments.single.end.x, 5); + expect(snapshots.last.segments.single.end.y, 5); + }); + + test('normalizes copied file paths before opening', () async { + final file = + File('${Directory.systemTemp.path}/gcode readline copied path.nc'); + await file.writeAsString('G0 X0 Y0\nG1 X3 Y4\n'); + addTearDown(() { + if (file.existsSync()) { + file.deleteSync(); + } + }); + + final pipeline = GcodeReadlinePipeline(); + final quotedPathSnapshots = + await pipeline.load(FileGcodeLineReader('"${file.path}"')).toList(); + final fileUriSnapshots = await pipeline + .load(FileGcodeLineReader(file.uri.toString())) + .toList(); + final escapedPathSnapshots = await pipeline + .load(FileGcodeLineReader(file.path.replaceAll(' ', r'\ '))) + .toList(); + + expect(quotedPathSnapshots.last.stage, GcodeLoadStage.ready); + expect(fileUriSnapshots.last.stage, GcodeLoadStage.ready); + expect(escapedPathSnapshots.last.stage, GcodeLoadStage.ready); + expect(escapedPathSnapshots.last.segments.single.end.x, 3); + expect(escapedPathSnapshots.last.segments.single.end.y, 4); + }); + }); +} diff --git a/packages/gcode_core/test/gcode_parser_test.dart b/packages/gcode_core/test/gcode_parser_test.dart new file mode 100644 index 0000000..1aac0a0 --- /dev/null +++ b/packages/gcode_core/test/gcode_parser_test.dart @@ -0,0 +1,153 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:gcode_core/gcode_core.dart'; + +void main() { + group('GcodeParser', () { + final parser = GcodeParser(); + + test('parses G0 uppercase', () { + final result = parser.parse('G0 X10 Y20'); + expect(result.commands, hasLength(1)); + expect(result.commands.first.code, 'G0'); + expect(result.commands.first.x, 10); + expect(result.commands.first.y, 20); + }); + + test('parses G1 uppercase', () { + final result = parser.parse('G1 X30 Y40 F500'); + expect(result.commands, hasLength(1)); + expect(result.commands.first.code, 'G1'); + expect(result.commands.first.x, 30); + expect(result.commands.first.y, 40); + expect(result.commands.first.feedRate, 500); + }); + + test('parses lowercase', () { + final result = parser.parse('g0 x10 y20'); + expect(result.commands, hasLength(1)); + expect(result.commands.first.code, 'G0'); + expect(result.commands.first.x, 10); + }); + + test('parses G00 and G01 aliases', () { + final r1 = parser.parse('G00 X5 Y5'); + expect(r1.commands.first.code, 'G0'); + + final r2 = parser.parse('G01 X5 Y5'); + expect(r2.commands.first.code, 'G1'); + }); + + test('parses semicolon comments', () { + final result = parser.parse('G1 X10 Y10 ; move to position'); + expect(result.commands, hasLength(1)); + expect(result.commands.first.comment, 'move to position'); + }); + + test('parses parentheses comments', () { + final result = parser.parse('G1 X10 Y10 (comment here)'); + expect(result.commands, hasLength(1)); + expect(result.commands.first.x, 10); + }); + + test('skips empty lines', () { + final result = parser.parse(''' + +G0 X0 Y0 + +G1 X10 Y10 + +'''); + expect(result.commands, hasLength(2)); + expect(result.errors, isEmpty); + }); + + test('rejects unsupported G2', () { + final result = parser.parse('G2 X10 Y10 I5 J5'); + expect(result.errors, hasLength(1)); + expect(result.errors.first.message, contains('Unsupported code')); + }); + + test('rejects malformed X value', () { + final result = parser.parse('G1 Xabc Y10'); + expect(result.errors, hasLength(1)); + expect(result.errors.first.message, contains('Malformed parameter')); + }); + + test('rejects parameter with trailing junk', () { + final result = parser.parse('G1 X10abc Y10'); + expect(result.commands, isEmpty); + expect(result.errors, hasLength(1)); + expect(result.errors.first.message, contains('Malformed parameter')); + }); + + test('handles negative coordinates', () { + final result = parser.parse('G1 X-10.5 Y-20.3'); + expect(result.commands, hasLength(1)); + expect(result.commands.first.x, -10.5); + expect(result.commands.first.y, -20.3); + }); + + test('handles leading decimal coordinates', () { + final result = parser.parse('G1 X.5 Y-.25'); + expect(result.commands, hasLength(1)); + expect(result.commands.first.x, 0.5); + expect(result.commands.first.y, -0.25); + }); + + test('multi-line parse with errors preserves valid commands', () { + final result = parser.parse(''' +G0 X0 Y0 +G2 X10 Y10 +G1 X20 Y20 +'''); + expect(result.commands, hasLength(2)); + expect(result.errors, hasLength(1)); + expect(result.errors.first.lineNumber, 2); + }); + + test('parses G90 as valid command', () { + final result = parser.parse('G90'); + expect(result.commands, hasLength(1)); + expect(result.commands.first.code, 'G90'); + expect(result.errors, isEmpty); + }); + + test('parses G91 as valid command', () { + final result = parser.parse('G91'); + expect(result.commands, hasLength(1)); + expect(result.commands.first.code, 'G91'); + expect(result.errors, isEmpty); + }); + + test('parses G90 with comment only', () { + final result = parser.parse('G90 ; set absolute mode'); + expect(result.commands, hasLength(1)); + expect(result.commands.first.code, 'G90'); + expect(result.commands.first.comment, 'set absolute mode'); + }); + + test('parses sequential G90 and G91', () { + final result = parser.parse('G90\nG91\nG1 X10 Y10'); + expect(result.commands, hasLength(3)); + expect(result.commands[0].code, 'G90'); + expect(result.commands[1].code, 'G91'); + expect(result.commands[2].code, 'G1'); + }); + + test('parseRecord preserves readline metadata', () { + final parsed = parser.parseRecord( + const GcodeLineRecord( + lineNumber: 12, + rawLine: 'G1 X20 Y30', + byteOffset: 128, + ), + ); + + expect(parsed.kind, ParsedGcodeLineKind.command); + expect(parsed.record.lineNumber, 12); + expect(parsed.record.byteOffset, 128); + expect(parsed.command?.lineNumber, 12); + expect(parsed.command?.x, 20); + }); + }); +} diff --git a/packages/gcode_core/test/toolpath_builder_test.dart b/packages/gcode_core/test/toolpath_builder_test.dart new file mode 100644 index 0000000..2bfa5da --- /dev/null +++ b/packages/gcode_core/test/toolpath_builder_test.dart @@ -0,0 +1,203 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:gcode_core/gcode_core.dart'; + +void main() { + group('ToolpathBuilder', () { + test('builds linear segments from G1 commands', () { + final commands = [ + const GcodeCommand( + lineNumber: 1, + rawLine: 'G1 X10 Y0', + code: 'G1', + params: {'X': 10, 'Y': 0}, + ), + const GcodeCommand( + lineNumber: 2, + rawLine: 'G1 X10 Y10', + code: 'G1', + params: {'X': 10, 'Y': 10}, + ), + ]; + + final segments = ToolpathBuilder.build(commands); + + expect(segments, hasLength(2)); + expect(segments[0].type, GcodeSegmentType.linear); + expect(segments[0].start.x, 0); + expect(segments[0].start.y, 0); + expect(segments[0].end.x, 10); + expect(segments[0].end.y, 0); + expect(segments[1].end.x, 10); + expect(segments[1].end.y, 10); + }); + + test('builds rapid segments from G0 commands', () { + final commands = [ + const GcodeCommand( + lineNumber: 1, + rawLine: 'G0 X50 Y50', + code: 'G0', + params: {'X': 50, 'Y': 50}, + ), + ]; + + final segments = ToolpathBuilder.build(commands); + + expect(segments, hasLength(1)); + expect(segments[0].type, GcodeSegmentType.rapid); + }); + + test('keeps previous coordinate when X/Y omitted', () { + final commands = [ + const GcodeCommand( + lineNumber: 1, + rawLine: 'G1 X10 Y20', + code: 'G1', + params: {'X': 10, 'Y': 20}, + ), + const GcodeCommand( + lineNumber: 2, + rawLine: 'G1 X30', + code: 'G1', + params: {'X': 30}, + ), + ]; + + final segments = ToolpathBuilder.build(commands); + + expect(segments, hasLength(2)); + expect(segments[1].start.y, 20); // kept from previous + expect(segments[1].end.x, 30); + expect(segments[1].end.y, 20); + }); + + test('no segment when no movement', () { + final commands = [ + const GcodeCommand( + lineNumber: 1, + rawLine: 'G1 X0 Y0', + code: 'G1', + params: {'X': 0, 'Y': 0}, + ), + ]; + + final segments = ToolpathBuilder.build(commands); + expect(segments, isEmpty); + }); + + test('empty commands returns empty segments', () { + final segments = ToolpathBuilder.build([]); + expect(segments, isEmpty); + }); + + test('G90 sets absolute mode', () { + final commands = [ + const GcodeCommand( + lineNumber: 1, + rawLine: 'G90', + code: 'G90', + params: {}, + ), + const GcodeCommand( + lineNumber: 2, + rawLine: 'G1 X10 Y10', + code: 'G1', + params: {'X': 10, 'Y': 10}, + ), + ]; + + final segments = ToolpathBuilder.build(commands); + expect(segments, hasLength(1)); + expect(segments[0].end.x, 10); + expect(segments[0].end.y, 10); + }); + + test('G91 sets relative mode', () { + final commands = [ + const GcodeCommand( + lineNumber: 1, + rawLine: 'G91', + code: 'G91', + params: {}, + ), + const GcodeCommand( + lineNumber: 2, + rawLine: 'G1 X10 Y10', + code: 'G1', + params: {'X': 10, 'Y': 10}, + ), + const GcodeCommand( + lineNumber: 3, + rawLine: 'G1 X10 Y10', + code: 'G1', + params: {'X': 10, 'Y': 10}, + ), + ]; + + final segments = ToolpathBuilder.build(commands); + expect(segments, hasLength(2)); + expect(segments[0].end.x, 10); + expect(segments[0].end.y, 10); + expect(segments[1].start.x, 10); + expect(segments[1].start.y, 10); + expect(segments[1].end.x, 20); + expect(segments[1].end.y, 20); + }); + + test('mode changes do not create segments', () { + final commands = [ + const GcodeCommand( + lineNumber: 1, + rawLine: 'G90', + code: 'G90', + params: {}, + ), + const GcodeCommand( + lineNumber: 2, + rawLine: 'G91', + code: 'G91', + params: {}, + ), + ]; + + final segments = ToolpathBuilder.build(commands); + expect(segments, isEmpty); + }); + + test('mixed G90/G91 sequence', () { + final commands = [ + const GcodeCommand( + lineNumber: 1, + rawLine: 'G90', + code: 'G90', + params: {}, + ), + const GcodeCommand( + lineNumber: 2, + rawLine: 'G1 X10 Y10', + code: 'G1', + params: {'X': 10, 'Y': 10}, + ), + const GcodeCommand( + lineNumber: 3, + rawLine: 'G91', + code: 'G91', + params: {}, + ), + const GcodeCommand( + lineNumber: 4, + rawLine: 'G1 X10 Y10', + code: 'G1', + params: {'X': 10, 'Y': 10}, + ), + ]; + + final segments = ToolpathBuilder.build(commands); + expect(segments, hasLength(2)); + expect(segments[0].end.x, 10); // absolute: 0 -> 10 + expect(segments[0].end.y, 10); + expect(segments[1].end.x, 20); // relative: 10 + 10 + expect(segments[1].end.y, 20); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index effa07a..6e248e2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,13 +161,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" - file_picker_bridge: - dependency: "direct main" - description: - path: "../file_picker_bridge" - relative: true - source: path - version: "0.1.0" flutter: dependency: "direct main" description: flutter @@ -181,13 +174,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "8.1.6" - flutter_ioc_core: - dependency: "direct main" - description: - path: "../flutter_ioc_core" - relative: true - source: path - version: "0.1.0" flutter_lints: dependency: "direct dev" description: @@ -204,13 +190,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" - flutter_study_learning: - dependency: "direct main" - description: - path: "../flutter_study_learning" - relative: true - source: path - version: "0.1.0" flutter_svg: dependency: "direct main" description: @@ -232,17 +211,10 @@ packages: flutterguard_cli: dependency: "direct dev" description: - path: "../flutterguard/packages/flutterguard_cli" - relative: true - source: path - version: "0.1.1" - gcode_core: - dependency: "direct main" - description: - path: "../gcode_core" + path: "../flutterguard" relative: true source: path - version: "0.1.0" + version: "0.7.1" glob: dependency: transitive description: @@ -335,10 +307,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.flutter-io.cn" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -476,10 +448,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.10" + version: "0.7.11" two_dimensional_scrollables: dependency: "direct main" description: @@ -593,5 +565,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" + dart: ">=3.11.5 <4.0.0" flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 39bdf0b..3e496e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,20 +1,26 @@ name: main_app description: A Flutter app shell. publish_to: 'none' -version: 1.0.0+1 +version: 1.1.0 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ^3.11.5 + +workspace: + - packages/gcode_core + - packages/flutter_study_learning + - packages/file_picker_bridge + - packages/flutter_ioc_core dependencies: gcode_core: - path: ../gcode_core + path: packages/gcode_core flutter_study_learning: - path: ../flutter_study_learning + path: packages/flutter_study_learning file_picker_bridge: - path: ../file_picker_bridge + path: packages/file_picker_bridge flutter_ioc_core: - path: ../flutter_ioc_core + path: packages/flutter_ioc_core flutter: sdk: flutter go_router: ^14.2.0 @@ -35,7 +41,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^4.0.0 flutterguard_cli: - path: ../flutterguard/packages/flutterguard_cli + path: ../flutterguard flutter: uses-material-design: 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..8a6cf1c 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, }; @@ -42,6 +42,58 @@ const categoryMeta = { platform: [['dio_interceptor', 'usb_detector'], ['network_platform'], ['dio', 'usb_serial', 'device_info_plus', 'flutter_study_learning']], }; +const flutterGuardDependency = { + package: 'flutterguard_cli', + source: 'git', + url: 'https://github.com/lizy-coding/flutterguard.git', + ref: '9f9be84a73dc4b99a956a8529b8c334849566b03', + immutable: true, + lock_status: 'pending_dependency_resolution', +}; + +const workspacePackages = [ + { + name: 'gcode_core', + kind: 'flutter_package', + path: 'packages/gcode_core', + entrypoints: ['lib/gcode_core.dart'], + owns: ['gcode_parsing', 'line_reading', 'toolpath_building', 'flutter_visualization_widgets'], + depends: ['flutter_sdk'], + validation: ['flutter pub get', 'flutter analyze', 'flutter test'], + test_status: 'configured', + }, + { + name: 'flutter_study_learning', + kind: 'flutter_package', + path: 'packages/flutter_study_learning', + entrypoints: ['lib/flutter_study_learning.dart'], + owns: ['learning_scaffold_widgets', 'teaching_ui_components'], + depends: ['flutter_sdk'], + validation: ['flutter pub get', 'flutter analyze'], + test_status: 'missing_test_directory', + }, + { + name: 'file_picker_bridge', + kind: 'flutter_bridge_package', + path: 'packages/file_picker_bridge', + entrypoints: ['lib/file_picker_bridge.dart'], + owns: ['file_picker_api', 'method_channel_client'], + depends: ['flutter_sdk'], + validation: ['flutter pub get', 'flutter analyze', 'flutter test'], + test_status: 'configured', + }, + { + name: 'flutter_ioc_core', + kind: 'dart_package', + path: 'packages/flutter_ioc_core', + entrypoints: ['lib/flutter_ioc_core.dart'], + owns: ['ioc_container', 'registration_lifetimes', 'scoped_resolution'], + depends: [], + validation: ['dart pub get', 'dart analyze'], + test_status: 'missing_test_directory', + }, +]; + function writeJson(rel, value) { const file = path.join(root, rel); fs.mkdirSync(path.dirname(file), { recursive: true }); @@ -84,12 +136,20 @@ 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'], + package_contract: workspacePackages.map(({ path: packagePath }) => `${packagePath}/AI_ANALYSIS.md`), section: [ 'lib/AI_ANALYSIS.md', 'lib/app/AI_ANALYSIS.md', @@ -116,14 +176,200 @@ 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, content: ['route', 'category', 'status', 'entrypoints', 'analysis_parent'], avoid: ['class_descriptions', 'long_file_inventory', 'natural_language_notes'], }, + package_contract_policy: { + required_for_workspace_member: true, + content: ['package_type', 'workspace', 'entrypoints', 'owns', 'depends', 'validation', 'test_status'], + avoid: ['platform_claims_not_proven_by_manifest', 'natural_language_notes'], + }, + }); +} + +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', + }, + repository: { + layout: 'pub_workspace', + workspace_root: '.', + members: workspacePackages.map(({ path: packagePath }) => packagePath), + resolution_status: 'blocked', + resolution_blocker: 'test_analyzer_flutter_sdk_pin_conflict', + }, + internal_packages: workspacePackages.map(({ name, kind, path: packagePath, entrypoints }) => ({ + name, + type: kind, + path: packagePath, + entrypoint: entrypoints[0], + })), + external_tools: [flutterGuardDependency], + 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 . --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', + 'workspace_package_import', + ], + dependency_migration: { + layout: 'pub_workspace', + internal_packages: workspacePackages.map(({ path: packagePath }) => packagePath), + workspace_resolution_status: 'blocked', + workspace_resolution_blocker: 'test_analyzer_flutter_sdk_pin_conflict', + external_tool: flutterGuardDependency, + }, + 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`, + })), }); } @@ -132,17 +378,30 @@ function writeRootIndexes() { 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'], + depends: [ + 'packages/gcode_core', + 'packages/flutter_study_learning', + 'packages/file_picker_bridge', + 'packages/flutter_ioc_core', + `git:${flutterGuardDependency.url}#${flutterGuardDependency.ref}`, + ], + 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', + ...workspacePackages.map(({ path: packagePath }) => `${packagePath}/AI_ANALYSIS.md`), + ], + validation: ['bash tool/generate_harness_ai_analysis.sh', 'dart format .', 'flutter analyze', 'dart run flutterguard_cli:flutterguard scan . --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 +413,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,33 +423,33 @@ 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'], - depends: ['desktop_multi_window', '../file_picker_bridge'], + owns: ['business_free_capabilities', 'desktop_window_lifecycle', 'platform_boundaries'], + depends: ['desktop_multi_window', 'packages/file_picker_bridge'], children: ['multi_window/AI_ANALYSIS.md', 'platform/AI_ANALYSIS.md'], }); writeIndex({ 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', @@ -199,7 +458,7 @@ function writeLayerIndexes() { status: 'transition', entrypoints: ['AI_ANALYSIS.md'], owns: ['platform_boundary', 'host_channel_registry'], - depends: ['../file_picker_bridge', 'macos/Runner/AppDelegate.swift'], + depends: ['packages/file_picker_bridge', 'macos/Runner/AppDelegate.swift'], validation: ['flutter analyze', 'flutter build macos'], }); } @@ -260,10 +519,44 @@ function writeModuleContracts() { } } +function writePackageContracts() { + for (const packageMeta of workspacePackages) { + writeJson(`${packageMeta.path}/AI_ANALYSIS.md`, { + schema: 'vibecoding.harness.ai_analysis.v2', + mode: 'package_contract', + node: { + id: `flutter_study.workspace.${packageMeta.name}`, + kind: packageMeta.kind, + package: packageMeta.name, + path: packageMeta.path, + status: 'active', + }, + package_type: packageMeta.kind, + workspace: { + member: true, + resolution: 'workspace', + resolution_status: 'blocked', + resolution_blocker: 'test_analyzer_flutter_sdk_pin_conflict', + }, + entrypoints: packageMeta.entrypoints, + owns: packageMeta.owns, + depends: packageMeta.depends, + children: [], + contracts, + validation: packageMeta.validation, + test_status: packageMeta.test_status, + }); + } +} + writeSchema(); +writeProjectContext(); +writeRefactorPlan(); +writeModuleIndex(); writeRootIndexes(); writeLayerIndexes(); writeModuleIndexes(); writeModuleContracts(); +writePackageContracts(); console.log('agent index AI_ANALYSIS generation completed'); 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/migrate_sibling_packages.sh b/tool/migrate_sibling_packages.sh deleted file mode 100755 index 7b7c79a..0000000 --- a/tool/migrate_sibling_packages.sh +++ /dev/null @@ -1,424 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -PARENT="$(cd "$ROOT/.." && pwd)" -FORCE="${1:-}" - -PACKAGES=( - "gcode_core" - "flutter_study_learning" - "file_picker_bridge" - "flutter_ioc_core" -) - -if [[ ! -d "$ROOT/lib/modules/ui/gcode_visualizer/application" ]]; then - echo "Migration source directories are not present. The sibling package migration appears to have already been applied." >&2 - echo "Refusing to re-run to avoid overwriting the migrated sibling packages from incomplete in-app sources." >&2 - exit 1 -fi - -if [[ "$FORCE" != "--force" ]]; then - for package in "${PACKAGES[@]}"; do - if [[ -e "$PARENT/$package" ]]; then - echo "Refusing to overwrite $PARENT/$package. Re-run with --force." >&2 - exit 1 - fi - done -fi - -for package in "${PACKAGES[@]}"; do - if [[ "$FORCE" == "--force" ]]; then - rm -rf "$PARENT/$package" - fi -done - -mkdir -p "$PARENT/gcode_core/lib/src" "$PARENT/gcode_core/test/application" -cp -R "$ROOT/lib/modules/ui/gcode_visualizer/application" "$PARENT/gcode_core/lib/src/" -cp -R "$ROOT/lib/modules/ui/gcode_visualizer/data" "$PARENT/gcode_core/lib/src/" -cp -R "$ROOT/lib/modules/ui/gcode_visualizer/domain" "$PARENT/gcode_core/lib/src/" -cp -R "$ROOT/lib/modules/ui/gcode_visualizer/models" "$PARENT/gcode_core/lib/src/" -cp -R "$ROOT/lib/modules/ui/gcode_visualizer/parser" "$PARENT/gcode_core/lib/src/" -cp -R "$ROOT/lib/modules/ui/gcode_visualizer/services" "$PARENT/gcode_core/lib/src/" - -cat > "$PARENT/gcode_core/pubspec.yaml" <<'EOF' -name: gcode_core -description: Pure Dart G-code parsing, line reading, and toolpath building core. -publish_to: 'none' -version: 0.1.0 - -environment: - sdk: '>=3.0.0 <4.0.0' - -dev_dependencies: - test: ^1.25.0 - lints: ^4.0.0 -EOF - -cat > "$PARENT/gcode_core/lib/gcode_core.dart" <<'EOF' -library gcode_core; - -export 'src/application/gcode_readline_pipeline.dart'; -export 'src/data/readers/file_gcode_line_reader.dart'; -export 'src/data/readers/gcode_line_reader.dart'; -export 'src/data/readers/string_gcode_line_reader.dart'; -export 'src/domain/gcode_line_record.dart'; -export 'src/domain/gcode_load_snapshot.dart'; -export 'src/domain/gcode_load_stage.dart'; -export 'src/domain/parsed_gcode_line.dart'; -export 'src/models/gcode_command.dart'; -export 'src/models/machine_position.dart'; -export 'src/models/toolpath_segment.dart'; -export 'src/parser/gcode_parse_result.dart'; -export 'src/parser/gcode_parser.dart'; -export 'src/services/toolpath_builder.dart'; -EOF - -cat > "$PARENT/gcode_core/README.md" <<'EOF' -# gcode_core - -Pure Dart G-code core extracted from `flutter_study`. - -## Scope - -- Read G-code from strings or files line by line. -- Parse G0/G1 commands with X/Y/F parameters. -- Collect parse errors with line metadata. -- Build incremental or batch toolpath segments. - -This package contains no Flutter UI, animation, canvas drawing, or file picker code. - -## Test - -```bash -dart test -``` -EOF - -cat > "$PARENT/gcode_core/AI_ANALYSIS.md" <<'EOF' -# gcode_core 分析 - -> 纯 Dart G-code 解析与轨迹构建核心。 - -## 功能目标 - -把 G-code 文本/文件读取、单行解析、错误收集和刀路段构建从 Flutter UI 模块中拆出,供教学 UI、CLI 或其它可视化前端复用。 - -## 文件结构 - -``` -gcode_core/ -├── lib/ -│ ├── gcode_core.dart -│ └── src/ -│ ├── application/gcode_readline_pipeline.dart -│ ├── data/readers/ -│ ├── domain/ -│ ├── models/ -│ ├── parser/ -│ └── services/toolpath_builder.dart -└── test/ -``` - -## 边界 - -- 不依赖 Flutter。 -- 不打开系统文件选择器。 -- 不绘制 Canvas。 -- 不管理播放动画。 -EOF - -mkdir -p "$PARENT/flutter_study_learning/lib/src" -cp "$ROOT/lib/shared/learning/learning_scaffold.dart" "$PARENT/flutter_study_learning/lib/src/learning_scaffold.dart" -cat > "$PARENT/flutter_study_learning/pubspec.yaml" <<'EOF' -name: flutter_study_learning -description: Shared learning scaffold widgets for Flutter study modules. -publish_to: 'none' -version: 0.1.0 - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^4.0.0 - -flutter: - uses-material-design: true -EOF -cat > "$PARENT/flutter_study_learning/lib/flutter_study_learning.dart" <<'EOF' -library flutter_study_learning; - -export 'src/learning_scaffold.dart'; -EOF -cat > "$PARENT/flutter_study_learning/README.md" <<'EOF' -# flutter_study_learning - -Shared teaching page widgets for Flutter study modules. - -## Scope - -- `LearningScaffold` -- Learning objectives, concept chips, code snippets, state logs, pitfalls, and exercise cards - -This package has no module-specific business logic. -EOF -cat > "$PARENT/flutter_study_learning/AI_ANALYSIS.md" <<'EOF' -# flutter_study_learning 分析 - -> Flutter 学习模块的共享教学页面组件包。 - -## 边界 - -只承载教学表达组件,不持有任何模块状态、解析器、网络请求或平台能力。 -EOF - -mkdir -p "$PARENT/file_picker_bridge/lib/src" "$PARENT/file_picker_bridge/test" -cp "$ROOT/lib/shared/platform/file_picker/file_picker_service.dart" "$PARENT/file_picker_bridge/lib/src/" -cp "$ROOT/lib/shared/platform/file_picker/method_channel_file_picker.dart" "$PARENT/file_picker_bridge/lib/src/" -cp "$ROOT/test/shared/platform/file_picker/method_channel_file_picker_test.dart" "$PARENT/file_picker_bridge/test/" -cat > "$PARENT/file_picker_bridge/pubspec.yaml" <<'EOF' -name: file_picker_bridge -description: MethodChannel file picker API used by Flutter study modules. -publish_to: 'none' -version: 0.1.0 - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^4.0.0 - -flutter: - uses-material-design: true -EOF -cat > "$PARENT/file_picker_bridge/lib/file_picker_bridge.dart" <<'EOF' -library file_picker_bridge; - -export 'src/file_picker_service.dart'; -export 'src/method_channel_file_picker.dart'; -EOF -cat > "$PARENT/file_picker_bridge/README.md" <<'EOF' -# file_picker_bridge - -Business-neutral file picker API for Flutter study modules. - -Current host implementation is registered by `main_app` on macOS through the `file_picker_bridge/file_picker` MethodChannel. This package owns the Dart API and mock-friendly channel implementation; it can be upgraded into a full Flutter plugin when more platforms are needed. -EOF -cat > "$PARENT/file_picker_bridge/AI_ANALYSIS.md" <<'EOF' -# file_picker_bridge 分析 - -> 文件选择能力包,当前承载 Dart API 和 MethodChannel 客户端。 - -## 平台协议 - -- Channel: `file_picker_bridge/file_picker` -- Method: `pickFile` -- 返回: `null` 或 `{ path: String, name: String? }` - -## 迁移状态 - -macOS 原生实现仍在宿主应用 `macos/Runner/AppDelegate.swift` 中注册。后续若升级为完整 Flutter plugin,再迁移原生代码。 -EOF -perl -0pi -e "s#import 'package:main_app/shared/platform/file_picker/method_channel_file_picker.dart';#import 'package:file_picker_bridge/file_picker_bridge.dart';#g" \ - "$PARENT/file_picker_bridge/test/method_channel_file_picker_test.dart" - -mkdir -p "$PARENT/flutter_ioc_core/lib/src" -cp "$ROOT/lib/modules/state/flutter_ioc/ioc/container.dart" "$PARENT/flutter_ioc_core/lib/src/" -cp "$ROOT/lib/modules/state/flutter_ioc/ioc/types.dart" "$PARENT/flutter_ioc_core/lib/src/" -cat > "$PARENT/flutter_ioc_core/pubspec.yaml" <<'EOF' -name: flutter_ioc_core -description: Pure Dart IoC container core extracted from Flutter study. -publish_to: 'none' -version: 0.1.0 - -environment: - sdk: '>=3.0.0 <4.0.0' - -dev_dependencies: - test: ^1.25.0 - lints: ^4.0.0 -EOF -cat > "$PARENT/flutter_ioc_core/lib/flutter_ioc_core.dart" <<'EOF' -library flutter_ioc_core; - -export 'src/container.dart'; -export 'src/types.dart'; -EOF -cat > "$PARENT/flutter_ioc_core/README.md" <<'EOF' -# flutter_ioc_core - -Pure Dart IoC container extracted from the Flutter IoC teaching module. -EOF -cat > "$PARENT/flutter_ioc_core/AI_ANALYSIS.md" <<'EOF' -# flutter_ioc_core 分析 - -> 教学用 IoC 容器核心,支持 singleton/transient/scoped 生命周期、条件注册、属性注入和作用域。 - -不依赖 Flutter。 -EOF - -perl -0pi -e "s/\n gcode_core:\n path: \.\.\/gcode_core\n//g; s/\n flutter_study_learning:\n path: \.\.\/flutter_study_learning\n//g; s/\n file_picker_bridge:\n path: \.\.\/file_picker_bridge\n//g; s/\n flutter_ioc_core:\n path: \.\.\/flutter_ioc_core\n//g" "$ROOT/pubspec.yaml" -perl -0pi -e "s/dependencies:\n/dependencies:\n gcode_core:\n path: ..\/gcode_core\n flutter_study_learning:\n path: ..\/flutter_study_learning\n file_picker_bridge:\n path: ..\/file_picker_bridge\n flutter_ioc_core:\n path: ..\/flutter_ioc_core\n/" "$ROOT/pubspec.yaml" - -perl -0pi -e "s#import '../../../../shared/learning/learning_scaffold.dart';#import 'package:flutter_study_learning/flutter_study_learning.dart';#g" \ - "$ROOT/lib/modules/ui/gcode_visualizer/pages/gcode_visualizer_page.dart" \ - "$ROOT/lib/modules/basic/tree_state/pages/basic_widgets_page.dart" -perl -0pi -e "s#import '../domain/gcode_load_stage.dart';#import 'package:gcode_core/gcode_core.dart';#g" \ - "$ROOT/lib/modules/ui/gcode_visualizer/pages/gcode_visualizer_page.dart" -perl -0pi -e "s#import '../../../../shared/platform/file_picker/file_picker_service.dart';\nimport '../../../../shared/platform/file_picker/method_channel_file_picker.dart';\nimport '../application/gcode_readline_pipeline.dart';\nimport '../data/readers/file_gcode_line_reader.dart';\nimport '../data/readers/gcode_line_reader.dart';\nimport '../data/readers/string_gcode_line_reader.dart';\nimport '../domain/gcode_load_stage.dart';\nimport '../models/toolpath_segment.dart';\nimport '../parser/gcode_parse_result.dart';#import 'package:file_picker_bridge/file_picker_bridge.dart';\nimport 'package:gcode_core/gcode_core.dart';#s" \ - "$ROOT/lib/modules/ui/gcode_visualizer/state/gcode_player_controller.dart" -perl -0pi -e "s#import '../models/gcode_command.dart';\nimport '../models/toolpath_segment.dart';#import 'package:gcode_core/gcode_core.dart';#s" \ - "$ROOT/lib/modules/ui/gcode_visualizer/widgets/gcode_canvas.dart" -perl -0pi -e "s#import '../models/gcode_command.dart';\nimport '../parser/gcode_parse_result.dart';#import 'package:gcode_core/gcode_core.dart';#s" \ - "$ROOT/lib/modules/ui/gcode_visualizer/widgets/command_timeline.dart" -perl -0pi -e "s#import 'ioc/ioc.dart' as ioc;#import 'package:flutter_ioc_core/flutter_ioc_core.dart' as ioc;#g" \ - "$ROOT/lib/modules/state/flutter_ioc/module_entry.dart" - -perl -0pi -e "s#import 'package:main_app/shared/platform/file_picker/method_channel_file_picker.dart';#import 'package:file_picker_bridge/file_picker_bridge.dart';#g" \ - "$ROOT/test/shared/platform/file_picker/method_channel_file_picker_test.dart" - -rm -rf "$ROOT/lib/modules/ui/gcode_visualizer/application" \ - "$ROOT/lib/modules/ui/gcode_visualizer/data" \ - "$ROOT/lib/modules/ui/gcode_visualizer/domain" \ - "$ROOT/lib/modules/ui/gcode_visualizer/models" \ - "$ROOT/lib/modules/ui/gcode_visualizer/parser" \ - "$ROOT/lib/modules/ui/gcode_visualizer/services" \ - "$ROOT/lib/modules/state/flutter_ioc/ioc" \ - "$ROOT/lib/shared/learning" \ - "$ROOT/lib/shared/platform/file_picker" -rm -rf "$ROOT/test/shared/platform/file_picker" - -cat > "$ROOT/lib/modules/ui/gcode_visualizer/AI_ANALYSIS.md" <<'EOF' -# G-code Visualizer 模块分析 - -> G-code 解析与轨迹动画演示模块。纯解析与轨迹构建逻辑已迁移到同级包 `../gcode_core`,本模块只保留 Flutter 教学 UI、状态编排和绘制交互。 - -## 功能目标 - -调用 `gcode_core` 将 G-code 文本或文件解析为轨迹段,并通过 CustomPaint、AnimationController 和教学模板展示 G0/G1 刀路执行过程。 - -## 文件结构 - -``` -modules/ui/gcode_visualizer/ -├── module_entry.dart # 入口: 返回 GcodeVisualizerPage -├── AI_ANALYSIS.md # 模块分析文档 -├── pages/ -│ └── gcode_visualizer_page.dart # 教学页面,依赖 flutter_study_learning -├── state/ -│ └── gcode_player_controller.dart # ChangeNotifier + AnimationController,编排解析、文件选择和播放 -└── widgets/ - ├── command_timeline.dart # 指令列表,高亮当前执行行 - ├── gcode_canvas.dart # CustomPaint 轨迹画布 - ├── gcode_editor_panel.dart # 编辑器 + 文件路径输入/选择/提取按钮 - └── playback_controls.dart # 播放/暂停/重置/进度/速度控制 -``` - -## 外部包依赖 - -| 包 | 用途 | -|---|---| -| `gcode_core` | G-code 读取、解析、错误收集、轨迹构建 | -| `flutter_study_learning` | 教学页面模板组件 | -| `file_picker_bridge` | 文件选择 Dart API,macOS 原生通道仍由宿主应用注册 | - -## 数据流 - -``` -source text / path input / file picker - -> gcode_core GcodeLineReader - -> gcode_core GcodeReadlinePipeline - -> GcodePlayerController - -> GcodeCanvas / CommandTimeline / PlaybackControls -``` - -## 修改注意事项 - -1. 新增 G-code 语法、reader 或 toolpath 规则时修改 `../gcode_core`。 -2. 本模块只处理 Flutter UI、播放状态和教学表达。 -3. 文件选择能力来自 `file_picker_bridge`,模块只传入 G-code 扩展名和弹窗文案。 -4. 教学页面组件来自 `flutter_study_learning`。 -EOF - -cat > "$ROOT/lib/shared/AI_ANALYSIS.md" <<'EOF' -# Shared 层分析 - -> shared 层已完成第一轮外置迁移。教学模板、平台文件选择等可复用能力已迁移到项目同级 package,主应用通过 path dependency 引用。 - -## 当前定位 - -`lib/shared/` 只保留跨模块共享能力的项目内文档和后续过渡能力。新增稳定能力时优先评估是否直接进入同级 package。 - -## 已迁移能力 - -| 能力 | 同级包 | 当前使用方 | -|---|---|---| -| 教学模板 | `../flutter_study_learning` | tree_state, gcode_visualizer | -| 文件选择 Dart API | `../file_picker_bridge` | gcode_visualizer | - -## 维护规则 - -1. `shared/` 不得 import `modules/`。 -2. 新增 shared 代码前优先评估是否应放入同级 package。 -3. 业务模块只能依赖 package API,不直接依赖平台通道细节。 -EOF - -cat > "$ROOT/lib/shared/platform/AI_ANALYSIS.md" <<'EOF' -# Platform 共享层分析 - -> 平台能力正在从主应用 shared 层迁移到同级 package。 - -## 当前能力 - -| 能力 | 包 | 宿主原生实现 | -|---|---|---| -| 文件选择 | `../file_picker_bridge` | macOS `AppDelegate.swift` 注册 `file_picker_bridge/file_picker` | - -## 后续计划 - -当需要 Windows/iOS/Android 文件选择实现时,将 `file_picker_bridge` 从 Dart API package 升级为完整 Flutter plugin,并迁移 macOS 原生实现。 -EOF - -cat > "$ROOT/lib/modules/state/flutter_ioc/AI_ANALYSIS.md" <<'EOF' -# AI 模块分析: flutter_ioc - -> 自研 IoC 容器教学模块。IoC 核心逻辑已迁移到同级纯 Dart 包 `../flutter_ioc_core`,本模块保留 Provider 接入和计数器教学 UI。 - -## 文件结构 - -``` -modules/state/flutter_ioc/ -├── module_entry.dart # 创建 flutter_ioc_core.Container 并注入 Provider -├── module_root.dart # CounterScreen 教学 UI -├── model/counter_model.dart -└── AI_ANALYSIS.md -``` - -## 外部包依赖 - -| 包 | 用途 | -|---|---| -| `flutter_ioc_core` | Container、IoCContainer、生命周期、条件注册、属性注入 | - -## 修改注意事项 - -1. IoC 容器能力变更应修改 `../flutter_ioc_core`。 -2. 本模块只维护 Flutter/Provider 教学集成。 -EOF - -perl -0pi -e 's#lib/shared/learning/learning_scaffold.dart#package:flutter_study_learning#g; s#`lib/shared/platform/file_picker/`#`../file_picker_bridge`#g; s#`shared/platform/file_picker/`#`../file_picker_bridge`#g' \ - "$ROOT/README.md" "$ROOT/AI_PROJECT_CONTEXT.md" "$ROOT/REFACTOR_PLAN.md" "$ROOT/PLUGIN_DECOMPOSITION_PLAN.md" "$ROOT/lib/modules/basic/tree_state/AI_ANALYSIS.md" - -echo "Sibling package migration completed." diff --git a/tool/validate_agent_docs.js b/tool/validate_agent_docs.js new file mode 100644 index 0000000..0778454 --- /dev/null +++ b/tool/validate_agent_docs.js @@ -0,0 +1,148 @@ +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); +const failures = []; +const documents = new Map(); +const workspacePackages = [ + ['gcode_core', 'packages/gcode_core'], + ['flutter_study_learning', 'packages/flutter_study_learning'], + ['file_picker_bridge', 'packages/file_picker_bridge'], + ['flutter_ioc_core', 'packages/flutter_ioc_core'], +]; + +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`); + } + } +} + +for (const [packageName, packagePath] of workspacePackages) { + const analysisPath = `${packagePath}/AI_ANALYSIS.md`; + const manifestPath = `${packagePath}/pubspec.yaml`; + const contract = documents.get(analysisPath); + if (!contract) { + failures.push(`${analysisPath}:missing_package_contract`); + continue; + } + if (contract.mode !== 'package_contract') { + failures.push(`${analysisPath}:mode:package_contract`); + } + if (contract.node?.package !== packageName) { + failures.push(`${analysisPath}:package_name_mismatch`); + } + if (contract.node?.path !== packagePath) { + failures.push(`${analysisPath}:package_path_mismatch`); + } + + const manifest = fs.readFileSync(path.join(root, manifestPath), 'utf8'); + if (!new RegExp(`^name:\\s*${packageName}$`, 'm').test(manifest)) { + failures.push(`${manifestPath}:name_mismatch`); + } + if (!/^resolution:\s*workspace$/m.test(manifest)) { + failures.push(`${manifestPath}:missing_workspace_resolution`); + } +} + +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`);