# 规划操作预算与诊断收尾实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 让一次粗规划调用在建图、距离场、Dijkstra 和 Hybrid A* 阶段共用可取消的总超时预算,并发布真实的 Open List 诊断计数。 **Architecture:** 在 `Utils` 新增内部 `PlanningOperationBudget`,它不依赖 Map 或 CoarsePath,只报告继续、取消、超时。Map 与搜索分别将该中立结果映射为自己的结果;`CoarsePathPlanningService` 创建唯一预算并传递给下层。原有公开的无预算入口保持兼容,门面使用内部带预算入口。 **Tech Stack:** C# 10、.NET Standard 2.0、PowerShell 反射回归脚本、`Stopwatch`、`CancellationToken`。 ## Global Constraints - 位置使用 m、地图输入使用 mm、航向使用 rad、曲率使用 1/m;不得改变现有单位边界。 - 取消或超时必须返回空路径和空方向分段,绝不发布部分路径或部分 `PlanningGridMap`。 - 地图不得依赖 CoarsePath;共享预算只能放在 `ParkrobTrajplanner/Utils`。 - 每 256 个或更少循环工作单元检查一次预算;外部 `IMapObstacleSource.ProjectToWorld()` 是调用方提供的同步快照接口,只能在调用前后检查,不能强制抢占其内部执行。 - 不改变碰撞保守性、运动原语、代价公式、目标候选保护或 Open List 排序。 - 保留 `PlanningMapFactory.Create(request)`、`HybridAStarPlanner.Plan(request, token)`、`HybridAStarSearch.Search(request, token)` 和 `GridDijkstraHeuristic(map,row,col)` 的兼容入口。 - 不执行 Git 添加、提交、重置或工作区清理。 --- ## 文件结构 | 文件 | 职责 | | --- | --- | | `Utils/PlanningOperationBudget.cs` | 内部单调计时、取消检查和统一停止原因。 | | `Map/PlanningMapBuildResult.cs` | 地图构建状态:成功、失败、取消、超时。 | | `Map/Core/EnvironmentMapBuilder.cs`、`EnvironmentMapBuildResult.cs` | 将预算传入来源处理与栅格化,并保留停止原因。 | | `Map/Obstacles/MapObstacleRasterizer.cs` | 在圆形/矩形逐格写入期间定期停止。 | | `Map/Planning/{PlanningMapAdapter,ObstacleDistanceField,EuclideanDistanceTransform}.cs` | 在占据复制、EDT 和距离换算期间定期停止且不产出快照。 | | `Map/PlanningMapFactory.cs` | 可取消地等待创建锁、检查缓存、建图、哈希和写缓存。 | | `CoarsePath/Search/{GridDijkstraHeuristic,HybridAStarSearch}.cs` | 为 Dijkstra 和 Open List 使用共享预算,记录陈旧条目和峰值。 | | `CoarsePath/{HybridAStarPlanner,Contracts/PlanningDiagnostics}.cs` | 将搜索统计传给最终结果,并让公开 Planner 包装兼容预算。 | | `CoarsePath/Facade/CoarsePathPlanningService.cs` | 创建一次调用唯一预算,映射地图阶段停止状态。 | | `Map/README.md`、`CoarsePath/README.md` | 分别说明地图构建状态,以及粗规划总超时和取消状态。 | | `tests/verify_planning_map_factory.ps1`、`verify_planning_map_adapter.ps1`、`verify_coarse_path_search.ps1`、`verify_coarse_path_integration.ps1` | 预算与诊断的反射回归覆盖。 | ### Task 1: 建立独立的操作预算与地图终止契约 **Files:** - Create: `ClumsyPilot/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs` - Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs` - Modify: `ClumsyPilot/tests/verify_planning_map_factory.ps1` **Consumes:** `System.Diagnostics.Stopwatch`、`System.Threading.CancellationToken`。 **Produces:** `PlanningOperationStopReason`、`PlanningOperationBudget`、`PlanningMapBuildStatus`;下游 Map/CoarsePath 均只通过这些类型传递停止信息。 - [ ] **Step 1: 写失败测试,锁定新的地图状态与预算公开反射形状。** 在现存的 `verify_planning_map_factory.ps1` 断言存在内部 `MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget` 与三值 `PlanningOperationStopReason`,并断言 `PlanningMapBuildResult.Status` 存在;已取消、已超时结果的 `Succeeded` 必须为 `false`、`Map` 为 `$null`、`CacheHit` 为 `None`。 ```powershell $statusType = $assembly.GetType($ns + 'PlanningMapBuildStatus', $true) Assert-True ($statusType.GetEnumNames() -contains 'Cancelled') 'Map build status must expose cancellation.' Assert-True ($statusType.GetEnumNames() -contains 'TimedOut') 'Map build status must expose timeout.' Assert-True ($resultType.GetProperty('Status') -ne $null) 'Map result must expose an explicit status.' ``` - [ ] **Step 2: 运行两个脚本,确认因类型或属性不存在而失败。** Run: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 ``` Expected: 断言报告 `PlanningOperationBudget` 或 `PlanningMapBuildStatus` 缺失。 - [ ] **Step 3: 实现最小共享预算和显式地图状态。** `PlanningOperationBudget` 的核心接口固定如下;超时使用构造时启动的单调 `Stopwatch`,取消优先于超时: ```csharp internal enum PlanningOperationStopReason { None, Cancelled, TimedOut } internal sealed class PlanningOperationBudget { internal PlanningOperationBudget(CancellationToken cancellationToken, TimeSpan timeout); internal static PlanningOperationBudget Unlimited(CancellationToken cancellationToken); internal TimeSpan Elapsed { get; } internal PlanningOperationStopReason GetStopReason(); internal PlanningOperationStopReason CheckEvery(ref int workItemCount); } ``` `CheckEvery` 在第一次工作单元以及每 256 个工作单元检查;`Unlimited` 不启用时间限制但仍响应取消。将地图结果从布尔构造改为状态构造: ```csharp public enum PlanningMapBuildStatus { Success, Failed, Cancelled, TimedOut } public PlanningMapBuildStatus Status { get; } public bool Succeeded { get { return Status == PlanningMapBuildStatus.Success; } } internal static PlanningMapBuildResult Stopped(PlanningOperationStopReason reason, IReadOnlyList sourceResults) { return new PlanningMapBuildResult( reason == PlanningOperationStopReason.Cancelled ? PlanningMapBuildStatus.Cancelled : PlanningMapBuildStatus.TimedOut, reason == PlanningOperationStopReason.Cancelled ? "地图创建已取消。" : "地图创建已超时。", sourceResults, PlanningMapCacheHit.None, null); } ``` - [ ] **Step 4: 重跑两个脚本,确认新契约通过且旧地图缓存断言未回归。** Run: 与 Step 2 相同。 Expected: 地图工厂脚本输出 `Planning map factory checks passed.`。 ### Task 2: 让地图创建、栅格化和距离场遵守预算 **Files:** - Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs` - Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs` - Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs` - Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs` - Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs` - Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs` - Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs` - Modify: `ClumsyPilot/tests/verify_planning_map_factory.ps1` - Modify: `ClumsyPilot/tests/verify_planning_map_adapter.ps1` **Consumes:** Task 1 的 `PlanningOperationBudget` 与 `PlanningOperationStopReason`。 **Produces:** `PlanningMapFactory` 的内部 `Create(PlanningMapRequest, PlanningOperationBudget)`;它在取消/超时时返回 `PlanningMapBuildResult.Stopped`,不会写入 LRU 缓存。 - [ ] **Step 1: 写失败测试,覆盖预先取消、EDT 中超时与缓存不污染。** 在工厂脚本创建一个已取消的 `CancellationTokenSource`,通过反射调用新的内部带预算 `Create`,断言: ```powershell Assert-Equal 'Cancelled' $cancelledMapResult.Status.ToString() 'Cancelled map construction must report cancellation.' Assert-False $cancelledMapResult.Succeeded 'Cancelled map construction must not succeed.' Assert-Null $cancelledMapResult.Map 'Cancelled map construction must not publish a map.' Assert-Equal 'None' $cancelledMapResult.CacheHit.ToString() 'Stopped construction must not publish a cache hit.' ``` 在适配器脚本为含障碍的大栅格创建 `PlanningOperationBudget`,使用零超时调用内部 `TryCreate`,断言返回 `TimedOut` 且输出 `PlanningGridMap` 为 `$null`。随后用无预算入口再次创建相同地图,断言距离场仍可用,以证明停止时没有污染输入或缓存。 - [ ] **Step 2: 运行地图工厂和适配器脚本,确认新增反射入口缺失而失败。** Run: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 ``` Expected: 新带预算 `Create` 或 `TryCreate` 反射查找失败。 - [ ] **Step 3: 在地图管线的全部长循环中传递并检查预算。** 实现以下内部接口,所有 `Try*` 在停止时返回 `false` 并把 `stopReason` 设为非 `None`;普通参数错误仍按现有失败原因或异常处理。 ```csharp internal EnvironmentMapBuildResult Build(MapBuildRequest request, PlanningOperationBudget budget); internal static bool TryRasterize(EnvironmentGridMap map, IMapObstacle obstacle, PlanningOperationBudget budget, out PlanningOperationStopReason stopReason); internal static bool TryCreate(EnvironmentGridMap environmentMap, PlanningOperationBudget budget, out PlanningGridMap map, out PlanningOperationStopReason stopReason); internal static bool TryCreate(byte[] occupied, int rows, int cols, double resolutionMeters, PlanningOperationBudget budget, out ObstacleDistanceField field, out PlanningOperationStopReason stopReason); internal static bool TryComputeSquaredDistances(byte[] occupied, int rows, int cols, PlanningOperationBudget budget, out double[] squared, out PlanningOperationStopReason stopReason); internal PlanningMapBuildResult Create(PlanningMapRequest request, PlanningOperationBudget budget); ``` 保留现有公开 `Build`、`Rasterize`、`PlanningMapAdapter.Create`、`ObstacleDistanceField.Create` 与 `ComputeSquaredDistances`,让它们以 `PlanningOperationBudget.Unlimited(CancellationToken.None)` 包装新入口。对圆形/矩形逐格循环、EDT 两遍扫描、`Transform1D` 两个 `q` 循环、距离场扫描均调用 `budget.CheckEvery(ref workItemCount)`。 工厂以 `Monitor.TryEnter(_createGate, 16)` 循环等待创建锁;每次失败后检查预算。拿到锁后立刻再检查预算,随后在缓存读、来源处理、适配、占据哈希和每次缓存写入前检查。将 SHA-256 改为每 4096 字节调用 `TransformBlock` 的增量哈希,并在块间检查预算。任一非 `None` 停止原因直接返回 `PlanningMapBuildResult.Stopped`,且不会执行 `_cache.AddOccupancy` 或 `_cache.AddInput`。 - [ ] **Step 4: 重跑地图脚本,确认普通建图、两级缓存和新停止结果同时通过。** Run: 与 Step 2 相同。 Expected: `Planning map factory checks passed.` 与 `Planning map adapter checks passed.`。 ### Task 3: 为 Dijkstra 与 Hybrid A* 使用同一预算并记录 Open List 统计 **Files:** - Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs` - Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs` - Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` - Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` - Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` **Consumes:** Task 1 的预算和 Task 2 不可变地图;现有 `BinaryMinHeap`。 **Produces:** `GridDijkstraHeuristic.TryCreate`、带共享预算的内部搜索/规划入口,以及 `HybridAStarSearchResult.StaleOpenListEntryCount` 和 `PeakOpenListCount`。 - [ ] **Step 1: 写失败测试,覆盖 Dijkstra 中取消、总超时和统计透传。** 在搜索脚本上创建至少 500×500 格的已就绪空地图,在地图创建完成后启动 `CancellationTokenSource.CancelAfter(1)` 并调用搜索。断言返回 `Cancelled`、`SuccessNodeIndex` 为 `$null`、运行时间小于 2 秒。再用同一地图和 `SearchTimeout = TimeSpan.Zero` 断言 `SearchTimeout`,以证明启发式创建前即尊重总预算。 反射断言搜索结果的新属性存在,并让直接路径搜索至少满足: ```powershell Assert-True ($searchResultType.GetProperty('StaleOpenListEntryCount') -ne $null) 'Search result must expose stale Open List entries.' Assert-True ($searchResultType.GetProperty('PeakOpenListCount') -ne $null) 'Search result must expose Open List peak size.' Assert-True ($searchResult.PeakOpenListCount -ge 1) 'A successful search must retain at least one Open List entry.' Assert-True ($searchResult.StaleOpenListEntryCount -ge 0) 'Stale Open List entry count must never be negative.' ``` 在集成脚本断言 `PlanningResult.Diagnostics` 中的两项值与搜索结果一致;对产生重开/失效条目的固定障碍场景断言陈旧条目数大于零。 - [ ] **Step 2: 运行搜索和集成脚本,确认新增属性、带预算入口或中途取消断言失败。** Run: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 ``` Expected: 新属性或 Dijkstra 中止行为缺失导致失败;现有“搜索前取消”检查不应被视为通过中途取消测试。 - [ ] **Step 3: 实现预算感知的 Dijkstra、搜索与真实诊断。** `GridDijkstraHeuristic` 保留当前公开构造函数,并增加内部可失败构建: ```csharp internal static bool TryCreate(PlanningGridMap map, int goalRow, int goalCol, PlanningOperationBudget budget, out GridDijkstraHeuristic heuristic, out PlanningOperationStopReason stopReason); ``` `Build` 的每次出堆与每 256 个邻居检查预算;停止时不返回部分启发式。`HybridAStarSearch.Search(request, token)` 从 `request.Configuration.SearchTimeout` 创建预算作为兼容包装,新增内部: ```csharp internal HybridAStarSearchResult Search(PlanningRequest request, PlanningOperationBudget budget); ``` 删除搜索器内部新建的 `Stopwatch` 与 `IsTimedOut`,所有原有取消/超时位置改为读取 `budget.GetStopReason()` 并精确映射为 `PlanningStatus.Cancelled` 或 `PlanningStatus.SearchTimeout`。Dijkstra 返回停止原因时立即返回对应搜索状态;节点上限仍只在预算检查之后、真正扩展之前检查。 扩展搜索结果构造函数与只读属性: ```csharp public int StaleOpenListEntryCount { get; } public int PeakOpenListCount { get; } ``` 每次 `openList.Push` 后执行 `peakOpenListCount = Math.Max(peakOpenListCount, openList.Count)`。普通节点出堆后因 best-G 已更新、节点索引不匹配或已关闭而跳过时递增 `staleOpenListEntryCount`;目标候选的出队复核失败不算陈旧条目。所有 `CreateResult` 调用传递两项计数。 `HybridAStarPlanner` 的公开 `Plan` 保持签名并建立自己的预算;新增内部 `Plan(request, budget)` 供门面调用。诊断使用 `budget.Elapsed`,并把两个搜索统计填入原本为零的构造参数: ```csharp searchResult == null ? 0 : searchResult.StaleOpenListEntryCount, searchResult == null ? 0 : searchResult.PeakOpenListCount, ``` - [ ] **Step 4: 重跑搜索和集成脚本,确认取消、超时、目标候选与统计均通过。** Run: 与 Step 2 相同。 Expected: `Coarse path search primitive checks passed.`、`Coarse path Hybrid A star search checks passed.`、`Coarse path integration checks passed.` 和 `Coarse path facade checks passed.`。 ### Task 4: 让业务门面映射地图阶段终止状态并更新调用文档 **Files:** - Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs` - Modify: `ClumsyPilot/ParkrobTrajplanner/Map/README.md` - Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` - Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` **Consumes:** Task 1 的地图状态、Task 2 的带预算工厂入口、Task 3 的内部 Planner 入口。 **Produces:** 一次 `CoarsePathPlanningService.Plan` 的统一预算和对调用方稳定的 `PlanningStatus` 映射。 - [ ] **Step 1: 写失败测试,锁定门面状态映射和空结果。** 在集成脚本使用已取消 Token 调用门面,断言: ```powershell Assert-Equal 'Cancelled' $facadeResult.MapResult.Status.ToString() 'Facade must retain a cancelled map result.' Assert-Equal 'Cancelled' $facadeResult.PlanningResult.Status.ToString() 'Facade must map map-stage cancellation to planning cancellation.' Assert-Equal 0 $facadeResult.PlanningResult.Path.Count 'Cancelled facade planning must publish no path.' Assert-Equal 0 $facadeResult.PlanningResult.Segments.Count 'Cancelled facade planning must publish no segments.' ``` 对 `SearchTimeout = TimeSpan.Zero` 的有效 job,断言地图结果和规划结果分别为 `TimedOut`、`SearchTimeout`,且调试 sink 不会把已停止操作改写为成功。 - [ ] **Step 2: 运行集成脚本,确认当前门面把地图阶段停止误报为 `InvalidMap` 或继续建图。** Run: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 ``` Expected: 地图状态属性或正确的 `Cancelled`/`SearchTimeout` 映射不存在。 - [ ] **Step 3: 让门面创建并传递唯一预算,随后更新 README。** 门面从有效 `job.Configuration.SearchTimeout` 创建 `PlanningOperationBudget`;配置为空或时间值非法时使用无超时预算,让既有 Planner 预检继续返回原有无效配置状态。依次调用: ```csharp PlanningMapBuildResult mapResult = _mapFactory.Create(job == null ? null : job.MapRequest, budget); if (!mapResult.Succeeded) { PlanningStatus status = mapResult.Status == PlanningMapBuildStatus.Cancelled ? PlanningStatus.Cancelled : mapResult.Status == PlanningMapBuildStatus.TimedOut ? PlanningStatus.SearchTimeout : PlanningStatus.InvalidMap; return PublishDebug(job, mapResult, PlanningResult.Failure(status, diagnostics)); } PlanningResult planningResult = _planner.Plan(request, budget); ``` `Map/README.md` 在 `PlanningMapBuildResult` 的说明处增加 `Status`:`Success`、`Failed`、`Cancelled`、`TimedOut`;后两种不提供地图也不会进入缓存。`CoarsePath/README.md` 增加“总预算与取消”小节:`SearchTimeout` 是从门面开始的总预算,覆盖建图、距离场、Dijkstra 与 Hybrid A*;`Cancelled`/`SearchTimeout` 一律无路径;不应以 `InvalidMap` 重试用户主动取消。 - [ ] **Step 4: 重跑集成脚本,确认门面状态映射、缓存复用和 debug 旁路隔离均通过。** Run: 与 Step 2 相同。 Expected: `Coarse path integration checks passed.` 和 `Coarse path facade checks passed.`。 ### Task 5: 全量回归与验收记录 **Files:** - Modify only if a command reveals a concrete regression: the exact responsible source or test file from Tasks 1–4. - [ ] **Step 1: 执行 Debug 构建。** Run: ```powershell dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore ``` Expected: `0 个警告`、`0 个错误`。 - [ ] **Step 2: 执行全部现行 P0 地图与粗规划回归。** Run: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1 ``` Expected: 每个脚本退出码为 0 并输出 `passed`。 - [ ] **Step 3: 对照设计完成验收。** 逐项检查:预先取消和中途 Dijkstra 取消均返回 `Cancelled`;零总超时返回 `SearchTimeout`;地图停止不创建快照或缓存条目;正常输入的路径与缓存行为不变;诊断两项不再硬编码为零;README 已说明总预算语义。 ## 自检 - 规格覆盖:Task 1 定义共享预算和显式地图状态;Task 2 覆盖地图、EDT、锁和缓存;Task 3 覆盖 Dijkstra、Hybrid A*、统计和 Planner;Task 4 覆盖门面映射与文档;Task 5 覆盖完整回归。 - 类型一致性:所有耗时组件仅接收 `PlanningOperationBudget` 并输出 `PlanningOperationStopReason`;Map 使用 `PlanningMapBuildStatus`,粗规划使用既有 `PlanningStatus`。 - 范围:不触及 UI、Painter、场景工厂、Release 基准、运动模型或旧 TrapMap 脚本。