chore: save current workspace progress

This commit is contained in:
梁薄云
2026-08-09 22:13:18 +08:00
parent 650c2ab0e3
commit 2f4fd15e52
449 changed files with 76593 additions and 971 deletions
@@ -0,0 +1,266 @@
# TrapMap Image Export and Console Logging Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add optional 300 DPI full-map PNG export with hard pixel/file limits and an independent switch for TrapMap terminal diagnostics.
**Architecture:** A new `TrapMapImageExporter` renders the completed in-memory grid without depending on CycleGUI/Painter state. `TrapMapTest` owns the two manual switches and calls the exporter only after successful map construction. A shared `TrapMapLog` always writes `DLog` and conditionally mirrors the same message to `Console.WriteLine`.
**Tech Stack:** C# 10, .NET Standard 2.0, internal pure-C# RGBA rasterizer, exact `StbImageWriteSharp` 1.16.7 managed PNG encoder, BCL-only PowerShell PNG parser, existing TrapMap tests.
## Global Constraints
- Do not inspect or modify `TrajPlanner`.
- Do not commit or stage any file.
- Keep `UI.GetPainter("TrapMapTest")` as the world-coordinate Painter; do not reintroduce `false`.
- Keep `UI.GetPainter("MultiWheelTwoLegDetect.Filter", false)` as the car-coordinate ROI Painter.
- Defaults: `_saveFullMapImage = true`, `_enableTerminalDebugLog = true`.
- PNG: 300 DPI, 4 pixels per cell, maximum edge 4000 pixels, maximum final size `50 * 1024 * 1024` bytes.
- Output: `<Environment.CurrentDirectory>\TrapMapExports\TrapMap_yyyyMMdd_HHmmss_fff.png`.
- Export failure never changes `TrapMapBuilder.Succeeded` or clears `GridMap`.
- Do not capture the Clumsy viewport or add point-cloud/motion behavior.
---
### Task 1: Export contract, dependency, and pre-allocation limits
**Files:**
- Modify: `ClumsyPilot/ClumsyPilot.csproj`
- Create: `ClumsyPilot/TrapMapImageExporter.cs`
- Create: `ClumsyPilot/tests/verify_trapmap_image.ps1`
**Interfaces:**
- Consumes: `GridMapData`, `TrapMapVehiclePose`, workstation `Vector2`, tire metadata, output root.
- Produces: `TrapMapImageExportRequest`, `TrapMapImageExportResult`, and `TrapMapImageExporter.ExportIfEnabled(bool, TrapMapImageExportRequest)`.
- [ ] **Step 1: Add a failing image-export reflection test**
Create `verify_trapmap_image.ps1`. Load `ClumsyPilot/bin/Debug/netstandard2.0/ClumsyPilot.dll`; require types `MultiWheelC.TrapMapImageExporter`, `TrapMapImageExportRequest`, and `TrapMapImageExportResult`, and assert the assembly/output no longer contains a platform drawing dependency. Create a temporary directory under `$env:TEMP`, invoke the disabled path with an output directory that does not exist, and assert `Saved=false`, `Skipped=true`, and that no directory was created. Invoke an oversized request using a `1000×1` grid at 50mm so the four-pixels-per-cell canvas plus padding exceeds 4000, and assert rejection before any PNG/temp file exists.
Use these assertion helpers and cleanup guard:
```powershell
function Assert-Equal($expected, $actual, [string]$message) {
if ($expected -ne $actual) { throw "$message Expected=$expected Actual=$actual" }
}
$testRoot = Join-Path $env:TEMP ("trapmap-image-test-" + [guid]::NewGuid().ToString('N'))
try {
# reflection setup and assertions
} finally {
if (Test-Path -LiteralPath $testRoot) {
Remove-Item -LiteralPath $testRoot -Recurse -Force
}
}
```
- [ ] **Step 2: Run RED verification**
```powershell
dotnet restore ClumsyPilot\ClumsyPilot.csproj
dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1
```
Expected: compilation succeeds and the image script fails because `TrapMapImageExporter` is absent.
- [ ] **Step 3: Add the managed PNG dependency**
Add the exact managed encoder package inside the package `ItemGroup` in `ClumsyPilot.csproj`. Keep the target framework unchanged, expose the package path, and use one explicit build target that copies only its single managed `netstandard2.0` runtime asset. No drawing-runtime or native asset is required:
```xml
<ItemGroup>
<PackageReference Include="StbImageWriteSharp" Version="1.16.7"
GeneratePathProperty="true" />
</ItemGroup>
<Target Name="DeployManagedPngRuntime" AfterTargets="Build">
<Copy SourceFiles="$(PkgStbImageWriteSharp)\lib\netstandard2.0\StbImageWriteSharp.dll"
DestinationFiles="$(TargetDir)StbImageWriteSharp.dll" />
</Target>
```
Do not change the target framework.
- [ ] **Step 4: Implement request/result types and dimension validation**
Create `TrapMapImageExporter.cs` in namespace `MultiWheelC`. Use exact constants:
```csharp
public const int PixelsPerCell = 4;
public const int MaximumImageEdgePixels = 4000;
public const long MaximumFileSizeBytes = 50L * 1024L * 1024L;
public const float OutputDpi = 300f;
public const int OuterPaddingPixels = 24;
public const int HeaderHeightPixels = 140;
```
Request properties must include `GridMap`, `VehiclePose`, `WorkstationWorld`, `TireLayerStatus`, `TireLayerMessage`, `InputSource`, and `OutputRootDirectory`. Result properties must include `Saved`, `Skipped`, `FilePath`, `Message`, `FileSizeBytes`, `PixelWidth`, and `PixelHeight`.
`ExportIfEnabled(false, request)` returns a skipped result before validating the request or touching the filesystem. Enabled export validates non-null map/pose, finite workstation, then computes with `long`:
```csharp
long pixelWidth = 2L * OuterPaddingPixels + (long)grid.Cols * PixelsPerCell;
long pixelHeight = HeaderHeightPixels + 2L * OuterPaddingPixels
+ (long)grid.Rows * PixelsPerCell;
```
Reject non-positive or over-4000 dimensions before allocating the RGBA surface. Add `public static bool IsFileSizeAllowed(long byteCount)` returning `byteCount >= 0 && byteCount <= MaximumFileSizeBytes`; the actual save path must call this same function.
- [ ] **Step 5: Run GREEN contract checks**
Run the Task 1 commands. Expected: disabled/oversized assertions pass, no output directory exists for disabled export, and no RGBA buffer is allocated for oversized export.
### Task 2: Complete 300 DPI PNG rendering and atomic 50MB save
**Files:**
- Modify: `ClumsyPilot/TrapMapImageExporter.cs`
- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1`
**Interfaces:**
- Consumes: validated Task 1 request.
- Produces: a complete PNG or a failure result with no final/temporary file.
- [ ] **Step 1: Add failing PNG behavior assertions**
Extend the test with a `2×2` 50mm grid, mark one known occupied cell, set a finite vehicle/workstation, and export twice. Assert:
```powershell
Assert-Equal $true $result.Saved 'Small map must save.'
Assert-Equal $false $result.Skipped 'Enabled successful export is not skipped.'
if (-not (Test-Path -LiteralPath $result.FilePath)) { throw 'PNG file missing.' }
if ((Get-Item -LiteralPath $result.FilePath).Length -gt 50MB) { throw 'PNG exceeds 50MB.' }
if ([IO.Path]::GetExtension($result.FilePath) -ne '.png') { throw 'Output is not PNG.' }
if ((Split-Path $result.FilePath -Leaf) -notmatch '^TrapMap_\d{8}_\d{6}_\d{3}(_\d+)?\.png$') {
throw 'Timestamp filename is invalid.'
}
$image = Read-PngRgba $result.FilePath
Assert-Equal $result.PixelWidth $image.Width 'PNG width mismatch.'
Assert-Equal $result.PixelHeight $image.Height 'PNG height mismatch.'
Assert-Equal 11811 $image.PixelsPerMetreX 'PNG horizontal pHYs mismatch.'
Assert-Equal 11811 $image.PixelsPerMetreY 'PNG vertical pHYs mismatch.'
```
Assert the two file paths differ. Assert `IsFileSizeAllowed(50MB)` is true and `IsFileSizeAllowed(50MB + 1)` is false. Assert no `*.tmp` remains.
- [ ] **Step 2: Run RED behavior test**
Run the image test. Expected: it fails because enabled rendering/save is not implemented.
- [ ] **Step 3: Render the full grid**
After validation, create an internal RGBA8 surface and draw cells, grid, overlays, and 5×7 bitmap text with clipped integer primitives. Encode the buffer with `StbImageWriteSharp`, then insert a CRC-protected `pHYs` chunk containing `11811,11811,1` immediately after `IHDR`.
Exact layout and colors:
```text
Canvas background: White
Map top-left: (OuterPaddingPixels, HeaderHeightPixels + OuterPaddingPixels)
Unmarked cell interior: White
Grid lines: LightGray, 1px
Occupied cell interior: Red
Map border: Black, 2px
Vehicle outline/center/heading: Blue
Workstation circle/cross/text: LimeGreen
Header text: Black
```
For cell `(col,row)`, invert Y:
```csharp
int imageCol = col;
int imageRow = grid.Rows - 1 - row;
int x = mapLeft + imageCol * PixelsPerCell;
int y = mapTop + imageRow * PixelsPerCell;
```
Fill occupied interiors before drawing all vertical/horizontal grid lines. Convert vehicle rectangle corners and workstation through a shared world-to-pixel helper using `(worldX - XMin) / ResolutionMm` for X and `((YMin + Rows * ResolutionMm) - worldY) / ResolutionMm` for Y. The Y expression uses the discrete raster's actual upper edge, so overlays stay aligned when the requested world bounds are not an exact multiple of the resolution. Draw title strings for bounds, resolution, rows/cols, occupancy, obstacle count, tire status/message, and input source.
- [ ] **Step 4: Implement collision-safe atomic save and cleanup**
Create `<OutputRootDirectory>\TrapMapExports` only after all request/dimension validation. Select the millisecond timestamp name; if it exists, append `_1`, `_2`, etc. Encode into the exclusively reserved `finalPath + ".tmp"` stream, read `FileInfo.Length`, call `IsFileSizeAllowed`, delete the temp on rejection, then `File.Move(tempPath, finalPath)`.
Wrap rendering/saving in `try/catch/finally`; `finally` deletes only the current temp path if present. Never delete an existing final PNG. Return failure messages instead of throwing into the test runner.
- [ ] **Step 5: Run image and existing behavior tests**
```powershell
dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1
```
Expected: PNG assertions pass and existing behavior scripts retain their passing messages.
### Task 3: Terminal switch and successful-map export integration
**Files:**
- Modify: `ClumsyPilot/MovementTest.Trapmaptest.cs`
- Modify: `ClumsyPilot/tests/verify_trapmap_inputs.ps1`
- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1`
**Interfaces:**
- Consumes: Task 2 exporter.
- Produces: `_saveFullMapImage`, `_enableTerminalDebugLog`, shared dual-channel logging, and post-success export.
- [ ] **Step 1: Add failing source/integration contracts**
Require exact defaults, `EnableTerminalDebugLog` builder wiring, `TrapMapLog.Write`, and `TrapMapImageExporter.ExportIfEnabled`. Assert source still contains both world-Painter calls without `false`, retains the filter Painter with `false`, and export invocation occurs only after `_builder.Succeeded` is checked.
Add source assertions:
```powershell
if ($source -notmatch '_saveFullMapImage\s*=\s*true') { $failures.Add('Image switch default missing.') }
if ($source -notmatch '_enableTerminalDebugLog\s*=\s*true') { $failures.Add('Terminal switch default missing.') }
if ($source -notmatch 'TrapMapLog\.Write\(') { $failures.Add('Shared TrapMap logger missing.') }
if ($source -notmatch 'Console\.WriteLine\(') { $failures.Add('Terminal mirror missing.') }
if ($source -match 'GetPainter\("TrapMapTest",\s*false\)') { $failures.Add('TrapMap Painter regressed to local coordinates.') }
if ($source -notmatch 'GetPainter\("MultiWheelTwoLegDetect\.Filter",\s*false\)') { $failures.Add('Filter Painter lost local coordinates.') }
```
- [ ] **Step 2: Run RED contract test**
Run `verify_trapmap_inputs.ps1`. Expected: new switch/logger/export assertions fail.
- [ ] **Step 3: Implement the shared logger and switches**
Add a `TrapMapLog` static class with:
```csharp
public static void Write(string message, bool enableTerminal)
{
DLog.Log(message, "TrapMapTest");
if (enableTerminal)
Console.WriteLine($"[TrapMapTest] {message}");
}
```
Add `public bool EnableTerminalDebugLog = true` to the builder. Replace each TrapMap-owned two-argument `DLog.Log` call whose category is exactly `"TrapMapTest"` with `TrapMapLog.Write(message, EnableTerminalDebugLog)` in the builder and with the const switch in `TrapMapTest`. Do not replace unrelated log categories or `Hedingben.ToastText`.
Add the exact two constants to the test manual-edit section and pass terminal configuration into the builder.
- [ ] **Step 4: Invoke image export after successful map construction**
After the `_builder.Succeeded` failure return and after retrieving `grid`, construct the request from `_builder.GridMap`, `VehiclePose`, `WorkstationWorld`, tire status/message/input source, and `Environment.CurrentDirectory`. Call:
```csharp
var export = TrapMapImageExporter.ExportIfEnabled(_saveFullMapImage, request);
TrapMapLog.Write(export.Message, _enableTerminalDebugLog);
if (export.Saved)
Hedingben.ToastText($"栅格图片已保存: {export.FilePath}", "TrapMapTest");
```
Do not alter builder success when export fails/skips.
- [ ] **Step 5: Run full fresh verification**
```powershell
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_inputs.ps1
dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1
git diff --check
$staged = git diff --cached --name-only; if ($staged) { throw "Unexpected staged files: $staged" }
```
Expected: source, compile, grid, lifecycle, and image tests pass; no whitespace errors; no staged files. Confirm an exporter-generated test PNG reports 300 DPI and never exceeds 50MB before test cleanup.
@@ -0,0 +1,125 @@
# TrapMap Managed PNG Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove TrapMap's platform-specific drawing dependency and produce the same bounded 300 DPI PNG with a managed encoder that runs inside Clumsy.
**Architecture:** Keep `TrapMapImageExporter.ExportIfEnabled` and its file-reservation/publication behavior. Replace only the renderer with an internal RGBA raster surface, primitive drawing functions, a compact embedded bitmap font, and `StbImageWriteSharp` for PNG encoding; insert the 300 DPI `pHYs` chunk after encoding.
**Tech Stack:** C# 10, .NET Standard 2.0, `StbImageWriteSharp` 1.16.7, PowerShell contract/PNG parsing tests.
## Global Constraints
- Do not inspect or modify `TrajPlanner`.
- Do not commit or stage any file.
- Do not change grid construction, Painter behavior, movement behavior, switches, output location, naming, 300 DPI, 4 pixels per cell, 4000-pixel edge limit, or 50 MiB limit.
- The only new image package is `StbImageWriteSharp` version 1.16.7; do not add native assets or another graphics package.
- `TrapMapImageExporter` must have no runtime reference to `System.Drawing.Common` or `System.Drawing`.
- Preserve collision-safe temporary-file reservation, encoded-size validation, atomic publication, and contained export failures.
---
### Task 1: Replace System.Drawing rendering with a managed Stb PNG encoder
**Files:**
- Modify: `ClumsyPilot/ClumsyPilot.csproj`
- Modify: `ClumsyPilot/TrapMapImageExporter.cs`
- Modify: `ClumsyPilot/tests/verify_trapmap_image.ps1`
- Modify: `docs/superpowers/specs/2026-07-22-trap-map-image-and-console-design.md`
- Modify: `docs/superpowers/plans/2026-07-22-trap-map-image-and-console.md`
**Interfaces:**
- Preserve: `TrapMapImageExporter.ExportIfEnabled(bool, TrapMapImageExportRequest)` and all public request/result properties and constants.
- Add only private implementation units: `RgbaSurface`, integer drawing helpers, bitmap-font helpers, and `PngWriter`.
- `RenderToTemporaryPng` continues to consume the existing request/dimensions and write to the already exclusively reserved stream.
- [ ] **Step 1: Add dependency-removal and PNG-structure assertions**
Update `verify_trapmap_image.ps1` before production code. Require that:
```powershell
if ($project.PackageReference.Include -contains 'System.Drawing.Common') {
throw 'TrapMap must not depend on System.Drawing.Common.'
}
if ($project.Target.Name -contains 'DeployFrameworkDrawingRuntime') {
throw 'Legacy drawing-runtime deployment target remains.'
}
if (-not ($project.PackageReference | Where-Object {
$_.Include -eq 'StbImageWriteSharp' -and $_.Version -eq '1.16.7'
})) { throw 'Exact managed PNG package is missing.' }
if ($exporterSource -match 'System\.Drawing|\bBitmap\b|\bGraphics\b|ImageFormat') {
throw 'Exporter still uses the external drawing API.'
}
```
Parse the generated PNG without loading a drawing assembly. Verify signature, one `IHDR`, one `pHYs`, one or more `IDAT`, and `IEND`; verify every chunk CRC. Assert `IHDR` width/height and RGBA8 fields and `pHYs` values `11811,11811,1`. Decode representative pixels with a test-only PNG decoder or the Stb package and reuse the existing color/Y-inversion assertions.
Add a clean-output assertion after build:
```powershell
$drawingDll = Join-Path (Split-Path -Parent $AssemblyPath) 'System.Drawing.Common.dll'
if (Test-Path -LiteralPath $drawingDll) {
throw 'System.Drawing.Common.dll must not be deployed for TrapMap.'
}
```
- [ ] **Step 2: Run RED verification**
```powershell
dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore -v:minimal
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1
```
Expected: the image test fails because the package, deployment target, `using System.Drawing`, and renderer still exist.
- [ ] **Step 3: Remove the drawing package and deployment target**
Delete the `System.Drawing.Common` `PackageReference` and the entire `DeployFrameworkDrawingRuntime` target. Add `<PackageReference Include="StbImageWriteSharp" Version="1.16.7" />`. Do not change the target framework or other references. Ensure a clean build cannot retain the old DLL: the verification command must remove only `ClumsyPilot/bin/Debug/netstandard2.0/System.Drawing.Common.dll` before rebuilding, after resolving and validating that exact path is under the project output directory.
- [ ] **Step 4: Implement the RGBA raster surface**
Replace drawing types with a private surface backed by `byte[]` in RGBA order. Required primitives and semantics:
```csharp
SetPixel(int x, int y, byte r, byte g, byte b, byte a = 255);
FillRectangle(int x, int y, int width, int height, Color32 color);
DrawLine(int x0, int y0, int x1, int y1, Color32 color, int thickness);
DrawRectangle(int x, int y, int width, int height, Color32 color, int thickness);
DrawCircle(int centerX, int centerY, int radius, Color32 color, int thickness);
FillCircle(int centerX, int centerY, int radius, Color32 color);
FillPolygon(PointD[] points, Color32 color);
```
Clip every primitive to the surface. Use pre-clipped Bresenham lines, scale-normalized scanline polygon filling, and a canvas-clipped bounded circle scan whose work is proportional to visible rows/columns rather than radius. Preserve exact white, red, LightGray `(211,211,211)`, black, blue, and LimeGreen `(0,255,0)` colors. Convert vehicle/workstation world positions using the existing discrete-grid-aligned transform.
- [ ] **Step 5: Implement deterministic bitmap text**
Embed a private 5×7 ASCII glyph table for code points 32126. Draw scaled glyphs using integer pixels; unsupported characters render as `?`. Use a 2× scale for header text and 1× scale for the workstation label. Keep all five header baselines inside `HeaderHeightPixels=140` with fixed non-overlapping line boxes. Continue building the same five metadata lines, including occupancy rate; sanitize only the exported header text, not logs.
- [ ] **Step 6: Encode PNG and add 300 DPI metadata**
Use `StbImageWriteSharp.ImageWriter.WritePng` to encode the RGBA buffer. Then insert:
```text
pHYs: X=11811, Y=11811, unit=1
```
Encode into a temporary `MemoryStream`, validate the PNG signature and first `IHDR` chunk, then copy the signature+IHDR, append the 13-byte `pHYs` chunk, and copy the remaining encoded chunks. Use big-endian integers and standard CRC-32 over `pHYs`+data. Do not close the caller-owned reserved stream before the existing file-size/atomic-move flow finishes.
- [ ] **Step 7: Update documentation and run GREEN verification**
Remove all claims that TrapMap deploys or requires `System.Drawing.Common`; document the pure C# encoder and BCL-only runtime.
Run from a clean output state:
```powershell
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_inputs.ps1
dotnet build ClumsyPilot\ClumsyPilot.csproj --no-restore -v:minimal
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_grid.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_lifecycle.ps1
powershell -ExecutionPolicy Bypass -File ClumsyPilot\tests\verify_trapmap_image.ps1
git diff --check
$staged = git diff --cached --name-only; if ($staged) { throw "Unexpected staged files: $staged" }
```
Expected: build succeeds without a drawing DLL in output; all tests pass; PNG parser reports correct dimensions, CRCs, 300 DPI metadata, RGBA pixels, unique filenames, file size at or below 50 MiB, and no temporary files.
@@ -0,0 +1,427 @@
# Hybrid A* P0 规划核心 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,按任务顺序实施,并使用 `- [ ]` 更新执行状态。
**Goal:** 在既有 `PlanningGridMap` 之上提供可复用、可验证且不依赖 UI 的 Hybrid A* 粗路径规划服务。
**Architecture:** P0 先固定 m/rad/1/m 数据契约和连续车辆碰撞边界,再在该边界上实现恒曲率原语、二维启发式、确定性 Hybrid A*、路径重建和最终复核。`CoarsePathPlanningService` 是业务的唯一组合入口;`HybridAStarPlanner` 是只消费已建地图的下层门面。
**Tech Stack:** C# 10、.NET Standard 2.0、现有 `PlanningGridMap`、PowerShell 反射契约测试、`CancellationToken`
## Global Constraints
- 所有运行时代码位于 `ClumsyPilot/ParkrobTrajplanner/CoarsePath/`,命名空间为 `MultiWheelC.TrajectoryPlanning.CoarsePath` 或其子命名空间。
- Map 只保存外部障碍物;安全余量只在连续车辆碰撞检查时扩张车辆矩形,绝不写入 Map。
- 地图输入和障碍几何使用 mm;CoarsePath 的位置使用 m、航向使用 rad、曲率使用 1/m。
- 规划器只能接受 `PlanningGridMap`,不得引用 TwoLeg、定位、Painter、UI 或系统时间。
- 所有公开类型、构造函数、属性和方法使用中文 XML 文档,明确参数单位、边界以及返回或失败语义;内部几何/搜索不变量使用简短中文注释。
- `netstandard2.0` 禁止直接使用 `PriorityQueue``Math.Clamp``double.IsFinite``record``init`
- 固定约束:原语最大长度 0.50 m;积分最大步长 0.05 m;碰撞中心步长不超过 `min(0.025 m, Map.ResolutionMeters / 2)`;默认终点容差为 0.15 m、5°。
- 终点候选必须进入 Open List,只有作为最佳有效条目出队时才能成功;地图外始终按占据处理。
- 本计划不实现 P1 的 Clumsy `MovementTest`、Painter 绘制、Release 性能基准或旧 TrapMap 入口退役。
- 按用户现有约束,不执行 Git 自检、暂存、提交或推送。
---
## 文件结构
```text
ClumsyPilot/ParkrobTrajplanner/
├── CoarsePath/
│ ├── Contracts/ # 请求、结果、枚举和值对象
│ ├── Vehicle/ # 扩大车辆几何和连续碰撞
│ ├── Search/ # 原语、堆、启发式和 Hybrid A* 搜索
│ ├── Output/ # 回溯、稠密路径装配和最终验证
│ ├── Facade/ # 一次调用编排和调试旁路契约
│ ├── HybridAStarPlanner.cs
│ └── README.md # 粗规划调用方文档;链接至 ../Map/README.md
└── tests/
├── verify_coarse_path_collision.ps1
├── verify_coarse_path_search.ps1
└── verify_coarse_path_integration.ps1
```
## Task 1: 固定公共契约、状态与默认配置
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/Pose2D.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/TravelDirection.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/GoalDirectionConstraint.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/VehicleParameters.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/HybridAStarConfiguration.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningRequest.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningStatus.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPoint.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/CoarsePathPointSource.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PathSegment.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningResult.cs`
- Modify: `ClumsyPilot/tests/verify_planning_utils.ps1`
**Produces:**
```csharp
public sealed class Pose2D
{
public Pose2D(double xMeters, double yMeters, double headingRadians);
public double X { get; }
public double Y { get; }
public double Heading { get; }
}
public sealed class PlanningRequest
{
public PlanningGridMap Map { get; set; }
public Pose2D Start { get; set; }
public Pose2D Goal { get; set; }
public VehicleParameters Vehicle { get; set; }
public HybridAStarConfiguration Configuration { get; set; }
public double StartVehicleCurvature { get; set; }
public TravelDirection? StartDirection { get; set; }
public GoalDirectionConstraint GoalDirection { get; set; }
}
```
- [ ] **Step 1: 写失败的公共契约测试。**`verify_planning_utils.ps1` 载入程序集后添加反射断言,检查 `Pose2D` 构造函数、三个枚举、`PlanningRequest` 属性和每个默认值。默认配置断言如下:
```powershell
$config = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.HybridAStarConfiguration
Assert-Equal 0.50 $config.PrimitiveLengthMeters '原语最大长度'
Assert-Equal 0.05 $config.IntegrationStepMeters '积分步长'
Assert-Equal 0.025 $config.MaximumCollisionCheckStepMeters '碰撞步长'
Assert-Equal 5 $config.CurvatureLevelCount '曲率等级数'
Assert-Equal 200000 $config.MaximumExpandedNodes '节点上限'
```
- [ ] **Step 2: 运行测试并确认 RED。**
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1
```
预期:脚本因 `CoarsePath` 类型尚不存在而以非零退出。
- [ ] **Step 3: 实现最小契约。** `TravelDirection` 仅含 `Forward``Reverse``GoalDirectionConstraint` 仅含 `Any``Forward``Reverse``CoarsePathPointSource` 仅含 `Start``MotionPrimitive``GoalTruncation``PlanningStatus` 必须包含 `Success``Cancelled``InvalidRequest``InvalidMap``MapNotReady``InvalidVehicleParameters``InvalidCurvatureConfiguration``StartOutsideMap``StartInCollision``GoalOutsideMap``GoalInCollision``SearchTimeout``SearchNodeLimitExceeded``NoFeasiblePath``BacktrackingFailed``FinalValidationFailed``InternalError`
`HybridAStarConfiguration` 的构造默认值必须是:`Math.PI / 36d` 航向/终点航向容差、5 秒超时、`HeuristicWeight=1d``ReverseCostMultiplier=1.5d``GearSwitchPenaltyMeters=1d``CurvatureMagnitudeWeight=0.10d``CurvatureChangePenaltyMetersPerLevel=0.05d``ClearanceCostWeight=0.20d``ClearanceCostDistanceMeters=0.50d`
`PlanningResult` 只允许成功结果携带非空路径与分段;所有失败工厂方法返回空只读集合并保留诊断。`PlanningDiagnostics` 固定记录扩展、生成、重开、陈旧堆条目、Open List 峰值、路径长度、最小保守净空、耗时和终止原因。
- [ ] **Step 4: 重新运行工具契约测试并确认 GREEN。**
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1
```
预期:退出码为 0,既有 Utils/Map 契约仍可加载。
## Task 2: 实现扩大车辆足迹与连续碰撞检查
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleKinematics.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/VehicleFootprint.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/OrientedRectangleCellIntersection.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Vehicle/FootprintCollisionChecker.cs`
- Create: `ClumsyPilot/tests/verify_coarse_path_collision.ps1`
**Consumes:** `PlanningGridMap``Pose2D``VehicleParameters`
**Produces:**
```csharp
public sealed class FootprintCollisionChecker
{
public bool IsPoseCollisionFree(
Pose2D pose, PlanningGridMap map, VehicleParameters vehicle,
double additionalMarginMeters, out double bodyClearanceMeters);
public bool IsSweptMotionCollisionFree(
Pose2D from, Pose2D to, PlanningGridMap map, VehicleParameters vehicle,
double maximumCenterStepMeters, out double minimumBodyClearanceMeters);
}
```
- [ ] **Step 1: 写失败的连续碰撞测试。** 脚本通过 `PlanningMapFactory` 创建 50 mm 地图和单个薄矩形障碍,验证下面三种行为:车辆与障碍格擦边返回碰撞、距离场净空严格大于外接圆半径时返回安全、两个端点安全但中间穿过障碍时扫掠检查返回碰撞。
```powershell
$checker = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.FootprintCollisionChecker
$clearance = 0.0
$safe = $checker.IsPoseCollisionFree($pose, $map, $vehicle, 0.0, [ref]$clearance)
Assert-False $safe '矩形擦边必须视为碰撞'
```
- [ ] **Step 2: 运行碰撞脚本并确认 RED。**
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1
```
预期:因车辆命名空间和碰撞检查器不存在而失败。
- [ ] **Step 3: 实现最小连续几何。** `VehicleKinematics` 在最大曲率与最小转弯半径都存在时取 `Math.Min(maximumCurvature, 1d / minimumRadius)``VehicleFootprint``LengthMeters + 2 * SafetyMarginMeters``WidthMeters + 2 * SafetyMarginMeters` 构造以 `Pose2D` 为几何中心的旋转矩形、AABB 与外接圆。
`OrientedRectangleCellIntersection` 使用 SAT:矩形的两个单位轴和格子的世界 X/Y 轴都作为投影轴;任一轴存在严格分离才是不相交,投影接触算相交。`FootprintCollisionChecker` 依次验证四角均在地图内、用严格 `distance > radius + additionalMargin` 快速放行、遍历 AABB 内占据格并执行 SAT。扫掠检查将中心位移切分到 `min(maximumCenterStepMeters, map.ResolutionMeters / 2d)`,每一段的临时边距为 `0.5d * (centerDisplacement + circumscribedRadius * Math.Abs(headingDelta))`
- [ ] **Step 4: 扩展碰撞测试并运行 GREEN。** 加入 0°、45°、任意航向、栅格中心/亚栅格中心、薄障碍、边界外和扫掠场景;随后运行:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1
```
预期:构建与脚本退出码均为 0。
## Task 3: 实现原语积分、目标容差与内部截断
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitive.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/MotionPrimitiveGenerator.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GoalToleranceChecker.cs`
- Create: `ClumsyPilot/tests/verify_coarse_path_search.ps1`
**Consumes:** `Pose2D`、方向、车辆最大曲率、`HybridAStarConfiguration``FootprintCollisionChecker`
**Produces:** 含方向、曲率、实际长度和内部积分点的不可变 `MotionPrimitive`;目标检查器只判定位置、航向和目标进入方向。
- [ ] **Step 1: 写失败的原语测试。** 验证直行、圆弧、倒车、0.50 m 上限、积分点间距上限,以及目标在 0.30 m 处时原语恰好截断到第一个满足条件的内部点。
```powershell
$primitive = $generator.Generate($start, $curvature, $direction, $config, $map)
Assert-True ($primitive.Points.Count -ge 1) '原语必须产生内部积分点'
Assert-Equal 0.30 $truncated.ActualLengthMeters '0.30m 目标必须在原语内部截断'
```
- [ ] **Step 2: 运行搜索脚本并确认 RED。**
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1
```
预期:因原语类型尚不存在而失败。
- [ ] **Step 3: 实现解析积分和检查顺序。** 单步积分使用:
```csharp
double signedDistance = direction == TravelDirection.Forward ? step : -step;
double nextHeading = AngleMath.NormalizeRadians(heading + curvature * signedDistance);
if (Math.Abs(curvature) < 1e-12)
{
nextX = x + signedDistance * Math.Cos(heading);
nextY = y + signedDistance * Math.Sin(heading);
}
else
{
nextX = x + (Math.Sin(nextHeading) - Math.Sin(heading)) / curvature;
nextY = y - (Math.Cos(nextHeading) - Math.Cos(heading)) / curvature;
}
```
原语点步长不得超过 `min(IntegrationStepMeters, MaximumCollisionCheckStepMeters, Map.ResolutionMeters / 2d)`。每个内部点严格按“有限数值 → 从前一点的扫掠碰撞 → 终点容差”执行;命中目标即截断,并标记 `GoalTruncation`。起点已满足目标时生成零长度终点候选,不生成运动原语。
- [ ] **Step 4: 运行原语测试并确认 GREEN。** 加入五个曲率等级、曲率相邻变化最多一级和 `±π` 航向容差的断言;再运行 `verify_coarse_path_search.ps1`,预期退出码为 0。
## Task 4: 实现确定性 Open List、代价与二维启发式
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/BinaryMinHeap.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/SearchCostCalculator.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs`
- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1`
**Produces:** 内部二叉最小堆、等效米代价计算器和目标反向八邻域距离启发式。
- [ ] **Step 1: 写失败的堆、代价和启发式测试。** 验证堆的排序优先级为 `F``H`、较大 `G`、插入序号;验证八邻域斜向代价和禁止切过两个正交障碍的对角夹角;验证倒车、换向、曲率和净空代价项。
```powershell
Assert-Equal 'node-b' $heap.Pop().Id '相同 F 时应先选较小 H'
Assert-Throws { $calculator.Calculate($invalidInput) } '负权重必须拒绝'
Assert-True ([double]::IsPositiveInfinity($heuristic.GetCost($blockedRow, $blockedCol))) '二维不可达应为无穷'
```
- [ ] **Step 2: 运行搜索测试并确认 RED。**
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1
```
预期:因堆、代价或启发式类型不存在而失败。
- [ ] **Step 3: 实现最小支持结构。** `BinaryMinHeap` 使用 `List<T>`,比较器严格按 `F``H`、反向 `G`、插入序号。代价必须实现:
```text
length * directionMultiplier *
(1 + curvatureMagnitudeWeight * abs(curvature / maximumCurvature)
+ clearanceCostWeight * max(0, 1 - clearance / clearanceCostDistance))
+ gearSwitchPenalty
+ curvatureChangePenalty * abs(curvatureLevelDelta)
```
`GridDijkstraHeuristic` 从目标格反向传播四邻域 1 倍格长和对角 `sqrt(2)` 倍格长;对角移动前确认两个正交邻格均未占据。
- [ ] **Step 4: 运行搜索测试并确认 GREEN。** 重复构造同一输入两次,断言出队顺序相同;运行 `verify_coarse_path_search.ps1`,预期退出码为 0。
## Task 5: 实现 Hybrid A* 节点、重开与终点候选管理
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNode.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarNodeKey.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs`
- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1`
**Consumes:** Task 2–4 的碰撞、原语、堆、代价与启发式。
**Produces:** 接收已验证请求并返回成功节点索引或明确搜索失败状态的内部搜索器。
- [ ] **Step 1: 写失败的搜索测试。** 覆盖空图前进、单矩形绕行、允许倒车的狭窄场景、起始曲率、目标方向、无解、取消、超时、节点上限、较小 `G` 重开和终点候选出队顺序。
```powershell
$result = $search.Search($request, [Threading.CancellationToken]::None)
Assert-Equal 'Success' $result.Status '空图应规划成功'
Assert-True $result.ReopenedNodeCount -gt 0 '更小 G 到达同键时必须允许重开'
```
- [ ] **Step 2: 运行搜索脚本并确认 RED。**
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1
```
预期:因 `HybridAStarSearch` 不存在而失败。
- [ ] **Step 3: 实现离散键与搜索循环。** `HybridAStarNodeKey` 固定包含位置行列、航向索引、方向和曲率等级。普通状态的最佳 `G` 保存在 `Dictionary<HybridAStarNodeKey, double>`;发现严格更小的 `G` 时压入新条目,旧条目在弹出时丢弃。循环在扩展前检查 `CancellationToken`、配置超时和最大扩展数。
终点候选压入同一 Open List,但不放入普通键的去重表;它只能在作为当前最佳有效条目弹出、重新验证终点条件与末段碰撞后成功。Open List 耗尽返回 `NoFeasiblePath`
- [ ] **Step 4: 运行全量搜索场景并确认 GREEN。** 对每个固定场景重复运行两次并断言状态、路径代价和节点扩展顺序一致;运行 `verify_coarse_path_search.ps1`,预期退出码为 0。
## Task 6: 回溯、路径装配、最终验证与下层门面
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/PathBacktracker.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathAssembler.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Output/CoarsePathValidator.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs`
- Create: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
**Produces:**
```csharp
public sealed class HybridAStarPlanner
{
public PlanningResult Plan(
PlanningRequest request,
CancellationToken cancellationToken = default(CancellationToken));
}
```
- [ ] **Step 1: 写失败的路径输出测试。** 验证首点弧长为 0、弧长不递减、展开航向连续、终点截断来源、相邻重复点只允许作为换向对、分段的包含式索引覆盖全部路径。
```powershell
Assert-Equal 0.0 $result.Path[0].ArcLength '起点弧长必须为零'
Assert-True ($result.Segments[-1].EndIndex -eq ($result.Path.Count - 1)) '分段必须覆盖尾点'
Assert-Equal 'FinalValidationFailed' $invalid.Status '最终复核失败不能发布部分路径'
```
- [ ] **Step 2: 运行集成脚本并确认 RED。**
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
预期:因 `HybridAStarPlanner` 与输出类型不存在而失败。
- [ ] **Step 3: 实现回溯和最终复核。** 搜索节点只保留父索引和原语描述;`PathBacktracker` 在成功后使用同一解析积分公式重建内部点。`CoarsePathAssembler` 累计弧长,保持换向处两个相同位姿/弧长而方向不同的点,并让新方向点设置 `IsGearSwitchPoint=true`
`CoarsePathValidator` 使用与搜索相同的 `FootprintCollisionChecker` 和扫掠规则,检查有限数、曲率上限、起终点容差/方向、弧长单调性、换向对与分段覆盖。验证失败返回 `FinalValidationFailed`,路径和分段均为空。
`HybridAStarPlanner` 在调用搜索前映射空请求、Map 未就绪、车辆无效、曲率配置无效、起终点越界及起终点碰撞;其余异常收敛为 `InternalError` 并记录诊断。
- [ ] **Step 4: 运行碰撞、搜索和集成脚本并确认 GREEN。**
```powershell
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
```
预期:三个脚本均退出 0。
## Task 7: 一次调用服务、调试旁路契约与 README
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJob.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningJobResult.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/PlanningDebugOptions.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/IPlanningDebugSink.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
**Produces:**
```csharp
public sealed class CoarsePathPlanningService
{
public CoarsePathPlanningJobResult Plan(
CoarsePathPlanningJob job,
CancellationToken cancellationToken = default(CancellationToken));
}
```
- [ ] **Step 1: 写失败的一次调用测试。** 断言服务先建图、地图失败时不搜索、成功时同时返回 `PlanningMapBuildResult``PlanningResult`;同一服务实例两次使用相同地图请求时第二次是 `Input` 缓存命中。
```powershell
$service = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Facade.CoarsePathPlanningService
$first = $service.Plan($job, [Threading.CancellationToken]::None)
$second = $service.Plan($job, [Threading.CancellationToken]::None)
Assert-Equal 'Input' $second.MapResult.CacheHit.ToString() '服务必须长期持有地图工厂'
```
- [ ] **Step 2: 运行集成脚本并确认 RED。**
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
预期:因门面类型与 README 不存在而失败。
- [ ] **Step 3: 实现服务和文档。** `CoarsePathPlanningService` 构造时创建一个长期 `PlanningMapFactory` 与一个 `HybridAStarPlanner`,计划调用顺序固定为:
```text
PlanningMapFactory.Create(job.MapRequest)
-> 地图失败:包装 MapResult,返回空 PlanningResult
-> 地图成功:HybridAStarPlanner.Plan(job 转换的 PlanningRequest)
-> 仅依 Debug 选项向 IPlanningDebugSink 发布旁路数据
```
默认 sink 为空实现。任何 sink 异常只追加调试诊断,绝不改变地图哈希、规划状态、路径或分段。
README 必须包含以下小节:模块范围;Map 与 CoarsePath 的职责表;`CoarsePathPlanningService.Plan` 的可编译调用示例;mm/m/rad/1/m 单位表;`SourceVersion` 与缓存规则;`PlanningStatus` 处理示例;路径点和方向分段含义;第一版不支持的平滑、速度规划、控制和横移能力;到 `../Map/README.md` 的链接。
- [ ] **Step 4: 运行完整 P0 验收。**
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1
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_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
```
预期:构建和全部七个脚本退出码均为 0;不执行 P1 UI 或性能工作。
## Plan Self-Review
- 覆盖性:Task 1 覆盖公共契约;Task 2 覆盖连续车辆碰撞;Task 3–5 覆盖原语、代价、启发式、确定性搜索与终点候选;Task 6 覆盖输出与最终复核;Task 7 覆盖一次调用门面、文档和全量验收。
- 类型一致性:所有搜索和门面输入均以 `PlanningGridMap``PlanningRequest``CoarsePathPlanningJob` 为唯一跨层契约;Map 构建只存在于 Task 7 的服务门面。
- 范围:没有包含 Clumsy UI、Painter、性能基准或旧 TrapMap 迁移,这些均为 P1。
@@ -0,0 +1,242 @@
# Map 模块文档与注释实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为 Map 模块提供根目录结构说明,并为所有公共 API 提供中文、Python docstring 风格的调用说明。
**Architecture:** `Map/README.md` 只说明模块结构、数据流、单位与入口;`.cs` 中的 `/// <summary>` 是参数、返回和约束的唯一 API 文档来源。注释不得改变方法签名、建图算法、缓存键或任何运行时行为。
**Tech Stack:** C# 10、netstandard2.0、PowerShell 验证脚本、Markdown。
## Global Constraints
- 所有新增说明使用中文。
- 公共 API 文档使用可被 C# IDE 识别的 `///`,内容顺序为“功能、参数、返回、注意”。
- 参数说明必须给出单位、坐标系、可空性或输入约束中的适用项。
- 返回说明必须给出结果数据的业务意义;`bool` 说明其 true/false 语义。
- README 不复制逐个属性的完整参数表。
- 不修改运行逻辑,不执行 Git 自检、暂存、提交或重置。
---
### Task 1: 建立 Map README 与文档存在性检查
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/Map/README.md`
- Create: `ClumsyPilot/tests/verify_planning_map_documentation.ps1`
**Interfaces:**
- Consumes: `PlanningMapFactory.Create(PlanningMapRequest request)`、Map 现有目录结构。
- Produces: Map 模块入口说明和可重复运行的文档检查。
- [ ] **Step 1: 写入失败检查**
创建 PowerShell 脚本,读取 `Map/README.md`,断言它不存在时抛出异常;创建后继续断言包含以下固定标题:`# Map 模块说明``## 文件结构``## 建图数据流``## 坐标与单位``## 最小调用示例``## 缓存与版本``## 测试与调试`
- [ ] **Step 2: 运行检查确认失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1`
Expected: 因 `Map/README.md` 不存在而失败。
- [ ] **Step 3: 创建 README**
写入当前 `Core``Obstacles``Sources``Planning``Test``Test/Visualization` 的目录树;每个 `.cs` 文件后写一句职责。说明数据流为 `PlanningMapRequest → IMapObstacleSource → EnvironmentMapBuilder/MapObstacleRasterizer → EnvironmentGridMap → PlanningMapAdapter/ObstacleDistanceField → PlanningGridMap`。说明环境图使用世界 mm、规划查询使用 m、范围采用左闭右开;示例只经长期持有的 `PlanningMapFactory.Create` 调用;说明 `SourceVersion` 变化与两级缓存的关系;明确 PNG 为可选调试、旧 TrapMap 不属于新运行时入口。
- [ ] **Step 4: 运行检查确认通过**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1`
Expected: `Planning map documentation checks passed.`
### Task 2: 注释公共建图入口与结果契约
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapRequest.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBuildRequest.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs`
**Interfaces:**
- Consumes: 外部调用者提供的地图范围、分辨率、障碍物来源。
- Produces: 建图请求、构建结果、缓存命中状态的中文 API 契约。
- [ ] **Step 1: 扩展失败检查**
`verify_planning_map_documentation.ps1` 中对上述文件断言:`PlanningMapFactory.Create``PlanningMapRequest.Bounds``PlanningMapBuildResult.Map` 前方紧邻中文 `///` 注释,且包含 `参数:``返回:``单位:``注意:` 中适用的说明。
- [ ] **Step 2: 运行检查确认失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1`
Expected: 失败并指出缺失的入口契约说明。
- [ ] **Step 3: 添加入口契约注释**
`PlanningMapFactory`、构造和 `Create` 写明长期复用要求、请求输入、结果与三种缓存命中语义。为请求与结果的每个公共属性写明单位、可空性和失败/空图语义。为 `PlanningMapCacheHit` 的每个枚举值写明 `None``Input``Occupancy` 的实际含义。为内部 Map 构建请求和结果的 public 成员补充相同层级说明。
- [ ] **Step 4: 运行入口检查与编译**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1`
Run: `dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore`
Expected: 文档检查通过;编译 0 error。
### Task 3: 注释地图边界、环境栅格与障碍物契约
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/MapBoundsMm.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentGridMap.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/IMapObstacle.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/CircleObstacle.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/AxisAlignedRectangleObstacle.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs`
**Interfaces:**
- Consumes: 世界坐标毫米几何、有效栅格范围。
- Produces: 环境占据图以及几何到栅格的公开行为说明。
- [ ] **Step 1: 扩展失败检查**
`MapBoundsMm` 构造函数、`Contains``GetDimensions``EnvironmentGridMap` 构造函数和世界/栅格查询方法,以及两种障碍物构造函数与属性,断言有中文 `///`。脚本还断言 `MapObstacleRasterizer.Rasterize` 注释包含其是唯一写栅格入口的约束。
- [ ] **Step 2: 运行检查确认失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1`
Expected: 失败并显示尚未文档化的公共几何/栅格 API。
- [ ] **Step 3: 添加边界与几何注释**
为范围、行列、世界 mm 坐标、左闭右开边界、越界 `false`/占据行为、`out row/col` 的失败值写明说明。为圆和矩形的坐标、半径与 `IsValid` 写明单位和 true/false 条件。为环境构建器 `Build` 写明必需来源失败会整体失败、可选来源只记录状态的规则。
- [ ] **Step 4: 运行检查与 Map 适配器脚本**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1`
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1`
Expected: 两个脚本通过。
### Task 4: 注释障碍物来源与 TwoLeg 投影契约
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/IMapObstacleSource.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/ManualObstacleSource.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegProjectionInput.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleSource.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/TwoLegObstacleProjector.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleProjectionResult.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Sources/ObstacleSourceStatus.cs`
**Interfaces:**
- Consumes: 纯检测快照和外部障碍物几何。
- Produces: 世界 mm 几何、来源状态和诊断信息。
- [ ] **Step 1: 扩展失败检查**
对来源接口的 ID、版本、必需性和 `ProjectToWorld`,TwoLeg 输入构造函数/属性,以及投影结果工厂方法和状态枚举值断言中文 API 说明。
- [ ] **Step 2: 运行检查确认失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1`
Expected: 失败并指出缺失的来源或 TwoLeg 契约说明。
- [ ] **Step 3: 添加来源注释**
明确 `ProjectToWorld` 不得读传感器、定位、UI、时钟;`SourceVersion` 必须在快照内容变化时递增;`IsRequired` 的失败语义;TwoLeg 检测时世界位姿和两腿局部 mm 坐标、航向弧度、半径单位;`Applied/Empty/Unavailable/Invalid` 的规划含义。
- [ ] **Step 4: 运行来源工厂验证**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1`
Expected: `Planning map factory checks passed.`
### Task 5: 注释规划快照、距离场与缓存契约
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningGridMap.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/Planning/PlanningMapCache.cs`
**Interfaces:**
- Consumes: 环境占据栅格和建图输入/占据哈希。
- Produces: 不可变规划快照、保守距离和缓存复用行为说明。
- [ ] **Step 1: 扩展失败检查**
断言 `PlanningGridMap` 的公共属性与查询方法、适配器/距离场/EDT 的公共静态方法、缓存公共方法均有中文 `///`;检查 `PlanningGridMap` 注释含 m 与 mm 的单位区分。
- [ ] **Step 2: 运行检查确认失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1`
Expected: 失败并报告缺失的规划或缓存说明。
- [ ] **Step 3: 添加规划与缓存注释**
说明 `PlanningGridMap` 不可变、规划世界查询使用 m、越界视为占据/零净距、距离是保守下界;说明适配器从 mm 环境图转为 m 规划图;说明 EDT 输出平方距离;说明缓存容量为四、输入命中返回同一快照、占据命中共享数组但颁发新快照元数据。
- [ ] **Step 4: 运行适配器和工厂验证**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1`
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1`
Expected: 两个脚本通过。
### Task 6: 注释测试、PNG 调试公共 API并完成总验证
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/MovementTest.MapTest.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportRequest.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExportResult.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageExporter.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/PlanningMapImageRenderer.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/Map/Test/Visualization/ValidatedPngWriter.cs`
**Interfaces:**
- Consumes: 只读 `PlanningGridMap` 和可选 PNG 输出目录。
- Produces: 清晰的 MapTest 配置/日志语义和 PNG 导出结果说明。
- [ ] **Step 1: 扩展失败检查**
`PlanningMapTest.Test/TestStop`、PNG 请求/结果的每个属性、导出器常量与 `ExportIfEnabled`、渲染器和 PNG 写入器公共方法断言中文 `///`
- [ ] **Step 2: 运行检查确认失败**
Run: `powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1`
Expected: 失败并列出测试或可视化公共成员。
- [ ] **Step 3: 添加测试与 PNG 注释**
说明 MapTest 是手工 Clumsy 入口,日志/PNG 开关仅影响调试;说明 PNG 不参与建图和缓存;说明输出目录、像素尺寸、字节大小、Saved/Skipped 的语义;说明 `ValidatedPngWriter.Write` 输入是 RGBA 行主序字节及其宽高。
- [ ] **Step 4: 完整验证**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_utils.ps1
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_test_config.ps1
```
Expected: 所有脚本通过;编译 0 error。现有过时 API 警告若仍来自 `MovementTests.TireFollowing.cs``TireFollowing.cs`,记录为非本任务引入。
## Plan Self-Review
- Spec coverage: Task 1 覆盖 README 和目录结构;Task 2 至 Task 6 覆盖全部 public API 分层;Task 6 覆盖完整验证。
- Placeholder scan: 本计划没有 TODO、TBD 或未指定的验证命令。
- Type consistency: 文中使用的类型和方法名均来自当前 Map 源码;不引入新运行时接口。
@@ -0,0 +1,534 @@
# P1 粗路径 Clumsy UI 集成 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 交付可在 Clumsy 中后台运行的七个粗路径测试入口,显示真实规划栅格快照和完整路径信息,并支持传入 AMR 世界位姿与手动目标位姿。
**Architecture:** `CoarsePathScenarioFactory` 保持无 UI 的纯输入构造职责,提供六个可重复的回归场景及一个显式标注为空图演示的“AMR 位姿 + 手动终点”请求创建入口。`MovementTest.CoarsePathTest.cs` 只作为 UI 适配层:把 AMR/目标 mm+deg 转为核心所需的 m+rad,使用一个共享门面在 `Task.Run` 后台运行,并从不可变 `CoarsePathPlanningJobResult` 绘制地图快照和路径。
**Tech Stack:** C# / `netstandard2.0`、现有 Clumsy `MovementTest`/`Painter``CoarsePathPlanningService`、PowerShell 反射验证脚本。
## Global Constraints
- `PlanningMapRequest` 的地图、障碍物、AMR 输入和手动目标 X/Y 均为世界 mm;`Pose2D` 和路径 X/Y 为世界 m;核心航向为 rad。
- 项目上游的 AMR `th` 输入按 deg 适配为 `th * Math.PI / 180d`;不得沿用直接将该值传给 `Math.Cos/Sin` 的旧写法。
- AMR 起点必须是车辆几何中心;传感器安装点必须由上游先按外参转换。
- UI 和测试只能调用 `CoarsePathPlanningService.Plan(job, token)`;不得直接实例化 `PlanningMapFactory``HybridAStarPlanner`、栅格化器、碰撞器、原语生成器或搜索节点。
- 所有七个 MovementTest 都不得引用 `BasicPilotBase.Chassis``SendMotion``DriveTask` 或任何底盘控制 API。
- `Test` 不得等待后台任务或读取 `Task.Result``TestStop` 先取消令牌,再使运行编号失效、解绑任务并清空 Painter。
- 只在 `PlanningStatus.Success` 绘制路径、方向箭头、换向点和扩大车体检查框;失败、取消和超时只显示地图、起点、终点和状态。
- 代码兼容 `netstandard2.0`,不引入新 NuGet 包;公开类型/成员写中文 XML 文档,复杂单位与并发逻辑写简短中文行注释。
- 不改动 TrapMap 文件或旧 TrapMap 验证脚本;不执行 Git 状态、差异、提交或重置操作。
---
## 文件结构
| 文件 | 变更职责 |
| --- | --- |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` | 新建纯场景工厂、六场景枚举、AMR/目标位姿 mm+deg 到核心 `Pose2D` 的转换,以及空图演示手动目标请求。 |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` | 新建七个 UI 入口、共享会话执行器、任务取消、Painter 地图/路径/图例绘制与手动输入解析。 |
| `ClumsyPilot/tests/verify_coarse_path_integration.ps1` | 为工厂行为、单位转换、缓存/换向/无解、UI 源码边界与 README 内容新增真实程序集和文本断言。 |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 记录 P1 测试入口、输入单位、空图演示限制、图例、停止语义及无底盘命令边界。 |
### 固定接口
```csharp
namespace MultiWheelC.TrajectoryPlanning.CoarsePath.Test;
public enum CoarsePathTestScenario
{
ExplicitEmpty,
RectangleDetour,
ManualAndTwoLeg,
CacheHit,
ReverseGearSwitch,
NoFeasiblePath,
}
public static class CoarsePathScenarioFactory
{
public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario);
public static CoarsePathPlanningJob CreateManualGoalDemo(
double startXMillimeters, double startYMillimeters, double startHeadingDegrees,
double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees);
}
```
`CreateManualGoalDemo` 只构造带 2,000 mm 边缘留白的显式空图演示请求,并在 README/测试名称中明确其不代表真实环境安全。未来现场入口必须提供真实 `IMapObstacleSource` 快照,而不是修改此方法的语义。
### Task 1: 工厂契约与失败测试
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
- Create later in Task 2: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs`
**Consumes:** 已有 `CoarsePathPlanningService.Plan(CoarsePathPlanningJob, CancellationToken)``Find-Method``Assert-True``Assert-Equal` 与程序集加载逻辑。
**Produces:**`CoarsePathTestScenario``CoarsePathScenarioFactory` 的反射行为约束;Task 2 的最小实现必须使这些断言通过。
- [ ] **Step 1: 在集成脚本加入工厂反射测试**
在现有 facade 检查后、最终输出前插入以下 PowerShell。它要求类型和两个公开方法都存在,因此在工厂未创建时失败。
```powershell
$testNamespace = $coarsePath + 'Test.'
$scenarioEnumType = $assembly.GetType($testNamespace + 'CoarsePathTestScenario', $false)
$scenarioFactoryType = $assembly.GetType($testNamespace + 'CoarsePathScenarioFactory', $false)
Assert-True ($scenarioEnumType -ne $null) 'P1 scenario enum must exist.'
Assert-True ($scenarioFactoryType -ne $null) 'P1 scenario factory must exist.'
$factoryCreate = Find-Method $scenarioFactoryType 'Create' @($scenarioEnumType)
$factoryManual = Find-Method $scenarioFactoryType 'CreateManualGoalDemo' @(
[double], [double], [double], [double], [double], [double])
Assert-True ($factoryCreate -ne $null) 'P1 scenario factory must expose Create(scenario).'
Assert-True ($factoryManual -ne $null) 'P1 scenario factory must expose CreateManualGoalDemo with six doubles.'
$scenarioNames = @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'CacheHit', 'ReverseGearSwitch', 'NoFeasiblePath')
foreach ($scenarioName in $scenarioNames) {
$scenario = [Enum]::Parse($scenarioEnumType, $scenarioName)
$jobA = $factoryCreate.Invoke($null, @($scenario))
$jobB = $factoryCreate.Invoke($null, @($scenario))
Assert-True ($jobA -ne $null) "Scenario $scenarioName must return a job."
Assert-False ([object]::ReferenceEquals($jobA, $jobB)) "Scenario $scenarioName must return a new job per call."
}
$manualJob = $factoryManual.Invoke($null, @(1000.0, 2000.0, 90.0, 4000.0, 2000.0, 0.0))
Assert-Near 1.0 $manualJob.Start.X 'Manual AMR X must convert mm to m.'
Assert-Near 2.0 $manualJob.Start.Y 'Manual AMR Y must convert mm to m.'
Assert-Near ([Math]::PI / 2.0) $manualJob.Start.Heading 'Manual AMR heading must convert degrees to radians.'
Assert-Near 4.0 $manualJob.Goal.X 'Manual goal X must convert mm to m.'
Assert-Near 0.0 $manualJob.Goal.Heading 'Manual goal heading must convert degrees to radians.'
Assert-True $manualJob.MapRequest.AllowExplicitEmptyMap 'Manual goal demo must declare its empty map explicitly.'
```
- [ ] **Step 2: 运行脚本确认失败**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: 构建成功;脚本因 `P1 scenario enum must exist.` 失败。
### Task 2: 实现纯场景工厂并通过行为测试
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs`
- Test: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
**Consumes:** Task 1 的枚举和两个公开工厂方法;`MapBoundsMm``ManualObstacleSource``TwoLegObstacleSource``Pose2D``VehicleParameters``HybridAStarConfiguration`
**Produces:** 六个可重复 job 与一个空图演示手动 job,供 UI 入口和后续脚本行为断言共同使用。
- [ ] **Step 1: 先建立最小的公共类型和转换辅助函数**
创建工厂文件并定义以下枚举、转换函数和公共入口。所有输入先做有限值检查;非有限输入抛出 `ArgumentOutOfRangeException`,避免伪造核心请求。
```csharp
public enum CoarsePathTestScenario
{
ExplicitEmpty,
RectangleDetour,
ManualAndTwoLeg,
CacheHit,
ReverseGearSwitch,
NoFeasiblePath,
}
public static class CoarsePathScenarioFactory
{
private const double MillimetersPerMeter = 1000d;
private const double DegreesToRadians = Math.PI / 180d;
private const float ResolutionMillimeters = 50f;
private const double ManualMapPaddingMillimeters = 2000d;
public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario)
{
switch (scenario)
{
case CoarsePathTestScenario.ExplicitEmpty: return CreateExplicitEmpty();
case CoarsePathTestScenario.RectangleDetour: return CreateRectangleDetour();
case CoarsePathTestScenario.ManualAndTwoLeg: return CreateManualAndTwoLeg();
case CoarsePathTestScenario.CacheHit: return CreateRectangleDetour();
case CoarsePathTestScenario.ReverseGearSwitch: return CreateReverseGearSwitch();
case CoarsePathTestScenario.NoFeasiblePath: return CreateNoFeasiblePath();
default: throw new ArgumentOutOfRangeException(nameof(scenario));
}
}
public static CoarsePathPlanningJob CreateManualGoalDemo(
double startXMillimeters, double startYMillimeters, double startHeadingDegrees,
double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees)
{
EnsureFinite(startXMillimeters, nameof(startXMillimeters));
EnsureFinite(startYMillimeters, nameof(startYMillimeters));
EnsureFinite(startHeadingDegrees, nameof(startHeadingDegrees));
EnsureFinite(goalXMillimeters, nameof(goalXMillimeters));
EnsureFinite(goalYMillimeters, nameof(goalYMillimeters));
EnsureFinite(goalHeadingDegrees, nameof(goalHeadingDegrees));
return CreateJob(CreateManualDemoMap(startXMillimeters, startYMillimeters, goalXMillimeters, goalYMillimeters),
ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees),
ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), null, GoalDirectionConstraint.Any);
}
private static Pose2D ToPose(double xMillimeters, double yMillimeters, double headingDegrees)
=> new Pose2D(xMillimeters / MillimetersPerMeter, yMillimeters / MillimetersPerMeter,
headingDegrees * DegreesToRadians);
}
```
- [ ] **Step 2: 实现统一请求模板和六个固定场景**
使用统一的车辆和配置,避免场景间无意改变安全或搜索语义。模板必须是新对象:
```csharp
private static CoarsePathPlanningJob CreateJob(PlanningMapRequest mapRequest, Pose2D start, Pose2D goal,
TravelDirection? startDirection, GoalDirectionConstraint goalDirection)
{
return new CoarsePathPlanningJob
{
MapRequest = mapRequest,
Start = start,
Goal = goal,
Vehicle = new VehicleParameters
{
LengthMeters = 0.80d,
WidthMeters = 0.60d,
SafetyMarginMeters = 0.05d,
MaximumCurvaturePerMeter = 1d / 1.20d,
},
Configuration = new HybridAStarConfiguration(),
StartDirection = startDirection,
GoalDirection = goalDirection,
};
}
```
固定地图均使用 `new MapBoundsMm(0f, 6000f, 0f, 4000f)`、50 mm 分辨率。以下代码固定各场景的障碍来源和世界位姿,所有 `ManualObstacleSource` 版本为 `1L`、所有必需来源为 `true`
```csharp
private static CoarsePathPlanningJob CreateExplicitEmpty()
=> CreateJob(CreateMap(true, Array.Empty<IMapObstacleSource>()),
new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward);
private static CoarsePathPlanningJob CreateRectangleDetour()
=> CreateJob(CreateMap(false, new IMapObstacleSource[]
{
new ManualObstacleSource("manual", 1L, true, new IMapObstacle[]
{ new AxisAlignedRectangleObstacle(2700f, 3300f, 1200f, 2800f) }),
}), new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward);
private static CoarsePathPlanningJob CreateManualAndTwoLeg()
=> CreateJob(CreateMap(false, new IMapObstacleSource[]
{
new ManualObstacleSource("manual", 1L, true, new IMapObstacle[]
{
new CircleObstacle(2400f, 1300f, 220f),
new AxisAlignedRectangleObstacle(3000f, 3600f, 2000f, 2600f),
}),
new TwoLegObstacleSource("two-leg", 1L, true,
new TwoLegProjectionInput(true, 3900f, 2500f, 0d,
-180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.")),
}), new Pose2D(1d, 1d, 0d), new Pose2D(5d, 3d, 0d), null, GoalDirectionConstraint.Forward);
private static CoarsePathPlanningJob CreateNoFeasiblePath()
=> CreateJob(CreateMap(false, new IMapObstacleSource[]
{
new ManualObstacleSource("manual", 1L, true, new IMapObstacle[]
{ new AxisAlignedRectangleObstacle(2900f, 3100f, 0f, 4000f) }),
}), new Pose2D(1d, 2d, 0d), new Pose2D(5d, 2d, 0d), null, GoalDirectionConstraint.Forward);
```
`CreateMap` 返回新的 `PlanningMapRequest`,固定写入地图边界、分辨率、给定来源和 `AllowExplicitEmptyMap`。倒车换向场景使用空图、起点 `(1,2,0)`、终点 `(4,2,0)``StartDirection=Forward``GoalDirection=Reverse`,使路径必须以至少一次换向结束。
倒车换向场景使用空图、起点 `(1,2,0)`、终点 `(4,2,0)``StartDirection=Forward``GoalDirection=Reverse`,使路径必须以至少一次换向结束。若 P0 的离散搜索在此几何下无法稳定得到成功,只允许调整此场景的目标距离或障碍布局,且测试必须继续要求 `IsGearSwitchPoint=true`
`CreateManualDemoMap` 用起终点 X/Y 的最小/最大值各扩展 `ManualMapPaddingMillimeters`,按 50 mm 向外取整,并明确设置 `AllowExplicitEmptyMap=true` 和空的 `ObstacleSources`
- [ ] **Step 3: 扩展行为断言以覆盖所有场景的实际状态**
在 Task 1 的反射代码之后增加服务执行测试。它不引用任何 Painter 或 MovementTest
```powershell
$scenarioService = [Activator]::CreateInstance($serviceType)
foreach ($scenarioName in @('ExplicitEmpty', 'RectangleDetour', 'ManualAndTwoLeg', 'ReverseGearSwitch')) {
$job = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, $scenarioName)))
$result = $servicePlan.Invoke($scenarioService, @($job, [Threading.CancellationToken]::None))
Assert-Equal 'Success' $result.PlanningResult.Status.ToString() "Scenario $scenarioName must succeed."
}
$reverseJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'ReverseGearSwitch')))
$reverseResult = $servicePlan.Invoke($scenarioService, @($reverseJob, [Threading.CancellationToken]::None))
Assert-True (($reverseResult.PlanningResult.Path | Where-Object { $_.IsGearSwitchPoint }).Count -ge 1) 'Reverse scenario must expose a gear-switch point.'
$noPathJob = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'NoFeasiblePath')))
$noPathResult = $servicePlan.Invoke($scenarioService, @($noPathJob, [Threading.CancellationToken]::None))
Assert-Equal 'NoFeasiblePath' $noPathResult.PlanningResult.Status.ToString() 'Barrier scenario must be infeasible.'
Assert-Equal 0 $noPathResult.PlanningResult.Path.Count 'Infeasible scenario must not publish a path.'
$cacheJobA = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit')))
$cacheJobB = $factoryCreate.Invoke($null, @([Enum]::Parse($scenarioEnumType, 'CacheHit')))
$cacheFirst = $servicePlan.Invoke($scenarioService, @($cacheJobA, [Threading.CancellationToken]::None))
$cacheSecond = $servicePlan.Invoke($scenarioService, @($cacheJobB, [Threading.CancellationToken]::None))
Assert-Equal 'Input' $cacheSecond.MapResult.CacheHit.ToString() 'Cache-hit scenario must reuse the complete map input.'
Assert-Equal $cacheFirst.PlanningResult.Status $cacheSecond.PlanningResult.Status 'Map cache reuse must not change planning status.'
```
- [ ] **Step 4: 运行测试并固定数值场景**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: 工厂、单位转换、六个固定场景、缓存与无解断言通过;此时尚未加入 MovementTest 源码检查,因此脚本整体通过。
### Task 3: MovementTest 后台会话与完整 Painter 绘制
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
- Create: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs`
**Consumes:** Task 2 的 `CoarsePathScenarioFactory.Create``CreateManualGoalDemo``CoarsePathPlanningService``CoarsePathPlanningJobResult``PlanningGridMap``Painter`
**Produces:** 六个固定场景入口与一个“AMR 位姿 + 手动终点(空图演示)”入口;所有入口通过同一个后台执行器运行和绘制。
- [ ] **Step 1: 写入失败的 UI 源码结构断言**
在 PowerShell 脚本中加入下列纯文本检查,避免在自动化测试中实例化外部 UI:
```powershell
$movementTestPath = Join-Path $plannerRoot 'CoarsePath\Test\MovementTest.CoarsePathTest.cs'
if (-not (Test-Path -LiteralPath $movementTestPath -PathType Leaf)) {
throw 'P1 coarse-path MovementTest source file must exist.'
}
$movementTestContent = Get-Content -LiteralPath $movementTestPath -Raw
foreach ($required in @(
'[MovementTest(name = "粗路径-显式空图")]',
'[MovementTest(name = "粗路径-矩形绕行")]',
'[MovementTest(name = "粗路径-多来源障碍")]',
'[MovementTest(name = "粗路径-缓存命中")]',
'[MovementTest(name = "粗路径-倒车换向")]',
'[MovementTest(name = "粗路径-无解")]',
'[MovementTest(name = "粗路径-AMR起点手动终点(空图演示)")]',
'Task.Run', 'CancellationTokenSource', 'CoarsePathPlanningService',
'PlanningGridMap', 'IsOccupied', 'ResolutionMm', 'SnapshotId', '图例', 'IsGearSwitchPoint')) {
Assert-True $movementTestContent.Contains($required) "MovementTest must contain: $required"
}
foreach ($forbidden in @('PlanningMapFactory', 'HybridAStarPlanner', 'MapObstacleRasterizer',
'FootprintCollisionChecker', 'MotionPrimitiveGenerator', 'HybridAStarSearch',
'BasicPilotBase.Chassis', 'SendMotion', 'DriveTask', '.Wait()', '.Result')) {
Assert-False $movementTestContent.Contains($forbidden) "MovementTest must not depend on: $forbidden"
}
```
- [ ] **Step 2: 运行脚本确认 UI 结构检查失败**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: 在工厂断言通过后,因 `P1 coarse-path MovementTest source file must exist.` 失败。
- [ ] **Step 3: 实现共享会话执行器与七个薄入口**
在新文件中使用 `namespace MultiWheelC;`,引用 `System.Threading``System.Threading.Tasks``System.Drawing``System.Numerics``ClumsyCore``MDCSToolBox.Clumsy.Movements` 和 Map/CoarsePath 命名空间。定义一个内部静态执行器,核心形状如下:
```csharp
internal static class CoarsePathMovementTestRunner
{
private static readonly object SyncRoot = new object();
private static readonly CoarsePathPlanningService Service = new CoarsePathPlanningService();
private static readonly Painter Painter = UI.GetPainter("CoarsePathPlanningV1", true);
private static long _nextRunId;
private static long _activeRunId;
private static CancellationTokenSource _activeCancellation;
private static Task _activeTask;
internal static void Start(string displayName, CoarsePathPlanningJob job)
{
CancellationTokenSource previous;
long runId;
var cancellation = new CancellationTokenSource();
lock (SyncRoot)
{
previous = _activeCancellation;
runId = ++_nextRunId;
_activeRunId = runId;
_activeCancellation = cancellation;
Painter.Clear();
_activeTask = Task.Run(() => Service.Plan(job, cancellation.Token));
_activeTask.ContinueWith(task => Complete(runId, displayName, job, cancellation, task),
CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
}
previous?.Cancel();
}
internal static void Stop()
{
CancellationTokenSource cancellation;
lock (SyncRoot)
{
cancellation = _activeCancellation;
}
cancellation?.Cancel();
lock (SyncRoot)
{
if (!ReferenceEquals(_activeCancellation, cancellation)) return;
_activeCancellation = null;
_activeTask = null;
_activeRunId = ++_nextRunId;
}
Painter.Clear();
}
}
```
`Complete` 必须捕获 `task.Exception`,但正常情况下只接受 `CoarsePathPlanningJobResult`。在锁内确认 `runId == _activeRunId`、任务未取消并且 `task.Status == TaskStatus.RanToCompletion` 后才绘制;无论绘制与否都在 finally 中释放该任务专用 `CancellationTokenSource`。不可在锁内等待任务。
添加一个抽象 `CoarsePathScenarioMovementTest`,其 `Test` 调用 `CoarsePathMovementTestRunner.Start(DisplayName, CoarsePathScenarioFactory.Create(Scenario))`,其 `TestStop` 调用 `Stop()`。实现六个带固定属性名称的密封子类。第七个类在 `Test` 中只读取一次六个 UI 输入:AMR 起点 X/Y/航向和目标 X/Y/航向(分别为 mm/mm/deg),调用 `CreateManualGoalDemo` 后启动;输入解析失败时仅记录错误并不启动任务。
- [ ] **Step 4: 实现确定的地图和结果绘制辅助方法**
在同一执行器内只消费 `job``CoarsePathPlanningJobResult`,按固定顺序调用以下辅助方法:
```csharp
private static void DrawMap(PlanningGridMap map);
private static void DrawPose(Color color, string label, Pose2D pose);
private static void DrawGoalTolerance(Pose2D goal, HybridAStarConfiguration configuration);
private static void DrawSuccessfulPath(PlanningResult result, VehicleParameters vehicle);
private static void DrawLegendAndStatus(string displayName, CoarsePathPlanningJobResult result, int gridStride);
```
`DrawMap``[Bounds.XMin, Bounds.XMax) × [Bounds.YMin, Bounds.YMax)` 的粗边界和 X/Y 参考。`gridStride = Max(1, Ceiling(Max(Rows, Cols) / 100d))`;每 `gridStride` 个真实栅格画一条线,状态文字写入 `分辨率=...mm,显示每...格`。遍历 `row/col`,仅对 `map.IsOccupied(row,col)` 为 true 的单元以四条边线画深色格框,确保显示的是最终快照而非原始几何。
`DrawPose` 将 m 转 mm,以圆、朝向短线和标签分别绘制绿色起点、橙色终点。`DrawGoalTolerance` 将位置容差 m 转 mm,绘制橙色容差圆。`DrawSuccessfulPath` 仅在 `result.Status == PlanningStatus.Success` 时运行:相邻路径点按当前点 `Direction` 使用青色(前进)或蓝色(倒车)连线;每隔 10 点画短箭头;`IsGearSwitchPoint` 画紫色圆与“换向”;首、末、换向和每 20 点调用旋转矩形绘制,半长/半宽严格按车辆长宽加安全余量。`DrawLegendAndStatus` 在边界左上方显示边界、占据格、起点、终点、前进、倒车、换向和扩大车体颜色说明,另显示 `SnapshotId``MapResult.Status``CacheHit``PlanningResult.Status``Elapsed` 和终止原因。
- [ ] **Step 5: 运行构建与集成脚本**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: 构建成功;工厂行为、七入口结构、后台取消约束和 Painter 数据来源检查全部通过。
### Task 4: README 与文档断言
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
**Consumes:** Task 2 的 `CreateManualGoalDemo` 单位契约和 Task 3 的七个入口名称、图例颜色和停止行为。
**Produces:** 可独立使用的 P1 UI 说明,以及对其关键安全声明的自动化保护。
- [ ] **Step 1: 为 README 写失败断言**
在现有 README 检查后加入:
```powershell
foreach ($requiredReadmeText in @(
'## P1Clumsy 手动测试与可视化',
'粗路径-AMR起点手动终点(空图演示)',
'AMR 位姿输入:X/Y 使用世界 mmth 使用 deg',
'Pose2DX/Y 使用 m,航向使用 rad',
'显式空图只能用于演示',
'不会发送底盘运动命令',
'TestStop',
'栅格边界',
'占据格',
'换向')) {
Assert-True $coarsePathReadmeContent.Contains($requiredReadmeText) "CoarsePath README must document: $requiredReadmeText"
}
```
- [ ] **Step 2: 运行脚本确认 README 检查失败**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: 所有代码检查通过;脚本因 `CoarsePath README must document: ## P1Clumsy 手动测试与可视化` 失败。
- [ ] **Step 3: 在 README 增加 P1 专节**
在“第一版限制”之前增加 `## P1Clumsy 手动测试与可视化`,逐项写明:
1. 七个 MovementTest 名称及对应场景;缓存测试连续运行两次,第二次展示 `Input` 命中。
2. AMR/手动目标输入契约:世界 `X/Y(mm)``th(deg)`,转换成 `Pose2D` 的 m/rad;起点是车辆几何中心。
3. 空图手动目标入口只能演示坐标、路径和取消流程;现场必须提供真实障碍物快照。
4. 可视化图例:边界、抽稀格线、占据格、起点、终点及容差、前进、倒车、换向和扩大车体检查框;失败不显示部分路径。
5. `Test` 在后台规划,`TestStop` 取消令牌并清空图层;测试只显示结果,绝不发送底盘运动命令或执行路径跟踪。
- [ ] **Step 4: 运行 README 与集成检查**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: 构建成功;README 和全部 P1 集成检查通过。
### Task 5: 全量回归与手动核验说明
**Files:**
- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs`
- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs`
- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`
- Verify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
**Consumes:** Tasks 1–4 的代码、文档和脚本。
**Produces:** 通过 Debug 回归的 P1 UI 集成首个交付;不进入 Release 性能基准。
- [ ] **Step 1: 执行 Debug 构建和所有现存 P0/P1 功能脚本**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
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 errors;每个存在的脚本返回 0 并输出其 `passed` 消息。
- [ ] **Step 2: 手动 Clumsy 验收**
在 Clumsy 的 MovementTest 列表依次运行“粗路径-矩形绕行”和“粗路径-AMR起点手动终点(空图演示)”。检查:测试启动后界面仍可操作;图层拥有边界、格线、占据格、图例、起终点与状态;成功案例有方向区分路径和扩大车体框;点击停止后图层清空且没有任何底盘运动命令。
- [ ] **Step 3: 记录交付边界**
在完成报告中明确:P1 UI 集成已完成;下一 P1 子项目是 Release 性能、资源和确定性基准;TrapMap 迁移/清理继续排除;没有执行 Git 操作。
## 自检
- 覆盖性:Task 2 交付纯场景与单位转换;Task 3 交付后台七入口和完整视觉要素;Task 4 交付 README;Task 5 交付自动化与手动验收。规格中的空图限制、实际占据快照、停止语义、无底盘命令和不显示部分路径均有对应任务。
- 占位符:已检查任务不含未决占位、延后实现或泛化错误处理类措辞;每个实现任务均给出文件、接口、测试、命令和具体代码形状。
- 类型一致性:所有任务统一使用 `CoarsePathTestScenario``CoarsePathScenarioFactory.Create``CreateManualGoalDemo``CoarsePathPlanningJob``CoarsePathPlanningJobResult``PlanningGridMap`;AMR 输入始终是 mm+deg,核心位姿始终是 m+rad。
@@ -0,0 +1,369 @@
# 规划操作预算与诊断收尾实施计划
> **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<ObstacleProjectionResult> 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<int>`。
**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 14.
- [ ] **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*、统计和 PlannerTask 4 覆盖门面映射与文档;Task 5 覆盖完整回归。
- 类型一致性:所有耗时组件仅接收 `PlanningOperationBudget` 并输出 `PlanningOperationStopReason`Map 使用 `PlanningMapBuildStatus`,粗规划使用既有 `PlanningStatus`。
- 范围:不触及 UI、Painter、场景工厂、Release 基准、运动模型或旧 TrapMap 脚本。
@@ -0,0 +1,436 @@
# 固定粗路径案例使用实时 AMR 位姿 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让全部 `粗路径规划-*` 固定 MovementTest 从一次冻结的当前 AMR 世界坐标和航向开始规划,并把案例地图、目标和障碍平移到 AMR 附近。
**Architecture:** `CoarsePathScenarioFactory` 保留确定性的基准案例入口,并新增接受 AMR mm/deg 位姿的入口。一个仅属于固定场景工厂的平移对象将基准案例的起点映射为 AMR 起点,平移地图和障碍,并以起终点基准航向差计算新的终点航向。MovementTest 运行器在前台读取一次 `DetourInterface.getCartLocation()`,校验并冻结快照后才提交已有后台规划流程。
**Tech Stack:** C# / .NET Standard 2.0、Clumsy `MovementTest` 与 Painter、现有 `CoarsePathPlanningService`、PowerShell 反射验证脚本。
## Global Constraints
- 保留 `CoarsePathScenarioFactory.Create(CoarsePathTestScenario)` 的现有确定性行为,自动化离线测试继续使用它。
- 新实时入口的 X/Y 单位为世界 mm、航向单位为 deg;核心 `Pose2D` 仍为 m/rad。
- 固定场景仅做位置平移;TwoLeg 的 `DetectionHeadingRadians` 不得因 AMR 航向改变。
- 起点航向必须等于冻结 AMR 航向;终点航向必须保持基准案例的起终点航向差,并规范化到 `[-pi, pi]`
- 不改变 Hybrid A*、车辆参数、碰撞模型、地图缓存实现、手动 `粗路径规划` 的终点/障碍/超时输入流程,或任何底盘控制行为。
- 实时位姿为空、读取抛异常或 X/Y/航向不是有限数时,不得创建后台任务;必须向用户报告以 `AMR 位姿不可用` 开头的原因。
- 不暂存工作区中的无关改动;每次提交均明确列出文件路径。
## File Structure
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs`
- 固定场景的基准坐标定义、实时 AMR 工厂重载、平移与航向转换的唯一实现位置。
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs`
- 固定场景的 AMR 快照读取、输入失败提示,以及冻结位姿的 Painter 状态显示。
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
- 对新工厂重载、平移几何、航向规则、缓存和基准入口不回归的程序集级验证。
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1`
- 对运行器只读一次实时位姿、传入实时工厂入口及 AMR 失败/状态文案的源级验证。
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`
- 说明六个固定案例的实时锚定规则、缓存命中条件和定位异常行为。
---
### Task 1: 为固定案例建立可验证的实时 AMR 平移工厂
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:303-399`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs:115-284`
**Interfaces:**
- Consumes: `CoarsePathTestScenario``PlanningMapRequest``MapBoundsMm``ManualObstacleSource``TwoLegObstacleSource``TwoLegProjectionInput`
- Produces: `public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)`
- Produces: 每个固定案例以 `FixedScenarioTransform` 统一转换的起点、终点、地图边界和来源快照;旧 `Create(scenario)` 仍创建原始基准几何。
- [ ] **Step 1: 在程序集级验证脚本中写出失败测试**
`$factoryCreate` 定义之后加入实时重载查找;在基准案例循环之后加入以下断言。测试锚点 `(12000, -3000, 90)` 对单矩形场景的偏移应为 `(+11000, -5000)` mm;多来源锚点 `(7000, 8000, 45)` 对该基准场景的偏移应为 `(+6000, +7000)` mm。
```powershell
$factoryCreateAtAmr = Find-Method $scenarioFactoryType 'Create' @(
$scenarioEnumType, [double], [double], [double])
Assert-True ($factoryCreateAtAmr -ne $null) 'Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).'
foreach ($scenarioName in $scenarioNames) {
$scenario = [Enum]::Parse($scenarioEnumType, $scenarioName)
$liveJob = $factoryCreateAtAmr.Invoke($null, @($scenario, 12345.0, -6789.0, 135.0))
Assert-Near 12.345 $liveJob.Start.X "Live $scenarioName start X must equal the AMR X."
Assert-Near -6.789 $liveJob.Start.Y "Live $scenarioName start Y must equal the AMR Y."
Assert-Near (3.0 * [Math]::PI / 4.0) $liveJob.Start.Heading "Live $scenarioName heading must equal the AMR heading."
}
$rectangleScenario = [Enum]::Parse($scenarioEnumType, 'RectangleDetour')
$liveRectangle = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, 12000.0, -3000.0, 90.0))
Assert-Near 12.0 $liveRectangle.Start.X 'Live rectangle start X must equal the AMR X.'
Assert-Near -3.0 $liveRectangle.Start.Y 'Live rectangle start Y must equal the AMR Y.'
Assert-Near ([Math]::PI / 2.0) $liveRectangle.Start.Heading 'Live rectangle start heading must equal the AMR heading.'
Assert-Near 16.0 $liveRectangle.Goal.X 'Live rectangle goal X must preserve the four-metre relative offset.'
Assert-Near -3.0 $liveRectangle.Goal.Y 'Live rectangle goal Y must preserve the relative offset.'
Assert-Near ([Math]::PI / 2.0) $liveRectangle.Goal.Heading 'Live rectangle goal heading must preserve the zero baseline heading delta.'
Assert-Near 11000.0 $liveRectangle.MapRequest.Bounds.XMin 'Live rectangle map X minimum must translate with the AMR.'
Assert-Near -1000.0 $liveRectangle.MapRequest.Bounds.YMax 'Live rectangle map Y maximum must translate with the AMR.'
$rectangleProjection = $liveRectangle.MapRequest.ObstacleSources[0].ProjectToWorld()
$rectangleObstacle = $rectangleProjection.Obstacles[0]
Assert-Near 13700.0 $rectangleObstacle.XMin 'Live rectangle obstacle X minimum must translate with the AMR.'
Assert-Near -2200.0 $rectangleObstacle.YMax 'Live rectangle obstacle Y maximum must translate with the AMR.'
$multiScenario = [Enum]::Parse($scenarioEnumType, 'ManualAndTwoLeg')
$baselineMulti = $factoryCreate.Invoke($null, @($multiScenario))
$liveMulti = $factoryCreateAtAmr.Invoke($null, @($multiScenario, 7000.0, 8000.0, 45.0))
Assert-Near 7.0 $liveMulti.Start.X 'Live multi-source start X must equal AMR X.'
Assert-Near 8.0 $liveMulti.Start.Y 'Live multi-source start Y must equal AMR Y.'
Assert-Near ([Math]::PI / 4.0) $liveMulti.Goal.Heading 'Live multi-source goal heading must follow AMR heading.'
$baselineTwoLeg = $baselineMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles
$liveTwoLeg = $liveMulti.MapRequest.ObstacleSources[1].ProjectToWorld().Obstacles
Assert-Near ($baselineTwoLeg[0].CenterX + 6000.0) $liveTwoLeg[0].CenterX 'TwoLeg X must translate without rotation.'
Assert-Near ($baselineTwoLeg[0].CenterY + 7000.0) $liveTwoLeg[0].CenterY 'TwoLeg Y must translate without rotation.'
$noPathScenario = [Enum]::Parse($scenarioEnumType, 'NoFeasiblePath')
$liveNoPath = $factoryCreateAtAmr.Invoke($null, @($noPathScenario, 9000.0, -1000.0, -180.0))
Assert-Near 9000.0 $liveNoPath.Start.X 'Live infeasible scenario must use AMR X.'
Assert-Near -1.0 $liveNoPath.Start.Y 'Live infeasible scenario must use AMR Y.'
Assert-Near 8000.0 $liveNoPath.MapRequest.Bounds.XMin 'Live infeasible map must translate with its baseline start.'
$invalidLivePoseRejected = $false
try { $null = $factoryCreateAtAmr.Invoke($null, @($rectangleScenario, [double]::NaN, 0.0, 0.0)) }
catch [Reflection.TargetInvocationException] {
$invalidLivePoseRejected = $_.Exception.InnerException -is [ArgumentOutOfRangeException]
}
Assert-True $invalidLivePoseRejected 'Live factory must reject non-finite AMR coordinates.'
```
- [ ] **Step 2: 运行该测试,确认它因缺少实时工厂重载而失败**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: build succeeds; the script fails at `Scenario factory must expose Create(scenario, amrX, amrY, amrHeading).`
- [ ] **Step 3: 以单一平移对象实现实时工厂重载**
在 `CoarsePathScenarioFactory` 中保留一参 `Create`,并用一个私有锚点和变换对象驱动同一个 switch。使用以下接口形状;`CreateCore` 的每个 case 都先以该案例的基准起点创建 `FixedScenarioTransform`,再传给对应的场景构造函数。
```csharp
public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario)
{
return CreateCore(scenario, null);
}
public static CoarsePathPlanningJob Create(CoarsePathTestScenario scenario,
double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)
{
return CreateCore(scenario,
new FixedScenarioAnchor(amrXMillimeters, amrYMillimeters, amrHeadingDegrees));
}
private static CoarsePathPlanningJob CreateCore(CoarsePathTestScenario scenario,
FixedScenarioAnchor anchor)
{
switch (scenario)
{
case CoarsePathTestScenario.ExplicitEmpty:
return CreateExplicitEmpty(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
case CoarsePathTestScenario.RectangleDetour:
case CoarsePathTestScenario.CacheHit:
return CreateRectangleDetour(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
case CoarsePathTestScenario.ManualAndTwoLeg:
return CreateManualAndTwoLeg(FixedScenarioTransform.From(1000d, 1000d, 0d, anchor));
case CoarsePathTestScenario.ReverseGearSwitch:
return CreateReverseGearSwitch(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
case CoarsePathTestScenario.NoFeasiblePath:
return CreateNoFeasiblePath(FixedScenarioTransform.From(1000d, 2000d, 0d, anchor));
default:
throw new ArgumentOutOfRangeException(nameof(scenario));
}
}
```
Implement the two private classes in the same file. They must validate all public live inputs with existing `EnsureFinite`, keep the identity transform when `anchor` is null, convert fixed mm to metres only when creating `Pose2D`, and normalize headings without changing a position.
```csharp
private sealed class FixedScenarioAnchor
{
public FixedScenarioAnchor(double xMillimeters, double yMillimeters, double headingDegrees)
{
EnsureFinite(xMillimeters, nameof(xMillimeters));
EnsureFinite(yMillimeters, nameof(yMillimeters));
EnsureFinite(headingDegrees, nameof(headingDegrees));
XMillimeters = xMillimeters;
YMillimeters = yMillimeters;
HeadingRadians = NormalizeRadians((headingDegrees % 360d) * DegreesToRadians);
}
public double XMillimeters { get; }
public double YMillimeters { get; }
public double HeadingRadians { get; }
}
private sealed class FixedScenarioTransform
{
private FixedScenarioTransform(double deltaXMillimeters, double deltaYMillimeters, double headingDeltaRadians)
{
DeltaXMillimeters = deltaXMillimeters;
DeltaYMillimeters = deltaYMillimeters;
HeadingDeltaRadians = headingDeltaRadians;
}
public double DeltaXMillimeters { get; }
public double DeltaYMillimeters { get; }
public double HeadingDeltaRadians { get; }
public static FixedScenarioTransform From(double baselineStartXMillimeters,
double baselineStartYMillimeters, double baselineStartHeadingRadians, FixedScenarioAnchor anchor)
{
if (anchor == null) return new FixedScenarioTransform(0d, 0d, 0d);
return new FixedScenarioTransform(anchor.XMillimeters - baselineStartXMillimeters,
anchor.YMillimeters - baselineStartYMillimeters,
NormalizeRadians(anchor.HeadingRadians - baselineStartHeadingRadians));
}
public float X(float value) { return ToFiniteFloat(value + DeltaXMillimeters, nameof(value)); }
public float Y(float value) { return ToFiniteFloat(value + DeltaYMillimeters, nameof(value)); }
public Pose2D Pose(double xMeters, double yMeters, double headingRadians)
{
return new Pose2D((xMeters * MillimetersPerMeter + DeltaXMillimeters) / MillimetersPerMeter,
(yMeters * MillimetersPerMeter + DeltaYMillimeters) / MillimetersPerMeter,
NormalizeRadians(headingRadians + HeadingDeltaRadians));
}
}
private static double NormalizeRadians(double angle)
{
double normalized = angle % (2d * Math.PI);
if (normalized <= -Math.PI) return normalized + 2d * Math.PI;
return normalized > Math.PI ? normalized - 2d * Math.PI : normalized;
}
```
Update every fixed builder to accept `FixedScenarioTransform transform` and use `transform.Pose`, `transform.X` and `transform.Y` for all coordinates:
```csharp
new AxisAlignedRectangleObstacle(transform.X(2700f), transform.X(3300f),
transform.Y(1200f), transform.Y(2800f));
new CircleObstacle(transform.X(2400f), transform.Y(1300f), 220f);
new TwoLegProjectionInput(true, transform.X(3900f), transform.Y(2500f), 0d,
-180f, -180f, -180f, 180f, 140f, "P1 fixed TwoLeg snapshot.");
```
Change `CreateMap` to accept the transform and translate all four `MapBoundsMm` limits with the correct axis. It must not change resolution, source IDs, source versions, required flags, empty-map flags, vehicle parameters, direction constraints or existing timeout assignments.
- [ ] **Step 4: 运行工厂和回归验证,确认新旧入口都通过**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: build has zero errors; script ends with `Coarse path P1 scenario checks passed.` Existing assertions for the one-argument factory must continue to pass.
- [ ] **Step 5: 提交仅包含工厂与程序集级测试的可审查变更**
```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs ClumsyPilot/tests/verify_coarse_path_integration.ps1
git commit -m "feat: anchor coarse path scenarios to AMR pose"
```
### Task 2: 让固定 MovementTest 冻结 AMR 快照并显示诊断
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:15-52`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs:221-371,512-540`
**Interfaces:**
- Consumes: 新的 `CoarsePathScenarioFactory.Create(scenario, xMm, yMm, headingDeg)`、`DetourInterface.getCartLocation()` 和现有 `Run(string, CoarsePathPlanningJob)`。
- Produces: `RunScenario` 只读取一次位姿,创建已冻结的实时 job;手动入口继续调用现有两参 `Run`。
- Produces: 私有 `AmrPoseSnapshot`,仅保存有限 X/Y/deg 和格式化后的状态文本,不向规划后台读取定位。
- [ ] **Step 1: 在 UI 源级验证中加入失败断言**
在现有 `$source` 断言后追加以下代码。使用 ASCII 的方法名和 `AMR` 文本,避免 Windows PowerShell 5 对中文脚本字符串的编码歧义。
```powershell
$runnerStart = $source.IndexOf('internal static class CoarsePathPlanningTestRunner')
Assert-True ($runnerStart -ge 0) 'Shared runner source must exist.'
$runnerSource = $source.Substring($runnerStart)
Assert-Match $runnerSource 'RunScenario[\s\S]*getCartLocation\s*\(' 'Fixed scenarios must read the current AMR pose.'
Assert-Match $runnerSource 'CoarsePathScenarioFactory\.Create\s*\(\s*scenario\s*,' 'Fixed scenarios must use the AMR-aware factory overload.'
Assert-Match $runnerSource 'AMR' 'The runner must expose AMR pose diagnostics.'
Assert-Match $runnerSource 'ArgumentException\("AMR' 'Invalid AMR input must be reported without starting planning.'
Assert-Match $runnerSource 'double\.IsNaN|double\.IsInfinity' 'The runner must reject non-finite AMR coordinates.'
Assert-Match $runnerSource 'DrawStatus\s*\([\s\S]*AmrPoseSnapshot' 'Result status must receive the frozen AMR snapshot.'
```
- [ ] **Step 2: 运行 UI 脚本,确认新增实时位姿约束尚未满足**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: script fails at `Fixed scenarios must read the current AMR pose.` or `Fixed scenarios must use the AMR-aware factory overload.`
- [ ] **Step 3: 实现单次 AMR 读取、校验、冻结与绘制传递**
在 `CoarsePathPlanningTestRunner` 内加入以下私有快照类型和工厂调用路径。任何读取或校验异常都在前台转换为 `AMR 位姿不可用:...`,因此不会进入 `Task.Run`。
```csharp
private sealed class AmrPoseSnapshot
{
public AmrPoseSnapshot(double xMillimeters, double yMillimeters, double headingDegrees)
{
EnsureFiniteAmrValue(xMillimeters, "X");
EnsureFiniteAmrValue(yMillimeters, "Y");
EnsureFiniteAmrValue(headingDegrees, "航向");
XMillimeters = xMillimeters;
YMillimeters = yMillimeters;
HeadingDegrees = headingDegrees;
}
public double XMillimeters { get; }
public double YMillimeters { get; }
public double HeadingDegrees { get; }
public string DisplayText
{
get
{
return "AMR 起点:X=" + XMillimeters.ToString("F0", CultureInfo.InvariantCulture) +
" mmY=" + YMillimeters.ToString("F0", CultureInfo.InvariantCulture) +
" mm,航向=" + HeadingDegrees.ToString("F1", CultureInfo.InvariantCulture) + " deg";
}
}
}
private static void EnsureFiniteAmrValue(double value, string name)
{
if (double.IsNaN(value) || double.IsInfinity(value))
throw new ArgumentException(name + " 必须是有限数。");
}
internal static void RunScenario(CoarsePathTestScenario scenario, string scenarioName)
{
try
{
var pose = DetourInterface.getCartLocation();
var snapshot = new AmrPoseSnapshot(pose.x, pose.y, pose.th);
CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create(scenario,
snapshot.XMillimeters, snapshot.YMillimeters, snapshot.HeadingDegrees);
Run(scenarioName, job, snapshot);
}
catch (Exception exception)
{
ShowInputFailure(new ArgumentException("AMR 位姿不可用:" + exception.Message, exception));
}
}
```
Keep the existing `internal static void Run(string scenarioName, CoarsePathPlanningJob job)` as a thin compatibility overload that calls a new private `Run(string, CoarsePathPlanningJob, AmrPoseSnapshot)`. Thread the snapshot through `DrawPending`, the `ContinueWith` lambda, `Finish`, `DrawResult`, and `DrawStatus`. In `DrawStatus`, render `snapshot.DisplayText` after the scene line only when the snapshot is non-null; increase subsequent Y offsets consistently so vehicle and termination-reason text do not overlap. Manual `CoarsePathPlanningTest.Test()` must keep its present `Run(..., job)` call and user-entered timeout behavior.
- [ ] **Step 4: 编译并运行 UI 验证,确认后台边界未回归**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: build has zero errors; script ends with `Coarse path UI source checks passed.` Existing assertions still confirm one shared service, background `Task.Run`, cancellation and no blocking `Task.Result`/`Wait`.
- [ ] **Step 5: 提交仅包含运行器与 UI 验证的可审查变更**
```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs ClumsyPilot/tests/verify_coarse_path_ui.ps1
git commit -m "feat: use live AMR pose in coarse path tests"
```
### Task 3: 记录实时固定场景语义并执行完整回归
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md:289-347`
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:54-100`
**Interfaces:**
- Consumes: 已实现的实时工厂入口和 MovementTest 状态文本。
- Produces: 可供现场使用者理解的固定案例锚定、缓存和定位异常说明;文档验证继续只检查稳定 ASCII 标识符。
- [ ] **Step 1: 在 README 源级验证中加入失败断言**
在 `$readmeStructure` 数组中加入以下 ASCII 项,并在数组后加入 `Contains` 断言:
```powershell
foreach ($requiredLiveAmrText in @(
'Create(CoarsePathTestScenario scenario, double amrXMillimeters',
'AMR',
'Input',
'TwoLeg')) {
Assert-True ($readme.Contains($requiredLiveAmrText)) "P1 README must document live fixed scenarios: $requiredLiveAmrText."
}
```
- [ ] **Step 2: 运行 UI 文档验证,确认文档尚未描述实时固定案例**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: script fails because README does not yet contain the new `Create(CoarsePathTestScenario scenario, double amrXMillimeters` signature.
- [ ] **Step 3: 更新 P1 手动测试与可视化章节**
在固定案例表之前新增“固定案例的实时 AMR 锚点”小节,逐条说明:六个 `粗路径规划-*` 入口读取一次 `getCartLocation`;起点等于这份冻结的世界 mm/deg 位姿;地图边界、目标、圆形/轴对齐矩形和 TwoLeg 检测原点统一平移;终点航向保持和基准起点的航向差;TwoLeg 朝向不旋转;手动 `粗路径规划` 的输入流程不变。
在同一小节以单行 inline code 明确列出公开签名:`Create(CoarsePathTestScenario scenario, double amrXMillimeters, double amrYMillimeters, double amrHeadingDegrees)`。
加入公开签名示例:
```csharp
CoarsePathPlanningJob job = CoarsePathScenarioFactory.Create(
CoarsePathTestScenario.RectangleDetour,
amrXMillimeters, amrYMillimeters, amrHeadingDegrees);
```
将缓存命中表项改为“相同 AMR X/Y 下的重复完整地图输入”;明确 AMR 位置已移动时未命中是正常的 `None`/非 `Input` 缓存结果,且仅航向变化不改变地图输入、仍可命中。补充运行器在定位读取、空值访问或有限数校验失败时显示 `AMR 位姿不可用` 且不启动后台规划;状态区会显示该次冻结的 AMR 起点。保留关于 `getCartLocation` 可能阻塞和必须在定位准备完成后运行的既有警告。
- [ ] **Step 4: 运行完整粗路径与地图回归集**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: build reports zero warnings and zero errors. Every verification script exits 0; integration ends with `Coarse path P1 scenario checks passed.` and UI ends with `Coarse path UI source checks passed.`
- [ ] **Step 5: 提交文档与最后验证脚本变更**
```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md ClumsyPilot/tests/verify_coarse_path_ui.ps1
git commit -m "docs: explain live AMR coarse path scenarios"
```
@@ -0,0 +1,256 @@
# Coarse Path Search Elapsed Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在保留总耗时和总超时语义的前提下,记录并显示地图就绪后生成最终粗路径的独立耗时。
**Architecture:** `PlanningDiagnostics` 增加兼容的 `PathSearchElapsed` 只读字段。`HybridAStarPlanner` 在调用 `HybridAStarSearch.Search` 前启动本地秒表,并把搜索、回溯、装配和最终复核的耗时传入诊断对象;门面总预算和 `Elapsed` 不改变。MovementTest 图层和 Toast 同时显示总耗时与路径搜索耗时。
**Tech Stack:** C# / .NET Standard 2.0、`System.Diagnostics.Stopwatch`、PowerShell 回归脚本、Clumsy `MovementTest` Painter。
## Global Constraints
- `PlanningDiagnostics.Elapsed` 继续表示从 `CoarsePathPlanningService.Plan` 入口开始的总耗时。
- `PathSearchElapsed` 不包括地图来源读取、缓存、栅格化和距离场构建。
- `PathSearchElapsed` 包含二维启发式、Hybrid A*、回溯、装配、方向分段和最终复核。
- 搜索开始前失败时 `PathSearchElapsed` 必须为 `TimeSpan.Zero`;搜索阶段失败时保留已消耗时间。
- 新构造函数参数必须放在现有参数之后并提供默认值,保持现有位置参数调用的兼容性。
- 不修改 `PlanningOperationBudget`、取消机制、超时预算或地图缓存行为。
- 不执行 Git 操作。
## 文件结构
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs` — 公开独立路径搜索耗时。
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` — 在地图就绪后的路径产出阶段计时,并写入诊断对象。
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` — 在状态图层和 Toast 显示两种耗时。
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` — 说明两个耗时的计时边界。
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` — 验证真实路径搜索耗时和地图阶段失败的零值。
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` — 验证 UI/README 使用新字段。
---
### Task 1: 路径搜索耗时诊断契约
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:100-108,199-204`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs:10-60`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs:1-105,185-210`
**Interfaces:**
- Consumes: `PlanningResult.Diagnostics.Elapsed``HybridAStarPlanner.Plan(PlanningRequest, PlanningOperationBudget)`
- Produces: `PlanningDiagnostics.PathSearchElapsed : TimeSpan`
- Contract: 成功结果满足 `TimeSpan.Zero <= PathSearchElapsed <= Elapsed`;地图阶段取消的结果为 `TimeSpan.Zero`
- [ ] **Step 1: 在集成脚本写入失败断言**
在空地图成功规划的现有诊断断言之后加入:
```powershell
Assert-True ($result.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) `
'Planner diagnostics must retain a non-negative path-search elapsed time.'
Assert-True ($result.Diagnostics.PathSearchElapsed -le $result.Diagnostics.Elapsed) `
'Path-search elapsed time must not exceed total planning elapsed time.'
```
`$cancelledFacadeResult` 的现有断言之后加入:
```powershell
Assert-Equal ([TimeSpan]::Zero) $cancelledFacadeResult.PlanningResult.Diagnostics.PathSearchElapsed `
'Map-stage cancellation must not report path-search time.'
```
- [ ] **Step 2: 运行集成脚本并确认红灯**
Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1`
Expected: FAIL,提示 `PathSearchElapsed` 不存在或无法通过新增的路径搜索耗时断言;此前的地图和门面检查仍先通过。
- [ ] **Step 3: 以兼容形式扩展诊断对象**
`PlanningDiagnostics` 构造函数的最后一个参数之后增加:
```csharp
TimeSpan pathSearchElapsed = default(TimeSpan)
```
并在构造函数中加入:
```csharp
PathSearchElapsed = pathSearchElapsed;
```
`Elapsed` 属性之后加入:
```csharp
/// <summary>
/// 地图就绪后搜索、回溯、装配和最终复核得到最终粗路径的耗时;不含建图。搜索开始前失败时为零。
/// </summary>
public TimeSpan PathSearchElapsed { get; }
```
同时更新构造函数 XML 注释,明确 `elapsed` 是总耗时而 `pathSearchElapsed` 是不含建图的路径产出耗时。
- [ ] **Step 4: 在规划器的正确边界计时**
`HybridAStarPlanner.cs` 顶部加入:
```csharp
using System.Diagnostics;
```
`Plan(PlanningRequest request, PlanningOperationBudget budget)``try` 外部声明:
```csharp
Stopwatch pathSearchStopwatch = null;
```
在第二次 `budget.GetStopReason()` 通过、且紧接 `_search.Search(request, budget)` 前写入:
```csharp
pathSearchStopwatch = Stopwatch.StartNew();
HybridAStarSearchResult searchResult = _search.Search(request, budget);
```
`Failure` 签名扩展为:
```csharp
private static PlanningResult Failure(PlanningStatus status, PlanningOperationBudget budget, string reason,
HybridAStarSearchResult searchResult, Stopwatch pathSearchStopwatch = null)
```
并在内部取得:
```csharp
TimeSpan pathSearchElapsed = pathSearchStopwatch == null ? TimeSpan.Zero : pathSearchStopwatch.Elapsed;
return PlanningResult.Failure(status, CreateDiagnostics(searchResult, budget.Elapsed, 0d, 0d, reason,
pathSearchElapsed));
```
`_search.Search` 之后的每个失败返回和 `catch` 都传入 `pathSearchStopwatch`。成功结果调用改为:
```csharp
return PlanningResult.Success(path, segments, CreateDiagnostics(searchResult, budget.Elapsed, pathLengthMeters,
minimumClearanceMeters, string.Empty, pathSearchStopwatch.Elapsed));
```
最后将 `CreateDiagnostics` 扩展为接收最后一个 `TimeSpan pathSearchElapsed` 参数,并将其作为 `PlanningDiagnostics` 的最后一个实参传入。
- [ ] **Step 5: 运行集成脚本并确认绿灯**
Run: `dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore`
Expected: exit code 0;只允许项目既有的过时 API 警告。
Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1`
Expected: `Coarse path integration checks passed.``Coarse path facade checks passed.``Coarse path P1 scenario checks passed.`
### Task 2: 图层、Toast 与 README 展示
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:15-70`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs:494-513`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md:285-337`
**Interfaces:**
- Consumes: `result.PlanningResult.Diagnostics.Elapsed` 和 Task 1 提供的 `PathSearchElapsed`
- Produces: 状态图层与 Toast 中的“总耗时”和“路径搜索”文本,以及对应 README 说明。
- Contract: 展示值均使用毫秒、`InvariantCulture``F0` 格式;不改变后台任务、取消或 Painter 图层名称。
- [ ] **Step 1: 为 UI 和 README 写入失败检查**
在 UI 源码断言区域加入:
```powershell
Assert-True (([regex]::Matches($source, 'PathSearchElapsed')).Count -ge 2) `
'The status layer and Toast must both show path-search elapsed time.'
Assert-Match $source '路径搜索' 'The UI must label the independent path-search elapsed time.'
```
`$readmeStructure` 数组加入:
```powershell
'PathSearchElapsed',
'路径搜索耗时',
```
- [ ] **Step 2: 运行 UI 脚本并确认红灯**
Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1`
Expected: FAIL,提示状态图层与 Toast 尚未显示 `PathSearchElapsed`,或 README 尚未说明该字段。
- [ ] **Step 3: 同时更新状态图层和 Toast**
`DrawStatus` 中将单一耗时文本替换为:
```csharp
Painter.DrawText(Color.White, "栅格:" + resolution + ",规划:" + result.PlanningResult.Status + ",总耗时:" +
result.PlanningResult.Diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) +
" ms,路径搜索:" + result.PlanningResult.Diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) +
" ms", x, y + 240f);
```
`BuildToastMessage` 中将返回字符串替换为:
```csharp
return "粗路径[" + scenarioName + "]:地图=" + result.MapResult.Status + ",规划=" +
result.PlanningResult.Status + ",总耗时=" +
result.PlanningResult.Diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) +
"ms,路径搜索=" + result.PlanningResult.Diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) +
"ms。";
```
- [ ] **Step 4: 补充 README 的耗时定义**
在 “P1 手动测试与可视化” 的 “后台执行、停止与图层” 小节,在当前 `PlanningGridMap` 说明之后插入:
```markdown
状态图层和 Toast 同时显示总耗时与 `PathSearchElapsed`(路径搜索耗时)。总耗时从
`CoarsePathPlanningService.Plan` 入口开始,包含地图创建;路径搜索耗时从地图和起终点
预检通过、即将进入 Hybrid A* 时开始,包含二维启发式、Hybrid A*、回溯、装配与最终复核,
不包含建图。搜索开始前即失败时该值为 0 ms。
```
- [ ] **Step 5: 运行 UI 检查并确认绿灯**
Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1`
Expected: `Coarse path P1 UI source checks passed.`
### Task 3: 最终回归与验收
**Files:**
- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs`
- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs`
- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs`
- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`
**Interfaces:**
- Consumes: 任务 1 和任务 2 的已完成代码与脚本。
- Produces: 已验证的构建、集成回归和 UI/README 检查结果。
- [ ] **Step 1: 重新阅读计时边界**
确认 `PathSearchElapsed` 只在 `_search.Search` 前启动;所有搜索后成功和失败路径均使用同一秒表;任何搜索前返回保持零值;总预算仍由 `PlanningOperationBudget` 控制。
- [ ] **Step 2: 运行最终构建**
Run: `dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore`
Expected: exit code 0。
- [ ] **Step 3: 运行最终自动化验证**
Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1`
Expected: `Coarse path P1 UI source checks passed.`
Run: `powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1`
Expected: `Coarse path integration checks passed.``Coarse path facade checks passed.``Coarse path P1 scenario checks passed.`
- [ ] **Step 4: 进行手动界面验收**
在可用 Clumsy 界面运行“粗路径规划-显式空图”或“粗路径规划-单矩形绕行”,确认状态图层和 Toast 都包含 `总耗时``路径搜索` 两个毫秒值,且点击停止仍能取消当前任务。
@@ -0,0 +1,837 @@
# Coarse Path Manual Test Diagnostics Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**`[MovementTest(name = "粗路径规划")]` 支持每次输入正数秒总预算,并在失败时显示真实终止原因、搜索统计、总耗时和路径搜索耗时。
**Architecture:** `HybridAStarSearchResult` 在搜索边界保留原始终止原因,`HybridAStarPlanner` 负责把原因、资源限制和节点统计装配成公开诊断。`PlanningDiagnostics` 增加兼容的 `PathSearchElapsed`MovementTest 只覆盖本次任务的超时配置并在图层和 Toast 中显示诊断;搜索、安全和路径发布规则保持不变。
**Tech Stack:** C# / .NET Standard 2.0、`System.Diagnostics.Stopwatch`、PowerShell 反射回归脚本、Clumsy `MovementTest`/Painter。
## Global Constraints
- 普通规划失败继续通过 `PlanningResult` 返回,不把超时、无解、碰撞或资源上限改成异常。
- `PlanningDiagnostics.Elapsed` 继续表示从 `CoarsePathPlanningService.Plan` 开始、包含建图的总耗时。
- `PlanningDiagnostics.PathSearchElapsed` 从搜索前预检通过后开始,包含二维启发式、Hybrid A*、回溯、装配和最终复核,不包含建图。
- 搜索前失败的 `PathSearchElapsed``TimeSpan.Zero`;搜索后的所有出口满足 `TimeSpan.Zero <= PathSearchElapsed <= Elapsed`
- 手动超时只接受 `TimeSpan` 可表示范围内的有限正数秒;`0` 不表示不限时。
- 不修改碰撞步长、终点容差、Open List 排序、地图边界策略、倒车开关或成功路径发布条件。
- 不实现 Reeds-Shepp/Dubins 精确连接、横移、蟹行、原地旋转、平滑、速度规划或控制。
- 手动入口继续使用长 `0.80 m`、宽 `0.60 m`、安全余量 `0.05 m`、最小转弯半径 `1.20 m` 的固定演示车辆参数。
- 工作区已有未提交内容;每次提交只能暂存任务中明确列出的文件,不得暂存其他路径。
## File Structure
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs` — 搜索结果保留发生位置一致的原始终止原因。
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs` — 公开地图就绪后的独立路径搜索耗时。
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs` — 计时搜索/发布阶段并组合原因、资源上限和节点统计。
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` — 读取单次超时并在图层/Toast 显示诊断。
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` — 记录手动预算、演示车辆、两类耗时和第一版能力边界。
- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1` — 验证搜索原始原因和内部异常不再静默。
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1` — 验证规划器诊断、路径搜索耗时和慢可行场景。
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1` — 验证手动超时、图层/Toast 和 README 文本。
---
### Task 1: Preserve Search Termination Reasons
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_search.ps1:302-391`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs:15-318`
**Interfaces:**
- Consumes: `HybridAStarSearch.Search(PlanningRequest, CancellationToken)` 和内部 `Search(PlanningRequest, PlanningOperationBudget)`
- Produces: `HybridAStarSearchResult.TerminationReason : string`,成功时为空,所有失败状态非空。
- Produces: `CreateResult(..., int? successNodeIndex, string terminationReason = null)`,未显式提供原因时按状态生成稳定中文原因。
- [ ] **Step 1: Add failing search-result reason assertions**
在搜索结果属性断言后加入:
```powershell
Assert-True ($searchResultType.GetProperty('TerminationReason') -ne $null) `
'Search result must expose its original termination reason.'
```
在取消、节点上限和超时状态断言后分别加入:
```powershell
Assert-True (-not [string]::IsNullOrWhiteSpace($cancelledResult.TerminationReason)) `
'Cancelled search must retain a non-empty reason.'
Assert-True (-not [string]::IsNullOrWhiteSpace($limitedResult.TerminationReason)) `
'Node-limited search must retain a non-empty reason.'
Assert-True (-not [string]::IsNullOrWhiteSpace($timedOutResult.TerminationReason)) `
'Timed-out search must retain a non-empty reason.'
Assert-False ($cancelledResult.TerminationReason -eq $limitedResult.TerminationReason) `
'Cancelled and node-limited searches must retain different reasons.'
Assert-False ($limitedResult.TerminationReason -eq $timedOutResult.TerminationReason) `
'Node-limited and timed-out searches must retain different reasons.'
```
在脚本末尾通过内部预算重载制造一个可重复的未预期错误,并断言异常类型没有被吞掉:
```powershell
$internalSearchMethod = $searchType.GetMethods([Reflection.BindingFlags]'Instance,NonPublic') |
Where-Object {
$_.Name -eq 'Search' -and
$_.GetParameters().Length -eq 2 -and
$_.GetParameters()[1].ParameterType -eq $operationBudgetType
} |
Select-Object -First 1
Assert-True ($internalSearchMethod -ne $null) `
'Search must retain its internal shared-budget overload.'
$internalErrorResult = $internalSearchMethod.Invoke($searcher, @($searchRequest, $null))
Assert-Equal 'InternalError' $internalErrorResult.Status.ToString() `
'A missing internal budget must be mapped to InternalError.'
Assert-True ($internalErrorResult.TerminationReason.Contains('ArgumentNullException')) `
'Internal search errors must retain the exception type.'
```
- [ ] **Step 2: Run the search script and verify RED**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1
```
Expected: build succeeds; the script fails first with `Search result must expose its original termination reason.`
- [ ] **Step 3: Extend `HybridAStarSearchResult`**
Add the final constructor parameter, assignment, and public property:
```csharp
internal HybridAStarSearchResult(
PlanningStatus status,
IEnumerable<HybridAStarNode> nodes,
int expandedNodeCount,
int generatedNodeCount,
int reopenedNodeCount,
int staleOpenListEntryCount,
int peakOpenListCount,
int? successNodeIndex,
string terminationReason)
{
Status = status;
Nodes = new ReadOnlyCollection<HybridAStarNode>(
new List<HybridAStarNode>(nodes ?? Array.Empty<HybridAStarNode>()));
ExpandedNodeCount = expandedNodeCount;
GeneratedNodeCount = generatedNodeCount;
ReopenedNodeCount = reopenedNodeCount;
StaleOpenListEntryCount = staleOpenListEntryCount;
PeakOpenListCount = peakOpenListCount;
SuccessNodeIndex = status == PlanningStatus.Success ? successNodeIndex : null;
TerminationReason = status == PlanningStatus.Success
? string.Empty
: terminationReason ?? string.Empty;
}
/// <summary>搜索边界记录的原始终止原因;成功时为空字符串,失败时非空。</summary>
public string TerminationReason { get; }
```
- [ ] **Step 4: Centralize default reasons and retain exception details**
Replace `CreateResult` with:
```csharp
private static HybridAStarSearchResult CreateResult(
PlanningStatus status,
IEnumerable<HybridAStarNode> nodes,
int expandedNodeCount,
int generatedNodeCount,
int reopenedNodeCount,
int staleOpenListEntryCount,
int peakOpenListCount,
int? successNodeIndex,
string terminationReason = null)
{
string reason = status == PlanningStatus.Success
? string.Empty
: terminationReason ?? GetDefaultTerminationReason(status);
return new HybridAStarSearchResult(status, nodes, expandedNodeCount, generatedNodeCount, reopenedNodeCount,
staleOpenListEntryCount, peakOpenListCount, successNodeIndex, reason);
}
private static string GetDefaultTerminationReason(PlanningStatus status)
{
switch (status)
{
case PlanningStatus.Cancelled:
return "Hybrid A* 搜索已取消。";
case PlanningStatus.InvalidRequest:
return "Hybrid A* 搜索请求缺少必要对象或包含非法数值。";
case PlanningStatus.InvalidMap:
return "Hybrid A* 搜索地图结构无效。";
case PlanningStatus.MapNotReady:
return "Hybrid A* 搜索地图尚未准备好。";
case PlanningStatus.InvalidVehicleParameters:
return "Hybrid A* 搜索车辆参数无效。";
case PlanningStatus.InvalidCurvatureConfiguration:
return "Hybrid A* 搜索曲率、离散、代价或资源配置无效。";
case PlanningStatus.StartOutsideMap:
return "Hybrid A* 搜索起始扩大车体不完全位于地图内。";
case PlanningStatus.StartInCollision:
return "Hybrid A* 搜索起始扩大车体与障碍物相交或擦边。";
case PlanningStatus.GoalOutsideMap:
return "Hybrid A* 搜索目标扩大车体不完全位于地图内。";
case PlanningStatus.GoalInCollision:
return "Hybrid A* 搜索目标扩大车体与障碍物相交或擦边。";
case PlanningStatus.SearchTimeout:
return "Hybrid A* 搜索使用的总规划预算已耗尽。";
case PlanningStatus.SearchNodeLimitExceeded:
return "Hybrid A* 搜索达到扩展节点上限。";
case PlanningStatus.NoFeasiblePath:
return "Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。";
case PlanningStatus.BacktrackingFailed:
return "Hybrid A* 成功节点无法回溯为完整父链。";
case PlanningStatus.FinalValidationFailed:
return "Hybrid A* 路径未通过最终复核。";
case PlanningStatus.InternalError:
return "Hybrid A* 搜索发生未预期内部错误。";
default:
return "Hybrid A* 搜索以未识别状态终止:" + status + "。";
}
}
```
Change the two `NoFeasiblePath` exits so the caller can distinguish their location:
```csharp
if (openList.Count == 0)
return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount,
reopenedNodeCount, staleOpenListEntryCount, peakOpenListCount, null,
"二维启发式标记起点不可达目标,或起始方向无法进入 Open List。");
```
```csharp
return CreateResult(PlanningStatus.NoFeasiblePath, nodes, expandedNodeCount, generatedNodeCount,
reopenedNodeCount, staleOpenListEntryCount, peakOpenListCount, null,
"Hybrid A* 搜索的 Open List 已耗尽,未找到满足运动和碰撞约束的路径。");
```
Replace the catch block with:
```csharp
catch (Exception exception)
{
string reason = "Hybrid A* 搜索内部错误:" + exception.GetType().Name +
(string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message);
return CreateResult(PlanningStatus.InternalError, nodes, expandedNodeCount, generatedNodeCount,
reopenedNodeCount, staleOpenListEntryCount, peakOpenListCount, null, reason);
}
```
- [ ] **Step 5: Run the search script and verify GREEN**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1
```
Expected:
```text
Coarse path search primitive checks passed.
Coarse path Hybrid A star search checks passed.
```
- [ ] **Step 6: Commit the search reason contract**
```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs ClumsyPilot/tests/verify_coarse_path_search.ps1
git commit -m "fix: retain coarse path search failure reasons"
```
### Task 2: Add Planner-Level Diagnostics and Path Search Timing
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:75-135,189-210,357-360`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs:6-60`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs:1-226`
**Interfaces:**
- Consumes: Task 1 `HybridAStarSearchResult.TerminationReason`.
- Produces: `PlanningDiagnostics.PathSearchElapsed : TimeSpan`.
- Produces: `BuildSearchFailureReason(HybridAStarSearchResult, HybridAStarConfiguration) : string`.
- Contract: planner success and search-stage failure satisfy `0 <= PathSearchElapsed <= Elapsed`; map/preflight failure remains zero.
- [ ] **Step 1: Add failing planner-diagnostic assertions**
After the existing successful planner diagnostic assertions add:
```powershell
Assert-True ($result.Diagnostics.GetType().GetProperty('PathSearchElapsed') -ne $null) `
'Planner diagnostics must expose path-search elapsed time.'
Assert-True ($result.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) `
'Successful planning must retain non-negative path-search time.'
Assert-True ($result.Diagnostics.PathSearchElapsed -le $result.Diagnostics.Elapsed) `
'Path-search time must not exceed total elapsed time.'
```
After the invalid-map result assertions add:
```powershell
Assert-Equal ([TimeSpan]::Zero) $mapFailureResult.PlanningResult.Diagnostics.PathSearchElapsed `
'Map failure must report zero path-search time.'
```
After creating a valid planner request, add a search-stage node-limit case:
```powershell
$nodeLimitedRequest = [Activator]::CreateInstance($requestType)
$nodeLimitedRequest.Map = $request.Map
$nodeLimitedRequest.Start = $request.Start
$nodeLimitedRequest.Goal = $request.Goal
$nodeLimitedRequest.Vehicle = $request.Vehicle
$nodeLimitedRequest.Configuration = [Activator]::CreateInstance($configurationType)
$nodeLimitedRequest.Configuration.MaximumExpandedNodes = 0
$nodeLimitedRequest.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(2)
$nodeLimitedRequest.Configuration.GoalPositionToleranceMeters = 0.001
$nodeLimitedRequest.Configuration.GoalHeadingToleranceRadians = 0.001
$nodeLimitedRequest.Configuration.AllowReverse = $false
$nodeLimitedRequest.GoalDirection = [Enum]::Parse($goalDirectionType, 'Forward')
$nodeLimitedResult = $plan.Invoke($planner, @($nodeLimitedRequest, [Threading.CancellationToken]::None))
Assert-Equal 'SearchNodeLimitExceeded' $nodeLimitedResult.Status.ToString() `
'A zero node limit must fail after planner preflight.'
Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -ge [TimeSpan]::Zero) `
'Search-stage node-limit failure must retain path-search time.'
Assert-True ($nodeLimitedResult.Diagnostics.PathSearchElapsed -le $nodeLimitedResult.Diagnostics.Elapsed) `
'Failed path-search time must not exceed total elapsed time.'
Assert-True (-not [string]::IsNullOrWhiteSpace($nodeLimitedResult.Diagnostics.TerminationReason)) `
'Planner diagnostics must retain a node-limit reason.'
```
Extend the no-path scenario assertions:
```powershell
Assert-True ($noPathResult.PlanningResult.Diagnostics.TerminationReason.Contains('Open List')) `
'No-path diagnostics must retain the exact search exhaustion reason.'
Assert-True ($noPathResult.PlanningResult.Diagnostics.TerminationReason.Contains('扩展=')) `
'No-path diagnostics must include search statistics.'
```
- [ ] **Step 2: Run integration checks and verify RED**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: build succeeds; the script fails first with `Planner diagnostics must expose path-search elapsed time.`
- [ ] **Step 3: Extend `PlanningDiagnostics` compatibly**
Append the optional constructor parameter:
```csharp
TimeSpan pathSearchElapsed = default(TimeSpan)
```
Assign it after `Elapsed`:
```csharp
Elapsed = elapsed;
PathSearchElapsed = pathSearchElapsed;
TerminationReason = terminationReason ?? string.Empty;
```
Add the property:
```csharp
/// <summary>
/// 地图和起终点预检通过后,二维启发式、Hybrid A*、回溯、装配和最终复核的耗时;
/// 不含建图,搜索前失败时为零。
/// </summary>
public TimeSpan PathSearchElapsed { get; }
```
Update the constructor XML comment so `elapsed` is described as total service elapsed time and `pathSearchElapsed` as the map-ready path-production elapsed time.
- [ ] **Step 4: Start the path-search stopwatch at the exact boundary**
Add:
```csharp
using System.Diagnostics;
using System.Globalization;
```
Declare the stopwatch before the `try`:
```csharp
Stopwatch pathSearchStopwatch = null;
```
Start it immediately before invoking the search:
```csharp
pathSearchStopwatch = Stopwatch.StartNew();
HybridAStarSearchResult searchResult = _search.Search(request, budget);
```
For `searchResult == null`, search failure, backtracking failure, assembly failure and final validation failure, pass `pathSearchStopwatch` into `Failure`. On success, pass its elapsed value:
```csharp
return PlanningResult.Success(path, segments, CreateDiagnostics(searchResult, budget.Elapsed,
pathLengthMeters, minimumClearanceMeters, string.Empty, pathSearchStopwatch.Elapsed));
```
Replace the planner catch block with:
```csharp
catch (Exception exception)
{
string reason = "规划内部错误:" + exception.GetType().Name +
(string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message);
return Failure(PlanningStatus.InternalError,
budget ?? PlanningOperationBudget.Unlimited(CancellationToken.None),
reason, null, pathSearchStopwatch);
}
```
- [ ] **Step 5: Compose actionable search diagnostics**
For non-success search results, use:
```csharp
if (searchResult.Status != PlanningStatus.Success)
return Failure(searchResult.Status, budget,
BuildSearchFailureReason(searchResult, request.Configuration),
searchResult, pathSearchStopwatch);
```
Add:
```csharp
private static string BuildSearchFailureReason(HybridAStarSearchResult searchResult,
HybridAStarConfiguration configuration)
{
string reason = string.IsNullOrEmpty(searchResult.TerminationReason)
? "Hybrid A* 搜索以 " + searchResult.Status + " 状态终止。"
: searchResult.TerminationReason;
string limit = string.Empty;
if (searchResult.Status == PlanningStatus.SearchTimeout)
{
limit = "总预算=" + configuration.SearchTimeout.TotalSeconds.ToString(
"F3", CultureInfo.InvariantCulture) + "s";
}
else if (searchResult.Status == PlanningStatus.SearchNodeLimitExceeded)
{
limit = "节点上限=" + configuration.MaximumExpandedNodes.ToString(
CultureInfo.InvariantCulture) + "";
}
return reason + limit +
"扩展=" + searchResult.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + "" +
"生成=" + searchResult.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + "" +
"重开=" + searchResult.ReopenedNodeCount.ToString(CultureInfo.InvariantCulture) + "" +
"陈旧条目=" + searchResult.StaleOpenListEntryCount.ToString(CultureInfo.InvariantCulture) + "" +
"Open List峰值=" + searchResult.PeakOpenListCount.ToString(CultureInfo.InvariantCulture) + "。";
}
```
Replace the two helper signatures and bodies:
```csharp
private static PlanningResult Failure(PlanningStatus status, PlanningOperationBudget budget, string reason,
HybridAStarSearchResult searchResult, Stopwatch pathSearchStopwatch = null)
{
TimeSpan pathSearchElapsed = pathSearchStopwatch == null
? TimeSpan.Zero
: pathSearchStopwatch.Elapsed;
return PlanningResult.Failure(status, CreateDiagnostics(searchResult, budget.Elapsed, 0d, 0d,
reason, pathSearchElapsed));
}
private static PlanningDiagnostics CreateDiagnostics(HybridAStarSearchResult searchResult, TimeSpan elapsed,
double pathLengthMeters, double minimumClearanceMeters, string reason, TimeSpan pathSearchElapsed)
{
return new PlanningDiagnostics(
searchResult == null ? 0 : searchResult.ExpandedNodeCount,
searchResult == null ? 0 : searchResult.GeneratedNodeCount,
searchResult == null ? 0 : searchResult.ReopenedNodeCount,
searchResult == null ? 0 : searchResult.StaleOpenListEntryCount,
searchResult == null ? 0 : searchResult.PeakOpenListCount,
pathLengthMeters,
minimumClearanceMeters,
elapsed,
reason,
pathSearchElapsed);
}
```
All search-preflight `Failure(...)` calls continue omitting the optional stopwatch and therefore report zero. All calls after `_search.Search` pass the running stopwatch.
- [ ] **Step 6: Run integration checks and verify GREEN**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected:
```text
Coarse path integration checks passed.
Coarse path facade checks passed.
Coarse path P1 scenario checks passed.
```
- [ ] **Step 7: Commit planner diagnostics**
```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs ClumsyPilot/tests/verify_coarse_path_integration.ps1
git commit -m "feat: add actionable coarse path diagnostics"
```
### Task 3: Add Manual Timeout Input, UI Diagnostics, and Documentation
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1:19-92`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs:104-190,340-519`
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md:114-116,285-359`
**Interfaces:**
- Consumes: Task 2 `PlanningDiagnostics.PathSearchElapsed`.
- Produces: `ReadPositiveTimeoutInput(string) : TimeSpan`.
- Produces: `DrawStatus(string, CoarsePathPlanningJob, CoarsePathPlanningJobResult, PlanningGridMap)`.
- Contract: only the manual `[MovementTest(name = "粗路径规划")]` prompts for and overrides its request timeout; fixed scenarios keep their existing budgets.
- [ ] **Step 1: Add failing UI and README source checks**
After the current manual input assertions add:
```powershell
Assert-Match $source 'ReadPositiveTimeoutInput\s*\(' `
'The manual UI must read a finite positive timeout.'
Assert-Match $source 'Configuration\.SearchTimeout\s*=\s*searchTimeout' `
'The manual UI must apply the timeout to the current job.'
Assert-Match $source 'TimeSpan\.FromSeconds\s*\(' `
'The manual timeout must convert seconds to TimeSpan.'
Assert-Match $source 'timeoutSeconds\s*<=\s*0' `
'The manual timeout must reject zero and negative values.'
Assert-True (([regex]::Matches($source, 'PathSearchElapsed')).Count -ge 2) `
'The status layer and Toast must both show path-search elapsed time.'
Assert-Match $source 'BuildToastMessage[\s\S]*TerminationReason' `
'The failure Toast must include the termination reason.'
Assert-Match $source 'ExpandedNodeCount' `
'The status layer must show expanded-node statistics.'
Assert-Match $source 'VehicleKinematics\.TryGetMaximumCurvaturePerMeter' `
'The status layer must show the effective turning radius.'
```
Append these entries to `$readmeStructure`:
```powershell
'PathSearchElapsed',
'1.20 m',
'Reeds-Shepp',
```
- [ ] **Step 2: Run UI checks and verify RED**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: FAIL with `The manual UI must read a finite positive timeout.`
- [ ] **Step 3: Read and apply one-shot manual timeout**
Add:
```csharp
using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle;
```
In `CoarsePathPlanningTest.Test()`, read the timeout after goal heading and apply it after creating the job:
```csharp
double goalHeadingDeg = ReadFiniteInput("粗路径终点航向(世界 deg");
TimeSpan searchTimeout = ReadPositiveTimeoutInput("粗路径规划总超时(秒,必须大于 0)");
IReadOnlyList<ManualCoarsePathObstacle> obstacles = ReadManualObstacles();
long snapshotVersion = obstacles.Count == 0 ? 0L :
Interlocked.Increment(ref _nextManualObstacleSnapshotVersion);
CoarsePathPlanningJob job = CoarsePathScenarioFactory.CreateManualObstacleDemo(
amrPose.x, amrPose.y, amrPose.th, goalXmm, goalYmm, goalHeadingDeg,
obstacles, snapshotVersion);
job.Configuration.SearchTimeout = searchTimeout;
CoarsePathPlanningTestRunner.Run("AMR 位姿 + 手动终点 + 手动障碍物", job);
```
Add:
```csharp
private static TimeSpan ReadPositiveTimeoutInput(string prompt)
{
double timeoutSeconds = ReadFiniteInput(prompt);
if (timeoutSeconds <= 0d)
throw new ArgumentOutOfRangeException(nameof(prompt), "输入必须为正数:" + prompt);
try
{
return TimeSpan.FromSeconds(timeoutSeconds);
}
catch (OverflowException)
{
throw new ArgumentOutOfRangeException(nameof(prompt), "输入超出允许范围:" + prompt);
}
}
```
- [ ] **Step 4: Show timings, search counts, vehicle configuration, and failure reason**
Change the call site to:
```csharp
DrawStatus(scenarioName, job, result, map);
```
Replace `DrawStatus` with:
```csharp
private static void DrawStatus(string scenarioName, CoarsePathPlanningJob job,
CoarsePathPlanningJobResult result, PlanningGridMap map)
{
float x = map == null ? 0f : map.Bounds.XMin + 150f;
float y = map == null ? -250f : map.Bounds.YMin + 150f;
string snapshot = map == null ? "无" : map.SnapshotId.ToString(CultureInfo.InvariantCulture);
string resolution = map == null ? "无" :
map.ResolutionMm.ToString("F0", CultureInfo.InvariantCulture) + " mm";
PlanningDiagnostics diagnostics = result.PlanningResult.Diagnostics;
string reason = diagnostics.TerminationReason ?? string.Empty;
string turningRadius = "无";
if (job != null && VehicleKinematics.TryGetMaximumCurvaturePerMeter(
job.Vehicle, out double maximumCurvaturePerMeter))
{
turningRadius = (1d / maximumCurvaturePerMeter).ToString(
"F2", CultureInfo.InvariantCulture) + " m";
}
Painter.DrawText(Color.White, "场景:" + scenarioName, x, y);
Painter.DrawText(Color.White, "地图:" + result.MapResult.Status + ",缓存:" +
result.MapResult.CacheHit + ",快照:" + snapshot, x, y + 120f);
Painter.DrawText(Color.White, "栅格:" + resolution + ",规划:" +
result.PlanningResult.Status + ",总耗时:" +
diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) +
" ms,路径搜索:" +
diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) +
" ms", x, y + 240f);
Painter.DrawText(Color.White, "节点:扩展=" +
diagnostics.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + ",生成=" +
diagnostics.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + "Open List峰值=" +
diagnostics.PeakOpenListCount.ToString(CultureInfo.InvariantCulture), x, y + 360f);
if (job != null && job.Vehicle != null)
{
Painter.DrawText(Color.White, "演示车辆:长=" +
job.Vehicle.LengthMeters.ToString("F2", CultureInfo.InvariantCulture) + " m,宽=" +
job.Vehicle.WidthMeters.ToString("F2", CultureInfo.InvariantCulture) + " m,余量=" +
job.Vehicle.SafetyMarginMeters.ToString("F2", CultureInfo.InvariantCulture) +
" m,最小转弯半径=" + turningRadius, x, y + 480f);
}
if (!string.IsNullOrEmpty(reason))
Painter.DrawText(Color.LightYellow, "原因:" + reason, x, y + 600f);
}
```
Replace `BuildToastMessage` with:
```csharp
private static string BuildToastMessage(string scenarioName, CoarsePathPlanningJobResult result)
{
PlanningDiagnostics diagnostics = result.PlanningResult.Diagnostics;
string message = "粗路径[" + scenarioName + "]:地图=" + result.MapResult.Status +
",规划=" + result.PlanningResult.Status + ",总耗时=" +
diagnostics.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) +
"ms,路径搜索=" +
diagnostics.PathSearchElapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) + "ms";
if (result.PlanningResult.Status != PlanningStatus.Success &&
!string.IsNullOrEmpty(diagnostics.TerminationReason))
{
message += ",原因=" + diagnostics.TerminationReason;
}
return message + "。";
}
```
- [ ] **Step 5: Document the exact manual-test boundaries**
After “总预算与取消” add:
```markdown
### 总耗时与路径搜索耗时
`PlanningDiagnostics.Elapsed` 是从 `CoarsePathPlanningService.Plan` 开始的总耗时,包含
地图来源、缓存、栅格化、距离场和路径规划。`PathSearchElapsed` 是地图和起终点预检
通过后的路径搜索耗时,包含二维启发式、Hybrid A*、回溯、装配、方向分段和最终复核;
搜索开始前失败时为零。
```
In “AMR 位姿、手动终点与障碍物”, add:
```markdown
手动入口还要求输入一次“粗路径规划总超时”,单位为秒,只接受 `TimeSpan` 可表示范围内
的有限正数秒;`0`、负数、NaN、Infinity 或溢出值都会在启动规划前拒绝。该值只覆盖
本次 `CoarsePathPlanningJob.Configuration.SearchTimeout`,不会改变固定场景或全局默认值。
此入口使用固定演示车辆:长 `0.80 m`、宽 `0.60 m`、四周安全余量 `0.05 m`、最小转弯
半径 `1.20 m`。这些值不是从现场 AMR 配置读取的,判断现场可行性前必须确认车辆参数一致。
```
In “后台执行、停止与图层”, add:
```markdown
状态图层显示规划状态、总耗时、`PathSearchElapsed`(路径搜索耗时)、扩展/生成节点数、
Open List 峰值、失败原因和固定演示车辆参数。Toast 同时显示两种耗时,并在失败时附加
`TerminationReason`,因此超时、节点上限、无解、碰撞和内部错误不会只显示成泛化失败。
```
Extend “第一版限制” with:
```markdown
- Reeds-Shepp 或 Dubins 精确终点连接;
- 原地旋转。
当前只生成汽车式恒曲率前进/倒车原语,并允许在原语边界换向;未实现的 Reeds-Shepp、
横移、蟹行和原地旋转是整个粗规划核心的第一版能力边界,不是 MovementTest 单独关闭。
```
- [ ] **Step 6: Run UI checks and verify GREEN**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected:
```text
Coarse path P1 UI source checks passed.
```
- [ ] **Step 7: Commit the manual-test UX**
```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md ClumsyPilot/tests/verify_coarse_path_ui.ps1
git commit -m "feat: expose coarse path test diagnostics"
```
### Task 4: Regress the Slow Feasible Case and Run the Full Suite
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1:273-369`
- Verify: all files modified by Tasks 1-3
**Interfaces:**
- Consumes: Task 3 manual scenario factory and configurable `SearchTimeout`.
- Produces: regression proof that the formerly 5-second-limited feasible pose succeeds under a caller-selected longer budget.
- Contract: the regression uses a 30 秒 upper bound without asserting an exact wall-clock duration.
- [ ] **Step 1: Add the slow feasible-case regression**
After the manual empty-map factory assertions, add:
```powershell
$slowFeasibleJob = $factoryManual.Invoke($null, @(
1000.0, 2000.0, 0.0, 1500.0, 2500.0, 90.0))
$slowFeasibleJob.Configuration.SearchTimeout = [TimeSpan]::FromSeconds(30)
$slowFeasibleJob.Configuration.MaximumExpandedNodes = 1000000
$slowFeasibleService = [Activator]::CreateInstance($serviceType)
$slowFeasibleResult = $servicePlan.Invoke(
$slowFeasibleService, @($slowFeasibleJob, [Threading.CancellationToken]::None))
Assert-Equal 'Success' $slowFeasibleResult.PlanningResult.Status.ToString() `
'The previously five-second-limited feasible pose must succeed with a caller-selected longer budget.'
Assert-True ($slowFeasibleResult.PlanningResult.Diagnostics.PathSearchElapsed -le
$slowFeasibleResult.PlanningResult.Diagnostics.Elapsed) `
'Slow feasible path-search time must remain within total elapsed time.'
```
- [ ] **Step 2: Run the integration script and confirm the regression passes**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected:
```text
Coarse path integration checks passed.
Coarse path facade checks passed.
Coarse path P1 scenario checks passed.
```
The slow case is allowed up to 30 seconds and should normally complete near the observed 10-second baseline; do not assert an exact wall-clock value.
- [ ] **Step 3: Run all coarse-path and map regressions**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.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
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: build exits 0 and every script prints its existing `passed` summary without an unhandled exception.
- [ ] **Step 4: Inspect the final diff for scope and accidental edits**
Run:
```powershell
git diff -- ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/Contracts/PlanningDiagnostics.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md ClumsyPilot/tests/verify_coarse_path_search.ps1 ClumsyPilot/tests/verify_coarse_path_integration.ps1 ClumsyPilot/tests/verify_coarse_path_ui.ps1
```
Expected: only the approved reason propagation, timing, manual timeout, UI, documentation and tests are present; collision/search semantics and unrelated worktree files are unchanged.
- [ ] **Step 5: Perform the Clumsy UI acceptance**
Run `[MovementTest(name = "粗路径规划")]` with:
```text
Start from current AMR pose corresponding to: 1000 mm, 2000 mm, 0 deg
Goal X: 1500 mm
Goal Y: 2500 mm
Goal heading: 90 deg
Timeout: 30 seconds
Manual obstacle count: 0
```
Confirm:
```text
Planning status: Success
Toast and status layer both show total elapsed and path-search elapsed
Status layer shows expanded/generated/Open List peak and the fixed demo vehicle parameters
```
Then run an intentionally impossible barrier or a deliberately short positive timeout and confirm the Toast includes a non-empty reason.
- [ ] **Step 6: Commit the regression coverage**
```powershell
git add -- ClumsyPilot/tests/verify_coarse_path_integration.ps1
git commit -m "test: cover slow feasible coarse path planning"
```
@@ -0,0 +1,185 @@
# CoarsePath README 结构化重构 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将 CoarsePath README 重构为与 Map README 相同的“结构—数据流—契约—最小示例—分步指南—常见错误”说明方式,同时保留准确的 P0/P1 边界。
**Architecture:** 保持所有生产代码不变。先为 README 的结构性事实增加稳定的 ASCII 文本断言,再将现有 README 的正确内容重组为面向调用者的模块说明,最后运行文档、构建与集成回归,证明这只是文档交付。
**Tech Stack:** Markdown、PowerShell、.NET `netstandard2.0` Debug 构建、现有 CoarsePath 验证脚本。
## Global Constraints
- 只修改 `CoarsePath/README.md` 与其文档断言;不得改动 Map、CoarsePath、P1 UI 或测试场景的运行行为。
- README 只陈述当前已实现并经自动化验证的 P0/P1 能力;实际 Clumsy 的人工视觉验收仍要明确为待执行。
- 业务调用示例固定使用 `CoarsePathPlanningService.Plan(job, cancellationToken)`;不得鼓励 UI 或调用方直接拼接搜索组件。
- Map 障碍物投影、栅格化和缓存细节只链接到 `../Map/README.md`,不复制为 CoarsePath 实现说明。
- 坐标说明必须保持:Map 输入为 mm,核心位姿/路径为 m,核心航向为 rad;P1 UI 的 AMR 输入航向为 deg 并在边界转换。
- 显式空图只能描述为 P1 单位/可视化演示,不能描述为真实作业地图。
- 不恢复、清理或迁移 TrapMap 文件或旧 TrapMap 验证脚本;不执行 Git 状态、差异、提交或重置操作。
---
## 文件结构
| 文件 | 修改职责 |
| --- | --- |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 重组现有 P0/P1 内容,加入实际目录树、规划数据流、分步指南与常见错误。 |
| `ClumsyPilot/tests/verify_coarse_path_ui.ps1` | 用 ASCII 关键字保护 README 的结构、核心边界和 P1 说明。 |
### Task 1: 为 README 重构建立失败的结构断言
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1`
- Verify later: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`
**Consumes:** 现有 `$readmePath``$readme``Assert-True` 及 P1 UI 源码检查。
**Produces:** 文档结构保护;README 缺少新的 Map 风格章节或 P1 边界时脚本失败。
- [ ] **Step 1: 在现有 README 断言后加入目标结构的失败检查**
在当前 `$requiredText` 循环之后插入以下 PowerShell。所有匹配项保持 ASCII,避免 Windows PowerShell 无 BOM 脚本中的中文编码差异:
```powershell
$readmeStructure = @(
'File Structure',
'Planning Data Flow',
'Build Status and Stop',
'Coordinates and Units',
'Minimal Call Example',
'Cache and SourceVersion',
'Detailed Usage Guide',
'P1 Manual Tests and Visualization',
'Common Errors',
'First-Version Limits',
'CoarsePathPlanningService.Plan(job, cancellationToken)',
'CoarsePathPlanningJob',
'PlanningGridMap',
'SourceVersion',
'CoarsePathPlanningV1',
'CancellationTokenSource',
'NoFeasiblePath',
'IsGearSwitchPoint',
'../Map/README.md'
)
foreach ($requiredText in $readmeStructure) {
Assert-True ($readme.Contains($requiredText)) "Restructured CoarsePath README must document $requiredText."
}
```
- [ ] **Step 2: 运行脚本确认 README 仍缺少新结构**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: `Restructured CoarsePath README must document File Structure.`;源码 UI 断言仍通过。
### Task 2: 重构 CoarsePath README 的模块说明与调用文档
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`
- Test: `ClumsyPilot/tests/verify_coarse_path_ui.ps1`
**Consumes:** Map README 的组织方式;现有 CoarsePath README 的真实 P0/P1 契约;`CoarsePathPlanningService.Plan(job, cancellationToken)`
**Produces:** 一份可从零开始阅读的 CoarsePath 模块说明,内容与当前实现一致。
- [ ] **Step 1: 用 Map 风格的顶层章节替换现有 README 的章节顺序**
保留 README 标题 `# CoarsePath 粗路径规划(P0/P1`,然后按以下顺序重新组织内容;将每个二级标题同时写为中文说明和括号中的 ASCII 稳定标识,例如 `## 文件结构(File Structure`,使人类读者与 Task 1 断言都能使用:
1. `## 模块说明(Module Overview`:说明 Map 提供只读快照,CoarsePath 输出已复核的粗路径;唯一业务入口是 `CoarsePathPlanningService.Plan(job, cancellationToken)`;列出不负责的控制、速度、实时重规划等职责。
2. `## 文件结构(File Structure`:使用 `text` 目录树列出实际 `Contracts/``Vehicle/``Search/``Output/``Facade/``Test/` 文件,逐项写出与当前目录对应的职责。
3. `## 规划数据流(Planning Data Flow`:画出 `CoarsePathPlanningJob -> CoarsePathPlanningService -> PlanningMapFactory.Create -> PlanningGridMap -> HybridAStarPlanner -> PlanningResult -> CoarsePathPlanningJobResult`;在失败分支注明地图失败不启动搜索。
4. `## 构建状态与停止(Build Status and Stop`:说明 `MapResult``PlanningResult` 必须一起处理,解释 `Success``Cancelled``SearchTimeout``NoFeasiblePath` 与空路径规则。
5. `## 坐标与单位(Coordinates and Units`:用表格列出地图 mm、`Pose2D`/路径 m、核心航向 rad、P1 AMR 输入 deg;明确起点为车身几何中心和安全余量由 `VehicleParameters.SafetyMarginMeters` 表达。
6. `## 最小调用示例(Minimal Call Example`:保留并精简当前服务调用示例;包含 `PlanningMapRequest``Pose2D``VehicleParameters``HybridAStarConfiguration``MapResult``PlanningResult` 的失败处理。
7. `## 缓存与 SourceVersionCache and SourceVersion`:说明服务长期存活、`Input`/`Occupancy`/`None` 缓存层级,及来源内容变更必须递增 `SourceVersion`
8. `## 详细使用指南(Detailed Usage Guide`:用六步小节解释长期服务、准备地图请求、填写起终点、填写车辆、调整搜索配置、调用及消费路径/方向段;链接 `../Map/README.md` 说明障碍物来源和栅格化。
9. `## P1 手动测试与可视化(P1 Manual Tests and Visualization`:包含七个 MovementTest 的场景表、`CoarsePathPlanningTest``getCartLocation`/手动目标转换、`CancellationTokenSource`/`Task.Run`/`TestStop` 停止语义、`CoarsePathPlanningV1` 图层及颜色图例。明确人工视觉验收尚待在实际 Clumsy 中执行。
10. `## 常见错误(Common Errors`:以“现象 / 原因 / 处理”表格写入:mm 当作 m、deg 当作 rad、`SourceVersion` 未递增、隐式空图、未处理非成功结果、把粗路径当作底盘可执行轨迹。
11. `## 第一版限制(First-Version Limits`:保留并归并路径平滑、速度/时间轨迹、底盘控制、实时重规划、真实作业地图、Release 基准等明确非目标。
- [ ] **Step 2: 对照实际目录和 P1 实现,校验每个文件树项与说明的真实性**
确认目录树只引用下列已存在组件:
```text
Contracts/: Pose2D, PlanningRequest, PlanningResult, PlanningStatus,
CoarsePathPoint, PathSegment, VehicleParameters, HybridAStarConfiguration
Vehicle/: VehicleKinematics, VehicleFootprint, FootprintCollisionChecker,
OrientedRectangleCellIntersection
Search/: BinaryMinHeap, GridDijkstraHeuristic, GoalToleranceChecker,
MotionPrimitive, MotionPrimitiveGenerator, SearchCostCalculator,
HybridAStarNode, HybridAStarNodeKey, HybridAStarSearch
Output/: PathBacktracker, CoarsePathAssembler, CoarsePathValidator
Facade/: CoarsePathPlanningJob, CoarsePathPlanningJobResult,
CoarsePathPlanningService, PlanningDebugOptions, IPlanningDebugSink
Test/: CoarsePathScenarioFactory, MovementTest.CoarsePathTest
```
不要在 README 中承诺不存在的平滑器、控制器、实时数据源或 Release 基准。
- [ ] **Step 3: 运行文档结构检查确认通过**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: `Coarse path P1 UI source checks passed.`
### Task 3: 验证文档重构没有影响 P0/P1 行为
**Files:**
- Verify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`
- Verify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1`
- Verify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
**Consumes:** Tasks 12 的 README 与断言。
**Produces:** 从最终工作区获得的文档、构建和集成验证证据。
- [ ] **Step 1: 构建项目**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
```
Expected: `0 个错误`;允许项目已有的两条过时 API 警告。
- [ ] **Step 2: 运行 P1 文档/UI 结构检查**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: `Coarse path P1 UI source checks passed.`
- [ ] **Step 3: 运行粗路径集成回归**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: 依次输出 `Coarse path integration checks passed.``Coarse path facade checks passed.``Coarse path P1 scenario checks passed.`
## 自检
- **规格覆盖:** Task 2 覆盖设计中的十个 README 章节、P0/P1 已完成边界、Map 链接、单位、空图限制与人工验收状态;Task 1 保护可自动检查的结构事实;Task 3 给出最终证据。
- **完整性检查:** 本计划不含未决实现、泛化错误处理或未命名的验证步骤;每项改动均有文件路径、具体内容与命令。
- **一致性:** 所有调用名、状态名、场景工厂、P1 图层和坐标单位均与现有 CoarsePath 代码一致;计划不引入新 C# 接口或依赖。
@@ -0,0 +1,365 @@
# P1 手动障碍物输入 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让“粗路径规划”MovementTest 支持一次输入最多 20 个圆形或轴对齐矩形障碍物,并通过既有门面规划、快照绘制和取消流程验证结果。
**Architecture:** 纯几何输入和 Map 请求构造保留在 `CoarsePathScenarioFactory`,UI 只读取、验证和冻结操作者输入。每次含障碍物的手动运行由共享执行器颁发单调递增快照版本,保证 Map 缓存不会错误复用旧障碍物;Painter 继续只读取最终 `PlanningGridMap`
**Tech Stack:** C# / `netstandard2.0`、现有 `ManualObstacleSource`、Clumsy `MovementTest`/`UI.GetInput`、PowerShell 反射与源码验证脚本。
## Global Constraints
- 不改变 `CoarsePathPlanningService.Plan(job, token)` 作为唯一业务规划入口的边界;UI 不得直接创建地图工厂、搜索器、碰撞器或原语。
- 手动输入的 X/Y、圆半径和矩形长宽全部使用世界 mm;AMR/目标航向输入使用 deg;核心 `Pose2D` 使用 m/rad。
- 障碍物数量范围固定为 0–20;圆半径、矩形 X 长度和 Y 宽度必须是有限正数;矩形始终与世界坐标轴平行。
- 有障碍物时使用必需的 `ManualObstacleSource("manual-user-input", version, true, ...)` 且关闭显式空图;零障碍物时才允许显式空图。
- 手动地图边界必须覆盖起点、终点及每个障碍物完整外轮廓,再保留 2000 mm 留白并按 50 mm 向外取整。
- 含障碍物手动提交必须使用单调递增快照版本;固定场景的缓存命中行为不得改变。
- 保留后台 `Task.Run``CancellationTokenSource``TestStop`、结果快照绘制和无底盘命令边界。
- 不支持旋转矩形、多边形、文件导入、拖拽编辑或运行中修改障碍物;不执行 Git 操作。
---
## 文件结构
| 文件 | 修改职责 |
| --- | --- |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` | 新增手动障碍物纯数据类型、工厂方法、几何校验、动态边界和来源快照构造。 |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` | 为“粗路径规划”读取数量、类型、中心和尺寸,生成单调来源版本并提交工厂请求。 |
| `ClumsyPilot/tests/verify_coarse_path_integration.ps1` | 通过程序集反射验证工厂、障碍来源、空图分支、几何边界和无效尺寸。 |
| `ClumsyPilot/tests/verify_coarse_path_ui.ps1` | 验证 UI 入口包含手动障碍物输入与工厂调用,同时保持无直接地图/搜索依赖。 |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 补充手动障碍物的输入顺序、单位、上限、矩形方向和空图限制。 |
### Task 1: 手动障碍物工厂契约与行为验证
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
- Modify later: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs`
**Consumes:** 现有 `$assembly``$testNamespace``$scenarioFactoryType``Find-Method``Assert-True``Assert-Equal``Assert-Near`
**Produces:** `ManualCoarsePathObstacleKind``ManualCoarsePathObstacle``CreateManualObstacleDemo` 的反射/行为契约。
- [ ] **Step 1: 在 P1 工厂断言后加入失败的手动障碍物检查**
`$manualJob` 的现有断言之后插入下面代码。它使用数组传入 `IReadOnlyList<ManualCoarsePathObstacle>`,并检查请求尚未存在时的类型/方法失败。
```powershell
$manualObstacleKindType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacleKind', $false)
$manualObstacleType = $assembly.GetType($testNamespace + 'ManualCoarsePathObstacle', $false)
Assert-True ($manualObstacleKindType -ne $null) 'Manual obstacle kind enum must exist.'
Assert-True ($manualObstacleType -ne $null) 'Manual obstacle value type must exist.'
$manualCircle = Find-Method $manualObstacleType 'Circle' @([double], [double], [double])
$manualRectangle = Find-Method $manualObstacleType 'AxisAlignedRectangle' @([double], [double], [double], [double])
$manualObstacleFactory = $scenarioFactoryType.GetMethods() | Where-Object {
$_.Name -eq 'CreateManualObstacleDemo' -and $_.GetParameters().Length -eq 8
} | Select-Object -First 1
Assert-True ($manualCircle -ne $null) 'Manual obstacle type must create circles from center and radius.'
Assert-True ($manualRectangle -ne $null) 'Manual obstacle type must create rectangles from center and X/Y dimensions.'
Assert-True ($manualObstacleFactory -ne $null) 'Scenario factory must expose CreateManualObstacleDemo with six poses, obstacles and version.'
$manualObstacles = [Array]::CreateInstance($manualObstacleType, 2)
$manualObstacles.SetValue($manualCircle.Invoke($null, @([double]6500, [double]2000, [double]200)), 0)
$manualObstacles.SetValue($manualRectangle.Invoke($null, @([double]-2000, [double]500, [double]600, [double]400)), 1)
$manualObstacleJob = $manualObstacleFactory.Invoke($null, @(
1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $manualObstacles, [long]77))
Assert-False $manualObstacleJob.MapRequest.AllowExplicitEmptyMap 'Manual obstacles must disable the explicit-empty-map mode.'
Assert-Equal 1 $manualObstacleJob.MapRequest.ObstacleSources.Count 'Manual obstacles must create one unified source.'
Assert-Equal 'manual-user-input' $manualObstacleJob.MapRequest.ObstacleSources[0].SourceId 'Manual source ID must be stable.'
Assert-Equal 77 $manualObstacleJob.MapRequest.ObstacleSources[0].SourceVersion 'Manual source version must be preserved.'
Assert-True ($manualObstacleJob.MapRequest.Bounds.XMin -le -4300.0) 'Manual map must include the rectangle outline and padding.'
Assert-True ($manualObstacleJob.MapRequest.Bounds.XMax -ge 8700.0) 'Manual map must include the circle outline and padding.'
$emptyManualObstacles = [Array]::CreateInstance($manualObstacleType, 0)
$emptyManualJob = $manualObstacleFactory.Invoke($null, @(
1000.0, 2000.0, 0.0, 4000.0, 2000.0, 0.0, $emptyManualObstacles, [long]0))
Assert-True $emptyManualJob.MapRequest.AllowExplicitEmptyMap 'Zero manual obstacles must retain explicit empty-map mode.'
Assert-Equal 0 $emptyManualJob.MapRequest.ObstacleSources.Count 'Zero manual obstacles must not create a fake source.'
try {
$null = $manualCircle.Invoke($null, @([double]1000, [double]1000, [double]0))
throw 'Zero-radius manual circle must be rejected.'
}
catch [Reflection.TargetInvocationException] {
Assert-True ($_.Exception.InnerException -is [ArgumentOutOfRangeException]) 'Invalid manual geometry must report argument range.'
}
```
- [ ] **Step 2: 构建并运行脚本确认新契约失败**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: 构建成功;脚本报出 `Manual obstacle kind enum must exist.`
- [ ] **Step 3: 在场景工厂实现不可变手动障碍物类型**
`CoarsePathScenarioFactory.cs` 的固定场景枚举之后加入如下公共类型。构造函数保持私有,强制圆形与矩形分别通过语义明确的静态工厂创建;所有几何输入均为 mm。
```csharp
/// <summary>手动障碍物的支持几何类型。</summary>
public enum ManualCoarsePathObstacleKind
{
/// <summary>由圆心和半径定义的圆形障碍物。</summary>
Circle,
/// <summary>由几何中心、X 方向长度和 Y 方向宽度定义的轴对齐矩形障碍物。</summary>
AxisAlignedRectangle,
}
/// <summary>手动粗路径测试的不可变障碍物输入;全部几何数据使用世界 mm。</summary>
public sealed class ManualCoarsePathObstacle
{
private ManualCoarsePathObstacle(ManualCoarsePathObstacleKind kind, double centerXMillimeters,
double centerYMillimeters, double sizeXMillimeters, double sizeYMillimeters)
{
Kind = kind; CenterXMillimeters = centerXMillimeters; CenterYMillimeters = centerYMillimeters;
SizeXMillimeters = sizeXMillimeters; SizeYMillimeters = sizeYMillimeters;
}
public ManualCoarsePathObstacleKind Kind { get; }
public double CenterXMillimeters { get; }
public double CenterYMillimeters { get; }
public double SizeXMillimeters { get; }
public double SizeYMillimeters { get; }
public static ManualCoarsePathObstacle Circle(double centerXMillimeters, double centerYMillimeters,
double radiusMillimeters)
{
EnsureFinite(centerXMillimeters, nameof(centerXMillimeters));
EnsureFinite(centerYMillimeters, nameof(centerYMillimeters));
EnsurePositiveFinite(radiusMillimeters, nameof(radiusMillimeters));
return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.Circle, centerXMillimeters,
centerYMillimeters, radiusMillimeters, radiusMillimeters);
}
public static ManualCoarsePathObstacle AxisAlignedRectangle(double centerXMillimeters,
double centerYMillimeters, double lengthXMillimeters, double widthYMillimeters)
{
EnsureFinite(centerXMillimeters, nameof(centerXMillimeters));
EnsureFinite(centerYMillimeters, nameof(centerYMillimeters));
EnsurePositiveFinite(lengthXMillimeters, nameof(lengthXMillimeters));
EnsurePositiveFinite(widthYMillimeters, nameof(widthYMillimeters));
return new ManualCoarsePathObstacle(ManualCoarsePathObstacleKind.AxisAlignedRectangle,
centerXMillimeters, centerYMillimeters, lengthXMillimeters, widthYMillimeters);
}
}
```
`EnsureFinite` 与新增 `EnsurePositiveFinite` 定义为可被同一命名空间类型调用的内部静态校验辅助方法,或在 `ManualCoarsePathObstacle` 中实现等价私有辅助方法;无效值必须抛出 `ArgumentOutOfRangeException`
- [ ] **Step 4: 实现手动障碍物请求和动态边界**
`CoarsePathScenarioFactory` 加入下面公共方法,并让现有 `CreateManualGoalDemo` 调用它的零障碍物分支,以保留当前空图契约:
```csharp
public static CoarsePathPlanningJob CreateManualObstacleDemo(
double startXMillimeters, double startYMillimeters, double startHeadingDegrees,
double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees,
IReadOnlyList<ManualCoarsePathObstacle> obstacles, long obstacleSnapshotVersion)
{
ValidateManualPoseInputs(startXMillimeters, startYMillimeters, startHeadingDegrees,
goalXMillimeters, goalYMillimeters, goalHeadingDegrees);
IReadOnlyList<ManualCoarsePathObstacle> items = obstacles ??
throw new ArgumentNullException(nameof(obstacles));
if (items.Count > MaximumManualObstacleCount)
throw new ArgumentOutOfRangeException(nameof(obstacles));
if (items.Count == 0)
return CreateJob(CreateManualDemoMap(startXMillimeters, startYMillimeters,
goalXMillimeters, goalYMillimeters, Array.Empty<ManualCoarsePathObstacle>()),
ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees),
ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), null, GoalDirectionConstraint.Any);
if (obstacleSnapshotVersion <= 0)
throw new ArgumentOutOfRangeException(nameof(obstacleSnapshotVersion));
IMapObstacle[] mapObstacles = ConvertManualObstacles(items);
IMapObstacleSource[] sources =
{
new ManualObstacleSource("manual-user-input", obstacleSnapshotVersion, true, mapObstacles),
};
return CreateJob(CreateManualDemoMap(startXMillimeters, startYMillimeters,
goalXMillimeters, goalYMillimeters, items),
ToPose(startXMillimeters, startYMillimeters, startHeadingDegrees),
ToPose(goalXMillimeters, goalYMillimeters, goalHeadingDegrees), null, GoalDirectionConstraint.Any);
}
```
Use `CreateManualMapRequest(items, sources)` rather than leaving the above source array unused: it must create a `PlanningMapRequest` with the dynamic bounds, `ResolutionMm = 50f`, those sources and `AllowExplicitEmptyMap = false`. `ConvertManualObstacles` must map a circle to `new CircleObstacle(centerX, centerY, radius)` and a rectangle to `new AxisAlignedRectangleObstacle(centerX - lengthX / 2, centerX + lengthX / 2, centerY - widthY / 2, centerY + widthY / 2)` after range-safe float conversion.
Refactor `CreateManualDemoMap` to accept an obstacle collection and include its circle/rectangle extents before adding 2000 mm padding and applying `ToGridLowerBound`/`ToGridUpperBound`. Zero obstacles must retain `Array.Empty<IMapObstacleSource>()` and `AllowExplicitEmptyMap = true`.
- [ ] **Step 5: 运行工厂行为检查确认通过**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: 构建成功;输出既有三行集成通过信息,且手动圆/矩形、空障碍物和无效半径断言均通过。
### Task 2: MovementTest 逐项输入与来源版本
**Files:**
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1`
- Modify later: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs`
**Consumes:** Task 1 的 `ManualCoarsePathObstacle.Circle``ManualCoarsePathObstacle.AxisAlignedRectangle``CoarsePathScenarioFactory.CreateManualObstacleDemo`
**Produces:** `CoarsePathPlanningTest` 在启动规划前读取并冻结最多 20 个手动障碍物,随后使用递增版本提交给工厂。
- [ ] **Step 1: 加入失败的 UI 源码边界断言**
在现有手动工厂断言后加入:
```powershell
Assert-Match $source 'ManualCoarsePathObstacle' 'The manual UI must construct typed manual obstacles.'
Assert-Match $source 'CreateManualObstacleDemo\s*\(' 'The manual UI must submit obstacles through the factory.'
Assert-Match $source 'MaximumManualObstacleCount\s*=\s*20' 'The manual UI must bound obstacle input to 20.'
Assert-Match $source 'ReadManualObstacles\s*\(' 'The manual UI must read the requested obstacle sequence.'
Assert-Match $source 'Interlocked\.Increment\s*\(' 'The manual UI must issue a fresh obstacle snapshot version.'
Assert-Match $source 'Circle\s*\(' 'The manual UI must support circle input.'
Assert-Match $source 'AxisAlignedRectangle\s*\(' 'The manual UI must support axis-aligned rectangle input.'
```
- [ ] **Step 2: 运行 UI 脚本确认新断言失败**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: `The manual UI must construct typed manual obstacles.`
- [ ] **Step 3: 实现输入辅助方法与提交逻辑**
`CoarsePathPlanningTest` 中新增:
```csharp
private const int MaximumManualObstacleCount = 20;
private static long _nextManualObstacleSnapshotVersion;
private static IReadOnlyList<ManualCoarsePathObstacle> ReadManualObstacles()
{
int count = ReadBoundedIntegerInput("手动障碍物数量(0-20", 0, MaximumManualObstacleCount);
var obstacles = new List<ManualCoarsePathObstacle>(count);
for (int index = 0; index < count; index++)
{
int kind = ReadBoundedIntegerInput("障碍物 " + (index + 1) + " 类型(1圆形,2矩形)", 1, 2);
double centerX = ReadFiniteInput("障碍物 " + (index + 1) + " 中心 X(世界 mm");
double centerY = ReadFiniteInput("障碍物 " + (index + 1) + " 中心 Y(世界 mm");
if (kind == 1)
{
double radius = ReadPositiveFiniteInput("障碍物 " + (index + 1) + " 半径 rmm");
obstacles.Add(ManualCoarsePathObstacle.Circle(centerX, centerY, radius));
}
else
{
double lengthX = ReadPositiveFiniteInput("障碍物 " + (index + 1) + " X方向长度(mm");
double widthY = ReadPositiveFiniteInput("障碍物 " + (index + 1) + " Y方向宽度(mm");
obstacles.Add(ManualCoarsePathObstacle.AxisAlignedRectangle(centerX, centerY, lengthX, widthY));
}
}
return obstacles;
}
```
`ReadBoundedIntegerInput` 复用 `UI.GetInput` 和当前文化/InvariantCulture 解析,拒绝非整数或超出 `[minimum, maximum]` 的输入;`ReadPositiveFiniteInput``ReadFiniteInput` 返回后拒绝 `<= 0d`。所有失败继续由现有 `ShowInputFailure` 显示。
`Test()` 中的终点读取后调用 `ReadManualObstacles()`。当集合非空时,用 `Interlocked.Increment(ref _nextManualObstacleSnapshotVersion)` 取得版本;集合为空时使用 `0L`。随后替换现有工厂调用:
```csharp
IReadOnlyList<ManualCoarsePathObstacle> obstacles = ReadManualObstacles();
long snapshotVersion = obstacles.Count == 0 ? 0L :
Interlocked.Increment(ref _nextManualObstacleSnapshotVersion);
CoarsePathPlanningJob job = CoarsePathScenarioFactory.CreateManualObstacleDemo(
amrPose.x, amrPose.y, amrPose.th, goalXmm, goalYmm, goalHeadingDeg,
obstacles, snapshotVersion);
CoarsePathPlanningTestRunner.Run("AMR 位姿 + 手动终点 + 手动障碍物", job);
```
保留 `TestStop``Run`、Painter 和底盘禁止边界,不在 UI 内构造 `ManualObstacleSource``PlanningMapRequest` 或搜索对象。
- [ ] **Step 4: 运行 UI 结构检查确认通过**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: `Coarse path P1 UI source checks passed.`
### Task 3: README 输入说明与最终回归
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`
- Modify: `ClumsyPilot/tests/verify_coarse_path_ui.ps1`
- Verify: `ClumsyPilot/tests/verify_coarse_path_integration.ps1`
**Consumes:** Tasks 1–2 的工厂与 UI 输入契约。
**Produces:** README 中与实际输入顺序一致的手动障碍物说明,以及最终的构建、UI 和集成证据。
- [ ] **Step 1: 为 README 增加失败的 ASCII 文档断言**
`$readmeStructure` 的数组中加入:
```powershell
'CreateManualObstacleDemo',
'ManualCoarsePathObstacle',
'manual-user-input',
'0-20',
'AxisAlignedRectangle',
```
- [ ] **Step 2: 运行 UI 脚本确认 README 断言失败**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
```
Expected: `Restructured CoarsePath README must document CreateManualObstacleDemo.`
- [ ] **Step 3: 更新 README 的 P1 手动测试段落**
`## P1 手动测试与可视化(P1 Manual Tests and Visualization` 的“AMR 位姿与手动终点”小节中,替换“空图入口”的单一说明,加入以下事实:
1. 目标输入之后先输入 `0-20` 的障碍物数量;
2. 每项输入 `1` 圆形或 `2` 矩形、中心 X/Y(mm),圆形半径或矩形 X 长度/Y 宽度(mm);
3. 矩形是 `AxisAlignedRectangle`,不支持旋转;尺寸必须为正;
4. `CreateManualObstacleDemo` 将它们包装为 `manual-user-input` 快照,有障碍物时关闭显式空图;
5. 零障碍物才是坐标/取消演示的显式空图;真实作业仍必须提供真实障碍物来源;
6. 地图边界自动覆盖起终点和障碍物完整外轮廓,保留 2000 mm 留白并按 50 mm 对齐;
7. 每次含障碍物提交使用新版本,Painter 仍显示最终 `PlanningGridMap` 占据格而不是原始几何。
- [ ] **Step 4: 运行完整验证**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_ui.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
```
Expected: 构建为 `0 个错误`UI 脚本输出 `Coarse path P1 UI source checks passed.`;集成脚本依次输出既有三行 `passed` 消息。
## 自检
- **规格覆盖:** Task 1 覆盖几何类型、非空/空地图、动态边界、版本和无效尺寸;Task 2 覆盖 0–20 输入、形状输入、版本和后台门面边界;Task 3 覆盖 README 与回归。
- **完整性检查:** 每个实现步骤指定了文件、调用签名、验证规则和命令;不引入未命名接口或外部依赖。
- **一致性:** `ManualCoarsePathObstacle``CreateManualObstacleDemo``manual-user-input``obstacleSnapshotVersion` 在所有任务中使用相同名称和单位定义。
@@ -0,0 +1,73 @@
# Path smoothing six-figure report implementation plan
> **Execution:** Implement in this workspace without staging or committing. The worktree contains unrelated user changes; touch only the path-smoothing report code, its tests, and its documentation.
**Goal:** Replace each scenario's legacy composite `comparison.svg/png` output with six focused SVG/PNG figures and one CSV, using discrete trajectory samples only (no path-connecting strokes).
**Architecture:** Keep `SmoothingFigureModel` as the immutable source data extracted from comparison results. Add a figure-set layer that selects series, camera bounds, map decorations, axis configuration, and title/legend per output figure. Both renderers consume that same figure definition, so SVG and PNG communicate exactly the same data. The exporter creates all twelve images and the CSV in temporary sibling files, then publishes the completed set and removes legacy composite images.
**Technology:** C#/.NET 10 (`System.Drawing.Common` for PNG); hand-authored SVG; existing PowerShell verification harness and `PathSmoothingPngVerificationHost`.
---
## Task 1: Define six figure views from the common report model
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs`
- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs`
1. Extend the C# verification host first with assertions for six ordered figure kinds/stems, selected series, equal-scale world bounds, labels with units, and point-only series metadata. Run the host and confirm it fails because no figure set exists.
2. Remove `DashArray` as a trajectory styling contract from `SmoothingFigureSeries` and legend entries. Preserve source points, status, colors, raw baseline flag, violations, map obstacles, start, goal, and metric rows.
3. Implement immutable figure definitions with fixed stems:
- `01-coarse-path-overview`: raw only; map obstacles and start/goal.
- `02-all-paths-comparison`: raw plus all three smoothing methods; paths and axes/legend only.
- `03-cubic-bspline-overview`, `04-local-cubic-bezier-overview`, `05-piecewise-quintic-overview`: faded raw reference plus the named method; map obstacles and start/goal.
- `06-curvature-comparison`: raw plus all smoother curvature samples.
4. Compute a trajectory-driven world view for each overhead figure: union only visible series points plus its relevant start/goal, add 10% padding with a 0.25 m minimum extent, and expand the smaller world dimension so projected X and Y scale are equal. Do not use full map bounds to zoom out a figure.
5. Include deterministic “nice” axis ticks/labels in metres for overhead figures and arc length/curvature units for the final figure. Preserve failed/infeasible method labels in legends even when their geometry has no points.
6. Rerun the host checks; expected result: it passes definition-level checks while renderer-output checks remain to be updated in Tasks 23.
## Task 2: Render six focused point-cloud figures and publish the set
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingSvgRenderer.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExporter.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingReportExportResult.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs`
- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs`
1. Add output-level tests in the verification host for the twelve exact image names, the single CSV, non-empty parseable SVGs, readable 600 dpi PNGs, and absence of temporary files. Run them and confirm the legacy one-image exporter fails these expectations.
2. Refactor the SVG renderer to render one figure definition at a time. Draw axes, ticks, numeric labels, unit labels, legend point swatches, map rectangles (when requested), and start/goal markers. Draw every trajectory and curvature sample as a small marker; do not emit a trajectory `<path>`, polyline, dash array, or line segment.
3. Apply the identical layout semantics in the PNG renderer. Draw points rather than calling a line-drawing API for path samples; give raw reference samples a reduced alpha in individual smoother figures. Keep 600 dpi metadata and the existing required-font behavior.
4. Refactor the exporter to build the six definitions and write twelve temporary image files plus the CSV before publishing. Return collections of SVG and PNG paths with the one CSV path. Delete `comparison.svg/png` after a successful new-set publish; on failure, clean temporary files and retain existing published outputs.
5. Update demo/host call sites from singular `SvgPath`/`PngPath` to the path collections. Run the verification host; expected result: six SVGs, six PNGs, and CSV are all present and valid.
## Task 3: Update external verification, runner documentation, and visually inspect outputs
**Files:**
- Modify: `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1`
- Modify: `ClumsyPilot/tests/verify_path_smoothing_png.ps1`
- Modify: `ClumsyPilot/tests/verify_path_smoothing_documentation.ps1`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md`
- Modify: `ClumsyPilot/tests/run_path_smoothing_comparison.ps1` (only if it states/assumes legacy filenames)
- Modify: `docs/superpowers/specs/2026-07-30-path-smoothing-six-figure-report-design.md` (only if implementation exposes a necessary clarified contract)
1. Update PowerShell tests to assert exactly six SVG + six PNG filenames, one CSV, no legacy composite output, required unit labels, marker-based trajectory rendering, and no trajectory dash/line styles. Ensure test source uses safe UTF-8 handling rather than brittle localized literal matching.
2. Update the README to document the six filenames, marker-only semantics, method statuses, coordinate units, and the one-command runner output structure.
3. Run the focused report verification scripts and the PNG host. Regenerate at least one fixture report with the current 0.025 m smoothing output sampling.
4. Render/open representative PNGs for visual QA: raw overview, all-path overlay, each individual smoother, and curvature. Check that curves fill the frame, coordinates/units are legible, all points are visible, individual figures retain context, and there are no joined path lines.
5. Run `dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore` and relevant contract/service/integration tests. Confirm `git diff --check` and report exact files changed; do not stage or commit.
## Acceptance checklist
- Each scenario produces exactly `01` through `06` SVGs and corresponding PNGs plus one CSV.
- Raw and smoothed trajectories use every sampled point and zero connecting lines.
- Overhead figures use equal X/Y scale, trajectory-focused bounds, numeric axes, and metre units.
- Curvature uses `s (m)` and `κ (m⁻¹)` axes with a complete legend and statuses.
- SVG and PNG agree on the six figure contents, fonts, units, colors, and point-only semantics.
- Every PNG is 600 dpi; failed export leaves no temporary files or partial newly generated set.
@@ -0,0 +1,249 @@
# Local G2 Dailywork Reports Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**`dailywork_report/` 中交付两份清晰、可追溯的 Local G2 五次 Hermite 中文报告,并为每份提供一个可离线打开的 HTML 可视化附录。
**Architecture:** 报告按“算法事实”和“问题证据”拆分,避免把候选层能力误写成已完成的端到端功能。每份 HTML 均为独立单文件,以内嵌 CSS、SVG 和少量原生 JavaScript 将 Markdown 的核心结构可视化;不引入构建工具或外部资源。
**Tech Stack:** Markdown、HTML5、内嵌 CSS、内嵌 SVG、原生 JavaScript、PowerShell 验证。
## Global Constraints
- 目录根为 `dailywork_report/`,大小写和下划线必须保持一致。
- 建立 `Map_rep/``coarsepath_rep/` 作为空的未来报告入口;本次不填充其业务内容。
- 本次四份正式内容只放在 `dailywork_report/pathsmoothing_rep/`
- 所有文字使用中文;首次出现的英文技术术语必须有中文解释或可由相邻中文短语理解。
- 明确区分:专项测试通过、已复现故障、静态分析确认的逻辑缺口、待验证集成风险。
- 不修改任何路径平滑、地图、粗路径或测试实现。
- HTML 不使用 CDN、网络请求、第三方库、外部图片或构建步骤。
- 工作区已有无关改动;本任务不暂存、不提交。
---
### Task 1: 建立稳定的日报目录边界
**Files:**
- Create: `dailywork_report/Map_rep/.gitkeep`
- Create: `dailywork_report/coarsepath_rep/.gitkeep`
- Create: `dailywork_report/pathsmoothing_rep/`(由后续两个任务创建内容)
**Interfaces:**
- Consumes: 已批准的 `docs/superpowers/specs/2026-07-31-local-g2-dailywork-reports-design.md`
- Produces: 可承载地图、粗路径和路径平滑报告的稳定目录边界。
- [ ] **Step 1: 创建两个空模块目录的保留文件**
使用 `apply_patch` 创建两个空的 `.gitkeep` 文件,内容保持为空:
```text
dailywork_report/Map_rep/.gitkeep
dailywork_report/coarsepath_rep/.gitkeep
```
- [ ] **Step 2: 验证目录边界**
运行:
```powershell
$paths = @(
'dailywork_report/Map_rep/.gitkeep',
'dailywork_report/coarsepath_rep/.gitkeep'
)
foreach ($path in $paths) {
if (-not (Test-Path -LiteralPath $path)) { throw "Missing report directory marker: $path" }
}
Write-Output 'Dailywork report directory checks passed.'
```
预期:输出 `Dailywork report directory checks passed.`
---
### Task 2: 编写 Local G2 算法主报告及流程可视化附录
**Files:**
- Create: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md`
- Create: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html`
**Interfaces:**
- Consumes: `docs/superpowers/plans/2026-07-30-local-g2-path-presmoothing.md``docs/superpowers/specs/2026-07-30-local-g2-path-presmoothing-design.md``ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/` 下的实现。
- Produces: 一份说明 Local G2 候选层工作方式、输入、输出、约束和当前接入边界的报告;一份与该报告事实一致的可视化附录。
- [ ] **Step 1: 写入 Markdown 主报告的固定章节**
按下列一级标题顺序撰写,并在每节中给出可核对的事实:
```markdown
# Local G2 五次 Hermite 路径平滑算法说明
## 1. 目标、位置与非目标
## 2. 输入:进入算法前必须具备什么
## 3. 模块架构:每个模块负责什么
## 4. 数据流:从粗路径到候选安全路径
## 5. 输出:路径、段、指标、区域报告与状态
## 6. 安全与质量门
## 7. 当前实现进度与边界
```
必须写清:输入为成功的 Hybrid A* 粗路径、路径方向段、地图、车辆参数、`LocalG2QuinticOptions`、取消令牌;距离单位为米、航向为弧度、曲率为 `1/m`。数据流必须依次解释预处理、曲率跳变检测、窗口规划、五次 Hermite 候选构造、局部拼接、统一几何分析、完整车体验证、质量评价。输出必须解释 `SmoothedPathPoint``SmoothedPathSegment`、曲率 `κ`、曲率导数 `dκ/ds`、区域报告和诊断。
“当前实现进度与边界”必须明确:任务 1–7 已达到候选构造与评价层;`LocalG2PreSmoothingPipeline` 与服务分派尚未实现,因此不能声称目前可正式发布完整的 Local G2 SQP 初始路径。
- [ ] **Step 2: 写入单文件 HTML 算法附录**
HTML 必须包含 `<main>`、一个“输入”卡片区、一个按顺序排列的 SVG 流程图、一个“输出”卡片区和一个“当前边界”提示区。SVG 流程节点必须使用以下稳定文字:
```text
Hybrid A* 粗路径
预处理与方向分段
曲率跳变检测
窗口规划
五次 Hermite 候选
拼接与统一几何分析
完整车体安全与质量评价
计划中的发布流水线(尚未接入)
```
用绿色标记已实现的候选层节点,用琥珀色标记“尚未接入”的发布流水线节点。HTML 中的“输入”和“输出”文字必须与 Markdown 报告一致,且页面顶部必须写明“离线静态可视化附录”。
- [ ] **Step 3: 校验主报告与 HTML 的算法事实**
运行:
```powershell
$markdown = Get-Content -Raw -Encoding UTF8 'dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md'
$html = Get-Content -Raw -Encoding UTF8 'dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html'
$markdownTerms = @('输入:进入算法前必须具备什么', '输出:路径、段、指标、区域报告与状态', 'dκ/ds', '尚未实现')
$htmlTerms = @('<main', '<svg', 'Hybrid A* 粗路径', '五次 Hermite 候选', '尚未接入', '离线静态可视化附录')
foreach ($term in $markdownTerms) { if (-not $markdown.Contains($term)) { throw "Algorithm report missing: $term" } }
foreach ($term in $htmlTerms) { if (-not $html.Contains($term)) { throw "Algorithm visualization missing: $term" } }
Write-Output 'Algorithm report checks passed.'
```
预期:输出 `Algorithm report checks passed.`
---
### Task 3: 编写问题分析报告及风险可视化附录
**Files:**
- Create: `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-report.md`
- Create: `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html`
**Interfaces:**
- Consumes: 本轮已运行的构建与专项测试结果、`verify_path_smoothing_integration.ps1``RectangleDetour` 失败、`LocalG2WindowPlanner.cs``LocalG2PathSplicer.cs``LocalG2CandidateEvaluator.cs`
- Produces: 三个问题的证据分级、可理解的场景例子、成因、影响与下一步验证/修复措施;一份与主报告一致的风险可视化。
- [ ] **Step 1: 写入 Markdown 问题报告的固定章节和证据分类**
使用下列一级标题:
```markdown
# Local G2 路径平滑问题分析与后续措施
## 1. 阅读本报告前:证据等级说明
## 2. 问题一:RectangleDetour 原始基线复验失败
## 3. 问题二:窗口合并范围与候选长度上限不一致
## 4. 问题三:连续处理多个区域时的弧长定位风险
## 5. 进入任务 8 前的行动顺序与验收条件
```
每个问题必须按“现象 → 生动例子 → 为什么发生 → 影响 → 证据等级 → 下一步措施 → 验收条件”顺序写作。
问题一必须标记为“已复现故障”,引用如下实际结果,不增添未验证的数值原因:
```text
Raw baseline RectangleDetour must remain a feasible, verified copy of the coarse path.
Expected=Success Actual=InvalidInput
```
问题二必须标记为“静态分析确认的逻辑缺口”,说明默认 `0.8 m` 候选总长度上限与 `event ± 0.8 m` 合并包络的差异;使用“相距 1.0 m 的两个弯被合并后无法装进 0.8 m 窗口”的例子。
问题三必须标记为“待验证集成风险”,说明一次拼接会重算局部弧长,后续区域仍可能使用旧起止弧长;不得写成已经复现的线上故障。
- [ ] **Step 2: 写入单文件 HTML 问题附录**
HTML 顶部必须显示三种证据徽章:`已复现故障``静态分析确认``待验证风险`。页面主体必须提供三个编号问题卡片,每张卡片含“现象”“例子”“成因”“措施”四个短区块。使用内嵌 SVG 表达:
```text
问题一:粗路径成功 → 原始基线复验 InvalidInput → G2 尚未开始
问题二:两个相距 1.0 m 的事件 → 被合并 → 0.8 m 窗口无候选
问题三:先平滑区域 A → 弧长重算 → 区域 B 使用旧坐标
```
页面末尾必须列出行动优先级:先定位问题一、再为问题二添加窗口边界回归、最后为问题三添加双区域顺序替换回归。
- [ ] **Step 3: 校验问题分类、现象和行动顺序**
运行:
```powershell
$markdown = Get-Content -Raw -Encoding UTF8 'dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-report.md'
$html = Get-Content -Raw -Encoding UTF8 'dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html'
$markdownTerms = @('已复现故障', 'Expected=Success Actual=InvalidInput', '静态分析确认的逻辑缺口', '待验证集成风险', '相距 1.0 m', '任务 8')
$htmlTerms = @('<main', '<svg', '已复现故障', '静态分析确认', '待验证风险', '区域 A', '区域 B')
foreach ($term in $markdownTerms) { if (-not $markdown.Contains($term)) { throw "Issue report missing: $term" } }
foreach ($term in $htmlTerms) { if (-not $html.Contains($term)) { throw "Issue visualization missing: $term" } }
Write-Output 'Issue report checks passed.'
```
预期:输出 `Issue report checks passed.`
---
### Task 4: 做离线交付检查和可视化人工审阅
**Files:**
- Verify: `dailywork_report/Map_rep/.gitkeep`
- Verify: `dailywork_report/coarsepath_rep/.gitkeep`
- Verify: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md`
- Verify: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html`
- Verify: `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-report.md`
- Verify: `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html`
**Interfaces:**
- Consumes: 前三项任务的六个文件。
- Produces: 可离线打开、层级明确、相互一致的报告包。
- [ ] **Step 1: 验证完整文件集与禁止外部依赖**
运行:
```powershell
$files = @(
'dailywork_report/Map_rep/.gitkeep',
'dailywork_report/coarsepath_rep/.gitkeep',
'dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-report.md',
'dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html',
'dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-report.md',
'dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html'
)
foreach ($file in $files) { if (-not (Test-Path -LiteralPath $file)) { throw "Missing deliverable: $file" } }
$html = @(
Get-Content -Raw -Encoding UTF8 'dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html'
Get-Content -Raw -Encoding UTF8 'dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html'
) -join "`n"
if ($html -match 'https?://' -or $html -match '<script[^>]+src=') { throw 'HTML appendices must be self-contained.' }
Write-Output 'Dailywork report package checks passed.'
```
预期:输出 `Dailywork report package checks passed.`
- [ ] **Step 2: 在本地浏览器进行人工可读性审阅**
依次打开两个 HTML 文件,检查以下具体条件:
```text
算法附录:流程从左到右或从上到下可顺序阅读;绿色已实现节点与琥珀色未接入节点容易区分;输入和输出没有被流程图遮挡。
问题附录:三类证据徽章颜色和文字均可分辨;三个问题卡片的例子、成因、措施没有被截断;行动顺序位于页面末尾且与 Markdown 一致。
```
- [ ] **Step 3: 检查工作区改动范围**
运行:
```powershell
git diff --check
git status --short -- dailywork_report docs/superpowers/specs/2026-07-31-local-g2-dailywork-reports-design.md docs/superpowers/plans/2026-07-31-local-g2-dailywork-reports.md
```
预期:无空白错误;改动只包含本计划的设计、计划和日报交付文件。
@@ -0,0 +1,404 @@
# Local G2 Interactive Visualization Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将两份 Local G2 HTML 报告附录从流程卡片重做为可逐步查看路径变化、并能对照错误与正确预期的离线交互式可视化。
**Architecture:** 两页均继续为独立 HTML 文件。算法页使用一个固定坐标系的 SVG 和六个可切换状态,曲线由内嵌 JavaScript 的五次 Hermite 基函数计算并绘制;问题页使用三个可切换的 SVG 场景,每个场景同时呈现“实际发生 / 正确应有 / 差异原因”。所有教学几何固定标为典型示例,真实测试结论只以已知状态与原始文本呈现。
**Tech Stack:** HTML5、CSS、内嵌 SVG、原生 JavaScript、PowerShell 静态验证。
## Global Constraints
- 仅在 `dailywork_report/pathsmoothing_rep/` 下两个指定 HTML 路径重建内容;它们在本任务基线提交中尚未跟踪,因此 Git 可将首次纳入版本控制的重建页面显示为新增文件。不修改路径平滑算法、测试、地图、粗路径或两份 Markdown 报告的事实内容。
- 不使用任何外部资源:不使用 CDN、网络请求、外部图片、外部字体、第三方库或构建步骤。
- 算法页必须具备六个可访问步骤:粗路径、局部窗口、五次 Hermite、局部替换、曲率—弧长、安全与质量门。
- 问题页必须具备三个可访问问题场景;每个场景同时可见“实际发生”“正确应有”“差异原因”。
- 所有典型坐标、曲率图形和车辆示意均必须明确标注为机制解释,不得暗示为项目运行时实测结果。
- `RectangleDetour` 只陈述已复现的 `Success → InvalidInput`;失败点仍标记“待定位”,不得画出伪造的具体坏样本或根因。
- 用户已授权整体删除并从头重建两份旧 HTML;工作区存在无关改动,每个任务的提交只能包含其对应的一份 HTML,不得带入其他文件。
## File Structure
| 文件 | 职责 |
|---|---|
| `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html` | 六步 Local G2 路径几何演示、Hermite 曲线生成、键盘/按钮步骤导航与安全质量门示意。 |
| `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html` | 三个问题的实际/正确对照、场景切换与证据边界标注。 |
---
### Task 1: 重做算法页为六步路径几何演示
**Files:**
- Modify: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html`
**Interfaces:**
- Consumes: 无运行时数据;仅使用固定、标为典型示例的二维点和已确认的 G2 术语/门限。
- Produces: `setStep(index)``buildQuinticPath()``stepButtons``#algorithm-diagram``#step-title``#step-description``#step-status``#prev-step``#next-step`,供 HTML 初次渲染、按钮和左右方向键共用。
- [ ] **Step 1: 先运行会失败的结构验证**
Run:
```powershell
$file = 'dailywork_report\pathsmoothing_rep\01-local-g2-quintic-hermite-algorithm-visualization.html'
$html = Get-Content -Raw -Encoding UTF8 $file
$required = @('id="algorithm-diagram"', 'data-step="0"', 'data-step="5"', 'id="prev-step"', 'id="next-step"', 'function buildQuinticPath', 'function setStep')
$missing = @($required | Where-Object { -not $html.Contains($_) })
if ($missing.Count -gt 0) { throw "Algorithm visual contract missing: $($missing -join ', ')" }
```
Expected: FAIL because the current static flow page has no interactive step contract.
- [ ] **Step 2: 整体删除旧卡片页面并从头重建,让主 SVG 成为视觉中心**
Use `apply_patch` to整体删除当前文件内容并添加一个响应式新文档,结构如下:
```html
<main id="local-g2-algorithm-demo" data-step="0">
<header>…候选层已实现、正式发布流水线尚未接入…</header>
<p class="evidence-note">典型示例:用于解释机制,不代表某次测试的精确坐标。</p>
<nav class="stepper" aria-label="Local G2 平滑步骤">
<button type="button" class="step-button" data-step="0" aria-pressed="true">0 粗路径</button>
<button type="button" class="step-button" data-step="1" aria-pressed="false">1 局部窗口</button>
<button type="button" class="step-button" data-step="2" aria-pressed="false">2 Hermite 约束</button>
<button type="button" class="step-button" data-step="3" aria-pressed="false">3 局部替换</button>
<button type="button" class="step-button" data-step="4" aria-pressed="false">4 连续性效果</button>
<button type="button" class="step-button" data-step="5" aria-pressed="false">5 安全质量门</button>
</nav>
<section class="diagram-shell" aria-live="polite">
<div class="step-copy"><span id="step-status"></span><h2 id="step-title"></h2><p id="step-description"></p></div>
<svg id="algorithm-diagram" viewBox="0 0 1200 720" role="img" aria-labelledby="algorithm-svg-title algorithm-svg-desc">
<title id="algorithm-svg-title">Local G2 五次 Hermite 局部路径平滑步骤</title>
<desc id="algorithm-svg-desc">典型粗路径在局部曲率跳变处被五次 Hermite 曲线安全替换的六步示意。</desc>
<!-- 始终可见的坐标、粗路径和步骤图层 -->
</svg>
</section>
<div class="step-controls"><button id="prev-step" type="button">上一步</button><button id="next-step" type="button">下一步</button></div>
</main>
```
CSS requirements:
- `svg { width: 100%; height: auto; }`,不再使用 `min-width` 和横向滚动容器;窄屏按 `viewBox` 等比缩放。
- 用同一组语义颜色稳定表达:灰色原始粗路径、蓝色窗口/约束、绿色已接受候选、琥珀色发布边界、红色拒绝或阻断;同时配合实线/虚线、文字和符号。
- `.scene-layer` 默认淡出,`[data-step="N"] .scene-N` 显示;`prefers-reduced-motion: reduce` 时禁用转场。
- 不设置固定视口高度、不设置内部滚动,并保证按钮触摸目标和焦点状态清晰。
- [ ] **Step 3: 用真实五次 Hermite 基函数绘制典型候选曲线**
In the page script, define the fixed example endpoints and use the six quintic Hermite basis functions—not an SVG cubic Bézier substitute—to sample the visual candidate:
```javascript
const hermite = {
p0: { x: 290, y: 462 }, p1: { x: 690, y: 258 },
d0: { x: 180, y: 0 }, d1: { x: 210, y: -135 },
a0: { x: 0, y: -18 }, a1: { x: 22, y: -12 }
};
function quinticBasis(t) {
const t2 = t * t, t3 = t2 * t, t4 = t3 * t, t5 = t4 * t;
return [
1 - 10 * t3 + 15 * t4 - 6 * t5,
t - 6 * t3 + 8 * t4 - 3 * t5,
0.5 * (t2 - 3 * t3 + 3 * t4 - t5),
10 * t3 - 15 * t4 + 6 * t5,
-4 * t3 + 7 * t4 - 3 * t5,
0.5 * (t3 - 2 * t4 + t5)
];
}
function buildQuinticPath() {
const points = [];
for (let i = 0; i <= 48; i += 1) {
const [h00, h10, h20, h01, h11, h21] = quinticBasis(i / 48);
points.push({
x: h00 * hermite.p0.x + h10 * hermite.d0.x + h20 * hermite.a0.x + h01 * hermite.p1.x + h11 * hermite.d1.x + h21 * hermite.a1.x,
y: h00 * hermite.p0.y + h10 * hermite.d0.y + h20 * hermite.a0.y + h01 * hermite.p1.y + h11 * hermite.d1.y + h21 * hermite.a1.y
});
}
return points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x.toFixed(1)} ${point.y.toFixed(1)}`).join(' ');
}
```
Set the generated string on `#quintic-candidate`. Draw a separate raw polyline that shares the same window endpoints but has a visible heading/curvature break in its interior. Add persistent labels for start, end, direction, window boundary and `κ` jump; do not attach real-world units or claim these fixed coordinates are measured data.
- [ ] **Step 4: 实现六个可读状态的 SVG 图层**
Create six SVG groups, each carrying both `scene-layer` and `scene-0` through `scene-5` as appropriate. They must communicate these exact visual effects:
```text
scene-0: 原始离散点、方向箭头、突变点,候选曲线隐藏。
scene-1: 左右窗口边界和淡蓝色局部带高亮,其余粗路径降低不透明度。
scene-2: 两端切向箭头、二阶趋势弧线、虚线 quintic-candidate 可见。
scene-3: 灰色原折线与绿色候选曲线叠加,接缝用“替换开始/结束”标记。
scene-4: 上方替换后路径;下方 κ—s 趋势示意显示原始跳变与候选连续过渡,并标“趋势示意,非实测数据”。
scene-5: 三个车辆轮廓沿候选曲线放置;净空带、通过标记和“碰撞 / 净空 / 曲率 / 偏移”四个质量门可见。
```
Use `<path>`, `<circle>`, `<line>`, `<text>`, `<marker>` and simple `<g transform>` vehicle rectangles; do not use raster images. Put the existing “正式发布流水线尚未接入”的事实边界 below the visual, outside the six state layers.
- [ ] **Step 5: 接入状态更新与键盘操作**
Use one state function and no inline event handlers:
```javascript
const steps = [
['0 / 5', '原始 Hybrid A* 粗路径', '可行离散路径在局部接口处仍可能有曲率跳变。'],
['1 / 5', '检测并框定局部窗口', '只处理跳变附近,不重新搜索整条路径。'],
['2 / 5', '由端点约束构造五次 Hermite 候选', '位置、切向和曲率趋势共同确定局部曲线。'],
['3 / 5', '替换窗口内部的原始几何', '窗口外路径保持不变,接缝需要连续。'],
['4 / 5', '观察曲率—弧长连续性', '目标是消除接口处的趋势跳变,而不是只让外形更圆。'],
['5 / 5', '经安全与质量门决定接受或回退', '候选必须同时满足车体安全、净空、曲率和偏移约束。']
];
function setStep(index) {
const next = Math.max(0, Math.min(steps.length - 1, index));
const [status, title, description] = steps[next];
document.getElementById('local-g2-algorithm-demo').dataset.step = String(next);
document.getElementById('step-status').textContent = `步骤 ${status}`;
document.getElementById('step-title').textContent = title;
document.getElementById('step-description').textContent = description;
stepButtons.forEach((button) => button.setAttribute('aria-pressed', String(Number(button.dataset.step) === next)));
document.getElementById('prev-step').disabled = next === 0;
document.getElementById('next-step').disabled = next === steps.length - 1;
}
```
Declare `stepButtons` before `setStep`, register each button, register `#prev-step`/`#next-step`, handle only unmodified `ArrowLeft` and `ArrowRight` key presses, then call `setStep(0)`. Do not override keyboard interaction when the event target is a form control.
- [ ] **Step 6: 重新运行算法页验证,确认由失败转为通过**
Run the Step 1 command again, then run:
```powershell
$file = 'dailywork_report\pathsmoothing_rep\01-local-g2-quintic-hermite-algorithm-visualization.html'
$html = Get-Content -Raw -Encoding UTF8 $file
$terms = @('典型示例:用于解释机制', '曲率—弧长', '趋势示意,非实测数据', '完整车体安全', '正式发布流水线尚未接入')
$missing = @($terms | Where-Object { -not $html.Contains($_) })
if ($missing.Count -gt 0) { throw "Algorithm visual content missing: $($missing -join ', ')" }
Write-Output 'Algorithm interactive visualization checks passed.'
```
Expected: `Algorithm interactive visualization checks passed.`
---
### Task 2: 重做问题页为错误与正确预期的几何对照
**Files:**
- Modify: `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html`
**Interfaces:**
- Consumes: 已确认的三类证据、`RectangleDetour` 端到端输出、窗口约束数值和弧长陈旧风险的事实边界。
- Produces: `selectIssue(issueId)``issueButtons``#issue-visual``#issue-evidence``#issue-title``#issue-actual``#issue-expected``#issue-cause``#issue-action``#prev-issue``#next-issue`
- [ ] **Step 1: 先运行会失败的结构验证**
Run:
```powershell
$file = 'dailywork_report\pathsmoothing_rep\02-local-g2-issues-and-next-actions-visualization.html'
$html = Get-Content -Raw -Encoding UTF8 $file
$required = @('id="issue-visual"', 'data-issue="baseline"', 'data-issue="window"', 'data-issue="arclength"', 'function selectIssue', 'id="issue-actual"', 'id="issue-expected"', 'id="issue-cause"')
$missing = @($required | Where-Object { -not $html.Contains($_) })
if ($missing.Count -gt 0) { throw "Issue visual contract missing: $($missing -join ', ')" }
```
Expected: FAIL because the current page has only static issue cards and one causal flow diagram.
- [ ] **Step 2: 整体删除旧问题卡片页面并构建共享选择器与三栏事实说明**
Use `apply_patch` to整体删除当前文件内容并添加一个新文档,结构如下:
```html
<main id="local-g2-issue-demo" data-issue="baseline">
<header>…三种证据等级…</header>
<nav class="issue-selector" aria-label="选择要查看的 Local G2 问题">
<button type="button" class="issue-button" data-issue="baseline" aria-pressed="true">问题一:基线失败</button>
<button type="button" class="issue-button" data-issue="window" aria-pressed="false">问题二:窗口约束</button>
<button type="button" class="issue-button" data-issue="arclength" aria-pressed="false">问题三:弧长错位</button>
</nav>
<section class="issue-stage" aria-live="polite">
<div class="issue-copy"><span id="issue-evidence"></span><h2 id="issue-title"></h2></div>
<svg id="issue-visual" viewBox="0 0 1200 700" role="img" aria-labelledby="issue-svg-title issue-svg-desc"></svg>
<div class="compare-copy">
<article><h3>实际发生</h3><p id="issue-actual"></p></article>
<article><h3>正确应有</h3><p id="issue-expected"></p></article>
<article><h3>差异原因</h3><p id="issue-cause"></p></article>
</div>
<p class="next-action"><strong>下一步:</strong><span id="issue-action"></span></p>
</section>
<div class="issue-controls"><button id="prev-issue" type="button">上一个问题</button><button id="next-issue" type="button">下一个问题</button></div>
</main>
```
Use a single shared scale and distinct, labelled SVG lanes rather than three textual cards. Keep the priority/action order in a compact section below the interactive visual, not above it.
- [ ] **Step 3: 画出三个“实际 / 正确”对照场景,并保持证据边界**
Create `.issue-scene` SVG groups and make the selected group visible using `[data-issue="…"]` CSS. Each scene must implement these marks:
```text
baseline:
- 上方:概念性 RectangleDetour 绕障路径、障碍物、灰色原始路径;红色问号标“首次失败样本待定位”。
- 中间:实际链路 Hybrid A* Success → 原始基线复验 InvalidInput ⛔ → G2 候选未开始。
- 下方:正确链路 Hybrid A* Success → 原始基线复验 Success → G2 候选评价。
- 固定脚注:概念性几何,不代表尚未定位的实际坏样本。
window:
- 上方:同一条典型路径上的事件 A/B 和弧长标尺,中心距离直接标为 1.0 m。
- 中间左侧“实际”:两个 ±0.8 m 影响范围重叠后合并,合并总区间标“> 0.8 m”,红色叉号和“无候选”。
- 中间右侧“正确”:可行的拆分窗口或一致的长度策略,绿色窗口 A/B 和“可评价候选”。
- 下方:明确标“静态分析确认的逻辑缺口”。
arclength:
- 上方:替换前路径的 A、B 两个局部窗口和原始弧长标尺。
- 下方左侧“实际风险”:A 替换后路径长度改变,B-old 仍按旧弧长落在偏早位置;用虚线箭头表达旧映射。
- 下方右侧“正确”:重算/稳定锚点后 B-new 落在预期局部;用实线箭头表达新映射。
- 固定脚注:待正式多区域流水线接入后通过回归测试验证。
```
Do not draw a red collision marker or concrete bad curvature sample in `baseline`; only the question marker is allowed there. Pair every colored status with text (`实际`, `正确`, `待定位`, `无候选`, `重定位`) and a different line style or marker.
- [ ] **Step 4: 接入问题状态数据、导航和可访问性**
In the page script, use immutable descriptive data and one update function:
```javascript
const issues = {
baseline: {
evidence: '已复现故障 · RectangleDetour',
title: '原始基线在 Local G2 开始前被拒绝',
actual: 'Hybrid A* 粗路径规划成功,但原始基线统一复验返回 InvalidInput;候选生成没有开始。',
expected: '同一条成功规划的粗路径应先作为可行基线通过统一复验,再进入候选评价。',
cause: '首次非法数值或超限曲率样本尚未定位;不能把该失败归因于 G2 候选。',
action: '记录首次异常的方向段、样本、曲率、净空和验证结果,在不放松安全门的前提下定位源头。'
},
window: {
evidence: '静态分析确认的逻辑缺口',
title: '合并范围比可用候选窗口更宽',
actual: '相距 1.0 m 的事件被 ±0.8 m 范围合并,但候选总长度不能超过 0.8 m,结果没有候选。',
expected: '窗口合并和最大总长度应采用一致语义,或在不满足时拆分为可行局部窗口。',
cause: '影响范围的合并规则与候选总长度约束没有共同的可行性判断。',
action: '加入 1.0 m 间距回归,统一窗口长度语义并验证候选仍可生成。'
},
arclength: {
evidence: '待验证集成风险',
title: '区域 A 替换后,区域 B 的弧长坐标可能陈旧',
actual: 'A 拼接并重算弧长后,B 若仍使用替换前坐标,可能指向错误局部。',
expected: '处理 B 前应按当前路径重定位,或由稳定锚点映射恢复其原始语义位置。',
cause: '候选记录的原始弧长与每次拼接后重新计算的当前弧长处于不同坐标系。',
action: '构造 A 改变长度、B 仍准确定位的双区域回归,再接入正式流水线。'
}
};
function selectIssue(issueId) {
const issue = issues[issueId];
if (!issue) return;
document.getElementById('local-g2-issue-demo').dataset.issue = issueId;
document.getElementById('issue-evidence').textContent = issue.evidence;
document.getElementById('issue-title').textContent = issue.title;
document.getElementById('issue-actual').textContent = issue.actual;
document.getElementById('issue-expected').textContent = issue.expected;
document.getElementById('issue-cause').textContent = issue.cause;
document.getElementById('issue-action').textContent = issue.action;
issueButtons.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.issue === issueId)));
}
```
Declare an ordered `issueIds = ['baseline', 'window', 'arclength']`, implement previous/next by index, register button clicks, then call `selectIssue('baseline')`. Make the initial baseline state useful without JavaScript by placing its copy in the HTML before the script runs, then allow JavaScript to overwrite it with the same factually equivalent text.
- [ ] **Step 5: 重新运行问题页验证,确认由失败转为通过**
Run the Step 1 command again, then run:
```powershell
$file = 'dailywork_report\pathsmoothing_rep\02-local-g2-issues-and-next-actions-visualization.html'
$html = Get-Content -Raw -Encoding UTF8 $file
$terms = @('Success → InvalidInput', '首次失败样本待定位', '1.0 m', '±0.8 m', '> 0.8 m', '弧长重算', '实际发生', '正确应有', '差异原因')
$missing = @($terms | Where-Object { -not $html.Contains($_) })
if ($missing.Count -gt 0) { throw "Issue visual content missing: $($missing -join ', ')" }
Write-Output 'Issue interactive visualization checks passed.'
```
Expected: `Issue interactive visualization checks passed.`
---
### Task 3: 离线完整性、交互契约和可视化验收
**Files:**
- Verify: `dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html`
- Verify: `dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html`
**Interfaces:**
- Consumes: 两页已实现的 DOM id、数据属性、函数名和离线资源限制。
- Produces: 可复查的 PowerShell 验证输出,以及在浏览器可用时的人工交互验收结论。
- [ ] **Step 1: 验证离线性、文件结构和静态交互契约**
Run:
```powershell
$files = @(
'dailywork_report\pathsmoothing_rep\01-local-g2-quintic-hermite-algorithm-visualization.html',
'dailywork_report\pathsmoothing_rep\02-local-g2-issues-and-next-actions-visualization.html'
)
$contracts = @{
$files[0] = @('<!doctype html>', '<svg', '<script>', 'function buildQuinticPath', 'function setStep', 'id="algorithm-diagram"', 'data-step="5"')
$files[1] = @('<!doctype html>', '<svg', '<script>', 'function selectIssue', 'id="issue-visual"', 'data-issue="arclength"', 'id="issue-cause"')
}
foreach ($file in $files) {
$html = Get-Content -Raw -Encoding UTF8 $file
$missing = @($contracts[$file] | Where-Object { -not $html.Contains($_) })
if ($missing.Count -gt 0) { throw "$file missing: $($missing -join ', ')" }
if ($html -match 'https?://|<script[^>]+\bsrc\s*=|<img[^>]+\bsrc\s*=') { throw "$file must not depend on external resources." }
if (($html -split '<svg').Count -lt 2) { throw "$file must contain a main SVG." }
}
Write-Output 'Offline HTML and interaction contracts passed.'
```
Expected: `Offline HTML and interaction contracts passed.`
- [ ] **Step 2: 验证 JavaScript 所查询的 DOM 节点均存在**
Run:
```powershell
$checks = @{
'dailywork_report\pathsmoothing_rep\01-local-g2-quintic-hermite-algorithm-visualization.html' = @('local-g2-algorithm-demo','step-status','step-title','step-description','prev-step','next-step','quintic-candidate')
'dailywork_report\pathsmoothing_rep\02-local-g2-issues-and-next-actions-visualization.html' = @('local-g2-issue-demo','issue-evidence','issue-title','issue-actual','issue-expected','issue-cause','issue-action','prev-issue','next-issue')
}
foreach ($entry in $checks.GetEnumerator()) {
$html = Get-Content -Raw -Encoding UTF8 $entry.Key
$missing = @($entry.Value | Where-Object { -not $html.Contains("id=`"$_`"") })
if ($missing.Count -gt 0) { throw "$($entry.Key) queried ids missing: $($missing -join ', ')" }
}
Write-Output 'DOM query targets passed.'
```
Expected: `DOM query targets passed.`
- [ ] **Step 3: 进行浏览器交互和窄屏人工验收,或如实记录不可用状态**
When an in-app browser is available, open each local HTML and verify:
```text
算法页:初始第 0 步可见;连续点六个步骤均可切换;上/下一步禁用状态正确;左右键切换;窄宽度下标签、曲线和按钮不重叠。
问题页:三个问题均可切换;每个场景同时可看到实际发生、正确应有、差异原因;问题一没有伪造坏样本;窄宽度下文本可读。
```
If no browser is available, do not substitute unapproved browser tooling or claim visual QA passed. Record that offline/static checks pass but browser-based visual inspection remains unavailable.
- [ ] **Step 4: 检查改动范围与空白字符**
Run:
```powershell
git diff --check -- dailywork_report/pathsmoothing_rep/01-local-g2-quintic-hermite-algorithm-visualization.html dailywork_report/pathsmoothing_rep/02-local-g2-issues-and-next-actions-visualization.html
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
git status --short -- dailywork_report/pathsmoothing_rep/
```
Expected: no whitespace errors; each任务提交仅包含其对应 HTML 文件,工作区状态不出现由本次工作带入的其他文件。
@@ -0,0 +1,558 @@
# Local G2 Diagnostic Visualization Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Export a factual Local G2 visualization that shows the ordinary strict `Unchanged` result and the recorded clearance-rejected diagnostic candidate without publishing or recommending that candidate.
**Architecture:** The existing comparison service remains the source for normal Local G2 status and output. A test/demo-only evidence loader reads a compact immutable record of `single-turn/s0/r0/w5/seed2`, reconstructs a visual-only spliced path with the existing preprocessor/splicer/analyzer, and appends it to an immutable figure model. The existing SVG/PNG/CSV exporter then produces the six standard figures plus a seventh diagnostic figure only for that augmented model.
**Tech Stack:** C# 10 targeting `netstandard2.0`, Newtonsoft.Json 13.0.4 already referenced by `ClumsyPilot.csproj`, existing `System.Drawing` PNG renderer, PowerShell verification scripts, .NET 10 Windows verification host.
## Global Constraints
- Do not modify `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/LocalG2/`, evaluator, validator, publication, or recommendation logic.
- The normal comparison must retain the actual Local G2 `PathSmoothingStatus`; a diagnostic candidate must never create a `PathSmoothingResult` or a recommendation.
- Evidence is fixed to fixture SHA-256 `3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563`, batch SHA-256 `ac8166828813d85bf6f8b58f985839e5b2a049e75cb94186ed04f59d540e4eed`, and stable key `single-turn/s0/r0/w5/seed2`.
- Preserve normal-export file stems `01-coarse-path-overview` through `06-curvature-comparison` and its six-file contract.
- The diagnostic candidate must be labelled `净空拒绝,未发布`; do not render a collision cross because the evidence records a clearance rejection, not an occupied-cell collision.
- Keep trajectories point-only. Do not add SVG paths or dashed stroke rendering.
- Place generated artifacts only below `ClumsyPilot/obj/path_smoothing_reports`.
- Use targeted `git add -- <paths>` and `git commit --only -- <paths>`; do not include unrelated worktree changes.
---
## File Structure
| Path | Responsibility |
| --- | --- |
| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs` | Adds normal Local G2 to the immutable default offline comparison order. |
| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs` | Holds normal Local G2 and diagnostic-candidate colors. |
| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs` | Adds normal Local G2 series and CSV metric row. |
| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs` | Creates an immutable model copy with one added diagnostic series. |
| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs` | Moves a five-entry legend upward enough to remain inside the fixed figure height. |
| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs` | Names the optional seventh figure. |
| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs` | Adds Local G2 to normal comparison figures and conditionally creates figure 07. |
| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json` | Immutable compact evidence extract used by the visual-only route. |
| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs` | Parses and verifies evidence identity, geometry, and rejection state. |
| `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs` | Reconstructs renderable candidate geometry and invokes the existing exporter. |
| `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs` | Verifies and exports the augmented seven-file report. |
| `ClumsyPilot/tests/verify_path_smoothing_comparison.ps1` | Checks Local G2 default order and normal comparison semantics. |
| `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1` | Checks normal Local G2 figure/CSV content and visual layout contracts. |
| `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1` | Checks evidence parsing and deterministic rejection validation. |
| `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1` | Calls the verification host for the seven-file diagnostic report. |
| `ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1` | Builds and exports the user-facing Local G2 diagnostic image. |
## Task 1: Add Normal Local G2 To Existing Comparison Reports
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs`
- Modify: `ClumsyPilot/tests/verify_path_smoothing_comparison.ps1`
- Modify: `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1`
- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs`
**Interfaces:**
- Consumes: `PathSmoothingComparisonRequest(PathSmoothingRequest, IReadOnlyList<SmoothingMethod> methods = null)` and `SmoothingFigureModelBuilder.Build(PathSmoothingComparisonResult, PlanningGridMap, Pose2D, Pose2D, string, string)`.
- Produces: A default method order of `CubicBSpline`, `LocalCubicBezier`, `PiecewiseQuintic`, `LocalG2Quintic`; figure key `local-g2`; CSV method `LocalG2Quintic`; unchanged standard six-figure export count.
- [ ] **Step 1: Write the failing default-order and standard-model assertions**
In `verify_path_smoothing_comparison.ps1`, parse the Local G2 enum and construct a request without the optional methods list. Add these assertions after the explicit three-method request assertions:
```powershell
$localG2 = [Enum]::Parse($methodType, 'LocalG2Quintic')
$defaultComparisonRequest = [Activator]::CreateInstance($comparisonRequestType, @((New-SmoothingRequest), $null))
Assert-Equal 4 $defaultComparisonRequest.Methods.Count 'Default comparison must include Local G2.'
Assert-Equal 'CubicBSpline,LocalCubicBezier,PiecewiseQuintic,LocalG2Quintic' (($defaultComparisonRequest.Methods | ForEach-Object ToString) -join ',') 'Default comparison order must be stable.'
```
In `verify_path_smoothing_svg_csv.ps1`, add `#56B4E9` to the expected normal SVG colors, assert that `$model.Series` includes a series whose `Key` is `local-g2`, and assert that the generated CSV contains `LocalG2Quintic,Unchanged` for the frozen `single-turn` request. Add assertions that the standard figure set still contains only the six existing stems.
In `Program.cs`, add a `VerifyNormalLocalG2Series(SmoothingFigureModel model)` call immediately after `VerifySixFigureDefinitionContract(model)`. It must require exactly one `local-g2` series and a `PathSmoothingStatus` value defined by the enum. If its strict output path is visible, require that it has at least two samples. Do not hard-code `Unchanged` for this artificial high-curvature host fixture; the frozen `single-turn` evidence assertion in the SVG/CSV test owns that requirement.
- [ ] **Step 2: Run the focused checks and confirm they fail before implementation**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1
```
Expected: the comparison assertion reports three default methods and the SVG/CSV check cannot find `local-g2` or `#56B4E9`.
- [ ] **Step 3: Implement the smallest normal-comparison extension**
Append the enum in the existing default array, preserving all existing order:
```csharp
private static readonly SmoothingMethod[] DefaultMethods =
{
SmoothingMethod.CubicBSpline,
SmoothingMethod.LocalCubicBezier,
SmoothingMethod.PiecewiseQuintic,
SmoothingMethod.LocalG2Quintic,
};
```
Add these fixed colors to `IeeeFigureStyle`:
```csharp
public const string LocalG2Color = "#56B4E9";
public const string LocalG2DiagnosticColor = "#B1373E";
```
In `SmoothingFigureModelBuilder`, append the normal series and metric row after the existing piecewise-quintic entries. Use the exact stable key and label:
```csharp
series.Add(CreateSeries(
Find(comparison, SmoothingMethod.LocalG2Quintic),
SmoothingMethod.LocalG2Quintic,
"local-g2",
"局部 G2",
IeeeFigureStyle.LocalG2Color,
string.Empty,
false,
map));
CreateRow(Find(comparison, SmoothingMethod.LocalG2Quintic), "LocalG2Quintic", "局部 G2")
```
In `SmoothingFigureSetBuilder.Build`, resolve `local-g2` with the other standard series and include it in only the existing all-path and curvature comparisons:
```csharp
SmoothingFigureSeries localG2 = Find(model, "local-g2");
// Add View(localG2, 1d) after View(quintic, 1d) in figures 02 and 06.
```
Keep individual figures `03` through `05` unchanged. In `SmoothingFigureDefinition`, use a five-entry-safe legend origin:
```csharp
public double LegendYPoints => LegendEntries.Count > 4 ? 332d : 340d;
```
Do not special-case `Unchanged`: `PathSmoothingComparisonService` already supplies its strict path and status. Do not modify that service or the ranker.
- [ ] **Step 4: Run the focused checks and confirm they pass**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_png.ps1
```
Expected: all three scripts exit `0`; normal exports retain exactly six figures; the Local G2 row is present with its actual `Unchanged` status.
- [ ] **Step 5: Commit only Task 1 files**
```powershell
git add -- `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs `
ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 `
ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1 `
ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs
git -c core.autocrlf=false diff --cached --check
git commit --only -m "feat: show Local G2 in smoothing comparisons" -- `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/IeeeFigureStyle.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModelBuilder.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureDefinition.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs `
ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 `
ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1 `
ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs
```
### Task 2: Freeze And Validate The Diagnostic Evidence Extract
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json`
- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs`
- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1`
**Interfaces:**
- Produces: `public sealed class LocalG2DiagnosticEvidenceLoader` with `public LocalG2DiagnosticEvidence LoadAndVerify(string evidencePath)`.
- Produces: `LocalG2DiagnosticEvidence` properties `ScenarioId`, `FixtureSha256`, `CandidateStableKey`, `CandidateSha256`, `CandidateIndex`, `SegmentIndex`, `WindowStartArcLengthMeters`, `WindowEndArcLengthMeters`, start/end curvatures, `CandidatePoints`, `EvaluatorResult`, `StopGate`, and `PublishedStatus`.
- Consumed later by: `LocalG2DiagnosticVisualizationDemo.Export(string fixturePath, string evidencePath, string outputDirectory, CancellationToken cancellationToken = default)`.
- [ ] **Step 1: Write the failing evidence-loader verification script**
Create `verify_path_smoothing_local_g2_diagnostic_evidence.ps1`. Load `ClumsyPilot.dll`, resolve `MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.LocalG2DiagnosticEvidenceLoader`, and invoke `LoadAndVerify` with the new fixture path. Assert all of these exact values:
```powershell
Assert-Equal 'single-turn' $evidence.ScenarioId 'Diagnostic evidence scenario must be stable.'
Assert-Equal 'single-turn/s0/r0/w5/seed2' $evidence.CandidateStableKey 'Diagnostic evidence key must be stable.'
Assert-Equal 5 $evidence.CandidateIndex 'Diagnostic evidence candidate index must be stable.'
Assert-Equal 10 $evidence.CandidatePoints.Count 'Diagnostic evidence must retain all ten recorded samples.'
Assert-Equal 'InsufficientClearance' $evidence.EvaluatorResult 'Diagnostic evidence must retain the observed evaluator result.'
Assert-Equal 'Clearance' $evidence.StopGate 'Diagnostic evidence must retain the observed stop gate.'
Assert-Equal 'Unchanged' $evidence.PublishedStatus 'Diagnostic evidence must retain the strict final status.'
```
Copy the JSON to a uniquely named temp path, replace only `"StopGate": "Clearance"` with `"StopGate": "Collision"`, and require that `LoadAndVerify` throws. Delete the temp copy in `finally`.
- [ ] **Step 2: Run the evidence check and confirm it fails before implementation**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_evidence.ps1
```
Expected: failure because the loader type and evidence file do not exist.
- [ ] **Step 3: Add the immutable compact evidence extract and loader**
Create the JSON file with this exact top-level contract and the ten recorded `CandidatePoints`. Preserve the displayed IEEE-754 decimal values; they are the evidence values, not rounded drawing inputs:
```json
{
"SourceMeasurementBatchSha256": "ac8166828813d85bf6f8b58f985839e5b2a049e75cb94186ed04f59d540e4eed",
"FixtureSha256": "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563",
"ScenarioId": "single-turn",
"CandidateStableKey": "single-turn/s0/r0/w5/seed2",
"CandidateSha256": "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392",
"CandidateIndex": 5,
"SegmentIndex": 0,
"WindowStartArcLengthMeters": 1.9199999999999982,
"WindowEndArcLengthMeters": 2.1199999999999983,
"StartGeometricCurvaturePerMeter": 0.41666666666666663,
"EndGeometricCurvaturePerMeter": 0.0,
"StartVehicleCurvaturePerMeter": 0.41666666666666663,
"EndVehicleCurvaturePerMeter": 0.0,
"EvaluatorResult": "InsufficientClearance",
"StopGate": "Clearance",
"PublishedStatus": "Unchanged",
"CandidatePoints": [
{ "X": 2.7216397035494579, "Y": 1.7279184433567971, "ReferenceArcLengthMeters": 1.9199999999999982, "HeadingRadians": 0.799999999999999, "UnwrappedHeadingRadians": 0.799999999999999, "Source": 4 },
{ "X": 2.7355188205174521, "Y": 1.7423187041196302, "ReferenceArcLengthMeters": 1.9399999999999982, "HeadingRadians": 0.80751185024996908, "UnwrappedHeadingRadians": 0.80751185024996908, "Source": 4 },
{ "X": 2.7492871654421882, "Y": 1.7568248978050907, "ReferenceArcLengthMeters": 1.9599999999999982, "HeadingRadians": 0.8157618125921976, "UnwrappedHeadingRadians": 0.8157618125921976, "Source": 4 },
{ "X": 2.7629234692741469, "Y": 1.7714552535786874, "ReferenceArcLengthMeters": 1.9799999999999982, "HeadingRadians": 0.82542852165703573, "UnwrappedHeadingRadians": 0.82542852165703573, "Source": 4 },
{ "X": 2.7764244476704873, "Y": 1.786210614200662, "ReferenceArcLengthMeters": 1.9999999999999982, "HeadingRadians": 0.83333333333333237, "UnwrappedHeadingRadians": 0.83333333333333237, "Source": 4 },
{ "X": 2.7925350453990334, "Y": 1.803999635249544, "ReferenceArcLengthMeters": 2.0239999999999982, "HeadingRadians": 0.835253334801853, "UnwrappedHeadingRadians": 0.835253334801853, "Source": 4 },
{ "X": 2.80865418003672, "Y": 1.8217809211927092, "ReferenceArcLengthMeters": 2.0479999999999983, "HeadingRadians": 0.83333333355178374, "UnwrappedHeadingRadians": 0.83333333355178374, "Source": 4 },
{ "X": 2.8248074262445737, "Y": 1.8395312269498194, "ReferenceArcLengthMeters": 2.0719999999999983, "HeadingRadians": 0.8318933325232104, "UnwrappedHeadingRadians": 0.8318933325232104, "Source": 4 },
{ "X": 2.8409691920174533, "Y": 1.8572737784943611, "ReferenceArcLengthMeters": 2.0959999999999983, "HeadingRadians": 0.83237333274470626, "UnwrappedHeadingRadians": 0.83237333274470626, "Source": 4 },
{ "X": 2.8571139169604551, "Y": 1.8750318365841865, "ReferenceArcLengthMeters": 2.1199999999999983, "HeadingRadians": 0.83333333333333237, "UnwrappedHeadingRadians": 0.83333333333333237, "Source": 4 }
]
}
```
Implement the public loader in the test namespace using `Newtonsoft.Json.JsonConvert.DeserializeObject<LocalG2DiagnosticEvidence>(File.ReadAllText(evidencePath))`. Use `InvalidDataException` for every rejected input. The validation must require the three exact SHA/key constants, `ScenarioId == "single-turn"`, `CandidateIndex == 5`, `SegmentIndex == 0`, `EvaluatorResult == "InsufficientClearance"`, `StopGate == "Clearance"`, and `PublishedStatus == "Unchanged"`.
Validate the two window values and all point coordinates/headings/reference arc lengths with this helper:
```csharp
private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value);
```
Require exactly ten points, first/last reference arcs equal the window endpoints within `1e-8d`, strictly increasing reference arcs, `Source == (int)SmoothedPathPointSource.LocalG2Transition`, and non-null start/end curvature values. Return only after all checks pass.
- [ ] **Step 4: Run the evidence verification and confirm it passes**
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_evidence.ps1
```
Expected: exit `0`; the valid extract loads and the altered stop gate is rejected.
- [ ] **Step 5: Commit only Task 2 files**
```powershell
git add -- `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs `
ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1
git -c core.autocrlf=false diff --cached --check
git commit --only -m "test: freeze Local G2 diagnostic evidence" -- `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs `
ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1
```
### Task 3: Build The Visual-Only Candidate And Optional Figure 07
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs`
- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs`
- Create: `ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1`
**Interfaces:**
- Produces: `internal SmoothingFigureModel WithAdditionalSeries(SmoothingFigureSeries series)` that preserves all existing model layout, metric rows, endpoints, and scales.
- Produces: `SmoothingFigureKind.LocalG2DiagnosticCandidate` and optional stem `07-local-g2-diagnostic-candidate`.
- Produces: `public SmoothingReportExportResult LocalG2DiagnosticVisualizationDemo.Export(string fixturePath, string evidencePath, string outputDirectory, CancellationToken cancellationToken = default)`.
- Produces: a host verification command that accepts a fixture path and evidence path, plus an export command that accepts fixture, evidence, and output-directory paths.
- [ ] **Step 1: Write the failing seven-file verification host branch and wrapper script**
In `Program.Main`, add command dispatch before the existing fixture/export cases:
```csharp
if (arguments.Length == 3 && arguments[0] == "--verify-local-g2-diagnostic")
{
VerifyLocalG2Diagnostic(arguments[1], arguments[2]);
Console.WriteLine("Local G2 diagnostic visualization verification completed.");
return 0;
}
if (arguments.Length == 4 && arguments[0] == "--export-local-g2-diagnostic")
{
ExportLocalG2Diagnostic(arguments[1], arguments[2], arguments[3]);
return 0;
}
```
Create the wrapper script to resolve the fixture and evidence paths and run this command:
```powershell
& dotnet run --project $hostProject --no-restore -- --verify-local-g2-diagnostic $resolvedFixturePath $resolvedEvidencePath
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
```
Run it before the demo/model implementation. Expected: build error because `VerifyLocalG2Diagnostic` and `ExportLocalG2Diagnostic` do not yet exist.
- [ ] **Step 2: Add immutable model augmentation and the optional figure definition**
Add this internal method to `SmoothingFigureModel`:
```csharp
internal SmoothingFigureModel WithAdditionalSeries(SmoothingFigureSeries series)
{
if (series == null) throw new ArgumentNullException(nameof(series));
var combined = new List<SmoothingFigureSeries>(Series.Count + 1);
for (int index = 0; index < Series.Count; index++)
{
if (Series[index].Key == series.Key)
throw new ArgumentException("Figure series keys must be unique.", nameof(series));
combined.Add(Series[index]);
}
combined.Add(series);
var copy = new SmoothingFigureModel(
ScenarioId, ScenarioLabel, WorldXMinMeters, WorldXMaxMeters, WorldYMinMeters, WorldYMaxMeters,
PathPanelX, PathPanelY, PathPanelWidth, PathPanelHeight,
CurvaturePanelX, CurvaturePanelY, CurvaturePanelWidth, CurvaturePanelHeight,
MetricsPanelX, MetricsPanelY, MetricsPanelWidth, MetricsPanelHeight,
Obstacles, combined, MetricRows, Start, Goal)
{
PathScaleX = PathScaleX,
PathScaleY = PathScaleY,
};
return copy;
}
```
Add `LocalG2DiagnosticCandidate` to `SmoothingFigureKind`. In `SmoothingFigureSetBuilder.Build`, retain the six normal definitions first. Use a non-throwing `TryFind` helper for key `local-g2-diagnostic`; when it succeeds, append exactly this overhead figure:
```csharp
BuildOverhead(
SmoothingFigureKind.LocalG2DiagnosticCandidate,
"07-local-g2-diagnostic-candidate",
"G2 诊断候选:净空拒绝,未发布;严格输出=原始路径",
model,
true,
View(raw, 0.45d),
View(diagnostic, 1d))
```
The diagnostic series must carry key `local-g2-diagnostic`, status `PathSmoothingStatus.Infeasible`, color `IeeeFigureStyle.LocalG2DiagnosticColor`, and an empty `ViolationMarkers` list. Do not reuse `SmoothingFigureModelBuilder.CreateSeries`, because its generic `Infeasible` behavior synthesizes a violation cross when it cannot identify an occupied point.
- [ ] **Step 3: Implement `LocalG2DiagnosticVisualizationDemo` with the existing geometry components**
The class stays in namespace `MultiWheelC.TrajectoryPlanning.PathSmoothing.Test` and creates no `PathSmoothingResult`. Its `Export` method must execute this exact sequence:
```csharp
LocalG2DiagnosticEvidence evidence = _evidenceLoader.LoadAndVerify(evidencePath);
PathSmoothingComparisonRequest request = FindFixtureRequest(fixturePath, evidence.ScenarioId);
PathSmoothingComparisonResult comparison = _comparisonService.Compare(request, cancellationToken);
PathSmoothingComparisonEntry localG2 = FindEntry(comparison, SmoothingMethod.LocalG2Quintic);
Require(localG2 != null && localG2.Status == PathSmoothingStatus.Unchanged,
"Strict Local G2 result must be Unchanged for the diagnostic evidence.");
RequireSameGeometry(comparison.RawPathBaseline.Path, localG2.Path);
_preprocessor.TryPrepare(request.SmoothingRequest, out PreparedPath prepared, out string reason);
LocalG2CandidateGeometry candidate = CreateCandidate(evidence);
_splicer.TryReplace(prepared, candidate, out PreparedPath spliced, out reason);
_analyzer.TryAnalyze(spliced.Segments, request.SmoothingRequest.Configuration.OutputSpacingMeters, out PathGeometryAnalysis analysis, out reason);
CoarsePathPoint first = request.SmoothingRequest.CoarsePath[0];
CoarsePathPoint last = request.SmoothingRequest.CoarsePath[request.SmoothingRequest.CoarsePath.Count - 1];
SmoothingFigureModel normal = _figureBuilder.Build(
comparison,
request.SmoothingRequest.Map,
new Pose2D(first.X, first.Y, first.Heading),
new Pose2D(last.X, last.Y, last.Heading),
evidence.ScenarioId,
evidence.ScenarioId);
SmoothingFigureModel augmented = normal.WithAdditionalSeries(CreateDiagnosticSeries(analysis.Path));
return _exporter.Export(new SmoothingReportExportRequest { Model = augmented, OutputDirectory = outputDirectory, FileStem = "comparison" });
```
`FindFixtureRequest` must call `SmoothingScenarioFactory.CreateFixtureRequests(fixturePath)`, locate exactly one request by the matching fixture index from `SmoothingScenarioFixtureLoader.LoadAndVerify(fixturePath)`, and reject missing or duplicate `single-turn` IDs. `CreateCandidate` must turn every evidence point into:
```csharp
new SmoothingPoint2D(
point.X, point.Y, point.ReferenceArcLengthMeters,
point.HeadingRadians, point.UnwrappedHeadingRadians,
0d, false, SmoothedPathPointSource.LocalG2Transition)
```
Construct `LocalG2CandidateGeometry` with evidence index/window/curvatures, `0d` left and right lengths, the converted point list, and `true` for `internalConnectionsAreG2`. The zero clearance exists only to satisfy geometry-analysis input validity; it must not be fed to a validator or a metric row.
`CreateDiagnosticSeries` must convert `analysis.Path` to `SmoothingFigurePoint` values, use label `G2 候选(净空拒绝,未发布)`, and supply `Array.Empty<SmoothingFigurePoint>()` as violation markers. Its path can be visualized but is not a published/safe path.
- [ ] **Step 4: Implement host verification and run it**
`VerifyLocalG2Diagnostic` must create a fresh temp output directory, invoke the demo, and require all of the following before deleting the directory in `finally`:
```csharp
Require(report.Status == SmoothingReportExportStatus.Success, "Diagnostic report export failed: " + report.Reason);
Require(report.SvgPaths.Count == 7 && report.PngPaths.Count == 7 && File.Exists(report.CsvPath), "Diagnostic export must publish seven SVGs, seven PNGs and one CSV.");
Require(Path.GetFileName(report.SvgPaths[6]) == "07-local-g2-diagnostic-candidate.svg", "Diagnostic SVG stem is incorrect.");
Require(Path.GetFileName(report.PngPaths[6]) == "07-local-g2-diagnostic-candidate.png", "Diagnostic PNG stem is incorrect.");
string diagnosticSvg = File.ReadAllText(report.SvgPaths[6]);
Require(diagnosticSvg.Contains("data-series=\"raw\"") && diagnosticSvg.Contains("data-series=\"local-g2-diagnostic\""), "Diagnostic SVG must contain raw and diagnostic samples.");
Require(diagnosticSvg.Contains("净空拒绝") && diagnosticSvg.Contains("未发布"), "Diagnostic SVG must disclose rejection and publication state.");
Require(!diagnosticSvg.Contains("violation-cross"), "Clearance rejection must not be drawn as an obstacle collision.");
Require(File.ReadAllText(report.CsvPath).Contains("LocalG2Quintic,Unchanged"), "CSV must retain the normal strict Local G2 row.");
```
Run `VerifyPng(File.ReadAllBytes(path))` for every returned PNG and use the existing `ContainsTemporaryFiles` helper to assert atomic publication. Also copy the evidence file to a temp file, alter the fixture hash, call the demo with a separate empty output path, require an exception, and require that the output directory was never created.
Run:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_visualization.ps1
```
Expected: exit `0`, seven valid figures, raw and diagnostic series in `07`, no fabricated collision cross, and bad evidence rejected atomically.
- [ ] **Step 5: Commit only Task 3 files**
```powershell
git add -- `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs `
ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs `
ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1
git -c core.autocrlf=false diff --cached --check
git commit --only -m "feat: export Local G2 diagnostic candidate" -- `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureModel.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureKind.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingFigureSetBuilder.cs `
ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs `
ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs `
ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_visualization.ps1
```
### Task 4: Add The User-Facing Export Script And Perform Full Acceptance
**Files:**
- Create: `ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1`
- Modify: `ClumsyPilot/tests/verify_path_smoothing_png.ps1`
**Interfaces:**
- Consumes: the diagnostic host export command, its fixture path, its evidence path, and its bounded output directory.
- Produces: `ClumsyPilot/obj/path_smoothing_reports/local-g2-single-turn/07-local-g2-diagnostic-candidate.png` and six companion standard figures.
- [ ] **Step 1: Write the failing runner assertion in the PNG smoke test**
In `verify_path_smoothing_png.ps1`, add a call to the not-yet-created runner with an explicit output directory below `obj/path_smoothing_reports`, followed by an existence assertion for its primary PNG. Its default runner paths must be:
```powershell
[string]$FixturePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\path-smoothing-fixtures.json'),
[string]$EvidencePath = (Join-Path $PSScriptRoot '..\ParkrobTrajplanner\PathSmoothing\Test\Fixtures\local-g2-diagnostic-single-turn.json'),
[string]$OutputDirectory = (Join-Path $PSScriptRoot '..\obj\path_smoothing_reports\local-g2-single-turn')
```
Add this smoke-test block after the existing host invocation, changing no unrelated test behavior:
```powershell
$runnerPath = Join-Path $PSScriptRoot 'run_local_g2_diagnostic_visualization.ps1'
$runnerOutput = Join-Path $PSScriptRoot '..\obj\path_smoothing_reports\local-g2-png-smoke'
& powershell -ExecutionPolicy Bypass -File $runnerPath -OutputDirectory $runnerOutput
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$primaryPng = Join-Path $runnerOutput '07-local-g2-diagnostic-candidate.png'
if (-not (Test-Path -LiteralPath $primaryPng)) {
throw "Local G2 diagnostic runner did not publish $primaryPng"
}
```
Run the PNG smoke test. Expected: PowerShell reports that `run_local_g2_diagnostic_visualization.ps1` does not exist, so the runner acceptance assertion fails before implementation.
- [ ] **Step 2: Implement the bounded export script and extend the PNG smoke test**
Follow the existing `run_path_smoothing_comparison.ps1` root validation exactly: resolve `$clumsyPilotRoot`, require `$OutputDirectory` to equal or be below `$clumsyPilotRoot\obj\path_smoothing_reports`, resolve both input files, build `ClumsyPilot.csproj`, then execute:
```powershell
& dotnet run --project $hostProject --no-restore -- --export-local-g2-diagnostic $resolvedFixturePath $resolvedEvidencePath $resolvedOutputDirectory
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Output "Local G2 diagnostic visualization written below $resolvedOutputDirectory"
```
In `verify_path_smoothing_png.ps1`, call the new diagnostic verification wrapper after the existing host verification so both standard six-file and diagnostic seven-file image contracts run in the normal PNG check. Retain the runner call and primary-PNG existence assertion added in Step 1; use a `local-g2-png-smoke` output subdirectory below the allowed report root.
- [ ] **Step 3: Run the full automated acceptance sequence**
Run these commands in order and inspect every exit code:
```powershell
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_comparison.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_svg_csv.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_evidence.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_local_g2_diagnostic_visualization.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_path_smoothing_png.ps1
powershell -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\run_local_g2_diagnostic_visualization.ps1
```
Expected: every command exits `0`; the primary PNG, matching SVG, and `comparison.csv` exist under `ClumsyPilot/obj/path_smoothing_reports/local-g2-single-turn`.
- [ ] **Step 4: Inspect the generated image and report the factual result**
Open the primary file with the local image viewer:
```text
ClumsyPilot/obj/path_smoothing_reports/local-g2-single-turn/07-local-g2-diagnostic-candidate.png
```
Confirm visually that the map is nonblank, the gray raw/final path and red diagnostic candidate are both visible, the legend/title disclose `净空拒绝` and `未发布`, there is no collision cross, and text remains inside the 4296-by-3120 PNG frame. Also open `02-all-paths-comparison.png` and `06-curvature-comparison.png` from the same directory to confirm that normal Local G2 appears with `Unchanged` alongside the existing methods.
- [ ] **Step 5: Commit only Task 4 source/test files**
Do not commit generated `obj` artifacts. Commit only the runner and PNG smoke-test changes:
```powershell
git add -- `
ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 `
ClumsyPilot/tests/verify_path_smoothing_png.ps1
git -c core.autocrlf=false diff --cached --check
git commit --only -m "test: add Local G2 diagnostic visualization runner" -- `
ClumsyPilot/tests/run_local_g2_diagnostic_visualization.ps1 `
ClumsyPilot/tests/verify_path_smoothing_png.ps1
```
## Final Verification Checklist
- [ ] Re-read [`2026-08-02-local-g2-diagnostic-visualization-design.md`](../specs/2026-08-02-local-g2-diagnostic-visualization-design.md) and map every acceptance criterion to a passing command or visual check above.
- [ ] Run `git -c core.autocrlf=false diff --check` only on the files changed by these tasks.
- [ ] Verify that no `PathSmoothing/LocalG2/` implementation, evaluator, validator, publishing, or ranking file changed.
- [ ] Verify each task commit contains only its listed paths.
- [ ] Report the primary image path and state plainly that the red curve is a rejected diagnostic candidate, while the strict Local G2 result remains `Unchanged`.
@@ -0,0 +1,915 @@
# Daily Summary Job Domain Visualization Upgrade Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `subagent-driven-development` (recommended) or `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Upgrade `daily-summary-job` so it understands the current task, renders the function or algorithm's real domain effect, and connects visible objects to the current problem, cause, consequence, correction, expected result, and task-matched verification evidence.
**Architecture:** Extend the normalized report facts with optional algorithm views and task-specific validation facts. Keep one diagnostic interaction shell, but render its central canvas through a declarative adapter registry selected from the current task's semantics. Split maintainable CSS and JavaScript assets during skill development, then inline them into the final self-contained HTML during rendering.
**Tech Stack:** Python 3.12 standard library, `unittest`, HTML5, CSS, vanilla JavaScript, SVG/DOM, Node syntax checks.
## Global Constraints
- Implement the approved design in `docs/superpowers/specs/2026-08-03-daily-summary-job-domain-visualization-upgrade-design.md`.
- Modify the personal skill at `C:\Users\admin\.codex\skills\daily-summary-job`; request filesystem approval when the execution environment requires it.
- Do not hard-code trajectory planning as the meaning of algorithm visualization. Select the view from the current task's purpose, observable business objects, inputs, outputs, and correctness constraints.
- A generic flow diagram may assist navigation, but it must not replace a domain effect view when spatial, numeric, temporal, state, search, or structured-data evidence exists.
- Distinguish actual observation, static reconstruction, conceptual preview, verified result, and conflicting evidence in both data and presentation.
- Build validation scenarios from the current task and its correctness constraints. Do not substitute an unrelated fixed test matrix.
- Preserve reports that omit the new optional fields.
- Final HTML must contain no external resource or network dependency.
- Do not modify business code, run expensive tests by default, stage changes, or create Git commits.
- Preserve the current UTF-8, date/module classification, checkpoint limits, stable issue IDs, and Markdown/HTML pairing behavior.
## User-Approved Scope Adjustment
This implementation ships only the generic declarative `composite-scene` renderer and the unified diagnosis shell. Do not implement dedicated `spatial-scene`, `cartesian-series`, `graph-network`, `state-machine`, or `data-flow` renderers in this round. Keep the adapter registry as an extension point, so future task-specific work can add those renderers without changing the report contract.
---
## File Structure
**Modify**
- `C:\Users\admin\.codex\skills\daily-summary-job\SKILL.md` — task understanding, visualization-brief generation, domain-view selection, and task-matched validation workflow.
- `C:\Users\admin\.codex\skills\daily-summary-job\references\report-schema.md` — optional algorithm-view, issue-target, solution-preview, verified-result, and task-validation contracts.
- `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py` — validate new facts, render the Markdown algorithm section, bundle assets, and validate rendered links.
- `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py` — schema, backward compatibility, Markdown, asset bundling, adapter, interaction, and CLI regression tests.
- `C:\Users\admin\.codex\skills\daily-summary-job\assets\interactive-report-template.html` — diagnostic-shell markup and asset placeholders.
- `C:\Users\admin\.codex\skills\daily-summary-job\agents\openai.yaml` — UI description and default prompt for task-matched domain visualization.
**Create**
- `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-styles.css` — domain canvas, view modes, diagnostic drawer, evidence states, responsiveness, and reduced-motion styles.
- `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js` — declarative scene-object renderer registry and built-in layout strategies.
- `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js` — report state, selectors, view switching, target highlighting, diagnosis rendering, and keyboard interaction.
The three development assets are embedded into every rendered report. They must never remain as runtime `<link>` or `<script src>` dependencies.
---
### Task 1: Extend the normalized fact contract without breaking old reports
**Files:**
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py:152-365`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py:32-340`
**Interfaces:**
- Consumes: existing `validate_report_data(data: dict[str, Any]) -> None`.
- Produces: optional `algorithm_views: list[dict]`, optional `task_validation: dict`, optional issue visualization fields, and `visual_target_index(data) -> dict[str, set[str]]`.
- [ ] **Step 1: Add a complete task-matched visualization fixture**
Add this helper to `ReportRenderingTests` and use it only in new visualization tests so the existing `sample_data()` remains a legacy-format fixture:
```python
def visualization_data(self):
data = self.sample_data()
data["algorithm_views"] = [
{
"id": "local-g2-smoother",
"name": "Local G2 路径平滑",
"purpose": "把粗路径转换为满足连续性、曲率和安全约束的可执行路径。",
"domain": "geometry-smoothing",
"adapter": "spatial-scene",
"evidence_state": "actual",
"inputs": [
{"id": "coarse-path", "label": "粗路径", "detail": "离散位姿序列", "source_ref": "tests/input.json"}
],
"outputs": [
{"id": "smooth-path", "label": "平滑路径", "detail": "连续候选轨迹", "source_ref": "tests/output.json"}
],
"constraints": [
{"id": "curvature-limit", "label": "曲率上限", "detail": "abs(kappa) <= 0.2", "status": "失败", "source_ref": "tests/output.json"}
],
"stages": [
{
"id": "candidate-evaluation",
"label": "候选评价",
"detail": "比较连续性、曲率和碰撞约束。",
"function_refs": ["PathSmoothing/CandidateEvaluator.cs"],
"target_ids": ["current-path", "curvature-peak"],
}
],
"scene": {
"coordinate_system": "cartesian",
"objects": [
{
"id": "current-path",
"kind": "polyline",
"label": "当前路径",
"evidence_state": "actual",
"source_ref": "tests/output.json",
"data": {"points": [[0, 0], [1, 0.4], [2, 1.1]]},
},
{
"id": "curvature-peak",
"kind": "annotation",
"label": "曲率峰值",
"evidence_state": "actual",
"source_ref": "tests/output.json",
"data": {"x": 1, "y": 0.4, "value": 0.31},
},
{
"id": "preview-path",
"kind": "polyline",
"label": "候选修正路径",
"evidence_state": "conceptual",
"source_ref": "docs/solution.md",
"data": {"points": [[0, 0], [1, 0.3], [2, 1.1]]},
},
],
"layers": [
{"id": "baseline", "label": "正常机制", "mode": "baseline", "object_ids": ["current-path"]},
{"id": "current", "label": "当前问题", "mode": "current", "object_ids": ["current-path", "curvature-peak"]},
{"id": "proposed", "label": "修正预演", "mode": "proposed", "object_ids": ["preview-path"]},
],
},
"source_refs": ["tests/input.json", "tests/output.json"],
}
]
data["issues"][0].update(
{
"algorithm_view_id": "local-g2-smoother",
"target_ids": ["curvature-peak"],
"effect_target_ids": ["current-path"],
"solution_preview": {
"summary": "重新约束连接段导数。",
"expected_result": "曲率峰值回到上限内。",
"evidence_state": "conceptual",
"target_ids": ["preview-path"],
},
}
)
data["task_validation"] = {
"task": "验证 Local G2 平滑候选是否满足当前路径约束。",
"correctness_criteria": [
{"id": "criterion-curvature", "statement": "全路径曲率不超过 0.2。", "source_ref": "tests/output.json"}
],
"checks": [
{
"id": "check-curvature",
"name": "曲率扫描",
"status": "失败",
"criterion_ids": ["criterion-curvature"],
"command": "verify_path_smoothing.ps1",
"result": "max_abs_curvature=0.31",
"evidence_ref": "tests/output.json",
}
],
"missing_evidence": [
{
"criterion_id": "criterion-curvature",
"needed": "修正后的相同输入扫描结果",
"suggested_check": "对同一输入重新运行曲率扫描。",
}
],
}
return data
```
- [ ] **Step 2: Write failing contract and compatibility tests**
Add tests with these exact assertions:
```python
def test_accepts_legacy_report_without_algorithm_views(self):
self.require_target().validate_report_data(self.sample_data())
def test_accepts_linked_algorithm_view_and_task_validation(self):
target = self.require_target()
data = self.visualization_data()
target.validate_report_data(data)
self.assertEqual(
{"current-path", "curvature-peak", "preview-path", "candidate-evaluation"},
target.visual_target_index(data)["local-g2-smoother"],
)
def test_rejects_unknown_visual_target(self):
data = self.visualization_data()
data["issues"][0]["target_ids"] = ["missing-target"]
with self.assertRaisesRegex(ValueError, "unknown visual target"):
self.require_target().validate_report_data(data)
def test_verified_result_requires_verified_state_and_validation_reference(self):
data = self.visualization_data()
data["issues"][0]["verified_result"] = {
"summary": "看起来已经改善。",
"evidence_state": "conceptual",
"target_ids": ["preview-path"],
"validation_refs": [],
}
with self.assertRaisesRegex(ValueError, "verified_result"):
self.require_target().validate_report_data(data)
def test_task_validation_rejects_unknown_criterion(self):
data = self.visualization_data()
data["task_validation"]["checks"][0]["criterion_ids"] = ["criterion-missing"]
with self.assertRaisesRegex(ValueError, "unknown correctness criterion"):
self.require_target().validate_report_data(data)
```
- [ ] **Step 3: Run the focused tests and confirm RED**
Run:
```powershell
python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py ReportRenderingTests
```
Expected: new tests fail because `visual_target_index` and visualization validation do not exist; existing legacy tests remain green.
- [ ] **Step 4: Add validation constants and helpers**
Add near the existing constants:
```python
VISUAL_EVIDENCE_STATES = {"actual", "static", "conceptual", "verified", "conflict"}
VIEW_MODES = {"baseline", "current", "proposed", "verified"}
VISUAL_ID = re.compile(r"[a-z0-9][a-z0-9-]{1,63}")
```
Add helpers before `validate_report_data`:
```python
def _require_visual_id(value: Any, field: str) -> str:
text = _require_text(value, field)
if not VISUAL_ID.fullmatch(text):
raise ValueError(f"invalid {field}: {text}")
return text
def _require_text_list(value: Any, field: str) -> list[str]:
return [_require_text(item, f"{field} item") for item in _require_list(value, field)]
def _validate_named_fact(item: Any, field: str, required: tuple[str, ...]) -> None:
if not isinstance(item, dict):
raise ValueError(f"{field} item must be an object")
for key in required:
_require_text(item.get(key), f"{field}.{key}")
def visual_target_index(data: dict[str, Any]) -> dict[str, set[str]]:
result: dict[str, set[str]] = {}
for view in data.get("algorithm_views", []):
targets = {stage["id"] for stage in view["stages"]}
targets.update(obj["id"] for obj in view["scene"]["objects"])
result[view["id"]] = targets
return result
```
- [ ] **Step 5: Validate algorithm views, issue links, and task criteria**
Implement `_validate_algorithm_views(data)` and `_validate_task_validation(data)` and call them from `validate_report_data` before issue-link validation. Require the exact fields used by `visualization_data()`, unique view/stage/object/layer IDs, valid evidence states, valid layer modes, stage target references, and layer object references. Require every scene object's `data` to be an object. Require `actual` and `verified` scene objects to carry a non-empty `source_ref`; `static` and `conceptual` objects may reference source or design evidence but must retain their explicit state. Permit any safe adapter slug so future tasks are not restricted to a fixed domain list.
For each issue, validate optional fields only when present:
```python
view_id = issue.get("algorithm_view_id")
if view_id is not None:
view_id = _require_visual_id(view_id, "issue.algorithm_view_id")
if view_id not in targets_by_view:
raise ValueError(f"unknown algorithm view: {view_id}")
for field in ("target_ids", "effect_target_ids"):
for target_id in _require_text_list(issue.get(field, []), f"issue.{field}"):
if target_id not in targets_by_view[view_id]:
raise ValueError(f"unknown visual target: {target_id}")
```
Require `solution_preview.evidence_state` to be `conceptual` or `static`. Require `verified_result.evidence_state == "verified"` and at least one non-empty `validation_refs` item.
- [ ] **Step 6: Run focused and full tests and confirm GREEN**
Run:
```powershell
python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py ReportRenderingTests
python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py
```
Expected: focused contract tests pass; the full suite retains all existing passes plus the new tests.
- [ ] **Step 7: Review the scoped diff without staging or committing**
Run:
```powershell
git diff --no-index -- NUL C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py
```
Expected: only the intended contract helpers and validation paths are present. Do not run `git add` or `git commit`.
---
### Task 2: Render algorithm purpose and task-matched validation in Markdown
**Files:**
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py:249-285`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py:342-452`
**Interfaces:**
- Consumes: validated `algorithm_views` and `task_validation` from Task 1.
- Produces: `_render_algorithm_markdown(data: dict[str, Any]) -> list[str]` and `_render_task_validation_markdown(data: dict[str, Any]) -> list[str]`.
- [ ] **Step 1: Write failing Markdown assertions**
```python
def test_renders_algorithm_function_domain_effect_and_task_validation(self):
markdown = self.require_target().render_markdown(self.visualization_data())
for expected in (
"## 2. 当前函数与算法功能",
"Local G2 路径平滑",
"把粗路径转换为满足连续性、曲率和安全约束的可执行路径",
"候选评价",
"PathSmoothing/CandidateEvaluator.cs",
"## 8. 当前任务匹配的验证",
"全路径曲率不超过 0.2",
"max_abs_curvature=0.31",
"修正后的相同输入扫描结果",
):
self.assertIn(expected, markdown)
def test_legacy_markdown_keeps_original_section_numbers(self):
markdown = self.require_target().render_markdown(self.sample_data())
self.assertIn("## 2. 今日完成的工作", markdown)
self.assertIn("## 7. 证据索引", markdown)
self.assertNotIn("当前函数与算法功能", markdown)
```
- [ ] **Step 2: Run the two tests and confirm RED**
Run:
```powershell
python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py ReportRenderingTests.test_renders_algorithm_function_domain_effect_and_task_validation ReportRenderingTests.test_legacy_markdown_keeps_original_section_numbers
```
Expected: the visualization-aware test fails; the legacy numbering test passes.
- [ ] **Step 3: Add deterministic Markdown helpers**
Implement `_render_algorithm_markdown` so each view shows purpose, domain, evidence state, inputs, outputs, constraints, stages, function references, and source references. Implement `_render_task_validation_markdown` so criteria, executed checks, and missing evidence are separate lists. Do not infer pass/fail or substitute generic tests.
Use this section order only when `algorithm_views` is non-empty:
```text
1. 今日结论摘要
2. 当前函数与算法功能
3. 今日完成的工作
4. 今日发现的问题
5. 问题如何被发现及证据
6. 已采取的改善和验证结果
7. 尚未解决的风险与下一步
8. 当前任务匹配的验证
9. 证据索引
```
Keep the current seven-section output byte-compatible in structure when `algorithm_views` is absent.
- [ ] **Step 4: Run focused and full tests and confirm GREEN**
Run the commands from Step 2, then:
```powershell
python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py
```
Expected: all Markdown and legacy tests pass.
- [ ] **Step 5: Inspect a rendered Markdown sample**
Run:
```powershell
python -X utf8 -c "import importlib.util; from pathlib import Path; p=Path(r'C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py'); s=importlib.util.spec_from_file_location('daily',p); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); print(m.render_markdown(__import__('json').loads(Path('sample-visual-report.json').read_text(encoding='utf-8'))))"
```
Before running, create `sample-visual-report.json` in a temporary directory from `visualization_data()` through the test helper or CLI fixture, then remove only that temporary file. Expected: algorithm function, task-specific criteria, run checks, and missing evidence are visibly separated.
---
### Task 3: Split development assets and inline them into the final HTML
**Files:**
- Create: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-styles.css`
- Create: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js`
- Create: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\interactive-report-template.html`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py:453-470,624-660`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py:286-342`
**Interfaces:**
- Produces: `load_visual_assets(asset_dir: Path | None = None) -> dict[str, str]`.
- Changes: `render_html(data, template, visual_assets=None) -> str` while preserving existing two-argument callers.
- [ ] **Step 1: Write failing asset-bundling tests**
```python
def test_inlines_visual_assets_without_runtime_dependencies(self):
target = self.require_html_target()
html = target.render_html(
self.visualization_data(), TEMPLATE_PATH.read_text(encoding="utf-8")
)
self.assertNotIn("__VISUAL_STYLES__", html)
self.assertNotIn("__VISUAL_ADAPTERS__", html)
self.assertNotIn("__VISUAL_RUNTIME__", html)
self.assertIn("DailySummaryVisuals", html)
self.assertNotRegex(html, r"<link\b|<script[^>]+src=|https?://")
def test_rejects_missing_or_duplicate_asset_placeholder(self):
target = self.require_html_target()
template = TEMPLATE_PATH.read_text(encoding="utf-8").replace("__VISUAL_RUNTIME__", "")
with self.assertRaisesRegex(ValueError, "visual asset placeholder"):
target.render_html(self.visualization_data(), template)
```
- [ ] **Step 2: Run the focused tests and confirm RED**
Run:
```powershell
python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py HtmlRenderingTests.test_inlines_visual_assets_without_runtime_dependencies HtmlRenderingTests.test_rejects_missing_or_duplicate_asset_placeholder
```
Expected: failures because the template and loader do not contain the new placeholders.
- [ ] **Step 3: Move styles and scripts into focused development files**
Move the current `<style>` content to `visualization-styles.css` and the current inline behavior to `visualization-runtime.js`. Initialize `visualization-adapters.js` with this stable public namespace:
```javascript
'use strict';
globalThis.DailySummaryVisuals = (() => {
const registry = new Map();
function register(name, renderer) {
if (!/^[a-z0-9][a-z0-9-]+$/.test(name) || typeof renderer !== 'function') {
throw new TypeError('invalid visualization adapter');
}
registry.set(name, renderer);
}
function select(name) {
return registry.get(name) || registry.get('composite-scene');
}
function render(view, root, context) {
const renderer = select(view.adapter);
if (!renderer) throw new Error('composite-scene adapter is not registered');
return renderer(view, root, context);
}
return { register, select, render };
})();
```
Replace template bodies with exact single placeholders:
```html
<style>__VISUAL_STYLES__</style>
...
<script id="report-data" type="application/json">__REPORT_DATA__</script>
<script>__VISUAL_ADAPTERS__</script>
<script>__VISUAL_RUNTIME__</script>
```
- [ ] **Step 4: Implement the asset loader and renderer replacement**
```python
VISUAL_ASSET_FILES = {
"__VISUAL_STYLES__": "visualization-styles.css",
"__VISUAL_ADAPTERS__": "visualization-adapters.js",
"__VISUAL_RUNTIME__": "visualization-runtime.js",
}
def load_visual_assets(asset_dir: Path | None = None) -> dict[str, str]:
root = Path(asset_dir or Path(__file__).parent.parent / "assets")
return {
placeholder: (root / filename).read_text(encoding="utf-8")
for placeholder, filename in VISUAL_ASSET_FILES.items()
}
def render_html(
data: dict[str, Any],
template: str,
visual_assets: dict[str, str] | None = None,
) -> str:
validate_report_data(data)
replacements = {
"__REPORT_DATA__": safe_json_for_html(data),
**(visual_assets or load_visual_assets()),
}
rendered = template
for placeholder, value in replacements.items():
if rendered.count(placeholder) != 1:
label = "report data placeholder" if placeholder == "__REPORT_DATA__" else "visual asset placeholder"
raise ValueError(f"template must contain exactly one {label}: {placeholder}")
rendered = rendered.replace(placeholder, value)
return rendered
```
When `--template` points to a custom template, continue loading the trusted bundled assets from the skill's `assets` directory unless a future explicit CLI option changes that contract.
- [ ] **Step 5: Run bundling tests, full tests, and syntax checks**
```powershell
python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py
node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js
node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js
```
Expected: Python suite passes; both Node checks exit 0.
---
### Task 4: Implement the generic declarative domain-effect renderer
**Files:**
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py`
**Interfaces:**
- Consumes: `algorithm_views[].scene.objects`, `scene.layers`, and the selected view mode.
- Produces: SVG/DOM elements carrying `data-target-id`, `data-evidence-state`, and accessible labels.
- Public JS API: `DailySummaryVisuals.register(name, renderer)`, `.select(name)`, and `.render(view, root, context)`.
- [ ] **Step 1: Add task-derived adapter assertions**
Use `visualization_data()` as the business fixture. Add static and generated-HTML assertions:
```python
def test_domain_adapter_renders_task_objects_not_fixed_demo_content(self):
html = self.require_html_target().render_html(
self.visualization_data(), TEMPLATE_PATH.read_text(encoding="utf-8")
)
for value in ("local-g2-smoother", "current-path", "curvature-peak", "preview-path"):
self.assertIn(value, html)
self.assertNotIn("固定轨迹示例", html)
def test_unknown_safe_adapter_has_composite_fallback(self):
data = self.visualization_data()
data["algorithm_views"][0]["adapter"] = "custom-business-domain"
html = self.require_html_target().render_html(
data, TEMPLATE_PATH.read_text(encoding="utf-8")
)
self.assertIn("custom-business-domain", html)
self.assertIn("composite-scene", html)
```
- [ ] **Step 2: Run the focused tests and confirm RED**
Expected: the fallback or declarative object hooks are missing.
- [ ] **Step 3: Add safe DOM/SVG construction helpers**
Implement helpers that assign text through `textContent` and SVG attributes through `setAttribute`; never concatenate untrusted labels into `innerHTML`:
```javascript
const SVG_NS = 'http://www.w3.org/2000/svg';
function element(name, attrs = {}, text = '') {
const node = document.createElement(name);
Object.entries(attrs).forEach(([key, value]) => node.setAttribute(key, String(value)));
if (text) node.textContent = text;
return node;
}
function svgElement(name, attrs = {}) {
const node = document.createElementNS(SVG_NS, name);
Object.entries(attrs).forEach(([key, value]) => node.setAttribute(key, String(value)));
return node;
}
function markTarget(node, object) {
node.dataset.targetId = object.id;
node.dataset.evidenceState = object.evidence_state;
node.setAttribute('tabindex', '0');
node.setAttribute('role', 'button');
node.setAttribute('aria-label', `${object.label}${object.evidence_state}`);
return node;
}
```
- [ ] **Step 4: Implement adapter strategies over shared primitives**
Register these layout strategies, while keeping their data task-driven:
- `composite-scene`: render supplied points, polylines, curves, regions, nodes, edges, state blocks, data items, annotations, and clear unsupported-kind cards for the remainder.
The generic renderer must filter visible objects from the selected `scene.layers[].object_ids`; it must not invent domain samples. Unknown adapter names must select `composite-scene`. Do not add dedicated renderer implementations in this task.
- [ ] **Step 5: Add evidence-state and target CSS**
In `visualization-styles.css`, use line style, icon/text, and color together:
```css
[data-evidence-state="actual"] { --state-color: var(--blue); }
[data-evidence-state="static"] { --state-color: var(--amber); }
[data-evidence-state="conceptual"] { --state-color: var(--amber); stroke-dasharray: 8 6; opacity: .82; }
[data-evidence-state="verified"] { --state-color: var(--green); }
[data-evidence-state="conflict"] { --state-color: var(--red); stroke-dasharray: 3 4; }
[data-target-id].is-highlighted { filter: drop-shadow(0 0 5px var(--state-color)); }
[data-target-id]:focus-visible { outline: 3px solid #e7a628; outline-offset: 3px; }
```
- [ ] **Step 6: Run Python tests and Node syntax checks**
Use the commands from Task 3 Step 5. Expected: all pass, and no test claims business correctness beyond the current fixture's actual evidence.
---
### Task 5: Build the four-mode interactive diagnosis shell
**Files:**
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\interactive-report-template.html`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-styles.css`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py`
**Interfaces:**
- State: `{ algorithmId, issueId, mode, solutionIndex }`.
- Modes: `baseline`, `current`, `proposed`, `verified`.
- Consumes: Task 1 issue links and Task 4 adapter API.
- [ ] **Step 1: Write failing interaction-hook tests**
```python
def test_renders_algorithm_selector_four_modes_domain_canvas_and_diagnosis_card(self):
html = self.require_html_target().render_html(
self.visualization_data(), TEMPLATE_PATH.read_text(encoding="utf-8")
)
for hook in (
'id="algorithm-selector"',
'data-view-mode="baseline"',
'data-view-mode="current"',
'data-view-mode="proposed"',
'data-view-mode="verified"',
'id="domain-canvas"',
'id="diagnosis-current"',
'id="diagnosis-cause"',
'id="diagnosis-impact"',
'id="diagnosis-solution"',
'id="diagnosis-expected"',
'id="task-validation"',
):
self.assertIn(hook, html)
def test_verified_mode_is_guarded_by_verified_result(self):
runtime = (TEMPLATE_PATH.parent / "visualization-runtime.js").read_text(encoding="utf-8")
self.assertIn("hasVerifiedResult", runtime)
self.assertIn("button.disabled", runtime)
self.assertIn("尚无修正后的匹配验证证据", runtime)
```
- [ ] **Step 2: Run the focused tests and confirm RED**
Expected: the new shell hooks and verified-result guard are absent.
- [ ] **Step 3: Replace the two-column issue workbench with the approved shell**
Add:
- algorithm and issue selectors;
- four mode buttons with `aria-pressed`;
- layer toggles;
- central `#domain-canvas`;
- algorithm-stage navigation;
- object diagnosis card;
- expandable source/test evidence;
- existing solution steps, validation gates, and roadmap below the canvas.
When `algorithm_views` is absent, hide the algorithm controls and retain the legacy text diagnosis behavior.
- [ ] **Step 4: Implement one state-driven render path**
In `visualization-runtime.js`, use one render function so selectors, modes, canvas, diagnosis, validation, and buttons never drift:
```javascript
const report = JSON.parse(document.getElementById('report-data').textContent);
const views = Array.isArray(report.algorithm_views) ? report.algorithm_views : [];
const issues = Array.isArray(report.issues) ? report.issues : [];
const state = {
algorithmId: views[0]?.id || '',
issueId: issues[0]?.id || '',
mode: views.length ? 'baseline' : 'current',
solutionIndex: 0,
};
function currentView() {
return views.find((view) => view.id === state.algorithmId) || null;
}
function currentIssue() {
return issues.find((issue) => issue.id === state.issueId) || null;
}
function hasVerifiedResult(issue) {
return Boolean(issue?.verified_result?.evidence_state === 'verified' && issue.verified_result.validation_refs?.length);
}
function renderApp() {
const view = currentView();
const issue = currentIssue();
renderSelectors(view, issue);
renderModeButtons(issue);
renderDomainCanvas(view, issue);
renderDiagnosis(issue);
renderTaskValidation(report.task_validation);
renderExistingReportSections(issue);
}
```
- [ ] **Step 5: Implement mode-to-layer and diagnosis behavior**
- `baseline`: show the normal algorithm layer and purpose/input/output/constraints.
- `current`: show current layer, highlight `target_ids`, then `effect_target_ids` in propagation order.
- `proposed`: show `solution_preview.target_ids`, expected result, and conceptual/static label.
- `verified`: enable only when `hasVerifiedResult(issue)`; show verified targets and validation references.
Clicking or pressing Enter/Space on a visual target must select the linked issue. Arrow keys change issues only when focus is not in a form control; mode buttons and targets retain visible focus.
- [ ] **Step 6: Add responsive and reduced-motion behavior**
At desktop width use mode rail + canvas + diagnosis drawer. Under 900px stack the drawer below the canvas. Under 560px use single-column selectors and controls. When `prefers-reduced-motion: reduce` is active, reveal the complete impact path immediately rather than animating it.
- [ ] **Step 7: Extend rendered-pair validation to cover algorithm facts**
Update `_validate_rendered_text` so every algorithm view ID, view evidence state, linked issue target ID, correctness criterion ID, and executed check ID exists in the generated HTML. Require the view name, purpose, criterion statement, and check result in Markdown. Keep the existing issue ID/evidence checks and external-resource rejection.
Add a negative test that removes `curvature-peak` from rendered HTML and expects `validate` to fail with `HTML is missing visual target: curvature-peak`.
- [ ] **Step 8: Run the full suite and generated-HTML validation**
```powershell
python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py
node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js
node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js
```
Expected: all tests and syntax checks pass; generated HTML contains no external resource.
---
### Task 6: Teach the skill the task-understanding and dynamic-validation workflow
**Files:**
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\SKILL.md`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\references\report-schema.md`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\agents\openai.yaml`
**Interfaces:**
- Consumes: data contract and renderer from Tasks 1-5.
- Produces: repeatable Agent instructions that create task-matched views and validation facts without requiring the user to fill JSON manually.
- [ ] **Step 1: Add the mandatory understanding sequence to SKILL.md**
Insert a concise workflow before report JSON construction:
```markdown
## Build a task-matched algorithm view
When today's work changes, diagnoses, or discusses a function or algorithm:
1. Identify its business purpose, inputs, outputs, stages, observable objects, and correctness constraints from current evidence.
2. Decide what domain effect lets a reader see the algorithm working. Prefer spatial scenes, numeric plots, search/state structures, timelines, or transformed data over a generic flowchart when the evidence supports them.
3. Build one `algorithm_views` entry from actual run/test data when available. Label source reconstruction as `static` and solution prediction as `conceptual`.
4. Link every visual issue to existing stage/object IDs. Show current targets, effect propagation, candidate changes, and verified results as separate states.
5. If the evidence cannot support a credible domain view, list the missing evidence and omit the invented scene.
```
- [ ] **Step 2: Replace generic validation wording with task-matched validation**
```markdown
## Match validation to the current task
Derive correctness criteria from the selected function or algorithm, then locate only tests, commands, samples, and runtime evidence that directly evaluate those criteria. Record checks actually run, their exact results, and missing evidence separately. If no matching test exists, propose a task-specific check and keep the conclusion unverified. Never claim coverage from an unrelated fixed scenario.
```
Retain the existing safety rule against expensive tests by default.
- [ ] **Step 3: Document the complete schema and one non-prescriptive example**
In `references/report-schema.md`, document every Task 1 field, allowed evidence states, object/layer linking rules, task-validation shape, verified-result requirements, and legacy behavior. Use one example only to illustrate the contract, and state explicitly that its domain does not constrain adapter selection.
- [ ] **Step 4: Regenerate UI metadata from the updated skill**
Run:
```powershell
python -X utf8 C:\Users\admin\.codex\skills\.system\skill-creator\scripts\generate_openai_yaml.py C:\Users\admin\.codex\skills\daily-summary-job --interface 'display_name=Daily Summary Job' --interface 'short_description=按当前任务生成带领域算法诊断的交互日报' --interface 'default_prompt=使用 $daily-summary-job 理解当前任务和算法,以匹配的领域效果图展示正常机制、问题、影响、修正方案与验证结果,并更新今日日报。'
```
Expected: `agents/openai.yaml` contains only the interface block with the three supplied values and valid UTF-8 Chinese.
- [ ] **Step 5: Validate skill structure and concise loading behavior**
```powershell
python -X utf8 C:\Users\admin\.codex\skills\.system\skill-creator\scripts\quick_validate.py C:\Users\admin\.codex\skills\daily-summary-job
```
Expected: `Skill is valid!`. Confirm `SKILL.md` stays under 500 lines and keeps detailed field definitions in `references/report-schema.md`.
---
### Task 7: End-to-end verification on the current task and backward compatibility
**Files:**
- Test: all files under `C:\Users\admin\.codex\skills\daily-summary-job`
- Generate temporary outputs only under a verified temporary directory or this project's `dailywork_report` when explicitly updating the real report.
**Interfaces:**
- Consumes: completed skill from Tasks 1-6.
- Produces: fresh verification evidence for schema, rendering, interaction hooks, self-containment, task matching, and legacy reports.
- [ ] **Step 1: Run the complete automated suite**
```powershell
python -X utf8 C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py
```
Expected: every test passes with zero failures. Record the actual test count; do not reuse the previous count of 31.
- [ ] **Step 2: Run skill and JavaScript validation**
```powershell
python -X utf8 C:\Users\admin\.codex\skills\.system\skill-creator\scripts\quick_validate.py C:\Users\admin\.codex\skills\daily-summary-job
node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-adapters.js
node --check C:\Users\admin\.codex\skills\daily-summary-job\assets\visualization-runtime.js
```
Expected: skill valid; both JavaScript files exit 0.
- [ ] **Step 3: Run a temporary legacy report round trip**
Use the existing `sample_data()` shape without `algorithm_views`. Run `render`, `validate`, and `render --update` in a new temporary project. Expected: Markdown and HTML are created, validate returns `valid: true`, and update reuses the same pair.
- [ ] **Step 4: Run a temporary current-task domain-view round trip**
Use the Task 1 `visualization_data()` facts, which match the current path-smoothing work rather than an unrelated generic test. Run `render`, then `validate`. Assert:
- `valid` is `true`;
- HTML contains the task's actual object IDs and values;
- current, proposed, and verified controls are present;
- verified mode is disabled because this fixture has no verified result;
- Markdown contains the task-specific criterion and missing evidence;
- no external URL, `<link>`, or `<script src>` exists.
- [ ] **Step 5: Verify failure gates with mutations**
Starting from the same task facts, independently mutate and reject:
- an unknown `target_id`;
- a proposed view marked `verified` without validation references;
- a check referencing an unknown correctness criterion;
- a layer referencing an unknown object;
- a rendered HTML file containing an external URL.
Expected: each mutation returns a non-zero CLI status and an error naming the violated contract.
- [ ] **Step 6: Perform live visual and interaction QA when a browser runtime is available**
Open the generated domain-view HTML and verify:
- algorithm and issue selection;
- all four mode controls;
- task-specific domain objects, not a fixed demo;
- click/keyboard target selection;
- cause and effect highlighting;
- solution-step preview;
- verified-mode guard;
- desktop and narrow-screen layout;
- reduced-motion behavior.
If the browser runtime is unavailable, record this exact check as `待验证风险`; source inspection and syntax checks do not replace visual QA.
- [ ] **Step 7: Review only skill and report artifacts; do not commit**
Run scoped file listings and diffs. Confirm no business source file, staging index, or Git commit was changed. Report created/modified skill files, verification commands, exact pass counts, and any remaining visual-QA risk.
---
## Plan Completion Criteria
- All seven tasks satisfy their focused tests before the next task begins.
- The full suite passes after each task that changes Python or JavaScript behavior.
- A legacy report and a task-matched domain report both pass CLI validation.
- The task-matched report visibly connects domain objects to problem, cause, consequence, solution preview, expected result, and available verification evidence.
- No fixed domain example is presented as a universal validation scenario.
- No external dependency, business-code edit, Git staging, or Git commit is introduced.
@@ -0,0 +1,432 @@
# Daily Summary Job Personal Skill Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Install a personal `daily-summary-job` skill that records compact development checkpoints and generates or updates evidence-grounded Markdown reports with self-contained interactive HTML visualizations.
**Architecture:** A concise `SKILL.md` orchestrates context/Git evidence collection and semantic classification. A standard-library Python helper validates the normalized JSON fact source, selects safe module/date/topic paths, and renders both deliverables from one source; an HTML asset provides all offline interaction.
**Tech Stack:** Markdown, YAML, Python 3.12 standard library, HTML5, CSS, inline SVG, native JavaScript, `unittest`, PowerShell verification.
## Global Constraints
- Install to `C:\Users\admin\.codex\skills\daily-summary-job`.
- Use the normalized skill name `daily-summary-job`; do not use `dailySummary_job` as a folder or YAML name.
- Trigger on demand from explicit `$daily-summary-job` invocations or clear natural-language daily progress/report intents; never run in the background.
- Never copy full conversations or full logs into checkpoints.
- Limit one checkpoint to 5 achievements, 5 issues, and 3 next steps; descriptions should be at most 120 Chinese characters where practical.
- Prefer existing `<module>_rep` naming; otherwise use a normalized module, `cross-module_rep`, or `general_rep`.
- Store final files under `dailywork_report/<module>_rep/YYYY-MM-DD/`.
- Generate Markdown and HTML from the same normalized JSON source.
- HTML must be a single offline file with no CDN, network request, third-party library, or external image.
- Do not modify ParkingRobot business code, stage files, or create Git commits.
---
### Task 1: Initialize the personal skill scaffold
**Files:**
- Create: `C:\Users\admin\.codex\skills\daily-summary-job\SKILL.md`
- Create: `C:\Users\admin\.codex\skills\daily-summary-job\agents\openai.yaml`
- Create directories: `scripts`, `references`, `assets`
**Interfaces:**
- Consumes: `skill-creator/scripts/init_skill.py` and the approved design.
- Produces: A discoverable personal skill skeleton with UI metadata.
- [ ] **Step 1: Confirm the target does not already exist**
Run:
```powershell
$target = 'C:\Users\admin\.codex\skills\daily-summary-job'
if (Test-Path -LiteralPath $target) { throw "Skill already exists: $target" }
```
Expected: no output.
- [ ] **Step 2: Initialize the skill with required resource folders**
Run with approval for writing outside the workspace:
```powershell
python 'C:\Users\admin\.codex\skills\.system\skill-creator\scripts\init_skill.py' daily-summary-job `
--path 'C:\Users\admin\.codex\skills' `
--resources scripts,references,assets `
--interface 'display_name=Daily Summary Job' `
--interface 'short_description=按需记录、分类并生成带证据与交互可视化的开发工作日报' `
--interface 'default_prompt=使用 $daily-summary-job 记录当前开发进展,并生成今日 Markdown 与交互式 HTML 日报。'
```
Expected: `daily-summary-job` is created and `agents/openai.yaml` contains the three interface values.
- [ ] **Step 3: Inspect only the new scaffold**
Run:
```powershell
Get-ChildItem -LiteralPath 'C:\Users\admin\.codex\skills\daily-summary-job' -Recurse
```
Expected: `SKILL.md`, `agents/openai.yaml`, and the three resource directories are present.
---
### Task 2: Implement deterministic path planning and checkpoint budgets with tests first
**Files:**
- Create: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py`
- Create: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py`
**Interfaces:**
- Produces: `find_project_root(start: Path) -> Path`, `normalize_slug(value: str, fallback: str) -> str`, `infer_module(changed_paths: list[str], report_root: Path, explicit: str | None) -> str`, `plan_paths(...) -> ReportPaths`, and `validate_checkpoint_budget(data: dict) -> None`.
- `ReportPaths` exposes `module_dir`, `date_dir`, `state_file`, `markdown_file`, and `html_file` as `Path` values.
- [ ] **Step 1: Write failing standard-library tests**
Create tests covering exact behavior:
```python
def test_prefers_existing_module_folder(self):
(self.root / "dailywork_report" / "pathsmoothing_rep").mkdir(parents=True)
module = target.infer_module(
["src/PathSmoothing/LocalG2/Pipeline.cs"],
self.root / "dailywork_report",
None,
)
self.assertEqual("pathsmoothing_rep", module)
def test_multiple_existing_modules_become_cross_module(self):
report_root = self.root / "dailywork_report"
(report_root / "Map_rep").mkdir(parents=True)
(report_root / "coarsepath_rep").mkdir()
module = target.infer_module(
["src/Map/Grid.cs", "src/CoarsePath/Search.cs"], report_root, None
)
self.assertEqual("cross-module_rep", module)
def test_unknown_scope_becomes_general(self):
self.assertEqual(
"general_rep",
target.infer_module(["README.md"], self.root / "dailywork_report", None),
)
def test_rejects_checkpoint_over_budget(self):
data = {"achievements": [{"title": str(i)} for i in range(6)], "issues": [], "next_steps": []}
with self.assertRaisesRegex(ValueError, "at most 5 achievements"):
target.validate_checkpoint_budget(data)
```
- [ ] **Step 2: Run the tests and confirm the expected import failure**
Run:
```powershell
python 'C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py'
```
Expected: FAIL because `prepare_report.py` does not yet provide the tested API.
- [ ] **Step 3: Implement safe normalization, module inference, and path planning**
Use a frozen dataclass and reject traversal:
```python
@dataclass(frozen=True)
class ReportPaths:
module_dir: Path
date_dir: Path
state_file: Path
markdown_file: Path
html_file: Path
def normalize_slug(value: str, fallback: str) -> str:
normalized = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
normalized = re.sub(r"[^a-zA-Z0-9]+", "-", normalized).strip("-").lower()
if not normalized or normalized in {".", ".."}:
normalized = fallback
return normalized[:64].rstrip("-") or fallback
```
Implement existing-folder matching before generic path inference. Preserve an existing folder's exact spelling, use `cross-module_rep` for more than one matched module, and `general_rep` when only generic files such as `README.md` are available.
`plan_paths` must reuse an existing state file with the same date/module/topic in update mode and otherwise choose the next two-digit sequence.
- [ ] **Step 4: Implement and enforce checkpoint budgets**
```python
def validate_checkpoint_budget(data: dict[str, Any]) -> None:
limits = {"achievements": 5, "issues": 5, "next_steps": 3}
for key, limit in limits.items():
values = data.get(key, [])
if not isinstance(values, list):
raise ValueError(f"{key} must be a list")
if len(values) > limit:
raise ValueError(f"checkpoint allows at most {limit} {key}")
```
- [ ] **Step 5: Run the focused tests**
Run the same test command.
Expected: all path, classification, update, traversal, and budget tests pass.
---
### Task 3: Define and validate the normalized fact source
**Files:**
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py`
- Create: `C:\Users\admin\.codex\skills\daily-summary-job\references\report-schema.md`
**Interfaces:**
- Produces: `validate_report_data(data: dict) -> None`, `render_markdown(data: dict) -> str`, and a documented JSON schema shared by checkpoints, generation, and update mode.
- [ ] **Step 1: Add failing schema and Markdown tests**
The fixture must include one issue for each evidence level and assert stable issue identifiers appear in Markdown:
```python
self.assertRaisesRegex(ValueError, "unsupported evidence level", target.validate_report_data, bad_data)
markdown = target.render_markdown(self.sample_data())
self.assertIn("## 3. 今日发现的问题", markdown)
self.assertIn("issue-baseline", markdown)
self.assertIn("待验证风险", markdown)
```
- [ ] **Step 2: Run tests and confirm the new API fails**
Expected: FAIL because validation and Markdown rendering are not implemented.
- [ ] **Step 3: Implement strict schema validation**
Require top-level fields `date`, `title`, `summary`, `modules`, `achievements`, `issues`, `validations`, `next_steps`, and `sources`. Require each issue to contain `id`, `title`, `module`, `evidence_level`, `discovery`, `actual`, `expected`, `cause`, `impact`, `improvements`, `validation`, `next_steps`, and `evidence`. Accept only these labels:
```python
EVIDENCE_LEVELS = {"已验证", "静态分析", "对话发现", "待验证风险", "结论冲突"}
```
Reject duplicate issue identifiers and non-list collection fields.
- [ ] **Step 4: Implement Markdown rendering from the validated data**
Render the approved seven main sections. Every issue heading includes its stable identifier and evidence level. Evidence is rendered as a compact table containing label, reference, and result; empty optional collections render as “无已记录项” rather than invented content.
- [ ] **Step 5: Document the exact schema and evidence rules**
`report-schema.md` must contain the complete JSON example, field table, five evidence labels, checkpoint budget, merge-by-issue-id rule, conflict behavior, and safe-language examples distinguishing verified facts from risks.
- [ ] **Step 6: Run the focused tests**
Expected: schema and Markdown tests pass.
---
### Task 4: Build the self-contained interactive HTML renderer
**Files:**
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py`
- Create: `C:\Users\admin\.codex\skills\daily-summary-job\assets\interactive-report-template.html`
**Interfaces:**
- Produces: `render_html(data: dict, template: str) -> str` and UI hooks `issue-button`, `evidence-filter`, `cause-node`, `solution-step`, `before-after-toggle`, `validation-gate`, and `roadmap-item`.
- [ ] **Step 1: Add failing HTML safety and interaction tests**
```python
html = target.render_html(self.sample_data(), template_text)
self.assertIn('id="daily-summary-app"', html)
self.assertIn('class="issue-button"', html)
self.assertIn('class="before-after-toggle"', html)
self.assertIn('@media (prefers-reduced-motion: reduce)', html)
self.assertNotRegex(html, r'https?://|<script[^>]+src=')
self.assertNotIn("</script><script>alert", html)
for issue in self.sample_data()["issues"]:
self.assertIn(issue["id"], html)
```
- [ ] **Step 2: Run tests and confirm rendering fails**
Expected: FAIL because the template and renderer do not exist.
- [ ] **Step 3: Create the offline data-driven template**
The template must contain:
```html
<main id="daily-summary-app" data-selected-issue="">
<header class="hero">...</header>
<nav class="filters" aria-label="筛选问题证据等级">...</nav>
<section class="overview" aria-label="今日工作总览">...</section>
<section class="problem-lab" aria-live="polite">...</section>
<section class="validation-funnel">...</section>
<section class="roadmap">...</section>
</main>
<script id="report-data" type="application/json">__REPORT_DATA__</script>
<script>/* native rendering and keyboard navigation */</script>
```
Use text and icons together for status; do not rely on color alone. Provide visible focus states, arrow-key issue navigation, responsive single-column fallbacks, and a no-animation media query. Display “概念示意” whenever a problem lacks numeric evidence.
- [ ] **Step 4: Implement safe JSON embedding and rendering**
```python
def safe_json_for_html(data: dict[str, Any]) -> str:
raw = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
return raw.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
def render_html(data: dict[str, Any], template: str) -> str:
validate_report_data(data)
if template.count("__REPORT_DATA__") != 1:
raise ValueError("template must contain exactly one report data placeholder")
return template.replace("__REPORT_DATA__", safe_json_for_html(data))
```
- [ ] **Step 5: Run the focused tests**
Expected: HTML safety, interaction-hook, evidence-consistency, and accessibility-source tests pass.
---
### Task 5: Add checkpoint, render, update, and validate CLI workflows
**Files:**
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py`
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\scripts\prepare_report.py`
**Interfaces:**
- Produces CLI subcommands `inspect`, `checkpoint`, `render`, and `validate`.
- All successful commands emit compact JSON to stdout; failures return nonzero with a specific message on stderr.
- [ ] **Step 1: Add failing end-to-end CLI tests**
Use `tempfile.TemporaryDirectory` to verify:
1. `checkpoint` creates one compact JSON under `.daily-summary-job/YYYY-MM-DD/checkpoints`.
2. `render` creates canonical state plus a Markdown/HTML pair under `<module>_rep/YYYY-MM-DD`.
3. `render --update` preserves the original sequence and paths.
4. A second topic receives the next sequence.
5. `validate` rejects mismatched issue identifiers or an external URL in HTML.
- [ ] **Step 2: Run tests and confirm CLI failures**
Expected: FAIL because the subcommands are not wired.
- [ ] **Step 3: Implement the four subcommands**
- `inspect`: report project root, local date, changed paths, existing report modules, inferred module, and evidence file candidates without writing.
- `checkpoint`: validate compact input, create the checkpoint directory, and write UTF-8 JSON atomically.
- `render`: validate full input, plan or reuse paths, render both outputs to temporary siblings, validate them, atomically replace the pair, and persist canonical state.
- `validate`: compare issue identifiers and evidence levels across canonical JSON, Markdown, and HTML; reject external resources.
Use `tempfile.NamedTemporaryFile(delete=False, dir=target.parent)` and `Path.replace` only after both staged files pass validation. Clean up staged files in `finally` without deleting existing deliverables.
- [ ] **Step 4: Run all script tests**
Run:
```powershell
python 'C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py' -v
```
Expected: all tests pass.
---
### Task 6: Write the concise skill workflow and metadata-aligned instructions
**Files:**
- Modify: `C:\Users\admin\.codex\skills\daily-summary-job\SKILL.md`
- Verify: `C:\Users\admin\.codex\skills\daily-summary-job\agents\openai.yaml`
**Interfaces:**
- Consumes: `scripts/prepare_report.py`, `references/report-schema.md`, and `assets/interactive-report-template.html`.
- Produces: A skill another Codex instance can invoke for record, generate, or update intents without loading unrelated history.
- [ ] **Step 1: Replace scaffold placeholders with final frontmatter**
Use only the required YAML keys:
```yaml
---
name: daily-summary-job
description: Record compact development checkpoints and generate or update evidence-grounded daily work reports with paired Markdown and self-contained interactive HTML. Use when the user asks to record current development progress, summarize today's coding work, organize problems and improvements, visualize problem/solution reasoning, or update an existing daily development report.
---
```
- [ ] **Step 2: Write the imperative workflow**
The body must tell the invoking agent to:
1. Determine record/generate/update intent without requiring fixed wording.
2. Read only current context and today's relevant evidence.
3. Run `inspect` before any write.
4. Preserve evidence boundaries and conflicts.
5. Create the normalized JSON using `report-schema.md`.
6. Use `checkpoint` for compact progress capture.
7. Use `render` for new reports and `render --update` for exact-topic updates.
8. Run `validate` and report precise paths.
9. Never fix business code, run Git commit, fabricate evidence, or read historical days by default.
- [ ] **Step 3: Verify interface metadata remains aligned**
`agents/openai.yaml` must show `Daily Summary Job`, the approved Chinese short description, and a default prompt explicitly containing `$daily-summary-job`. Do not add icons, colors, dependencies, or policy fields.
---
### Task 7: Validate the installed skill and run a disposable full workflow
**Files:**
- Verify only: `C:\Users\admin\.codex\skills\daily-summary-job\**`
- Create and remove only: a dedicated directory under the system temporary directory.
**Interfaces:**
- Produces: Validation evidence for skill structure, unit behavior, report generation, update stability, and offline HTML constraints.
- [ ] **Step 1: Run skill structure validation**
```powershell
python 'C:\Users\admin\.codex\skills\.system\skill-creator\scripts\quick_validate.py' 'C:\Users\admin\.codex\skills\daily-summary-job'
```
Expected: validation succeeds.
- [ ] **Step 2: Run the full script test suite**
```powershell
python 'C:\Users\admin\.codex\skills\daily-summary-job\scripts\test_prepare_report.py' -v
```
Expected: all tests pass.
- [ ] **Step 3: Create a disposable simulated project**
Create one explicit temporary project containing `src/Map`, `src/PathSmoothing`, and an existing `dailywork_report/pathsmoothing_rep`. Feed a checkpoint and a full report fixture containing achievements, two evidence levels, an improvement, validation results, and next steps.
- [ ] **Step 4: Run record, generate, update, and validation commands**
Expected:
- checkpoint path is date-scoped;
- multi-module input selects `cross-module_rep` unless explicitly overridden;
- generation creates one paired report;
- update keeps the same pair;
- every issue identifier appears in normalized JSON, Markdown, and HTML;
- HTML contains no `http://`, `https://`, external script, or external image reference.
- [ ] **Step 5: Inspect the final installed file set and repository scope**
Run:
```powershell
Get-ChildItem -LiteralPath 'C:\Users\admin\.codex\skills\daily-summary-job' -Recurse -File | Select-Object FullName,Length
git status --short -- 'docs/superpowers/specs/2026-08-03-daily-summary-job-skill-design.md' 'docs/superpowers/plans/2026-08-03-daily-summary-job-skill.md'
```
Expected: only the new skill files exist in the personal directory; the repository shows the two uncommitted documentation files and no task-caused business-code changes.
## Execution Choice
The user requested immediate execution without Git commits. Execute this plan inline with `superpowers:executing-plans`; do not dispatch subagents and do not pause for a separate execution-choice prompt.
@@ -0,0 +1,106 @@
# LocalG2-Only PathSmoothing Reorganization Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` or `superpowers:executing-plans` to execute this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Convert `PathSmoothing` into a LocalG2-only module, remove the three legacy smoothing algorithms, preserve LocalG2 visualization and fixture workflows, and organize the source tree and README using the established `CoarsePath` module pattern.
**Architecture:** The production facade always runs the LocalG2 pipeline. Shared path preparation and validation remain intact; B-spline, local Bezier, and piecewise quintic implementations and their configuration are removed. Offline reports remain a factual comparison of raw coarse path versus LocalG2 only, with visualization sources placed below an `Output` layer like `CoarsePath`.
**Tech Stack:** C# 10, .NET SDK, Newtonsoft.Json, existing System.Drawing/StbImageWriteSharp report exporter, PowerShell verification hosts.
## Global Constraints
- Do not read, search, enumerate, copy, modify, delete, stage, or commit `ClumsyPilot/ParkrobTrajplanner/auto_avoidance`; do not enumerate `ClumsyPilot/ParkrobTrajplanner` as a parent.
- Preserve LocalG2 candidate construction, validation, publication statuses, fixture data, diagnostic candidate visualization, and generated report artifacts below `ClumsyPilot/obj/path_smoothing_reports`.
- Remove all production references to `CubicBSpline`, `LocalCubicBezier`, and `PiecewiseQuintic` smoothing.
- Retain the raw-path baseline in reports. Normal reports must contain only raw and LocalG2 series and four figures; diagnostic reports may append the already-rejected LocalG2 candidate as a fifth figure.
- Maintain current default `MinimumClearanceReserveMeters = 0d`.
- Do not delete unrelated user work or generated report directories.
---
### Task 1: Establish a LocalG2-only verification contract
**Files:**
- Modify: `ClumsyPilot/tests/verify_path_smoothing_comparison.ps1`
- Modify: `ClumsyPilot/tests/verify_path_smoothing_svg_csv.ps1`
- Modify: `ClumsyPilot/tests/PathSmoothingPngVerificationHost/Program.cs`
**Interfaces:**
- The comparison request exposes exactly one requested method: `SmoothingMethod.LocalG2Quintic`.
- A normal report has a raw baseline plus one LocalG2 row/series; the diagnostic report retains its optional rejected candidate figure.
- [ ] Add failing assertions that reject the three removed enum names, require one requested comparison method, require two normal figure series, and require exactly two CSV rows after the header.
- [ ] Run the focused PowerShell checks and confirm they fail against the four-algorithm implementation.
- [ ] Update host assertions for the new two-series normal report while retaining the seven-file diagnostic contract.
- [ ] Re-run the focused checks after Tasks 2 and 3 and record the exit codes.
### Task 2: Remove legacy algorithms and simplify the production facade
**Files:**
- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Algorithms/`
- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs`
- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs`
- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingService.cs`
**Interfaces:**
- `SmoothingMethod` retains only `LocalG2Quintic`.
- `PathSmoothingConfiguration` defaults `Method` to `LocalG2Quintic` and exposes only shared safety/sampling fields and `LocalG2Quintic` options.
- `PathSmoothingService.Smooth(request, cancellationToken)` directly validates/prepares/builds the raw baseline and invokes `LocalG2PreSmoothingPipeline`.
- [ ] Delete legacy source files only after their callers are removed.
- [ ] Remove legacy smoothness/retry configuration and cloning code; preserve output spacing, collision step, clearance reserve, and LocalG2 options.
- [ ] Replace the multi-method resolver and fallback path in `PathSmoothingService` with its LocalG2-only route.
- [ ] Compile the isolated PathSmoothing host and confirm no source references to the removed methods remain in allowed paths.
### Task 3: Reorganize report sources into an Output layer and reduce the report model
**Files:**
- Move: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/` to `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/`
- Move: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Visualization/` to `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Visualization/`
- Delete: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Output/Comparison/SmoothingMethodRanker.cs`
- Modify: moved comparison request/result/service consumers and all moved visualization files.
**Interfaces:**
- `PathSmoothingComparisonRequest` owns one immutable LocalG2 request rather than a caller-selectable method list.
- `PathSmoothingComparisonResult` contains a raw baseline and exactly one LocalG2 entry.
- Normal figure and CSV builders emit `RawPath` and `LocalG2Quintic` only.
- [ ] Move source directories with their namespaces changed from `PathSmoothing.Comparison` and `PathSmoothing.Visualization` to `PathSmoothing.Output.Comparison` and `PathSmoothing.Output.Visualization`.
- [ ] Simplify comparison execution to warm up and measure LocalG2 only; retain deterministic timing/digest behavior for its sole entry.
- [ ] Remove visual style colors, legend rows, labels, metric rows, and all source references for the three deleted algorithms.
- [ ] Publish four normal figures with stable stems `01-coarse-path-overview`, `02-all-paths-comparison`, `03-local-g2-overview`, and `04-curvature-comparison`; append `05-local-g2-diagnostic-candidate` only to an augmented diagnostic model.
- [ ] Update all allowed source/test imports to the new `Output` namespaces.
### Task 4: Align test/demo entry points and document the module
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/PathSmoothingComparisonDemo.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFactory.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticVisualizationDemo.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/README.md`
**Interfaces:**
- Fixture reports use the LocalG2-only comparison request and retain all eight fixtures.
- The README mirrors the `CoarsePath/README.md` information architecture for LocalG2 inputs, safety gates, result statuses, report output, and known limitations.
- [ ] Update test/demo imports and expected report shapes for the Output namespaces and LocalG2-only model.
- [ ] Create `README.md` with the following ordered sections: Module Overview, File Structure, Smoothing Data Flow, Result Status and Publication Rules, Coordinates and Units, Minimal Call Example, Detailed Usage Guide, Fixture Reports and Visualization, Common Errors, and First-Version Limits.
- [ ] State explicitly that a candidate passing collision validation may still be retained when its quality gate fails, and that `0 m` reserve removes only the additional clearance reserve, not collision or curvature checks.
### Task 5: Verify source layout and retain visualization artifacts
**Files:**
- Verify: `ClumsyPilot/ParkrobTrajplanner/PathSmoothing/`
- Verify: `ClumsyPilot/obj/path_smoothing_reports/`
- [ ] Build and run the isolated current-source LocalG2 visualization host against all eight fixture scenarios.
- [ ] Confirm all normal report directories contain the expected four PNG/SVG figures and CSV, and that `02-all-paths-comparison.png` presents raw plus LocalG2 only.
- [ ] Run the focused comparison/SVG/diagnostic verification scripts where their dependencies are available; report any root-build limitation separately.
- [ ] Inspect at least the `single-turn` normal report and `05-local-g2-diagnostic-candidate.png` to confirm LocalG2 labels, nonblank rendering, and retained diagnostic semantics.
- [ ] Update `.superpowers/sdd/progress.md` with the actual cleanup results and verification evidence.
@@ -0,0 +1,457 @@
# EM Observation MovementTest Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
**Goal:** Build a real-localization, observe-only MovementTest that creates a configurable start/goal map, plans Hybrid A* → Local G2 → EM trajectories, and shows world, LS, and ST diagnostics without sending a chassis command.
**Architecture:** Put map construction, planning bootstrap, rolling EM requests, trajectory observation, and LS/ST derivation in pure, testable classes. Keep MDCS reads, prompts, painters, background timing, and cancellation in one thin MovementTest host. The host may only read DetourInterface and BasicPilotBase.Chassis; its only control output is a displayed TrajectoryControlCommand.
**Tech Stack:** C# 10, netstandard2.0, existing Clumsy MovementTest/Painter UI, MDCS localization and chassis read APIs, Hybrid A*, Local G2, EM planner, OSQP, and EMPlannerVerificationHost.
## Global Constraints
- All map geometry and UI world coordinates are mm; Pose2D, velocities, and EM geometry are m, m/s, and rad.
- Bounds are exactly the start/goal axis-aligned rectangle expanded by MapPaddingMeters on all sides. Obstacles must fit these bounds; they must not enlarge them.
- Default settings are: padding 2.0 m, resolution 50 mm, replan 0.20 s, observer period 0.05 s.
- Capture world pose through DetourInterface.getCartLocation() and signed body-longitudinal velocity from BasicPilotBase.Chassis.GetCarSpeed(true).Vx. Create a monotonically increasing state sequence id.
- Output is TrajectoryControlCommand for display only. Do not invoke SendXYThSpeed, SendMotion, SendTh, AccumulateSpeed, ComputeWheelsGeometrically, brake/wheel adapter methods, or a geometric controller.
- Keep runtime source under ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest. Do not change the existing coarse-path factory, whose unrelated manual demo uses an 8 m expansion.
- Stop/cancel must cancel worker activity and clear the World, LS, and ST painter layers.
---
## File structure
| File | Responsibility |
| --- | --- |
| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs | Settings, manual-obstacle DTOs, validation, exact map-job construction. |
| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs | Hybrid A* + Local G2 bootstrap, rolling EM requests, time observation, LS/ST models. |
| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs | Three painter layers and presentation text; no MDCS/hardware use. |
| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs | Discoverable test, MDCS state reader, prompts, background session, console, cancellation. |
| ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md | Operator configuration, layer interpretation, unit and safety guidance. |
| ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs | Deterministic regression checks and an actuator-call source audit. |
| ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs | Adds the trajectory-observation command. |
### Task 1: Configuration and exact rectangle-map inputs
**Files:**
- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs
- Create: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs
**Interfaces:**
- Consumes: Pose2D, VehicleParameters, PlanningMapRequest, MapBoundsMm, ManualObstacleSource, CircleObstacle, AxisAlignedRectangleObstacle.
- Produces: TrajectoryObservationSettings.Validate(), TrajectoryObservationObstacle.Circle(double, double, double), TrajectoryObservationObstacle.Rectangle(double, double, double, double), and TrajectoryObservationSetupFactory.CreateBootstrapJob(Pose2D, Pose2D, TrajectoryObservationSettings, IReadOnlyList<TrajectoryObservationObstacle>, long).
- [ ] **Step 1: Write failing map-bounds and obstacle checks**
Create the verification host class and invoke it with a new trajectory-observation argument:
~~~csharp
internal static class TrajectoryObservationChecks
{
public static void Run()
{
VerifiesStartGoalBoundsUseOnlyConfiguredPadding();
RejectsObstacleOutsideConfiguredBounds();
}
private static void VerifiesStartGoalBoundsUseOnlyConfiguredPadding()
{
var settings = new TrajectoryObservationSettings
{
MapPaddingMeters = 2d,
MapResolutionMillimeters = 50f,
};
CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob(
new Pose2D(10d, -5d, 0d), new Pose2D(13d, -1d, 0d), settings,
Array.Empty<TrajectoryObservationObstacle>(), 17L);
Verification.NearlyEqual(8000d, job.MapRequest.Bounds.XMin, "observer map x min");
Verification.NearlyEqual(15000d, job.MapRequest.Bounds.XMax, "observer map x max");
Verification.NearlyEqual(-7000d, job.MapRequest.Bounds.YMin, "observer map y min");
Verification.NearlyEqual(1000d, job.MapRequest.Bounds.YMax, "observer map y max");
Verification.NearlyEqual(50d, job.MapRequest.ResolutionMm, "observer map resolution");
}
}
~~~
Modify Program.Main to accept trajectory-observation, call TrajectoryObservationChecks.Run(), then write PASS trajectory-observation. Add the same call to em-all.
- [ ] **Step 2: Run the new check to prove it fails**
Run:
~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
~~~
Expected: compilation fails because TrajectoryObservationSettings and TrajectoryObservationSetupFactory do not exist.
- [ ] **Step 3: Implement the contracts and factory**
Create the editable configuration contract:
~~~csharp
public sealed class TrajectoryObservationSettings
{
public double MapPaddingMeters { get; set; } = 2d;
public float MapResolutionMillimeters { get; set; } = 50f;
public double ReplanPeriodSeconds { get; set; } = 0.20d;
public double ObserverPeriodSeconds { get; set; } = 0.05d;
public double VehicleLengthMeters { get; set; } = 0.80d;
public double VehicleWidthMeters { get; set; } = 0.60d;
public double SafetyMarginMeters { get; set; } = 0.05d;
public double MaximumCurvaturePerMeter { get; set; } = 1d / 1.20d;
public void Validate();
public VehicleParameters CreateVehicle();
}
~~~
Implement finite/positive validation. Implement the obstacle as world-mm circle or axis-aligned rectangle with GetBounds() and ToMapObstacle(). Build bounds with the following exact calculation, rounded outward to the configured grid:
~~~csharp
double padMm = settings.MapPaddingMeters * 1000d;
var bounds = new MapBoundsMm(
ToGridLower(Math.Min(start.X, goal.X) * 1000d - padMm, settings.MapResolutionMillimeters),
ToGridUpper(Math.Max(start.X, goal.X) * 1000d + padMm, settings.MapResolutionMillimeters),
ToGridLower(Math.Min(start.Y, goal.Y) * 1000d - padMm, settings.MapResolutionMillimeters),
ToGridUpper(Math.Max(start.Y, goal.Y) * 1000d + padMm, settings.MapResolutionMillimeters));
~~~
Reject an obstacle unless its full envelope is contained in bounds. With zero obstacles set AllowExplicitEmptyMap true. Otherwise construct exactly one required ManualObstacleSource named trajectory-observer-manual with the supplied positive snapshot version. Return a CoarsePathPlanningJob with new HybridAStarConfiguration, StartDirection = null, and GoalDirection = GoalDirectionConstraint.Any.
- [ ] **Step 4: Run focused and existing checks**
Run:
~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- foundation
~~~
Expected: both exit 0 and print PASS trajectory-observation and PASS foundation.
- [ ] **Step 5: Commit the input layer**
~~~powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs
git commit -m "feat: add observation test map inputs"
~~~
### Task 2: Pure planning bootstrap, time observation, and LS/ST derivation
**Files:**
- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs
- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
**Interfaces:**
- Consumes: CoarsePathPlanningService, PathSmoothingService, EmPlanningCoordinator, TrajectoryExecutor, FrenetProjector, and caller-supplied VehicleMotionState.
- Produces: TrajectoryObservationBootstrapper.Bootstrap(CoarsePathPlanningJob, CancellationToken), TrajectoryObservationController.StartCycle(DateTimeOffset, VehicleMotionState, CancellationToken), TrajectoryObservationController.Observe(DateTimeOffset, VehicleMotionState), and TrajectoryObservationCharts.Build(EmTrajectory, DirectionSegmentView, double).
- [ ] **Step 1: Add failing chart and time-sampling checks**
Extend TrajectoryObservationChecks.Run() by adding VerifiesLsAndStUsePublishedTrajectoryData(). Use a fixed two-point EmTrajectory whose EffectiveAtUtc is 2026-08-04T00:00:00Z, with TimeFromStart values 0 and 1, PathS values 4 and 5, and known signed speeds. Assert that Build returns two ST samples (0,4) and (1,5), two speed samples, and the expected LS projection count. Call Observe at 00:00:00.500Z and assert that TrajectoryExecutor selected an interpolated point with TimeFromStart == 0.5d.
- [ ] **Step 2: Run the new check to prove it fails**
Run:
~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
~~~
Expected: compilation fails because TrajectoryObservationCharts and TrajectoryObservationController do not exist.
- [ ] **Step 3: Implement the pipeline**
Bootstrap must use exactly this success gate:
~~~csharp
CoarsePathPlanningJobResult coarse = coarseService.Plan(job, cancellationToken);
if (coarse.PlanningResult.Status != PlanningStatus.Success)
return TrajectoryObservationBootstrapResult.FromFailure(
job, coarse, null, "Coarse planning status: " + coarse.PlanningResult.Status);
var smoothingRequest = new PathSmoothingRequest(
CopyFiniteClearance(coarse.PlanningResult.Path, coarse.MapResult.Map),
coarse.PlanningResult.Segments, coarse.MapResult.Map, job.Vehicle,
new PathSmoothingConfiguration());
PathSmoothingResult smooth = smoothingService.Smooth(smoothingRequest, cancellationToken);
if (!IsPublishedSmoothingStatus(smooth.Status))
return TrajectoryObservationBootstrapResult.FromFailure(
job, coarse, smooth, smooth.Diagnostics.TerminationReason);
return TrajectoryObservationBootstrapResult.Success(job, coarse, smooth, ReferencePathSegmenter.Create(smooth));
~~~
IsPublishedSmoothingStatus accepts only Complete, PartialImprovement, NotNeeded, and Unchanged. CopyFiniteClearance replaces a positive-infinite clearance with the finite map diagonal before copying each CoarsePathPoint.
TrajectoryObservationController owns EmPlanningCoordinator and TrajectoryExecutor. For a replan it creates:
~~~csharp
var request = new EmPlanningRequest(
bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Job.Vehicle, state, configuration,
segmentIndex, coordinator.PublishedTrajectory, now, now,
sessionId + "-trajectory-" + cycleId, sessionId + "-reference",
coordinator.PublishedTrajectory?.Metadata.TrajectoryId ?? string.Empty,
EmMotionModel.NonholonomicForwardReverse);
return coordinator.PlanLatestAsync(new PlanningCycleInput(request, now), cancellationToken);
~~~
Set configuration.Scheduling.ReplanPeriodSeconds from settings. Initial observation mode always uses segmentIndex 0. Observe must use PublishedTrajectory only; when non-null call UpdateCommand(now, state, trajectory, trajectory.Metadata.Direction, trajectory.Metadata.Direction, true) and return the selected point, command, and executor state for display only.
Build LS/ST from published data alone:
~~~csharp
ls.Add(new TrajectoryObservationLsSample(
segment.SourceStartArcLength + projection.ReferenceS, projection.LateralOffset));
st.Add(new TrajectoryObservationStSample(point.TimeFromStart, point.PathS));
speed.Add(new TrajectoryObservationSpeedSample(point.TimeFromStart, point.SignedLongitudinalVelocity));
~~~
Use seeded FrenetProjector calls and count failed projections. No pipeline class may reference UI, DetourInterface, BasicPilotBase, or a hardware class.
- [ ] **Step 4: Run diagnostics and regression checks**
Run:
~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- coordinator
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- executor
~~~
Expected: every command exits 0.
- [ ] **Step 5: Commit the pure pipeline**
~~~powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: add EM observation planning pipeline"
~~~
### Task 3: Presentation layers and observation text
**Files:**
- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs
- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
**Interfaces:**
- Consumes: bootstrap result, observation result, and chart data.
- Produces: TrajectoryObservationPresentation.DrawWorld(TrajectoryObservationBootstrapResult, TrajectoryObservationObservation), DrawLs(TrajectoryObservationCharts), DrawSt(TrajectoryObservationCharts), ClearAll(), and TrajectoryObservationPresentationText.Create(TrajectoryObservationObservation, TrajectoryObservationCharts).
- [ ] **Step 1: Add a failing presentation-text check**
Assert that TrajectoryObservationPresentationText.Create(observation, charts) contains the literal OBSERVE_ONLY: no chassis command is sent., selected point time/path-S, signed speed, yaw rate, and LS projection failure count. The check must not instantiate a Painter.
- [ ] **Step 2: Run the check to verify it fails**
Run:
~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
~~~
Expected: compilation fails because TrajectoryObservationPresentationText does not exist.
- [ ] **Step 3: Implement the three painters**
Create exactly these named layers:
~~~csharp
worldPainter = UI.GetPainter("TrajectoryObserver.World", true);
lsPainter = UI.GetPainter("TrajectoryObserver.LS", true);
stPainter = UI.GetPainter("TrajectoryObserver.ST", true);
~~~
DrawWorld clears only worldPainter then draws map bounds/grid/occupied cells, start, goal, coarse path, Local G2 path, real pose, and latest EM path. Convert every planner position from m to mm before calling DrawLine, DrawCircle, or DrawText.
DrawLs draws axes plus s-l samples. DrawSt draws t-s and a vertically separated t-v series with a legend. A missing trajectory draws a status string instead of throwing. ClearAll invokes Clear on all three painters and performs no other action.
- [ ] **Step 4: Run visual-model and compile verification**
Run:
~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
~~~
Expected: trajectory-observation passes and the project has zero compile errors.
- [ ] **Step 5: Commit presentation**
~~~powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: visualize EM observation diagnostics"
~~~
### Task 4: MDCS read-only MovementTest host
**Files:**
- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs
- Create: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md
- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
**Interfaces:**
- Consumes: DetourInterface.getCartLocation(), BasicPilotBase.Chassis.GetCarSpeed(true), setup/controller/presentation APIs.
- Produces: a [MovementTest(name = "EM轨迹规划观察闭环测试")] entry with Test() and TestStop().
- [ ] **Step 1: Write a failing actuator-free source audit**
Add VerifiesObservationSourceHasNoActuatorCalls() to TrajectoryObservationChecks.Run() and implement it in the verification host. It reads the observation runtime source files and fails on any of these tokens:
~~~csharp
new[]
{
".SendXYThSpeed(", ".SendMotion(", ".SendTh(", ".AccumulateSpeed(",
".ComputeWheelsGeometrically(", ".DriveStop(", ".PredefinedDriveStop("
}
~~~
The audit strings live only in the test host; none may appear in the new runtime observation files.
- [ ] **Step 2: Run the audit before the host exists**
Run:
~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
~~~
Expected: the check fails because MovementTest.TrajectoryObservationTest.cs is missing.
- [ ] **Step 3: Implement the host and lifecycle**
Use this discoverable configuration:
~~~csharp
[MovementTest(name = "EM轨迹规划观察闭环测试")]
public sealed class TrajectoryObservationMovementTest : MovementTest
{
public double GoalXmm = double.NaN;
public double GoalYmm = double.NaN;
public double GoalYawDeg = 0d;
public double MapPaddingMeters = 2d;
public float MapResolutionMm = 50f;
public double ReplanPeriodSeconds = 0.20d;
public double ObserverPeriodSeconds = 0.05d;
public override void Test();
public override void TestStop();
}
~~~
When GoalXmm or GoalYmm is non-finite, prompt for all goal values with the same finite parser/UI.GetInput pattern as CoarsePathPlanningTest. Prompt for 020 manual obstacles (circle or rectangle) and freeze all inputs before Task.Run begins.
The MDCS reader must use only this read path:
~~~csharp
var location = DetourInterface.getCartLocation();
if (location == null) throw new InvalidOperationException("Live localization is unavailable.");
if (BasicPilotBase.Chassis == null) throw new InvalidOperationException("Live chassis read interface is unavailable.");
var speed = BasicPilotBase.Chassis.GetCarSpeed(true);
return new VehicleMotionState(
new Pose2D(location.x / 1000d, location.y / 1000d, location.th * Math.PI / 180d),
speed.Vx, null, DateTimeOffset.UtcNow, Interlocked.Increment(ref stateSequence));
~~~
Bootstrap once in a cancellable Task.Run. After success, run Task.Delay(TimeSpan.FromSeconds(ObserverPeriodSeconds), token) between ticks. At each tick capture exactly one state, start a cycle only when controller.ShouldStartCycle(now), observe the latest published trajectory, draw all layers, and print a throttled status. Construct the service as new EmPlanningService(new OsqpNativeSolver()).
Every status includes:
~~~text
OBSERVE_ONLY: no chassis command is sent.
~~~
When a GearSwitch trajectory reaches its final time, draw and print 等待真实档位/方向确认;观察模式不会推进下一方向段, leave segment index 0, and do not create a direction-change action. Goal and rolling-stop commands may only be logged.
Use a lock/session id pattern matching CoarsePathPlanningTestRunner: replace the active CancellationTokenSource, cancel the old source without waiting, and allow only the current session to draw or log. TestStop cancels, disposes after task completion, and calls presentation.ClearAll.
Write README.md with configuration fields/units, obstacle examples, default values, chart interpretations, the VelocityX/VelocityY world-frame warning, and the explicit no-driving limitation.
- [ ] **Step 4: Verify runner safety and integration build**
Run:
~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
rg -n 'SendXYThSpeed\(|SendMotion\(|SendTh\(|AccumulateSpeed\(|ComputeWheelsGeometrically\(|DriveStop\(|PredefinedDriveStop\(' ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest -g '*.cs'
~~~
Expected: host and build exit 0. The rg command exits 1 because no runtime observation file calls a forbidden actuator method.
- [ ] **Step 5: Commit the MovementTest**
~~~powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: add read-only EM observation movement test"
~~~
### Task 5: End-to-end regression and operator handoff
**Files:**
- Modify: ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
- Modify: ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md
**Interfaces:**
- Consumes: completed observation-test components and existing EMPlannerVerificationHost checks.
- Produces: a reproducible all-up verification command and a launch/stop checklist.
- [ ] **Step 1: Add a failing bootstrap regression**
Use an empty-map setup with start (0.5, 0.5, 0) m and goal (3.5, 0.5, 0) m. Assert that bootstrap returns a successful map, PlanningStatus.Success, a publishable smoothing result, and at least one DirectionSegmentView. This check does not run native OSQP.
- [ ] **Step 2: Run the check to confirm its failure**
Run:
~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
~~~
Expected: the assertion identifies a missing or incorrect bootstrap result.
- [ ] **Step 3: Make the smallest corrective change**
Correct only TrajectoryObservationSetupFactory or TrajectoryObservationBootstrapper so the empty-map request produces a planning-ready map and publishable Local G2 path. Preserve the exact bounds rule and do not add UI, MDCS, or hardware dependencies to pure classes.
- [ ] **Step 4: Run all required evidence checks**
Run:
~~~powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all
dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
git diff --check
~~~
Expected: every command exits 0. In the vehicle UI, the entry appears as EM轨迹规划观察闭环测试 and starting/running/stopping it does not issue any chassis, motor, steering, or brake output.
- [ ] **Step 5: Commit final verification/documentation**
~~~powershell
git add -- ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md
git commit -m "test: verify EM observation movement test"
~~~
## Plan self-review
**Spec coverage:** Task 1 provides configurable start/goal map bounds, vehicle settings, and manual obstacles. Task 2 covers Hybrid A*, Local G2, rolling EM, time sampling, and derived LS/ST. Task 3 creates World/LS/ST painters. Task 4 reads live MDCS state, prints observation diagnostics, handles gear-switch observation, and ensures cancellation/no-write behavior. Task 5 supplies an end-to-end fixture and final evidence.
**Placeholder scan:** Every task names concrete files, commands, interface names, inputs, expected behavior, and commit content; no deferred implementation markers remain.
**Type consistency:** Map code produces PlanningMapRequest and CoarsePathPlanningJob; bootstrap produces PathSmoothingResult and DirectionSegmentView; rolling planning consumes VehicleMotionState and EmPlanningRequest; UI consumes EmTrajectory, TrajectoryControlCommand, and chart samples without changing EM contracts.
@@ -0,0 +1,334 @@
# EM Observation Diagnostics Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make every EM planning-cycle failure visible in the host terminal and the World/L-S/S-T observation canvases without changing observation-only safety behavior.
**Architecture:** Add a pure diagnostic formatter that keeps `EmPlanningStatus` and the original `FailureReason` from `PlanningCycleResult`. Extend the observation-loop tick with explicit start/completion events so the runner writes one pending line and one completed-cycle line per cycle, while the painters receive the current diagnostic every tick.
**Tech Stack:** C# / .NET Standard 2.0 plugin, `Hedingben.ToastText`, `UI.GetPainter`, EM planner contracts, .NET verification host.
## Global Constraints
- The MovementTest remains observe-only: do not add any chassis, brake, wheel, or actuator call.
- Terminal output uses `Console.WriteLine` and starts with `[TrajectoryObserver]`.
- Every completed cycle reports raw `EmPlanningStatus`, `published`, version, elapsed time, and the original nonempty `FailureReason`.
- A 50 ms observer tick must not emit a duplicate terminal record.
- World, L-S, and S-T painters must show the diagnostic even when `PublishedTrajectory` is null.
---
### Task 1: Add a pure planning diagnostic formatter
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`
**Interfaces:**
- Consumes: `PlanningCycleResult`, `EmTrajectory`, `TimeSpan`, and the in-flight flag.
- Produces: `TrajectoryObservationDiagnostic.Text`, a compact multi-line operator string.
- [ ] **Step 1: Write the failing test**
Add `VerifiesPlanningDiagnosticsKeepRawFailureReason();` to `Run()`, then add:
```csharp
private static void VerifiesPlanningDiagnosticsKeepRawFailureReason()
{
var failed = new PlanningCycleResult(
4L,
new PlanningCycleIdentity(3L, "diagnostic-reference", 7L, string.Empty, 0),
new EmPlanningResult(EmPlanningStatus.CorridorInfeasible, null,
"map=3;reference=diagnostic-reference;state=7;previous=;segment=0;reason=no connected corridor"),
false,
"map=3;reference=diagnostic-reference;state=7;previous=;segment=0;reason=no connected corridor");
TrajectoryObservationDiagnostic diagnostic = TrajectoryObservationDiagnostics.Create(
failed, TimeSpan.FromMilliseconds(18d), false, null);
Verification.True(diagnostic.Text.Contains("cycle=4"), "diagnostic has cycle version");
Verification.True(diagnostic.Text.Contains("status=CorridorInfeasible"), "diagnostic preserves raw status");
Verification.True(diagnostic.Text.Contains("published=False"), "diagnostic preserves publish state");
Verification.True(diagnostic.Text.Contains("elapsed=18ms"), "diagnostic preserves elapsed time");
Verification.True(diagnostic.Text.Contains("reason=map=3;reference=diagnostic-reference"),
"diagnostic preserves planner failure reason");
TrajectoryObservationDiagnostic pending = TrajectoryObservationDiagnostics.Create(
null, TimeSpan.Zero, true, null);
Verification.Equal("planning status=pending", pending.Text, "diagnostic reports pending before completion");
}
```
- [ ] **Step 2: Run the test to verify RED**
Run:
```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
```
Expected: build failure because `TrajectoryObservationDiagnostic` and `TrajectoryObservationDiagnostics` do not exist.
- [ ] **Step 3: Write the minimal formatter**
Create `TrajectoryObservationDiagnostics.cs`:
```csharp
using System;
using System.Globalization;
using MultiWheelC.TrajectoryPlanning.EMPlanner;
namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation;
public sealed class TrajectoryObservationDiagnostic
{
public TrajectoryObservationDiagnostic(string text)
{
Text = text ?? string.Empty;
}
public string Text { get; }
}
public static class TrajectoryObservationDiagnostics
{
public static TrajectoryObservationDiagnostic Create(PlanningCycleResult latestCycle,
TimeSpan elapsed, bool planningInFlight, EmTrajectory publishedTrajectory)
{
if (latestCycle == null)
return new TrajectoryObservationDiagnostic(planningInFlight
? "planning status=pending"
: "planning status=not-started");
string text = "planning cycle=" + latestCycle.Version.ToString(CultureInfo.InvariantCulture) +
" status=" + latestCycle.Result.Status +
" published=" + latestCycle.Published +
" elapsed=" + Math.Max(0d, elapsed.TotalMilliseconds).ToString("F0", CultureInfo.InvariantCulture) + "ms";
if (planningInFlight)
text += "\nreplan=pending";
if (publishedTrajectory != null)
text += "\ntrajectory=" + publishedTrajectory.Metadata.TrajectoryId;
if (!string.IsNullOrWhiteSpace(latestCycle.Result.FailureReason))
text += "\nreason=" + latestCycle.Result.FailureReason;
return new TrajectoryObservationDiagnostic(text);
}
}
```
- [ ] **Step 4: Run the test to verify GREEN**
Run the Step 2 command.
Expected: `PASS trajectory-observation`.
- [ ] **Step 5: Commit**
```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationDiagnostics.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: format EM observation diagnostics"
```
### Task 2: Report once at the start and completion of every planning cycle
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`
**Interfaces:**
- Produces: `TrajectoryObservationLoopTick.PlanningStarted` and `.PlanningCompleted`.
- Consumes: those flags in the MovementTest to issue one terminal/UI status record per lifecycle event.
- [ ] **Step 1: Write the failing test**
In `VerifiesObserverTicksWhilePlanningIsDelayed`, after the first tick, add:
```csharp
Verification.True(firstTick.PlanningStarted, "observer first tick reports a planning-cycle start");
Verification.True(!firstTick.PlanningCompleted, "observer first tick has no completed cycle");
```
After `finalTick` is created, add:
```csharp
Verification.True(finalTick.PlanningCompleted, "observer completion tick reports cycle completion");
```
In `VerifiesObservationSourceUsesRequiredOperatorText`, add:
```csharp
Verification.True(source.Contains("Console.WriteLine(\"[TrajectoryObserver] \" + text);"),
"observer status is mirrored to the host terminal");
```
- [ ] **Step 2: Run the test to verify RED**
Run the Task 1 test command.
Expected: assertions fail because lifecycle flags and terminal output do not exist.
- [ ] **Step 3: Implement lifecycle flags and output**
Change `TrajectoryObservationLoopTick` to accept and expose:
```csharp
bool planningStarted, bool planningCompleted
public bool PlanningStarted { get; }
public bool PlanningCompleted { get; }
```
In `TrajectoryObservationLoop.Tick`, use:
```csharp
bool planningCompleted = ConsumeCompletedPlanning(now);
bool planningStarted = false;
if (planningTask == null && controller.ShouldStartCycle(now))
{
planningStarted = true;
planningStartedAtUtc = now;
planningTask = controller.StartCycle(now, state, cancellationToken);
planningCompleted |= ConsumeCompletedPlanning(now);
}
```
Change `ConsumeCompletedPlanning` to return `false` when no completed Task is available and `true` after it assigns `latestCycle`, updates elapsed time, and clears `planningTask`. Pass both flags to the tick constructor.
In `RunSessionAsync`, after the tick is created, make one diagnostic and only log event records:
```csharp
TrajectoryObservationDiagnostic diagnostic = TrajectoryObservationDiagnostics.Create(
tick.LatestCycle, tick.LatestPlanningElapsed, tick.PlanningInFlight,
observation.PublishedTrajectory);
if (tick.PlanningStarted)
LogIfCurrent(sessionId, "planning status=pending");
if (tick.PlanningCompleted)
LogIfCurrent(sessionId, diagnostic.Text);
```
Remove the unconditional `if (tick.ShouldLog)` status call. Keep the existing session-start, stop, bootstrap-failure, and runtime-fault calls.
Change `PrintStatus` to:
```csharp
private static void PrintStatus(string message)
{
string text = ObserveOnlyNotice + "\n" + message;
Hedingben.ToastText(text, StatusChannel);
Console.WriteLine("[TrajectoryObserver] " + text);
}
```
- [ ] **Step 4: Run the test to verify GREEN**
Run the Task 1 test command.
Expected: `PASS trajectory-observation`; the delayed-planner test still proves the observer does not wait for planning.
- [ ] **Step 5: Commit**
```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: report EM observation planning cycles"
```
### Task 3: Persist planning diagnostics in all three painter layers
**Files:**
- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`
**Interfaces:**
- Consumes: `TrajectoryObservationDiagnostic.Text`.
- Produces: World/L-S/S-T empty states that show the precise planning status and reason.
- [ ] **Step 1: Write the failing test**
Add `VerifiesEmptyChartsReceivePersistentPlanningDiagnostic();` to `Run()` and add:
```csharp
private static void VerifiesEmptyChartsReceivePersistentPlanningDiagnostic()
{
string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot",
"ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPresentation.cs");
string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(presentationPath));
Verification.True(source.Contains("DrawLs(TrajectoryObservationCharts charts, string diagnosticText)"),
"LS painter accepts planning diagnostic input");
Verification.True(source.Contains("DrawSt(TrajectoryObservationCharts charts, string diagnosticText)"),
"ST painter accepts planning diagnostic input");
Verification.True(source.Contains("No published trajectory available for L-S chart.\n"),
"LS empty state includes diagnostic after chart label");
Verification.True(source.Contains("No published trajectory available for T-S/T-V charts.\n"),
"ST empty state includes diagnostic after chart label");
}
```
- [ ] **Step 2: Run the test to verify RED**
Run the Task 1 test command.
Expected: source checks fail because painter methods have no diagnostic parameter.
- [ ] **Step 3: Add painter parameters and wire the diagnostic**
Change signatures to:
```csharp
public void DrawWorld(TrajectoryObservationBootstrapResult bootstrap,
TrajectoryObservationObservation observation, TrajectoryObservationRuntimeState runtimeState,
string diagnosticText)
public void DrawLs(TrajectoryObservationCharts charts, string diagnosticText)
public void DrawSt(TrajectoryObservationCharts charts, string diagnosticText)
```
Add this helper in `TrajectoryObservationPresentation`:
```csharp
private static string EmptyChartMessage(string label, string diagnosticText)
{
return string.IsNullOrWhiteSpace(diagnosticText)
? label
: label + "\n" + diagnosticText;
}
```
For World, draw `EmptyChartMessage("No published trajectory available.", diagnosticText)` at `bootstrap.Map.Bounds.XMin + 100f, bootstrap.Map.Bounds.YMin + 300f` before returning from the empty trajectory path. For L-S and S-T, draw `EmptyChartMessage` with their existing label at `0f, 0f`.
Change `DrawIfCurrent` to accept `TrajectoryObservationDiagnostic diagnostic` and call:
```csharp
Presentation.DrawWorld(bootstrap, observation, runtimeState, diagnostic?.Text ?? string.Empty);
Presentation.DrawLs(charts, diagnostic?.Text ?? string.Empty);
Presentation.DrawSt(charts, diagnostic?.Text ?? string.Empty);
```
Pass the diagnostic created in Task 2 from `RunSessionAsync`. For the bootstrap-failure path, pass:
```csharp
new TrajectoryObservationDiagnostic("bootstrap failed: " + bootstrap.FailureReason)
```
- [ ] **Step 4: Verify focused test, build, and diff**
Run:
```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation
dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore
git diff --check
```
Expected: `PASS trajectory-observation`, zero build errors, and no diff whitespace errors.
- [ ] **Step 5: Commit**
```powershell
git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs
git commit -m "feat: show EM observation failure diagnostics"
```
@@ -0,0 +1,227 @@
# EM FullDirection Correctness Fixes Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use test-driven-development for every task. Execute tasks serially because they share the EM solver pipeline.
**Goal:** Fix confirmed FullDirection projection, cancellation, curvature-constraint, timeout-budget, and trajectory-coordinate defects without replacing the existing LS/ST planner.
**Architecture:** Keep `IEmPlanningService`, `EmPlanningRequest`, `LateralPlanner`, `LongitudinalPlanner`, and trajectory contracts compatible. Apply local fixes where one component owns the invariant; introduce only a shared internal lateral-curvature affine model and internal explicit-budget overloads where the same invariant necessarily crosses components.
**Tech Stack:** C# 10, .NET Standard 2.0 production assembly, .NET 8 verification host, solver-neutral `IQpSolver` tests.
## Global Constraints
- `FullDirectionSegment` plans exactly one complete direction segment; `RollingHorizon` behavior is not redesigned in this plan.
- FullDirection ego admission is restricted to the segment-start prefix `[0, min(L, MaximumProjectionDistanceMeters)]`; it must not select a later U-shape/self-overlap branch.
- A start heading error with magnitude greater than or equal to `π/2` is rejected before `tan(headingError)` is evaluated.
- `Cancelled` always carries a null trajectory/path/candidate, even if a strict fallback candidate exists.
- `SolverTimeoutSeconds` is one combined LS+ST solve budget for a service call, not a fresh budget for each optimizer.
- Every LS QP has a finite linearized vehicle-curvature hard-bound row at every station; nonlinear validation remains authoritative.
- `SegmentLocalS` stores interpolated direction-segment reference S; `PathS` stores optimized lateral-path arc length.
- Do not edit or revert the user's existing `EmPlannerConfiguration.cs` change, PathSmoothing work, Map work, or `ClumsyPilot.csproj` changes.
- Do not add actuator calls, change public EM request/result signatures, or commit/stage files from the dirty shared worktree.
---
### Task 1: Anchor FullDirection start projection and reject folded headings
**Files:**
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs`
**Interfaces:**
- Consumes: existing `FrenetProjector.TryProject` bounded-window overload.
- Produces: service-local FullDirection start-prefix admission; Rolling continues using `[0,L]`.
- [ ] **Step 1: Write failing service tests.** Add a FullDirection U-shaped all-forward reference whose later arm is closer to the measured pose, and assert `ProjectionFailed` rather than accepting a later `ReferenceS`. Add a same-position start pose with yaw `π`, and assert `ProjectionFailed` with a null trajectory.
```csharp
EmPlanningResult wrongBranch = service.Plan(fullURequest, CancellationToken.None);
Verification.Equal(EmPlanningStatus.ProjectionFailed, wrongBranch.Status,
"FullDirection cannot enter through a later U branch");
EmPlanningResult reversedHeading = service.Plan(oppositeHeadingRequest, CancellationToken.None);
Verification.Equal(EmPlanningStatus.ProjectionFailed, reversedHeading.Status,
"opposite start heading is rejected before slope conversion");
```
- [ ] **Step 2: Run the focused test and verify RED.**
```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-planning-service
```
Expected: the later U branch and/or opposite-heading assertion fails under the current global `[0,L]`, seed-zero projection.
- [ ] **Step 3: Implement the local admission rule.** In `EmPlanningService.Plan`, choose the projection upper bound from scope and validate heading before constructing lateral input.
```csharp
double startProjectionUpperS = request.PlanningScope == EmPlanningScope.FullDirectionSegment
? Math.Min(segment.LengthMeters, configuration.Frenet.MaximumProjectionDistanceMeters)
: segment.LengthMeters;
if (!projector.TryProject(request.VehicleState.Pose, segment, 0d, startProjectionUpperS,
configuration.Frenet.MaximumProjectionDistanceMeters, 0d, out FrenetProjection startProjection) ||
Math.Abs(startProjection.HeadingError) >= Math.PI / 2d)
{
return Failure(EmPlanningStatus.ProjectionFailed, request,
"Vehicle pose is not an admissible start state for the selected direction segment.");
}
```
- [ ] **Step 4: Re-run `em-planning-service` and verify GREEN.** Existing Rolling projection behavior must remain green.
### Task 2: Make cancellation terminal and non-publishable
**Files:**
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LongitudinalIntegrationChecks.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs`
**Interfaces:**
- Produces: `Cancelled` results with null candidate/path/trajectory at every layer.
- [ ] **Step 1: Write failing optimizer tests.** Use a solver that returns one valid candidate and cancels the supplied source before the next iteration. Assert both optimizers return `Cancelled`, not `SuccessWithFallback`, and expose no candidate.
```csharp
Verification.Equal(EmPlanningStatus.Cancelled, result.Status,
"cancellation is never converted to fallback success");
Verification.True(result.Path == null, "cancelled lateral result has no path");
```
- [ ] **Step 2: Verify RED with the focused groups.**
```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-integration
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- longitudinal-integration
```
Expected: at least one optimizer currently returns `SuccessWithFallback`.
- [ ] **Step 3: Implement minimal cancellation precedence.** Special-case cancellation in each `FallbackOrFailure`, and check the token after LS, after ST, after assembly, and immediately before service success publication.
```csharp
if (failureStatus == EmPlanningStatus.Cancelled)
return Failed(EmPlanningStatus.Cancelled, failureReason);
```
- [ ] **Step 4: Re-run both optimizer groups and `em-planning-service`; verify GREEN.**
### Task 3: Add shared linearized curvature hard constraints
**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralCurvatureLinearization.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralObjectiveBuilder.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralConstraintBuilder.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs`
**Interfaces:**
- Produces: `LateralCurvatureLinearization.Create(input, layout, iterate)` returning immutable station affines with indices, gradient, and constant.
- Consumers: objective terms and hard constraints use the exact same affine coefficients.
- [ ] **Step 1: Write a failing QP-shape test.** For a straight reference and zero iterate, set vehicle maximum curvature to `0.25 1/m`; assert every station has a row equivalent to `-0.25 <= DDL(i) <= 0.25`. Also assert the total constraint count increases by the station count.
```csharp
Verification.Equal(expectedOldRows + layout.StationCount, problem.ConstraintCount,
"one curvature hard-bound row is emitted per station");
Verification.True(HasBound(problem,
new Dictionary<int, double> { { layout.DDL(station), 1d } }, -0.25d, 0.25d),
"straight-path curvature affine is hard bounded");
```
- [ ] **Step 2: Run `lateral-model` and verify RED.**
- [ ] **Step 3: Extract the existing affine calculation without changing its formula.** Move `CreateCurvatureAffines` and its value type from `LateralObjectiveBuilder` to the new internal file. Keep the nonlinear formula in `LateralGeometryEvaluator`/independent validator unchanged.
- [ ] **Step 4: Add one curvature row per station in `LateralConstraintBuilder`.** Bounds are `[-maximumVehicleCurvature, +maximumVehicleCurvature]` after subtracting the affine constant.
```csharp
AddRow(constraints, lower, upper, ref row,
-maximumCurvature - affine.Constant,
maximumCurvature - affine.Constant,
affine.Indices, affine.Gradient);
```
- [ ] **Step 5: Update test solver problem classification so the added lateral rows are not mistaken for ST rows, then run `lateral-model`, `lateral-integration`, and `em-planning-service` GREEN.**
### Task 4: Relinearize rejected solved vectors and report iteration exhaustion honestly
**Files:**
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs`
**Interfaces:**
- Produces: rejected, parseable solver candidates may advance only the SQP iterate/warm start; they never replace `lastValidatedPath`.
- [ ] **Step 1: Write failing tests.** Configure a low curvature limit so the first candidate is strict and the second parseable candidate fails nonlinear validation. Assert the third QP/warm start is based on the second candidate, while a later timeout still returns the first strict path. Change the outer-limit assertion from ordinary `Success` to `SuccessWithFallback` with a non-empty reason.
- [ ] **Step 2: Run `lateral-integration` and verify RED.**
- [ ] **Step 3: Move iterate/warm-start advancement to immediately after a parseable solved candidate, while updating `lastValidatedPath` only after independent validation.** Return `SuccessWithFallback` when the outer loop ends with a strict candidate but without satisfying convergence.
- [ ] **Step 4: Run `lateral-integration` and `lateral-real-osqp` GREEN.**
### Task 5: Share one LS/ST solver timeout budget
**Files:**
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/EmPlanningServiceChecks.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPlanner.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/LongitudinalPlanner.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Longitudinal/SequentialLongitudinalOptimizer.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Facade/EmPlanningService.cs`
**Interfaces:**
- Keeps: existing public `Plan(input, token)` and `Optimize(input, token)` entry points.
- Adds: internal overloads accepting a finite positive `TimeSpan solveBudget`.
- [ ] **Step 1: Write a failing service test.** Delay a lateral fake solve by at least 100 ms under a 2 s configured timeout, record all `QpSolverSettings.MaximumSolveDuration` values, and assert the first ST call receives less than 1.95 s rather than a fresh 2 s.
- [ ] **Step 2: Run `em-planning-service` and verify RED.**
- [ ] **Step 3: Add internal explicit-budget overloads.** Default public overloads continue deriving budget from configuration; service starts one monotonic `Stopwatch` immediately before LS and passes `configuredBudget - elapsed` to LS and then ST. Non-positive remaining time returns `SolverTimedOut` with no trajectory.
- [ ] **Step 4: Add a final elapsed/cancellation check before publication and run `lateral-integration`, `longitudinal-integration`, and `em-planning-service` GREEN.**
### Task 6: Preserve reference S separately from optimized PathS
**Files:**
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryChecks.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/LateralPathInterpolator.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Trajectory/EmTrajectoryAssembler.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Validation/EmTrajectoryValidator.cs`
**Interfaces:**
- Extends internal `InterpolatedLateralPathPoint` with `ReferenceS`.
- Keeps public `EmTrajectoryPoint` shape unchanged.
- [ ] **Step 1: Write a failing curved/offset-path assembly test.** Construct a validated lateral path where reference-S and chord PathS differ; assert each output point's `SegmentLocalS` is the interpolated reference-S and `PathS` remains the ST progress value.
- [ ] **Step 2: Run `trajectory` and verify RED.** Current assembly writes `sample.PathS` into both fields.
- [ ] **Step 3: Interpolate `ReferenceS` using the same PathS bracket and pass `geometry.ReferenceS` as `SegmentLocalS`.** Update publication bounds so segment-local S is checked against direction-segment length/reference bound, while PathS is checked against the optimized path upper bound.
- [ ] **Step 4: Run `trajectory` and `em-core-all` GREEN.**
### Task 7: Core regression and diff hygiene
**Files:**
- Verify only; no broad formatting.
- [ ] **Step 1: Run the complete EM verification set.**
```powershell
dotnet build ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj --no-restore
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj --no-build -- em-all
```
- [ ] **Step 2: Check only scoped diffs.**
```powershell
git diff --check -- ClumsyPilot/ParkrobTrajplanner/EMPlanner ClumsyPilot/tests/EMPlannerVerificationHost
git status --short -- ClumsyPilot/ParkrobTrajplanner/EMPlanner ClumsyPilot/tests/EMPlannerVerificationHost
```
Expected: all groups pass; no whitespace errors; `EmPlannerConfiguration.cs` remains exactly the user's pre-existing modification.