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.
@@ -0,0 +1,111 @@
# TrapMap完整图片导出与终端日志开关设计
## 目标
`MovementTest.Trapmaptest.cs`增加两个相互独立的测试开关:
- 成功建图后,将完整栅格地图独立渲染为300 DPI PNG,不依赖Clumsy当前视口、缩放或其他Painter。
- 控制TrapMap调试信息是否同步打印到宿主进程终端,同时始终保留`DLog`日志。
现有`TrapMapTest` Painter初始化和清理已经使用世界坐标调用`UI.GetPainter("TrapMapTest")`,本次不重复修改。两腿检测ROI继续使用车体局部Painter及`false`参数。
## 用户开关与默认值
`TrapMapTest`手动编辑区增加:
```csharp
private const bool _saveFullMapImage = true;
private const bool _enableTerminalDebugLog = true;
```
两个值传入`TrapMapBuilder`或共享日志/导出组件。关闭图片开关时不得创建目录、临时文件或PNG。关闭终端开关时只抑制`Console.WriteLine`,不得抑制`DLog`和必要的UI提示。
## 图片内容
图片从最终发布的`GridMapData`离屏渲染,包含完整地图边界而不是屏幕截图:
- 白色背景。
- 浅灰色完整栅格线,确保每个小格可见。
- 红色占用栅格。
- 蓝色车辆轮廓、几何中心和朝向。
- 绿色工作站目标标记。
- 黑色地图外边界。
- 标题/图例区域,显示世界边界、分辨率、行列数、占据率、障碍物数量、轮胎层状态和输入来源。
世界X轴在图片中向右;世界Y轴向上,因此从`Cells[row,col]`映射到位图时反转图像Y方向。工作站仅绘制标记,不写入占用数据。
## 图片尺寸与文件约束
- 每个栅格使用`4×4`像素。
- PNG水平和垂直DPI都设置为`300`
- 包含边距和标题后,任一图片边长不得超过`4000`像素。
- 最终PNG文件大小不得超过`50 * 1024 * 1024`字节。
尺寸在分配RGBA像素缓冲区前检查。若超过4000像素,跳过导出并报告明确原因。编码先写入同目录临时文件,完成后检查实际字节数;超过50MB时删除临时文件,不留下超限最终文件。只有所有检查通过后,才原子移动/重命名为最终PNG。
典型`327×139`地图的栅格主体约为`1308×556`像素,另加标题和边距。
## 保存位置与命名
输出根目录使用宿主进程当前工作目录:
```text
TrapMapExports\TrapMap_yyyyMMdd_HHmmss_fff.png
```
毫秒时间戳避免同一秒多次测试覆盖。目录只在图片开关打开且地图成功后创建。临时文件使用同目录、同文件名加`.tmp`后缀,以保证最终重命名不跨磁盘。
## 日志行为
引入TrapMap专用日志入口,其行为为:
```text
所有消息 -> DLog.Log(message, "TrapMapTest")
终端开关开启 -> 额外Console.WriteLine("[TrapMapTest] " + message)
```
至少覆盖测试开始、输入参数、Detour位姿、车辆尺寸、地图边界/尺寸、轮胎层状态、图片保存成功/跳过/失败、最终统计和测试停止。图片错误不得因终端开关关闭而静默,仍必须进入`DLog`
## 组件边界
图片导出放在独立文件`ClumsyPilot/TrapMapImageExporter.cs`,避免继续扩大已经较长的MovementTest文件。组件只消费不可变的导出请求数据:地图、车辆位姿、工作站、轮胎层元数据和目标文件路径;它不读取Detour、雷达或UI,也不修改栅格。
`MovementTest.Trapmaptest.cs`负责开关、调用时机、日志和错误降级。导出发生在地图成功生成之后;导出失败不改变`TrapMapBuilder.Succeeded``GridMap`
实现使用内部纯C# RGBA光栅器绘制栅格、车辆、工作站和5×7位图文字,再由精确版本`StbImageWriteSharp` 1.16.7编码PNG。编码后立即在`IHDR`后插入`pHYs=11811/11811/unit1`,以保留300 DPI元数据。运行时不依赖平台绘图程序集或原生图形资产;除单个托管Stb编码程序集外,光栅、元数据和文件流程均只使用BCL。
## 错误处理
以下情况只导致图片导出失败,不导致建图失败:
- 图片开关关闭。
- 图片尺寸超过4000像素。
- 输出目录创建失败。
- RGBA缓冲区创建、绘制或PNG编码异常。
- 临时文件超过50MB。
- 临时文件重命名失败。
异常路径必须尽力删除本次临时文件,不得删除已有的成功PNG。
## 验证要求
至少验证:
1. 图片开关关闭时不创建文件和目录。
2. 小型已知栅格导出的PNG由BCL测试解码器重新读取;所有chunk CRC有效,`IHDR`为RGBA8`pHYs`表示300 DPI,像素尺寸符合4像素/格及布局规则。
3. PNG中占用格、车辆和工作站采样位置颜色正确,Y轴没有上下颠倒。
4. 超过4000像素的请求在RGBA缓冲区分配前失败。
5. 最终路径使用毫秒时间戳且不覆盖旧文件。
6. 成功文件严格小于或等于50MB,超限临时文件被删除。
7. 终端开关开启时消息同时进入DLog和终端;关闭时仍进入DLog但不写终端。
8. 图片失败时地图仍为成功状态。
9. 现有源码契约、栅格行为、生命周期测试及`ClumsyPilot`编译继续通过。
## 非目标
- 不截取Clumsy/CycleGUI窗口。
- 不保存紫色UI背景、小车3D模型、绿色两腿ROI或橙色检测猜测线。
- 不改变栅格数据格式或地图边界计算。
- 不接入新的点云来源。
- 不修改`TrajPlanner`
- 不提交或暂存本次工作区改动。
@@ -0,0 +1,52 @@
# TrapMap Managed PNG Export Design
## Goal
Replace `System.Drawing.Common` in TrapMap image export so Clumsy can save the full grid PNG without depending on platform-specific drawing assemblies.
## Chosen approach
Use a small internal RGBA rasterizer for grid primitives and `StbImageWriteSharp` 1.16.7 for PNG encoding. Keep the public request/result API and the existing TrapMap call site unchanged.
Alternatives rejected:
- Copying `System.Drawing.Common.dll` beside the build output is unreliable because Clumsy performs its own dependency attachment and can load the incompatible `netstandard2.0` facade first.
- `SkiaSharp` adds native Windows assets and more deployment points.
- `ImageSharp` has a larger dependency/licensing surface and its current release does not target this project's `netstandard2.0` runtime.
`StbImageWriteSharp` is selected because its package contains one managed `netstandard2.0` implementation and declares no dependencies. It has no native assets or alternate platform facades for Clumsy to select incorrectly.
## Rasterization
- Allocate an in-memory 32-bit RGBA pixel buffer after the existing 4000-pixel edge validation.
- Preserve the current canvas dimensions, 4 pixels per cell, colors, Y inversion, vehicle/workstation geometry, header height, and 50 MiB final-file limit.
- Draw filled rectangles, 1/2-pixel lines, circles, crosses, and vehicle polygons with deterministic integer raster operations.
- Render header and workstation text with a small embedded ASCII bitmap font. Non-ASCII metadata is sanitized to a printable fallback for the PNG header only; original messages remain unchanged in DLog/Console.
- Keep header lines non-overlapping and include bounds, resolution, rows/columns, occupied count/rate, obstacle count, tire status/message, and input source.
## PNG encoding
- Pass the RGBA pixel buffer to `StbImageWriteSharp.ImageWriter.WritePng`.
- Insert a standard `pHYs` chunk immediately after `IHDR`, with X/Y both 11,811 pixels per metre and unit `1` (300 DPI).
- Calculate the inserted chunk's CRC-32 and write its integers in big-endian order.
- Validate the completed PNG before publication; encoding or metadata failures remain contained export failures.
- Preserve the current collision-safe temporary-file reservation, actual encoded-size check, atomic move, and best-effort cleanup behavior.
## Project and deployment changes
- Remove the `System.Drawing.Common` package reference and `DeployFrameworkDrawingRuntime` target from `ClumsyPilot.csproj`.
- Add `StbImageWriteSharp` version 1.16.7. No other new package is allowed.
- The final build output must not require or deploy `System.Drawing.Common.dll` for TrapMap; it may deploy the single managed `StbImageWriteSharp.dll`.
- Existing Clumsy references are not modified.
## Verification
- TDD first proves the current exporter/package still depends on `System.Drawing.Common`.
- Reflection/source contracts assert the old package, build target, `using System.Drawing`, and drawing types are absent, and the exact Stb package is present.
- Decode the generated PNG in the test with a test-only decoder or framework reader and verify dimensions, 300 DPI metadata, representative colors, Y inversion, header separation, unique concurrent filenames, and no temporary files.
- Run source, build, grid, lifecycle, and image tests; require zero build errors and no new warnings.
## Non-goals
- No changes to grid construction, obstacle/tire inputs, vehicle motion, Painter visualization, export switches, output directory, or file-size/pixel limits.
- No screenshot capture and no new general-purpose graphics framework.
@@ -4,92 +4,126 @@
在现有栅格地图能力之上,实现四舵轮 AMR 的 Hybrid A* 空间粗路径:支持前进、倒车、换向、静态/投影障碍绕行、车体碰撞检查、可配置的终点位置与航向容差,以及稠密路径与方向分段输出。
本阶段**不包含**曲线平滑、B 样条、Bezier、局部 QP、SQP、Reeds-Shepp 精确终点连接、速度/加速度/时间参数化或底盘舵角控制。它们由后续独立模块处理。
本阶段**不包含**曲线平滑、B 样条、Bezier、局部 QP、SQP、Reeds-Shepp 精确终点连接、速度/加速度/时间参数化或底盘舵角控制。第一版采用汽车式恒曲率模型,不支持蟹行、纯横移和原地旋转。它们由后续独立模块处理。
## 已确认的约束
- 地图与外部人工障碍输入继续使用现有世界坐标单位:mm。
- Hybrid A* 内部统一使用 m、rad、1/m;单位转换只能经过地图适配与 `Utils`,不能散落在搜索代码中。
- 人工障碍物第一版支持以几何中心放置的轴对齐矩形和圆形。
- `TwoLegDetect` 是可选障碍物输入,不是规划地图是否可用的唯一条件;它的检测结果经投影后与人工障碍物合并。
- `TwoLegProjectionInput` 是可选障碍物输入,不是规划地图是否可用的唯一条件;上层采集到的 `TwoLegDetect` 结果经 DTO 投影后与人工障碍物合并。
- 环境占据地图只保存外部障碍物,绝不写入 AMR 自身足迹。AMR 尺寸、当前位姿和安全余量只用于粗路径的车体碰撞检测。
- 障碍物不做安全膨胀;安全余量仅通过碰撞检查时的扩大车辆矩形应用,防止双重膨胀。
- 第一版的终点条件为可配置的位置容差和车头航向容差。默认建议为 0.15 m 与 5°,而非精确连接到目标位姿。
- `PrimitiveLength=0.50 m` 是单条原语的最大长度,不是终点只能出现的离散间隔。每条原语必须在内部积分点检查目标条件;该原语内首次满足条件时立即截断并建立终点候选节点。终点候选仍须进入 Open List,只有当它作为当前最优有效节点出队时,整个搜索才返回成功。
- 碰撞检测必须保守:不得因航向离散、车辆中心的亚栅格偏移、距离场误差或原语离散采样而发布可能碰撞的路径。
- `ObstacleDistanceField` 只能作为保守快速放行和代价估计,任何可能高估真实净空的近似都不得用于跳过精确车体碰撞检查。
- 测试是交付的一部分:纯逻辑契约测试与参照 `Map``MovementTest` 集成/可视化入口都必须提供。
- 每个文件只承担一个明确职责;对外调用通过模块门面类完成,不让调用者拼装搜索、地图和碰撞的内部对象。
- 推荐调用方只使用 `CoarsePathPlanningService` 一次完成建图、粗路径搜索和可选调试发布;`PlanningMapFactory``HybridAStarPlanner` 是可独立测试、复用的下层模块门面。请求、结果、枚举、障碍物 DTO 和值对象仍然是可公开构造的数据契约。
## 目录和命名空间
```text
ClumsyPilot/ParkrobTrajplanner/
├── Initial_plan/ # 已有路线与方案文档,不放运行时代码
├── Utils/ # MultiWheelC.TrajectoryPlanning.Utils
│ ├── AngleMath.cs
│ ├── UnitConverter.cs
│ ├── CoordinateTransform.cs
│ ├── NumericGuard.cs
│ └── GridIndex.cs
├── Map/ # MultiWheelC.TrajectoryPlanning.Mapping
├── Initial_plan/ ------ 已有路线与方案文档,不放运行时代码
├── Utils/ ------ 命名空间 MultiWheelC.TrajectoryPlanning.Utils
│ ├── AngleMath.cs ------ 角度归一化、最短角差和航向离散索引
│ ├── UnitConverter.cs ------ mm/m、deg/rad 和半径/曲率单位转换
│ ├── CoordinateTransform.cs ------ 车体坐标系与世界坐标系二维刚体变换
│ ├── NumericGuard.cs ------ 有限值、正值和参数范围校验
│ └── GridIndex.cs ------ 不可变行列索引值对象
├── Map/ ------ 命名空间 MultiWheelC.TrajectoryPlanning.Mapping
│ ├── Core/
│ │ ├── EnvironmentGridMap.cs
│ │ ├── MapBuildRequest.cs
│ │ ├── EnvironmentMapBuildResult.cs
│ │ ── EnvironmentMapBuilder.cs
│ │ ├── EnvironmentGridMap.cs ------ 只保存外部障碍物的 mm 单位占据栅格
│ │ ├── MapBoundsMm.cs ------ 有限、非退化的 mm 地图边界值对象
│ │ ├── MapBuildRequest.cs ------ 环境地图边界、分辨率和障碍物图层输入
│ │ ── EnvironmentMapBuildResult.cs ------ 环境地图构建状态、来源摘要和失败原因
│ │ └── EnvironmentMapBuilder.cs ------ 校验并合并人工与投影障碍物图层
│ ├── Obstacles/
│ │ ├── IMapObstacle.cs
│ │ ├── AxisAlignedRectangleObstacle.cs
│ │ ├── CircleObstacle.cs
│ │ └── MapObstacleRasterizer.cs
│ │ ├── IMapObstacle.cs ------ 人工和投影障碍物的公共几何契约
│ │ ├── AxisAlignedRectangleObstacle.cs ------ 世界轴对齐矩形障碍物 DTO
│ │ ├── CircleObstacle.cs ------ 世界坐标圆形障碍物 DTO
│ │ └── MapObstacleRasterizer.cs ------ 通过形状与格子相交测试写入占据栅格
│ ├── Sources/
│ │ ├── ManualObstacleSource.cs
│ │ ── TwoLegObstacleProjector.cs
│ │ ├── IMapObstacleSource.cs ------ 纯快照障碍来源统一投影接口
│ │ ── ObstacleSourceStatus.cs ------ Applied、Empty、Unavailable、Invalid 来源状态
│ │ ├── ObstacleProjectionResult.cs ------ 来源版本、状态、诊断和世界障碍物集合
│ │ ├── ManualObstacleSource.cs ------ 把人工圆和矩形作为世界障碍物输出
│ │ ├── TwoLegProjectionInput.cs ------ 已验证两腿端点、检测状态和检测时车辆位姿 DTO
│ │ ├── TwoLegObstacleSource.cs ------ 把 TwoLeg 快照接入统一障碍来源接口
│ │ └── TwoLegObstacleProjector.cs ------ 把车体系两腿端点投影为世界坐标圆障碍
│ ├── Planning/
│ │ ├── PlanningGridMap.cs
│ │ ├── ObstacleDistanceField.cs
│ │ ── PlanningMapAdapter.cs
│ ├── PlanningMapRequest.cs
── PlanningMapBuildResult.cs
── PlanningMapFactory.cs
│ │ ├── PlanningGridMap.cs ------ 只读 m 单位占据图、距离场和规划可用状态
│ │ ├── EuclideanDistanceTransform.cs ------ 线性时间生成栅格中心精确欧氏距离
│ │ ── ObstacleDistanceField.cs ------ 生成不高估障碍净空的保守欧氏距离下界
│ ├── PlanningMapCache.cs ------ 线程安全的输入指纹与占据哈希快照缓存
│ └── PlanningMapAdapter.cs ------ 深拷贝占据数据并完成 mm 到 m 的边界适配
── PlanningMapRequest.cs ------ 地图门面的统一输入契约
│ ├── PlanningMapBuildResult.cs ------ 地图门面的统一输出契约
│ ├── PlanningMapFactory.cs ------ Map 模块唯一公共行为入口
│ └── Test/
── MovementTest.MapTest.cs
├── CoarsePath/ # MultiWheelC.TrajectoryPlanning.CoarsePath
── MovementTest.MapTest.cs ------ Clumsy UI 地图构建与可视化测试入口
│ └── Visualization/
│ ├── PlanningMapImageExportRequest.cs ------ 规划快照、叠加层和输出选项 DTO
│ ├── PlanningMapImageExportResult.cs ------ PNG 导出状态、路径、大小和诊断
│ ├── PlanningMapImageExporter.cs ------ 校验请求、编排渲染并原子发布 PNG
│ ├── PlanningMapImageRenderer.cs ------ 把地图、起终点、车体和路径绘制到 RGBA
│ └── ValidatedPngWriter.cs ------ Stb PNG 编码、结构和 CRC 完整性校验
├── CoarsePath/ ------ 命名空间 MultiWheelC.TrajectoryPlanning.CoarsePath
│ ├── Contracts/
│ │ ├── Pose2D.cs
│ │ ├── VehicleParameters.cs
│ │ ├── PlanningRequest.cs
│ │ ├── HybridAStarConfiguration.cs
│ │ ├── PlanningResult.cs
│ │ ├── PlanningStatus.cs
│ │ ├── CoarsePathPoint.cs
│ │ ── PathSegment.cs
│ │ ├── Pose2D.cs ------ m/rad 单位的不可变二维位姿
│ │ ├── TravelDirection.cs ------ Forward 与 Reverse 运动方向枚举
│ │ ├── GoalDirectionConstraint.cs ------ Any、Forward、Reverse 目标进入方向约束
│ │ ├── VehicleParameters.cs ------ 车体尺寸、安全余量和最大曲率参数
│ │ ├── PlanningRequest.cs ------ 地图、起终点、起始曲率和方向约束
│ │ ├── HybridAStarConfiguration.cs ------ 原语、离散、代价、限额和容差配置
│ │ ├── PlanningResult.cs ------ 状态、诊断、稠密路径和方向分段
│ │ ── PlanningStatus.cs ------ 输入、碰撞、搜索和验证结果枚举
│ │ ├── PlanningDiagnostics.cs ------ 节点、堆、耗时、路径和终止统计
│ │ ├── CoarsePathPoint.cs ------ 位姿、弧长、方向、曲率和保守净空
│ │ ├── CoarsePathPointSource.cs ------ 起点、普通原语和终点截断来源枚举
│ │ └── PathSegment.cs ------ 前进/倒车分段及其包含式索引范围
│ ├── Vehicle/
│ │ ├── VehicleKinematics.cs
│ │ ├── HeadingFootprintTemplate.cs
│ │ ├── HeadingFootprintTemplateCache.cs
│ │ └── FootprintCollisionChecker.cs
│ │ ├── VehicleKinematics.cs ------ 解析并校验车辆保守最大曲率
│ │ ├── VehicleFootprint.cs ------ 计算扩大车体矩形、包围盒和外接圆
│ │ ├── OrientedRectangleCellIntersection.cs ------ 精确判断旋转车体矩形与栅格矩形相交
│ │ └── FootprintCollisionChecker.cs ------ 边界、距离场、精确和扫掠碰撞检查
│ ├── Search/
│ │ ├── MotionPrimitive.cs
│ │ ├── MotionPrimitiveGenerator.cs
│ │ ├── HybridAStarNode.cs
│ │ ├── HybridAStarNodeKey.cs
│ │ ├── GoalToleranceChecker.cs
│ │ ├── GridDijkstraHeuristic.cs
│ │ ── HybridAStarSearch.cs
│ │ ├── MotionPrimitive.cs ------ 单条恒曲率原语及其实际截断长度描述
│ │ ├── MotionPrimitiveGenerator.cs ------ 解析积分前进/倒车原语并保留内部点
│ │ ├── BinaryMinHeap.cs ------ netstandard2.0 兼容且确定性排序的 Open List
│ │ ├── SearchCostCalculator.cs ------ 统一计算长度、倒车、换向、曲率和净空代价
│ │ ├── HybridAStarNode.cs ------ 连续位姿、代价、父索引和原语描述
│ │ ├── HybridAStarNodeKey.cs ------ 位置格、航向格、方向和曲率离散键
│ │ ── GoalToleranceChecker.cs ------ 位置、航向和目标进入方向容差判断
│ │ ├── GridDijkstraHeuristic.cs ------ 八邻域二维绕障距离启发
│ │ └── HybridAStarSearch.cs ------ 节点扩展、重开、限额和终点候选管理
│ ├── Output/
│ │ ├── PathBacktracker.cs
│ │ ├── CoarsePathAssembler.cs
│ │ └── CoarsePathValidator.cs
│ ├── HybridAStarPlanner.cs
│ │ ├── PathBacktracker.cs ------ 按父索引确定性重建原语内部点
│ │ ├── CoarsePathAssembler.cs ------ 生成弧长、换向点和包含式方向分段
│ │ └── CoarsePathValidator.cs ------ 复核数值、碰撞、曲率、终点和分段
│ ├── HybridAStarPlanner.cs ------ 只消费 PlanningGridMap 的纯搜索下层门面
│ ├── Facade/
│ │ ├── CoarsePathPlanningJob.cs ------ 一次调用所需地图、起终点、车辆和调试选项
│ │ ├── CoarsePathPlanningJobResult.cs ------ 同时返回地图构建结果和粗路径结果
│ │ ├── PlanningDebugOptions.cs ------ 地图、路径和碰撞调试发布开关
│ │ ├── IPlanningDebugSink.cs ------ 不影响规划状态的调试结果消费接口
│ │ └── CoarsePathPlanningService.cs ------ 建图、缓存、搜索和调试编排的一次调用入口
│ └── Test/
│ ├── CoarsePathScenarioFactory.cs
│ └── MovementTest.CoarsePathTest.cs
└── README.md # 只说明模块边界、调用入口文档链接
│ ├── CoarsePathScenarioFactory.cs ------ 生成固定、可复现的地图和规划场景
│ └── MovementTest.CoarsePathTest.cs ------ 后台运行规划并在 Clumsy UI 绘制结果
└── README.md ------ 粗规划模块边界、公共调用入口文档链接
ClumsyPilot/tests/
├── verify_planning_utils.ps1
├── verify_planning_map_adapter.ps1
├── verify_coarse_path_search.ps1
── verify_coarse_path_integration.ps1
├── verify_planning_utils.ps1 ------ 单位、角度、坐标和数值守卫测试
├── verify_planning_map_factory.ps1 ------ 地图输入、图层事务、快照和版本测试
├── verify_planning_map_adapter.ps1 ------ 地图栅格化、图层、适配和距离场测试
── verify_planning_map_image.ps1 ------ 只读快照 PNG 渲染、限制和原子发布测试
├── verify_coarse_path_collision.ps1 ------ 亚栅格、擦边和扫掠碰撞测试
├── verify_coarse_path_search.ps1 ------ 原语截断、搜索、方向、限额和重开测试
├── verify_coarse_path_integration.ps1 ------ Map 到最终路径验证的端到端测试
└── benchmark_coarse_path.ps1 ------ 参考与压力场景性能资源验收
```
`Occupancygird_Map/Map_test` 是当前原型位置。实施时会把其中仍然需要的地图能力按以上职责迁移到 `Map`,避免继续向两个现有大文件叠加功能;图片导出可以保留为独立地图测试辅助,不成为规划器依赖。
@@ -110,7 +144,7 @@ ClumsyPilot/tests/
### 对外调用门面
粗路径模块不直接实例化 `EnvironmentMapBuilder``MapObstacleRasterizer``TwoLegObstacleProjector` `PlanningMapAdapter``PlanningMapFactory``Map` 模块唯一的公共创建入口:
`HybridAStarPlanner` 不直接实例化任何地图对象;`CoarsePathPlanningService` 只持有 `PlanningMapFactory`,不接触 `EnvironmentMapBuilder``MapObstacleRasterizer`来源投影器`PlanningMapAdapter``PlanningMapFactory``Map` 模块唯一的公共创建入口:
```csharp
public sealed class PlanningMapFactory
@@ -119,7 +153,7 @@ public sealed class PlanningMapFactory
}
```
粗路径调用只依赖其稳定输出:
下层模块独立调用只依赖其稳定输出:
```csharp
var mapResult = new PlanningMapFactory().Create(mapRequest);
@@ -136,13 +170,44 @@ var result = new HybridAStarPlanner().Plan(new PlanningRequest
});
```
`PlanningMapRequest` 集中地图边界/分辨率、人工障碍物、可选 `TwoLegDetect` 投影输入和空旷地图声明;`PlanningMapBuildResult` 返回 `Succeeded`、失败原因、每个障碍物来源的摘要,以及成功时不可变的 `PlanningGridMap`。因此 A* 的 `PlanningRequest.Map` 始终是已完成校验、mm→m 适配距离场生成的输入,规划器不需要了解地图构造细节。
`PlanningMapRequest` 集中地图边界/分辨率、`IReadOnlyList<IMapObstacleSource>` 和空旷地图声明;`PlanningMapBuildResult` 返回 `Succeeded`、失败原因、每个障碍物来源的摘要、缓存命中类型,以及成功时不可变的 `PlanningGridMap`。因此 A* 的 `PlanningRequest.Map` 始终是已完成校验、投影、栅格化、mm→m 适配距离场生成的输入,规划器不需要了解地图构造细节。
### 统一障碍物来源与投影
所有人工、TwoLeg 和后续障碍物输入统一实现:
```csharp
public interface IMapObstacleSource
{
string SourceId { get; }
long SourceVersion { get; }
bool IsRequired { get; }
ObstacleProjectionResult ProjectToWorld();
}
```
`ProjectToWorld` 只能消费构造来源对象时已经取得的不可变快照,不得在内部读取传感器、定位、UI 或系统时间。它统一返回世界坐标 mm 几何体:
```text
ObstacleProjectionResult:
SourceId
SourceVersion
Status Applied/Empty/Unavailable/Invalid
Message
IReadOnlyList<IMapObstacle> Obstacles
```
必需来源返回 `Unavailable/Invalid` 时地图构建失败;可选来源返回上述状态时记录诊断并继续处理其他来源。多个来源产生重叠障碍物是合法的,占据写入具有幂等语义。
`ManualObstacleSource` 直接输出已经位于世界坐标系的圆和轴对齐矩形。`TwoLegProjectionInput` 是纯数据 DTO,明确包含检测状态、车体坐标系中的两个端点、端点半径、检测时刻的 AMR 世界位姿,以及 mm/deg 单位声明。`MovementTest` 或上层采集适配器负责调用现有 `TwoLegDetect`,随后把结果封装为快照;`TwoLegObstacleSource` 委托 `TwoLegObstacleProjector` 执行确定性车体到世界变换并输出零个或两个 `CircleObstacle`
新增障碍物来源只需投影为现有 `IMapObstacle` 几何体;如果需要新增多边形等几何类型,必须同时为 `MapObstacleRasterizer` 添加保守的形状-栅格相交实现和成功/边界/失败测试。任何来源都不得应用车辆安全余量。
### 环境地图与图层
`EnvironmentGridMap` 保存 mm 单位的地图边界、分辨率和仅含外部障碍物的占据单元。它不接受 `MarkVehicleFootprint` 一类接口。
`EnvironmentMapBuilder`地图模块的唯一对外构造入口
`EnvironmentMapBuilder``PlanningMapFactory` 使用的内部环境图构造器
```csharp
public sealed class EnvironmentMapBuilder
@@ -151,11 +216,11 @@ public sealed class EnvironmentMapBuilder
}
```
它依次校验地图参数、栅格化人工障碍物、投影可用的 `TwoLegDetect` 结果、合并占据单元,并报告每个来源是否生效。任何可选传感器输入失败都不会删除已成功构建的人工地图
它依次校验地图参数、`SourceId` 确定性排序来源、收集 `ObstacleProjectionResult`、合并所有成功投影的 `IMapObstacle` 并调用唯一栅格化器。任何可选来源失败都不会删除其他来源已成功构建的占据内容
人工障碍物实现 `IMapObstacle``AxisAlignedRectangleObstacle` 使用 `(CenterXmm, CenterYmm, WidthMm, HeightMm)``CircleObstacle` 使用 `(CenterXmm, CenterYmm, RadiusMm)`。两者的尺寸必须为有限正数,矩形轴与世界 X/Y 轴对齐。`MapObstacleRasterizer` 是唯一直接写入环境栅格的类。
`TwoLegObstacleProjector` 仅负责将检测坐标通过现有车体到世界系变换变成 `CircleObstacle`不负责地图边界、栅格化或规划可用性判断。
`MapObstacleRasterizer` 是唯一直接写入 `EnvironmentGridMap` 的类型;各来源和投影器都不能取得地图写入接口。`TwoLegObstacleProjector` 不负责地图边界、栅格化、缓存或规划可用性判断。
### 规划适配
@@ -168,47 +233,268 @@ public sealed class PlanningMapAdapter
}
```
适配时验证边界和分辨率、深拷贝占据数据、将长度从 mm 转为 m、将边界外固定解释为占据,并生成 `ObstacleDistanceField``PlanningGridMap` 是不可变的规划输入,包含 m 单位边界、分辨率、行列、占据数据、距离场、源地图版本`PlanningReady/PlanningBlockReason`
适配时验证边界和分辨率、深拷贝占据数据、将长度从 mm 转为 m、将边界外固定解释为占据,并生成 `ObstacleDistanceField``PlanningGridMap` 是不可变的规划输入,包含 m 单位边界、分辨率、行列、占据数据、距离场、来源版本摘要、快照标识`PlanningReady/PlanningBlockReason`
`PlanningReady` 的规则:有效的人工图层即可使地图可用于测试/规划;若没有人工障碍也没有 `TwoLegDetect`,在已明确配置“空旷地图”时仍可规划;未明确空旷语义的未观测区域则以 `PlanningBlockReason` 阻止规划,而不是暗中当作空闲
距离场使用精确的二维欧氏距离变换计算栅格中心到最近占据栅格中心的距离,再减去一个完整栅格对角线 `sqrt(2) * ResolutionMeters` 并截断到零,得到当前自由栅格内任意点到任意占据栅格矩形的保守下界。查询不得进行会抬高结果的插值;查询点使用其所在栅格的保守值。显式空旷地图的障碍物距离可以是正无穷,但车体边界检查仍必须先执行,地图外始终按占据处理
距离场的用途受以下规则约束:
- 当保守距离严格大于扩大车体外接圆半径时,碰撞检查器可以快速放行。
- 当保守距离小于或等于外接圆半径时,必须执行精确的扩大车体矩形与占据栅格矩形相交检查。
- `BodyClearance` 发布 `max(0, ConservativeCenterClearance - ExpandedFootprintCircumscribedRadius)`,作为车体净空的保守下界,不得声称为精确几何净空。
- 多源距离场实现、空图语义、地图边界和最大栅格数都必须有自动化测试。
`PlanningReady` 的规则:至少一个成功来源提供有效障碍语义即可使地图用于测试/规划;所有来源均为空时,只有已明确配置 `AllowExplicitEmptyMap=true` 才可规划。必需来源失败或未明确空旷语义时,以 `PlanningBlockReason` 阻止规划,而不是暗中把未观测区域当作空闲。
### 现有地图优化与规划适配
本阶段不在旧 `GridMapData` 外再包一层长期兼容适配器,而是把其中经过测试的几何规则迁移为纯逻辑、静态快照式地图管线。迁移目标是消除 UI、传感器、车辆自身足迹和规划查询之间的职责耦合,同时降低 Hybrid A* 高频占据查询与距离查询的开销。
#### 现有职责拆分
| 现有类型/函数 | 处理方式 | 新职责位置 |
| --- | --- | --- |
| `TrapMapBounds.TryCreate` | 保留有限值、退化边界、分辨率和最大栅格数校验;移除“车辆到工作站”业务假设 | `MapBoundsMm``MapBuildRequest` |
| `GridMapData.WorldToGrid/GridToWorld` | 保留 X→列、Y→行和 XMax/YMax 排他规则;统一处理最后一个非完整栅格 | `EnvironmentGridMap``PlanningGridMap` |
| `GridMapData.Cells byte[,]` | 改为私有行优先 `byte[]`,索引固定为 `row * Cols + col`;不暴露可写数组 | 两类 GridMap 的内部存储 |
| `GridMapData.MarkObstacle/MarkObstacles` | 移除默认安全距离;保留候选包围盒裁剪和形状-格矩形相交 | `MapObstacleRasterizer` |
| `GridMapData.MarkVehicleFootprint` | 从地图模块删除,不提供兼容开关 | `FootprintCollisionChecker` 查询时处理 |
| `GridMapData.Copy` | 不再暴露可变副本;适配时只进行一次占据缓冲区深拷贝 | `PlanningMapAdapter` |
| `TrapMapLayerComposer.Compose` | 泛化为“可选来源失败不破坏其他成功来源”的事务语义 | `IMapObstacleSource``EnvironmentMapBuilder` |
| `TrapMapBuilder.Get` | 拆除 `MovementDefinition`、定位读取、TwoLeg 调用、Toast、Painter 和协程依赖 | 上层采集适配器 + `CoarsePathPlanningService` |
| `TrapMapImageExporter.cs` | 保留经过验证的纯托管 RGBA/PNG 能力,拆分请求、结果、渲染、发布和 PNG 校验职责;输入改为只读规划快照 | `Map/Test/Visualization` |
#### 统一地图输入与静态快照
`PlanningMapRequest` 必须显式包含:
```text
MapBoundsMm Bounds
float ResolutionMm
IReadOnlyList<IMapObstacleSource> ObstacleSources
bool AllowExplicitEmptyMap
```
`Bounds` 使用 `[XMin, XMax) × [YMin, YMax)``ResolutionMm` 必须是 20~200 mm 的有限正数。来源 `SourceId` 必须非空且在一次请求内唯一,`SourceVersion` 必须非负,并在对应来源快照内容变化时递增。
`PlanningMapFactory.Create` 返回与来源输入隔离的不可变静态快照,不实现增量栅格更新或距离场局部修补。`PlanningGridMap` 保存各来源版本摘要、`InputFingerprint``OccupancyHash` 和通过 `Interlocked.Increment` 生成的进程内单调 `SnapshotId`。该计数器只标识快照,不保存地图内容。规划开始后只读取同一快照;上层即使收到新障碍物,也不得修改正在使用的占据缓冲区。
#### 两级指纹与快照复用
`PlanningMapCache``PlanningMapFactory` 的线程安全、容量为 4 的最近使用缓存;长期存在的 `CoarsePathPlanningService` 持有同一个工厂实例,因此多次规划可以复用快照。
每次创建按以下顺序判断:
1. 调用纯快照来源的 `ProjectToWorld`,按 `SourceId` 排序,并对边界、分辨率、空图策略、来源状态/版本和规范化世界几何体计算 `InputFingerprint`
2. 若缓存中存在相同 `InputFingerprint`,直接返回同一不可变 `PlanningGridMap`;不重新栅格化或生成距离场。
3. 输入指纹不同时重新栅格化,并对最终连续占据 `byte[]` 计算 `OccupancyHash`
4. 若地图几何参数和 `OccupancyHash` 与缓存快照相同,复用占据缓冲区与距离场,只生成包含新来源摘要和新 `SnapshotId` 的轻量快照。
5. `OccupancyHash` 不同时才重新执行距离场变换并缓存完整新快照。
浮点几何按其 IEEE 位模式和固定字段顺序计算确定性指纹,不通过简单的“坐标除以分辨率取整”判断变化,避免圆或矩形在格边附近发生漏失效。缓存项同时保留规范化输入描述;`InputFingerprint` 命中后仍执行结构相等比较。`OccupancyHash` 命中后仍比较地图几何参数和连续占据缓冲区长度/内容,不能只依赖哈希值判等。
起点、终点、车辆尺寸、安全余量、Hybrid A* 参数和可视化开关不属于地图指纹。TwoLeg 的检测状态从有效变为无检测/不可用/过期时,其来源结果必须改变;上层采集适配器负责根据检测有效期构造正确的 `TwoLegProjectionInput`,地图来源接口本身不读取系统时间。
#### 存储、坐标和查询优化
- 占据数据使用私有连续 `byte[]`,避免公开 `byte[,]` 带来的可变性和多维数组索引开销。
- `IsOccupied(row,col)``IsOccupiedWorld(x,y)` 和保守距离查询保持无分配、常数复杂度;地图外直接返回占据或零净空。
- 世界坐标到格索引使用 `floor((value - min) / resolution)``XMax``YMax` 排他。最后一个格子的几何上界必须裁剪到实际地图上界。
- 构造阶段使用 `checked` 计算 `Rows * Cols`,继续采用 4,000,000 格绝对上限;任何溢出或超限在分配前返回失败结果。
- 圆形与矩形栅格化只遍历其裁剪后的格索引包围盒,不扫描全图。与地图完全不相交的合法障碍物被忽略并记录在来源摘要中,而不是使地图构建失败。
- `EnvironmentGridMap` 只有程序集内部的占据写入入口;`PlanningGridMap` 不提供任何写入入口,也不返回内部缓冲区引用。
#### 距离场优化
`EuclideanDistanceTransform` 使用两次一维平方距离变换完成精确二维栅格中心距离计算,时间复杂度为 `O(Rows × Cols)`,不得为每个自由格遍历全部障碍格。中间数组按行列最大长度复用,最终距离使用连续 `double[]` 保存。
`ObstacleDistanceField` 在精确中心距离上执行前述保守修正并封装查询,不允许调用方直接取得未经修正的中心距离用于碰撞放行。显式空图不运行无意义的变换,直接构造正无穷障碍距离场;地图边界仍由规划地图和车体碰撞检查独立约束。
#### 地图迁移顺序
1. 先建立 `MapBoundsMm`、新占据存储和纯栅格化测试,不修改旧 UI 入口。
2. 建立 `PlanningMapFactory`、静态快照、距离场和规划查询测试。
3. 将人工障碍、TwoLeg DTO 与现有 Ghost/固定场景迁移为统一 `IMapObstacleSource`
4. 建立两级指纹缓存与 `CoarsePathPlanningService` 一次调用入口。
5. 将旧 `TrapMapImageExporter.cs` 拆分为 `Map/Test/Visualization` 下的五个文件,并将 PNG 导出与 `MovementTest.MapTest` 改为只消费新快照。
6. 新旧地图回归结果一致后,退役旧地图构建、车体写入和图层合成入口,并更新或删除对应旧反射测试。
迁移期间不得让 Hybrid A* 同时支持新旧两种地图类型;规划器从第一天起只接受 `PlanningGridMap`
## CoarsePath 模块
### 对外门面与契约
### 一次调用编排门面
调用方只调用 `HybridAStarPlanner`
常规调用方只调用:
```csharp
public sealed class CoarsePathPlanningService
{
public CoarsePathPlanningJobResult Plan(
CoarsePathPlanningJob job,
CancellationToken cancellationToken = default);
}
```
`CoarsePathPlanningJob` 集中以下输入:
```text
PlanningMapRequest MapRequest
Pose2D Start
Pose2D Goal
VehicleParameters Vehicle
HybridAStarConfiguration Configuration
double StartVehicleCurvature
TravelDirection? StartDirection
GoalDirectionConstraint GoalDirection
PlanningDebugOptions Debug
```
推荐调用形式:
```csharp
var result = planningService.Plan(new CoarsePathPlanningJob
{
MapRequest = new PlanningMapRequest
{
Bounds = bounds,
ResolutionMm = 50f,
ObstacleSources = new IMapObstacleSource[]
{
new ManualObstacleSource(manualObstacles),
new TwoLegObstacleSource(twoLegSnapshot),
},
AllowExplicitEmptyMap = true,
},
Start = startPose,
Goal = goalPose,
Vehicle = vehicle,
Configuration = configuration,
Debug = new PlanningDebugOptions
{
VisualizeMap = true,
VisualizePath = true,
},
}, cancellationToken);
```
一次调用内部固定执行:
```text
PlanningMapFactory.Create(MapRequest)
→ 地图失败则生成 FromMapFailure 结果
→ HybridAStarPlanner.Plan(PlanningRequest, cancellationToken)
→ IPlanningDebugSink 按 Debug 开关发布地图、路径和诊断
```
`CoarsePathPlanningJobResult` 同时保留 `PlanningMapBuildResult MapResult``PlanningResult PlanningResult`,使调用方能够取得实际使用的 `PlanningGridMap` 快照、缓存命中情况和粗路径状态。地图失败时不启动搜索;调试发布失败只写入调试诊断,不改变地图或路径规划状态。
`PlanningDebugOptions` 只包含 `VisualizeMap``VisualizePath``VisualizeCollisionChecks` 等旁路开关。`IPlanningDebugSink` 由 Clumsy `MovementTest` 适配实现;核心服务默认使用空实现,因此无 UI 环境与自动化测试不加载 Painter。
### 纯搜索下层门面
需要复用已有地图快照或单独测试搜索时调用 `HybridAStarPlanner`
```csharp
public sealed class HybridAStarPlanner
{
public PlanningResult Plan(PlanningRequest request);
public PlanningResult Plan(
PlanningRequest request,
CancellationToken cancellationToken = default);
}
```
`PlanningRequest` 组合 `PlanningGridMap`、起点 `Pose2D`、目标 `Pose2D``VehicleParameters``HybridAStarConfiguration`。它不接受地图构建器、UI 对象或传感器对象。
`PlanningRequest` 组合以下不可变输入:
`HybridAStarConfiguration` 集中所有可调参数,包括最大节点数、超时、航向分辨率、原语长度、积分步长、曲率等级、代价权重、是否允许倒车、位置容差和航向容差。初始值遵循已有技术方案:0.50 m 原语、0.05 m 积分、5° 航向离散、五级曲率、0.15 m 位置容差、5° 航向容差。
- `PlanningGridMap Map`
- `Pose2D Start``Pose2D Goal`
- `VehicleParameters Vehicle`
- `HybridAStarConfiguration Configuration`
- `double StartVehicleCurvature`,未提供时显式使用零曲率
- `TravelDirection? StartDirection``null` 表示起步方向不受约束
- `GoalDirectionConstraint GoalDirection`,取值为 `Any``Forward``Reverse`
`PlanningResult` 始终返回明确 `PlanningStatus`、诊断信息、零或一条 `IReadOnlyList<CoarsePathPoint>``IReadOnlyList<PathSegment>`。粗路径不包含时间、速度、加速度、舵轮角或轮速
它不接受地图构建器、UI 对象或传感器对象。起点曲率必须在车辆最大曲率内,并离散到最近的合法曲率等级;该索引作为起始搜索状态的一部分
`VehicleParameters` 明确使用车辆几何中心为 `Pose2D` 参考点,并包含 `LengthMeters``WidthMeters``SafetyMarginMeters`、可选 `MaximumCurvaturePerMeter` 与可选 `MinimumTurningRadiusMeters`。最大曲率和最小转弯半径同时存在时使用更保守的限制;两者都未提供时请求无效。
`HybridAStarConfiguration` 集中所有可调参数,包括最大节点数、超时、航向分辨率、原语最大长度、积分步长、碰撞采样步长、曲率等级、代价权重、是否允许倒车、位置容差和航向容差。默认值为:
| 参数 | 默认值 |
| --- | --- |
| `PrimitiveLengthMeters` | 0.50 m,表示最大长度 |
| `IntegrationStepMeters` | 0.05 m |
| `MaximumCollisionCheckStepMeters` | 0.025 m,且运行时不得大于 `Map.ResolutionMeters / 2` |
| `HeadingResolutionRadians` | 5° |
| `CurvatureLevelCount` | 5 |
| `GoalPositionToleranceMeters` | 0.15 m |
| `GoalHeadingToleranceRadians` | 5° |
| `MaximumExpandedNodes` | 200,000 |
| `SearchTimeout` | 5 s |
| `HeuristicWeight` | 1.0 |
| `ReverseCostMultiplier` | 1.5 |
| `GearSwitchPenaltyMeters` | 1.0 |
| `CurvatureMagnitudeWeight` | 0.10 |
| `CurvatureChangePenaltyMetersPerLevel` | 0.05 |
| `ClearanceCostWeight` | 0.20 |
| `ClearanceCostDistanceMeters` | 0.50 m |
搜索代价全部以“等效米”为单位:
```text
primitiveCost =
lengthMeters
× directionMultiplier
× (1
+ CurvatureMagnitudeWeight × abs(curvature / maximumCurvature)
+ ClearanceCostWeight × max(0, 1 - clearance / ClearanceCostDistanceMeters))
+ gearSwitchPenalty
+ CurvatureChangePenaltyMetersPerLevel × abs(curvatureLevelDelta)
```
其中前进的 `directionMultiplier=1`,倒车使用 `ReverseCostMultiplier`;没有换向时 `gearSwitchPenalty=0`。所有权重必须为有限非负值。默认 `HeuristicWeight=1.0`;若调用方调大该值,只承诺更快地寻找可行解,不承诺离散图上的最低代价。
`PlanningResult` 始终返回明确 `PlanningStatus`、诊断信息、零或一条 `IReadOnlyList<CoarsePathPoint>``IReadOnlyList<PathSegment>`。成功结果中的点契约固定为:
```text
CoarsePathPoint:
X、Y m
Heading、UnwrappedHeading rad
ArcLength m,非负且不递减
Direction Forward/Reverse
VehicleCurvature 1/m
BodyClearance m,保守下界
IsGearSwitchPoint bool
Source Start/MotionPrimitive/GoalTruncation
```
`PathSegment` 固定包含 `SegmentIndex``Direction``StartIndex``EndIndex``StartsAtGearSwitch``EndsAtGearSwitch``StartIndex``EndIndex` 都是包含端点的索引,所有分段按索引顺序完整覆盖整条路径。换向时保留两个坐标和航向相同、弧长相同但方向不同的相邻点:前一点结束旧分段,后一点开始新分段并设置 `IsGearSwitchPoint=true`。除这种换向对外,装配器删除相邻重复点。粗路径不包含时间、速度、加速度、舵轮角或轮速。
### 车辆、碰撞和搜索
- `VehicleKinematics` 根据车辆参数提供最大曲率;直接最大曲率和最小转弯半径同时存在时采用更保守的值。
- `HeadingFootprintTemplateCache` 为每一个离散航向预计算扩大车辆矩形覆盖的相对栅格偏移;扩大尺寸只在这里应用安全余量
- `FootprintCollisionChecker` 依次检查地图边界、距离场快速安全放行和精确矩形模板;它不执行搜索,也不改变地图
- `MotionPrimitiveGenerator` 仅生成恒曲率前进/倒车原语,并以不大于 0.05 m 的步长积分,保留内部积分点
- `VehicleFootprint` 以连续位姿计算扩大车辆矩形的四角、轴对齐包围盒和外接圆。安全余量只在这里同时加到长度和宽度两侧,不写入地图
- `OrientedRectangleCellIntersection` 使用分离轴定理判断连续位姿下的扩大车辆矩形是否与占据栅格矩形相交,不使用仅按离散航向和整数格偏移的模板,因此车辆中心的亚栅格偏移不会漏检
- `FootprintCollisionChecker` 依次执行扩大车体边界检查、保守距离场快速放行和包围盒内占据栅格的精确相交检查;它不执行搜索,也不改变地图
- `MotionPrimitiveGenerator` 仅生成恒曲率前进/倒车原语,以不大于 0.05 m 的步长保留输出积分点,并使用直线/圆弧解析公式更新位姿,不使用累计误差更大的显式欧拉积分。
- 相邻碰撞检查位姿的中心位移不得超过 `min(MaximumCollisionCheckStepMeters, Map.ResolutionMeters / 2)`。同时用 `0.5 × (中心位移 + 外接圆半径 × 航向变化绝对值)` 作为扫掠附加余量检查相邻区间端点,保守覆盖两个采样位姿之间的车体运动;该附加余量只用于区间碰撞验证,不写入输出车体尺寸。
- `GoalToleranceChecker` 只判断位置、航向和最后一段方向约束;位置和航向阈值来自 `HybridAStarConfiguration`
- `GridDijkstraHeuristic` 仅从目标在占据图上生成二维绕障距离启发;它不处理车辆运动学
- `HybridAStarSearch` 管理 Open List、Closed Set、节点扩展、代价、父索引与终点选择。Closed Set 键为位置格、航向格、方向和曲率等级。它不拼装最终路径
- `PathBacktracker` 从成功节点恢复原语的内部积分点;`CoarsePathAssembler` 去除相邻重复点、累计弧长、标记换向点并构造 `PathSegment``CoarsePathValidator` 用同一碰撞规则复核最终稠密输出
- 每条原语按积分点顺序执行数值合法性、碰撞和目标检查。如果某个内部积分点满足目标条件,当前原语在该点截断并生成终点候选;候选加入 Open List,只有当它作为最佳有效节点出队时才成功终止搜索
- `GridDijkstraHeuristic` 从目标在占据图上生成八邻域二维绕障距离启发,禁止穿过两个对角相邻障碍物的夹角;它不处理车辆运动学。若目标在二维图上不可达,规划返回 `NoFeasiblePath`
- `SearchCostCalculator` 只实现本节定义的等效米代价公式,集中处理倒车、换向、曲率、曲率变化与保守净空代价,不管理节点或 Open List
- `BinaryMinHeap` 是兼容 `netstandard2.0` 的内部最小堆,不依赖较新运行时的 `PriorityQueue`。排序依次使用 `F``H`、较大的 `G` 和单调递增插入序号,保证相同输入的搜索顺序可复现。
- `HybridAStarSearch` 使用 `Dictionary<HybridAStarNodeKey,double>` 保存每个离散键当前最佳 `G`。发现更小 `G` 时允许重新打开节点;Open List 中的旧条目通过比较最佳 `G` 惰性丢弃。Closed Set 键为位置格、航向格、方向和曲率等级。
- 搜索循环在扩展节点前检查取消、超时和节点上限。终点候选只有作为当前最佳有效节点出队时才返回成功。
- `PathBacktracker` 只存父节点索引和原语描述,在成功后确定性地重新生成内部积分点,避免为所有搜索节点长期保存稠密点。`CoarsePathAssembler` 按既定换向规则累计弧长并构造 `PathSegment`
- `CoarsePathValidator` 使用相同的连续位姿、扫掠余量和碰撞规则复核最终稠密输出,同时检查有限数值、曲率上限、目标容差、方向约束、弧长单调性、换向对和分段索引完整覆盖。
第一版允许前进、倒车和换向。换向只可发生在原语边界;相邻原语曲率等级最多变化一级。目标达到容差即成功,不尝试 Reeds-Shepp 精确连接。
## 状态与失败处理
`PlanningStatus` 至少区分:`Success``InvalidRequest``InvalidMap``MapNotReady``InvalidVehicleParameters``InvalidCurvatureConfiguration``StartOutsideMap``StartInCollision``GoalOutsideMap``GoalInCollision``SearchTimeout``SearchNodeLimitExceeded``NoFeasiblePath``BacktrackingFailed``FinalValidationFailed``InternalError`
`PlanningStatus` 至少区分:`Success``Cancelled``InvalidRequest``InvalidMap``MapNotReady``InvalidVehicleParameters``InvalidCurvatureConfiguration``StartOutsideMap``StartInCollision``GoalOutsideMap``GoalInCollision``SearchTimeout``SearchNodeLimitExceeded``NoFeasiblePath``BacktrackingFailed``FinalValidationFailed``InternalError`
所有输入错误在开始搜索前返回状态与可读原因;搜索或地图对象不能通过异常把部分路径发布给调用方。`PlanningDiagnostics` 记录扩展节点数、生成节点数、总路径长度、最小净空、耗时和终止原因,供之后的性能优化使用。
所有输入错误在开始搜索前返回状态与可读原因;取消、超时和节点上限均返回空路径,不发布部分结果。除参数为空这类编程错误外,搜索或地图对象不能通过异常把部分路径发布给调用方。`PlanningDiagnostics` 记录扩展节点数、生成节点数、重新打开节点数、丢弃的陈旧堆条目数、Open List 峰值、总路径长度、最小保守净空、耗时和终止原因,供之后的性能优化使用。
## 测试设计
@@ -217,17 +503,42 @@ public sealed class HybridAStarPlanner
| 测试文件/入口 | 覆盖内容 |
| --- | --- |
| `verify_planning_utils.ps1` | mm/m、deg/rad、角度环绕、车体/世界坐标变换和非法数值。 |
| `verify_planning_map_adapter.ps1` | 圆与轴对齐矩形的中心放置和栅格化、人工与 TwoLeg 图层合并、AMR 自身不占据环境图、边界外保守占据、mm→m 深拷贝、距离场与 `PlanningReady`。 |
| `verify_coarse_path_search.ps1` | 无障碍前进、单障碍绕行、允许倒车的狭窄场景、换向标记、位置/航向容差、越界/起终点碰撞/无解、曲率与 0.05 m 稠密点复核。 |
| `verify_coarse_path_integration.ps1` | 由人工障碍地图构建、适配、规划、最终验证的端到端结果与诊断。 |
| `verify_planning_map_factory.ps1` | 统一来源投影、必需/可选失败策略、来源确定性顺序、空图声明、输入指纹、占据哈希、完整/缓冲区缓存命中、来源版本摘要、静态快照隔离与并发访问。 |
| `verify_planning_map_adapter.ps1` | 圆与轴对齐矩形的包围盒裁剪和格矩形相交、AMR 自身不占据环境图、连续行优先存储、坐标边界、非完整末格、越界保守占据、mm→m 深拷贝、距离场不高估与 `PlanningReady`。 |
| `verify_planning_map_image.ps1` | 从 `PlanningGridMap` 渲染占据格、边界、起终点、车辆和路径叠加;覆盖关闭导出、非法尺寸、像素/文件上限、唯一命名、临时文件清理、PNG 结构与 CRC。 |
| `verify_coarse_path_collision.ps1` | 车体中心位于栅格中心和亚栅格位置时的正交/45°/任意航向,边角擦碰、薄障碍、地图边界、距离场快速放行与原语区间扫掠碰撞。 |
| `verify_coarse_path_search.ps1` | 无障碍前进、0.30 m 非整倍数终点截断、单障碍绕行、允许倒车的狭窄场景、起始曲率、目标进入方向、换向对、±π 航向容差、起点已满足目标、无解、取消、超时、节点上限、节点重新打开与确定性顺序。 |
| `verify_coarse_path_integration.ps1` | `CoarsePathPlanningService` 一次调用完成多来源建图、缓存复用、规划、回溯和最终验证;复核调试开关不改变地图指纹或规划结果。 |
| `benchmark_coarse_path.ps1` | Release 构建下的参考场景耗时、扩展节点数、Open List 峰值与托管内存增量。 |
| `MovementTest.MapTest` | 在 Clumsy UI 中显示人工与 TwoLeg 投影后的环境栅格;可选 PNG 导出只用于调试证据。 |
| `MovementTest.CoarsePathTest` | 使用固定可复现实例调用 `HybridAStarPlanner`,绘制地图、起终点、扩大车体检查点和粗路径;不向底盘发送运动命令。 |
| `MovementTest.CoarsePathTest` | 使用固定可复现实例调用 `CoarsePathPlanningService`,绘制地图、起终点、扩大车体检查点和粗路径;`TestStop` 取消规划并清理 Painter不向底盘发送运动命令。 |
每个新增公共契约均需有成功、边界和失败三类测试。测试场景由 `CoarsePathScenarioFactory` 统一生成,不在 `MovementTest` 中手写地图、原语或搜索细节。
`MovementTest.CoarsePathTest` 在后台任务中调用同步的 `CoarsePathPlanningService.Plan`,持有专用 `CancellationTokenSource``TestStop` 先取消规划,再清理任务引用和 Painter。UI 入口不得在界面线程上执行最长 5 s 的搜索,也不得调用任何底盘运动接口。
PowerShell 测试统一使用以下形式执行,绕过本机脚本执行策略差异,并在首个错误处停止:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\<script>.ps1
```
每个脚本首行设置 `$ErrorActionPreference = 'Stop'`。测试先执行一次项目构建,之后加载同一个 `bin/Debug/netstandard2.0/ClumsyPilot.dll`,不得混用 `obj``bin` 中的程序集。`netstandard2.0` 实现不得直接使用 `PriorityQueue``Math.Clamp``double.IsFinite` 或缺少兼容类型时的 `record/init`
### 性能与资源验收
- 地图参考场景:20 m × 20 m、0.05 m 分辨率、160,000 格和 100 个圆/矩形障碍。Release 构建预热后连续构建 20 次,首次完整构建 P95 不超过 200 ms;完整快照缓存命中 P95 不超过 5 ms。
- 地图极限场景:4,000,000 格、100 个障碍。完整构建必须在 3 s 内成功或以明确状态失败;成功时单次托管内存增量不超过 160 MB,不得出现整数溢出或部分发布快照。
- 默认硬限制:`MaximumExpandedNodes=200000``SearchTimeout=5 s`;任一限制触发后必须在下一次循环检查点终止。
- 参考场景:12 m × 8 m、0.05 m 分辨率、一个阻断直线路径的矩形障碍、起终点距离至少 8 m。Release 构建预热后连续运行 20 次,P95 规划耗时不超过 2 s,单次托管内存增量不超过 256 MB。
- 压力场景:20 m × 20 m、0.05 m 分辨率、160,000 栅格。无论成功或无解,都必须在 5 s 与 200,000 扩展节点内返回,托管内存增量不超过 512 MB。
- 性能脚本输出地图规模、状态、耗时、扩展/生成/重开节点数、Open List 峰值和内存增量;超过阈值返回非零退出码。
## 非目标与迁移边界
- 不修改 `TrajPlanner` 下的 Python 原型,也不把它作为运行时依赖。
- 不在本阶段实现平滑、SQP、时间轨迹或控制接口;后续模块只消费 `PlanningResult` 中稳定的粗路径与方向分段。
- 不保留将车辆自身写入规划占据图的兼容开关;若调试可视化需要车辆图形,应作为渲染叠加层。
- 当前地图文件中的职责会按以上边界迁移;不会在迁移后继续向原 `Map_test` 大文件追加规划功能
- 现有 `Occupancygird_Map/Map_test` 原型中的通用栅格化、TwoLeg 投影和 PNG 调试能力按以上职责迁移。迁移完成后,旧 `GridMapData``TrapMapBuilder``TrapMapLayerComposer` 与旧 `TrapMapTest` 不再作为公共运行时入口;旧反射测试必须更新到新命名空间和门面,或在等价覆盖后删除
- PNG 导出器若保留,只能作为内部测试/可视化适配器消费只读 `PlanningGridMap`,不得重新拥有地图构建、障碍膨胀或车辆足迹写入逻辑。
- 迁移验收必须证明 `CoarsePathPlanningService` 是推荐的一次调用入口,`PlanningMapFactory``HybridAStarPlanner` 只作为下层模块门面;旧地图构建器、投影器、栅格化器和搜索内部类型不得成为额外公共服务。
@@ -0,0 +1,94 @@
# Hybrid A* P0 规划核心实施设计
## 目标
在已完成的 `PlanningGridMap` 静态地图能力之上,交付可复用、确定性且可验证的 Hybrid A* 粗路径规划核心。常规调用方通过 `CoarsePathPlanningService.Plan(job)` 一次完成建图和规划;纯算法测试可直接使用 `HybridAStarPlanner.Plan(request)`
本设计只覆盖 P0-PLAN。P1 的 Clumsy UI 后台任务、Painter 绘制、Release 性能基准和旧 TrapMap 运行时入口退役不在本次实现范围内。
## 既有边界
- `Map` 模块只保存外部障碍物;不得写入 AMR 自身足迹,也不得对障碍物做车辆安全膨胀。
- `PlanningGridMap` 是规划器唯一接受的地图类型,世界查询坐标为 m;越界位置视为占据且净空为 0。
- 规划核心不读取传感器、定位、UI 或系统时间。取消令牌是唯一允许的外部控制输入。
- 所有公共契约和核心逻辑兼容 `netstandard2.0`:不使用 `PriorityQueue``Math.Clamp``double.IsFinite``record``init`
- 注释延续已有模块风格:公开类型和成员写中文 XML 文档,说明单位、边界、返回/失败语义;内部复杂几何或搜索不变量保留简短中文行注释。
## 方案选择
采用“碰撞核心先行、搜索核心随后接入”的两段实现。
先建立公开数据契约、车辆扩大矩形和连续碰撞检查,使安全语义可以在不依赖搜索器的情况下通过自动化测试固定下来。随后在这些稳定边界上实现恒曲率运动原语、启发式、确定性 Open List、Hybrid A*、路径装配/复核和一次调用服务门面。此顺序避免 UI 或搜索状态掩盖车辆擦边、扫掠和地图边界错误。
## 架构与数据流
```text
CoarsePathPlanningJob
-> PlanningMapFactory.Create(MapRequest)
-> PlanningMapBuildResult / PlanningGridMap
-> HybridAStarPlanner.Plan(PlanningRequest)
-> FootprintCollisionChecker
-> MotionPrimitiveGenerator
-> GridDijkstraHeuristic + HybridAStarSearch
-> PathBacktracker + CoarsePathAssembler
-> CoarsePathValidator
-> CoarsePathPlanningJobResult
```
`Facade` 只编排地图与规划,不接触栅格化、车辆几何或搜索节点。`HybridAStarPlanner` 在搜索前完成请求、地图、车辆、起点和终点的有效性检查;搜索成功后必须经过 `CoarsePathValidator` 才能发布路径。
### 公共契约
`Contracts` 定义 m/rad/1/m 单位的值对象、车辆参数、配置、请求、状态、诊断、路径点和分段。默认配置固定为:0.50 m 原语最大长度、0.05 m 积分步长、5° 航向分辨率、0.15 m 位置容差、5° 航向容差、200,000 节点和 5 s 搜索上限。
`PlanningResult` 对所有结果提供明确 `PlanningStatus` 和可读诊断。输入错误、地图未就绪、碰撞、无解、取消、超时、节点上限、回溯失败和最终校验失败都返回空路径;不会通过异常发布部分路径。
### 车辆碰撞
`VehicleFootprint` 将以车辆几何中心为参考的长宽与安全余量转换为扩大矩形、AABB 和外接圆。`VehicleKinematics` 从最大曲率和最小转弯半径推导更保守的最大曲率。
`FootprintCollisionChecker` 固定按以下顺序检查连续位姿:
1. 扩大矩形是否完整位于地图边界内;
2. 使用保守距离场与外接圆进行严格大于关系的快速放行;
3. 对包围盒内的每个占据格,使用 SAT 检查旋转矩形与格矩形是否相交或擦边;
4. 对相邻采样位姿,以中心平移与航向变化构造扫掠附加余量,检查中间区间。
因此距离场只能加速安全放行,不能替代精确碰撞判定;安全余量只作用于车辆扩大矩形,绝不回写地图。
### 搜索与路径输出
第一版只生成前进、倒车和原语边界换向的恒曲率原语。运动积分使用直线/圆弧解析公式,积分点间距不超过配置步长;碰撞采样的中心位移不超过 `min(0.025 m, Map.ResolutionMeters / 2)`
搜索键由位置格、航向格、方向和曲率等级组成。确定性二叉最小堆按 `F``H`、较大 `G` 和插入序号排序;更小 `G` 的状态允许重新打开,旧堆条目延迟丢弃。二维八邻域 Dijkstra 启发式禁止穿越障碍的对角夹角。
原语的每个内部积分点依次进行数值、碰撞和终点容差检查。首次满足终点条件时截断原语并将候选压入 Open List;只有该候选作为当前最优有效节点出队时才宣布成功。成功后由回溯器重建稠密积分点,由装配器生成累计弧长和包含式方向分段;换向处保留一对位置、航向和弧长相同但方向不同的相邻点。
### 文档
新增 `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md`,作为 P0 调用者文档。它说明模块边界、一次调用示例、输入单位、来源版本与地图缓存关系、状态处理方式、路径输出含义和第一版非目标。现有 `Map/README.md` 不复制粗路径内容,继续保留地图构建细节。
## 错误与取消语义
- `PlanningGridMap` 为空、不可规划或起终点不在地图内时,在搜索前返回对应状态。
- 起点/终点与扩大车辆矩形碰撞时,返回 `StartInCollision``GoalInCollision`
- 每次扩展节点前检查取消、超时和最大扩展数;触发后立即返回空路径和累计诊断。
- 任意内部不变量异常被收敛为 `InternalError`,不向调用方泄露部分路径。
- 地图构建失败时,服务直接包装 `PlanningMapBuildResult`,不启动 `HybridAStarPlanner`
## 测试策略
测试继续使用真实 `netstandard2.0` 程序集的 PowerShell 反射脚本,所有 P0 生产代码均遵循 Red-Green-Refactor:先在脚本中写可反射调用的失败断言并确认其因类型或行为缺失失败,再写最小实现,最后运行同一脚本确认通过。
1. `verify_coarse_path_collision.ps1`:参数边界、亚栅格位姿、任意航向、擦边、薄障碍、地图边界、距离场放行和扫掠碰撞。
2. `verify_coarse_path_search.ps1`:原语解析积分、非整倍数终点截断、方向约束、换向、启发式、堆确定性、重开、取消、超时、节点上限和无解。
3. `verify_coarse_path_integration.ps1`:从多来源 `PlanningMapRequest` 到最终路径的服务编排、地图缓存复用、最终复核和调试开关不改变规划结果。
每个脚本在测试前构建 `ClumsyPilot/ClumsyPilot.csproj`,随后只加载 `bin/Debug/netstandard2.0/ClumsyPilot.dll`。完成 P0 前必须重新运行构建与所有既有 Map 验证脚本,确保新规划代码没有破坏地图模块。
## P0 验收
- 固定静态场景可以经 `CoarsePathPlanningService.Plan` 得到连续、无碰撞、满足终点容差的稠密粗路径。
- 最终路径通过同一套扩大车体和扫掠规则复核;失败不发布部分路径。
- 路径点、方向分段、状态和诊断可由不依赖 Clumsy 的调用方直接消费。
- 使用说明可独立解释 Map 与 CoarsePath 边界、单位、调用方式及版本限制。
@@ -0,0 +1,56 @@
# Map 模块文档与注释设计
## 目标
让阅读 `ClumsyPilot/ParkrobTrajplanner/Map` 的开发者无需反查实现,即可理解模块文件职责、建图数据流和所有公共 API 的调用契约。
## 交付内容
### `Map/README.md`
README 是 Map 模块的入口说明,只保留不会由 IDE 自动展示的模块级信息:
- 当前目录树,以及每个文件的一句话职责;
- 从 `PlanningMapRequest``PlanningGridMap` 的建图数据流;
- 坐标系和单位约定;
- `PlanningMapFactory` 的最小调用示例;
- 缓存与 `SourceVersion` 的使用约束;
- 测试、PNG 调试和旧 TrapMap 的边界。
README 不复制逐个参数说明;参数的唯一权威说明位于声明处的代码注释。
### `.cs` 代码注释
覆盖 `Map` 内所有 `public` 类、接口、枚举、构造函数、方法和属性。注释使用可被 C# IDE 识别的 `///` XML 文档注释,但按 Python docstring 的阅读顺序组织:
1. 功能:该成员做什么;
2. 参数:名称、类型语义、单位、可空性或约束;
3. 返回:返回对象及字段的业务意义;
4. 注意:缓存、坐标转换、不可变性、线程安全或失败语义等调用者必须知道的约束。
不为纯私有实现逐项添加重复注释;复杂算法的私有方法只在其现有说明明显不足、且会妨碍维护时补充最小必要说明。
## 注释示例
```csharp
/// <summary>
/// 创建规划地图快照。
///
/// 参数:
/// - request:建图请求,包含世界范围、栅格分辨率和障碍物来源。
///
/// 返回:
/// - PlanningMapBuildResult:成功时含不可变地图、来源投影结果和缓存命中类型。
///
/// 注意:
/// - 应长期复用工厂实例,才能复用缓存。
/// </summary>
public PlanningMapBuildResult Create(PlanningMapRequest request)
```
## 验收
- `Map/README.md` 可独立说明文件结构、数据流和公共入口;
- 通过 `rg` 检查,所有 Map 公共 API 均有紧邻的中文 `///` 文档说明;
- 现有 Map Gate 与项目编译保持通过;
- 不修改 Map 的建图算法、缓存键或运行时行为。
@@ -0,0 +1,114 @@
# P1 粗路径 Clumsy UI 集成设计
## 目标
在不改变 P0 地图、Hybrid A* 与碰撞安全语义的前提下,为 Clumsy 增加可手动运行的粗路径场景测试。使用者能够从 MovementTest 列表启动固定场景,或传入 AMR 当前世界位姿并手动输入终点;界面显示栅格地图、起点、终点、连续路径、换向点和扩大车辆矩形检查点,并可随时停止正在进行的规划。
本设计只覆盖 P1 的首个交付:场景工厂、后台 MovementTest、Painter 可视化、自动化集成检查和模块 README。Release 性能基准属于 P1 的下一项交付;旧 TrapMap 迁移、旧验证脚本和旧入口的清理按已确认范围排除。
## 既有边界
- 业务入口仍唯一为 `CoarsePathPlanningService.Plan(job, cancellationToken)`MovementTest 不自行拼接 `PlanningMapFactory``HybridAStarPlanner`、栅格化器、碰撞器、原语或搜索节点。
- `PlanningMapRequest` 的边界、分辨率和障碍物几何使用 mm;`CoarsePathPlanningJob` 位姿、车辆尺寸和路径点使用 m,航向使用 rad。
- 项目传入的 AMR 位姿采用世界 `X/Y(mm)` 与航向 `th(deg)`。P1 只在 UI 边界将其一次性转换为 `Pose2D(X / 1000, Y / 1000, th × pi / 180)`;规划核心不接受度或 mm 位姿。当前车队路径代码将来自 `getCartLocation().th` 的姿态与度制角相加,并在调用三角函数前显式除以 180 再乘 pi,因此 P1 不沿用旧 `Movements.cs` 直接对 `.th` 调用 `Math.Cos/Sin` 的不一致写法。
- AMR 起点必须表示车辆几何中心。若上游定位的参考点是雷达、天线或其他安装点,上游必须先按外参转换到车辆几何中心;安全余量仍只由 `VehicleParameters` 表达。
- `PlanningResult` 只有 `Success` 才能携带完整路径;取消、超时、无解和失败不得在 UI 上表现为部分路径。
- `Painter` 在现有后台多车线程中已被调用,因此本设计允许规划任务完成后的后台回调操作该图层;规划核心本身始终不依赖 UI。
- 注释延续 P0 风格:公开类型与成员使用中文 XML 文档,说明单位、并发/停止语义和返回行为;会影响竞态的内部代码保留简短中文行注释。
## 方案选择
采用“六个薄 MovementTest 入口 + 共享后台执行器”的方案。
每个入口对应一个已命名的固定场景,便于在 Clumsy 的测试列表中直接运行;它们共用同一个静态 `CoarsePathPlanningService`,因此既能复用地图缓存,也不会让测试代码绕开门面。一个位于同一源文件内的执行器负责互斥会话、`Task` 生命周期、取消和绘制,避免六个入口复制并发逻辑。
不采用单一测试入口配合代码常量切换,因为手动验证需要反复改代码;也不采用运行时弹窗选项,因为这会增加 UI 输入状态和无法直接观察每个场景的可发现性。
## 文件与职责
| 文件 | 职责 |
| --- | --- |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/CoarsePathScenarioFactory.cs` | 创建不读取 UI、传感器、定位或时钟的固定 `CoarsePathPlanningJob` 场景。每次创建均返回新请求对象。 |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/Test/MovementTest.CoarsePathTest.cs` | 声明七个 MovementTest 入口,以及共享服务、后台执行、取消、结果日志、AMR 位姿/手动终点输入与 Painter 绘制。 |
| `ClumsyPilot/tests/verify_coarse_path_integration.ps1` | 通过真实程序集反射验证场景、门面调用约束、缓存、换向、无解、取消和测试代码结构。 |
| `ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` | 补充 P1 手动测试方法、颜色图例、单位转换、停止语义和非目标。 |
所有 UI 辅助类型保留在 `CoarsePath/Test` 内;不会向 `Map``Search``Vehicle``Facade` 增加 UI 依赖。
## 场景工厂
`CoarsePathScenarioFactory` 公开一个场景枚举和按枚举创建请求的方法。工厂的职责仅是构造纯输入;它不持有服务、缓存、Painter 或取消源。每个请求采用同一组可验证的车辆和搜索默认值,再按场景覆盖障碍物、起终点和方向约束。
场景固定使用世界 mm 地图边界和分辨率,向 `Pose2D` 写入对应的 m 坐标。障碍物只通过 `ManualObstacleSource``TwoLegObstacleSource` 进入 `PlanningMapRequest`,并为内容变化提供固定且正确的 `SourceVersion`
| 场景 | 地图与预期 |
| --- | --- |
| 显式空图 | `AllowExplicitEmptyMap=true`,直达前进路径成功,用于检查最短调用链。 |
| 单矩形绕行 | 中央矩形阻断直线,路径成功且必须绕障。 |
| 手工圆、矩形与 TwoLeg | 同时使用手工圆形、手工矩形和有效 TwoLeg 快照,路径成功,证明多来源经过同一门面。 |
| 缓存命中 | 连续以新建但完全相同的输入调用同一服务两次;第二次 `MapResult.CacheHit` 必须为 `Input`。 |
| 倒车换向 | 起步方向限制为前进、终点进入方向限制为倒车;成功路径必须出现标记的换向点。 |
| 无解 | 完全贯穿地图的障碍带隔开起点和终点,返回 `NoFeasiblePath` 且无路径。 |
| AMR 位姿与手动终点 | 起点使用上层传入并冻结的 AMR 世界位姿;操作者输入同一世界系的终点 X/Y/航向。该入口仅使用明确提供的障碍物快照,显式空图只能作为演示,不能代表现场无障碍。 |
实现期间先用自动化断言固定每个场景的状态;若需为当前 P0 运动原语调整数值,只能调整场景几何或请求参数,不能放宽碰撞、目标或失败语义。
## 后台会话与取消
共享执行器持有一个静态、长期存活的 `CoarsePathPlanningService`。任一入口启动时会创建新的会话:运行编号、专用 `CancellationTokenSource`、场景描述和后台 `Task`。启动新会话前取消旧会话,以确保同时最多只有一个可绘制的规划结果。
AMR 位姿和手动终点在创建任务前被转换、有限值校验并冻结,随后只作为 `CoarsePathPlanningJob` 数据传给后台。MovementTest 不在规划后台持续读取定位;若未来接入可能阻塞的 `DetourInterface.getCartLocation()`,它必须位于独立的上游快照提供者,不能阻塞 UI 或绕过本设计的输入契约。
```text
MovementTest.Test
-> 生成场景的全新 CoarsePathPlanningJob
-> 创建运行编号和 CancellationTokenSource
-> Task.Run(() => service.Plan(job, token))
-> 完成回调:仅当运行编号仍为当前会话时记录并绘制结果
MovementTest.TestStop
-> 取消当前 CancellationTokenSource
-> 使当前运行编号失效并解绑 Task 引用
-> 清空专用 Painter 图层
-> 旧任务完成后只释放其 CancellationTokenSource,不再绘制
```
`Test` 绝不等待 `Task`、不读取 `Task.Result`,因此不会阻塞 Clumsy 界面。`TestStop` 不等待规划任务退出;P0 的共享预算会将令牌传递至建图、EDT、Dijkstra 和 Hybrid A*,任务在其检查点返回 `Cancelled`。运行编号检查可防止已取消的旧任务在新任务结果之后覆盖画面。
任务异常只记录清晰的测试诊断并释放资源,不伪造 `PlanningResult`。正常停止、超时、无解与输入失败均使用门面实际返回的状态。
所有七个入口只创建规划请求、任务和绘制;不得引用 `BasicPilotBase.Chassis``SendMotion``DriveTask` 或任何底盘控制 API。
## 绘制规则
使用独立的全局世界坐标 Painter 图层,例如 `CoarsePathPlanningV1`。Painter 输入为 mm,因此所有来自 `Pose2D``CoarsePathPoint` 的 X/Y 必须乘以 1000;航向仍以 rad 计算旋转矩形。不得混用 Map 的 mm 和 CoarsePath 的 m。所有地图输入、AMR 起点和手动终点均处于同一个世界坐标系。
- 先绘制地图 `[XMin, XMax) × [YMin, YMax)` 的粗外边界、世界 X/Y 参考和栅格网络。格线遵循真实 `ResolutionMm`;当格线数量超过显示上限时,按整数格距抽稀,并在状态文本中保留真实分辨率与显示步距。
- 从 `PlanningGridMap.IsOccupied(row, col)` 绘制占据格,而不是重新绘制原始障碍物几何;因此显示内容与实际规划快照一致。空闲格使用背景,不为每个空格增加填充。
- 起点为绿色圆、方向短线和“起点”标签;终点为橙色圆、方向短线、“终点”标签及目标位置容差圈。
- 成功路径逐段连接:前进与倒车使用不同颜色,并以固定间距绘制方向箭头;路径不成功时不绘制任何路径段。
- `IsGearSwitchPoint=true` 的点使用紫色标记和“换向”标签。
- 对首点、末点、每个换向点和固定间隔点绘制旋转矩形。矩形半长/半宽为 `Vehicle.LengthMeters / 2 + SafetyMarginMeters``Vehicle.WidthMeters / 2 + SafetyMarginMeters`,仅用于显示 P0 已采用的扩大车体,不参与碰撞判断。
- 在地图角落绘制固定图例:边界、占据格、起点、终点、前进、倒车、换向与扩大车体检查框的颜色含义。无论成功与否,绘制文本状态:场景名称、地图快照 ID、地图构建状态、缓存层级、规划状态、耗时和终止原因。停止或新会话开始时先清空旧图层。
绘制只消费 `CoarsePathPlanningJobResult` 的只读结果;不修改 `PlanningMapRequest`、地图快照、路径、调试开关或服务缓存。
## 测试与验收
按 Red-Green-Refactor 顺序扩展 `verify_coarse_path_integration.ps1`:先增加以下会失败的反射/行为断言,再实现最小代码,最后运行相同脚本。
1. 断言 `CoarsePath/Test` 中的场景工厂和 MovementTest 文件存在;工厂提供六类固定场景与一个 AMR 位姿/手动终点入口,且每次创建返回独立请求。
2. 使用同一 `CoarsePathPlanningService` 运行工厂场景:空图、矩形、多来源和倒车换向均成功;倒车换向路径含 `IsGearSwitchPoint`;无解结果为 `NoFeasiblePath` 且路径为空。
3. 对缓存场景连续调用两次,断言第二个地图结果为 `Input` 命中,且路径状态和点数不因缓存改变。
4. 断言 AMR `0 deg``90 deg` 的 UI 输入分别转换为 `0 rad``pi/2 rad`,同时 X/Y 由 mm 转为 m;手动目标与起点都使用相同转换与有限值校验。
5. 对预先取消的后台调用断言门面映射为 `Cancelled`、地图或路径不发布部分结果;结构检查确认 MovementTest 使用 `Task.Run``CancellationTokenSource`,且没有等待任务。
6. 结构检查确认测试入口只通过 `CoarsePathPlanningService` 进行规划,且不引用底盘命令、栅格化器、碰撞器、原语生成器或搜索节点;并检查绘制代码消费 `PlanningGridMap` 的边界、分辨率与占据状态,包含图例和成功路径保护。
7. 更新 README 断言,确认 P1 的 UI、AMR 位姿单位、手动终点、取消和“无底盘命令”边界可被调用方查阅。
完成后运行 Debug 构建及现有 P0 Map/CoarsePath 验证脚本(不恢复或改动已被排除的旧 TrapMap 验证脚本)。
## 非目标
- 不实现路径平滑、速度规划、跟踪控制、底盘命令、实时重规划或传感器采集。
- 不改变地图指纹、缓存键、障碍物栅格化、车辆碰撞、终点判定、搜索代价或资源上限。
- 不在本交付中实现 Release 性能基准,也不清理、迁移或恢复任何 TrapMap 文件与脚本。
@@ -0,0 +1,59 @@
# 规划操作预算与诊断收尾设计
## 目标
在进入 P1 的 UI 集成前,使一次 `CoarsePathPlanningService.Plan` 调用的取消与超时语义覆盖完整链路:地图创建、距离场构建、二维 Dijkstra 启发式和 Hybrid A* 搜索。同时让现有 `PlanningDiagnostics` 中的 Open List 陈旧条目数与峰值容量反映真实搜索数据。
成功、无解、输入无效和碰撞安全语义不改变;任何取消或超时结果均不得发布部分路径或部分地图。
## 方案选择
采用一个内部共享的、基于单调 `Stopwatch``PlanningOperationBudget`。它保存调用方的 `CancellationToken`、整次调用开始时刻与总超时,并在每个耗时循环中返回三态结果:继续、已取消、已超时。
不采用“只在 Dijkstra 前后检查”的方案,因为大图 Dijkstra 和 EDT 仍可能长时间无响应;也不替换 Dijkstra 启发式,避免在收尾阶段改变 Hybrid A* 的搜索特性。
## 边界与数据流
```text
CoarsePathPlanningService.Plan(job, token)
-> PlanningOperationBudget(token, job.Configuration.SearchTimeout)
-> PlanningMapFactory.Create(mapRequest, budget)
-> EnvironmentMapBuilder / PlanningMapAdapter / EDT
-> HybridAStarPlanner.Plan(planningRequest, budget)
-> GridDijkstraHeuristic
-> HybridAStarSearch Open List
-> PlanningResult + PlanningDiagnostics
```
`PlanningOperationBudget` 放在不依赖 `Map``CoarsePath` 的公共工具层,仅暴露中立的停止原因。Map 与粗规划分别把该原因映射到自己的结果类型,避免 `Map` 反向依赖 `CoarsePath`
地图构建结果增加明确的终止状态(成功、普通构建失败、取消、超时)。`CoarsePathPlanningService` 将地图阶段的取消映射为 `PlanningStatus.Cancelled`,地图阶段的超时映射为 `PlanningStatus.SearchTimeout`;两种结果都保留地图构建诊断但路径和分段为空。
现有不带预算参数的 `PlanningMapFactory.Create``HybridAStarPlanner.Plan``GridDijkstraHeuristic` 入口保持可用,作为不受取消限制的兼容包装;业务门面只使用带共享预算的内部入口。
## 响应与一致性规则
- 每个耗时循环在开始处及每处理最多 256 个工作单元后检查预算;检查不改变正常情况下的栅格、启发式或 Open List 排序。
- 等待地图工厂创建锁时使用可轮询的获取方式,以便取消和超时也能中断排队等待。
- 缓存命中仍立即返回原有不可变快照;预算已停止时优先返回取消/超时,不能借缓存绕过调用方停止请求。
- 已被取消或超时的地图构建不得写入任何缓存,也不得发布部分 `PlanningGridMap`
- 规划器总耗时从门面开始计时;搜索器不重新开始独立的 5 秒窗口。
## 诊断
`HybridAStarSearchResult` 增加陈旧 Open List 条目数和 Open List 峰值。每次丢弃失效的普通节点时递增陈旧计数;每次成功入堆后更新峰值。`HybridAStarPlanner` 原样将这两个统计写入 `PlanningDiagnostics`
目标候选仍保留其当前规则:不受普通离散键的 best-G 压制,出队时复核。它们占用 Open List 容量,因此计入峰值;不因候选自身而计为陈旧条目。
## 测试与验收
- 在大于一个检查批次的地图上,Dijkstra 预计算期间取消,断言返回 `Cancelled`、空路径和有限响应时间。
- 使用足以覆盖 Dijkstra 工作的极短总超时,断言返回 `SearchTimeout`、空路径,且总耗时不超出预算一个检查批次的合理余量。
- 在地图适配器/EDT 处理中取消和超时,断言地图结果带对应状态、没有地图快照且缓存未被污染。
- 使用产生失效 Open List 条目的场景,断言陈旧数大于零;任意正常搜索断言峰值至少为一,并与最终 `PlanningDiagnostics` 一致。
- 重新运行 Debug 构建、所有既有 P0 地图/粗规划检查,以及新增取消、超时和诊断检查。
## 非目标
- 不修改运动原语、碰撞保守性、代价公式、目标候选排序或路径装配。
- 不在本次收尾中实现 UI、Painter、场景工厂或 Release 性能门槛;这些仍属于 P1。
@@ -60,9 +60,9 @@ Create(CoarsePathTestScenario scenario,
## 缓存语义
缓存键继续由完整的地图输入决定。由于地图边界和障碍随 AMR 坐标平移,只有两次固定案例使用完全相同的 AMR 位姿时,`粗路径规划-缓存命中` 才会命中现有输入缓存。
缓存键继续由完整的地图输入决定。由于地图边界和障碍随 AMR 坐标平移,两次固定案例冻结到相同的 AMR X/Y、从而形成相同的平移后地图输入时,`粗路径规划-缓存命中` 才会命中现有输入缓存。仅 AMR 航向变化不会改变地图输入,缓存仍可命中。
AMR 已移动时显示缓存未命中是正确结果,而不是规划失败。UI 继续显示实际的缓存状态。
AMR 位置已移动时显示缓存未命中是正确结果,而不是规划失败。UI 继续显示实际的缓存状态。
## 验证
@@ -0,0 +1,66 @@
# 粗路径搜索耗时设计
## 目标
为粗路径规划结果增加独立的“路径搜索耗时”。它用于回答:在规划地图已经可用后,从起点到终点得到可发布最终粗路径实际花费了多久。
现有 `PlanningDiagnostics.Elapsed` 保持不变,继续表示从 `CoarsePathPlanningService.Plan` 入口开始的总耗时。
## 计时边界
`PlanningDiagnostics.PathSearchElapsed` 的边界固定如下:
- 开始:`HybridAStarPlanner` 已完成输入、起终点和初始碰撞检查,即将调用 `HybridAStarSearch.Search`
- 包含:二维 Dijkstra 启发式预计算、Hybrid A* 节点扩展、路径回溯、路径装配、方向分段和最终碰撞复核。
- 结束:规划器准备返回对应的 `PlanningResult`
- 不包含:地图来源读取、地图缓存查询、障碍物栅格化、距离场构建,以及门面层在进入规划器前的工作。
因此,此字段表示“地图就绪后的路径求解与发布耗时”,而不是仅 Open List 循环的耗时。
## 数据契约
`PlanningDiagnostics` 新增只读 `TimeSpan PathSearchElapsed`
- 成功时记录完整路径搜索与发布阶段耗时。
- 搜索失败、无解、节点上限、超时、取消、回溯失败、装配失败或最终复核失败时,记录截至返回前已消耗的该阶段时间。
- 在进入搜索阶段前即失败(例如输入、起终点或初始碰撞检查失败)时为 `TimeSpan.Zero`
- 该字段必须为非负值,并且不超过总耗时 `Elapsed`
保持构造函数的现有调用兼容:新参数具有 `TimeSpan.Zero` 默认值。`HybridAStarPlanner` 是唯一写入实际计时值的边界。
## 实现方案
推荐方案是在 `HybridAStarPlanner.Plan` 中,于调用 `_search.Search` 前创建本地 `Stopwatch`,并在所有搜索后返回路径将要构造 `PlanningResult` 时读取其 `Elapsed``CreateDiagnostics` 接收这个独立耗时,并写入 `PlanningDiagnostics`
选择该方案的原因:
- 不修改门面的共享总预算和取消/超时语义。
- 不让 `HybridAStarSearch` 暴露计时实现细节。
- 计时覆盖用户定义的完整粗路径产出阶段,而非只覆盖节点扩展循环。
未采用的方案:
1. 直接复用 `PlanningOperationBudget.Elapsed`:会包含建图,不满足需求。
2. 仅在 `HybridAStarSearch` 内计时:会遗漏回溯、装配和最终复核,无法表示最终粗路径产出时间。
3. 为建图、启发式、搜索、复核分别公开多组指标:诊断更细,但超出当前需求。
## 可视化与文档
`MovementTest.CoarsePathTest` 的状态图层和 Toast 同时显示:
```text
总耗时:<Elapsed> ms,路径搜索:<PathSearchElapsed> ms
```
README 明确区分:总耗时覆盖建图和路径规划;路径搜索耗时仅覆盖地图就绪后的最终粗路径搜索、回溯、装配与复核。
## 验证
自动化验证应覆盖:
1. `PlanningDiagnostics` 默认搜索耗时为零,且新字段可由调用方读取。
2. 一个真实可行规划返回非负的路径搜索耗时,且不大于总耗时。
3. 既有总预算、取消、超时和路径状态断言不改变。
4. UI 源码检查确认图层和 Toast 读取并显示新字段。
5. README 包含新字段的计时边界说明。
@@ -0,0 +1,46 @@
# CoarsePath README 结构化重构设计
## 目标
`ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md` 重构为与 `Map/README.md` 一致的说明风格,使调用者能够从模块职责、文件位置和数据流开始,逐步理解粗路径的调用、状态处理、P1 手动测试与明确的非目标。
本次只重构文档内容与现有文档检查;不改变 `Map``CoarsePath`、P1 UI 或任何测试场景的运行行为。
## 当前事实
- `Map` 负责障碍物来源、栅格化、不可变 `PlanningGridMap` 与缓存;它是粗路径的输入依赖。
- `CoarsePath` 已具备 P0 核心:车辆扩大足迹碰撞、前进/倒车原语、Dijkstra 启发式、Hybrid A*、路径回溯、最终复核与业务门面。
- P1 已具备:六个固定场景、AMR 位姿与手动终点空图演示、后台取消、Painter 结果可视化与 UI 结构检查。
- 尚不包含平滑、速度/时间轨迹、底盘控制、实时重规划、真实作业障碍物接入与 Release 基准。
## README 目标结构
1. **模块说明**:定义 `CoarsePath` 的输入、输出、唯一业务入口与职责边界。
2. **文件结构**:按 `Contracts``Vehicle``Search``Output``Facade``Test` 列出实际文件及职责。
3. **规划数据流**:说明 `CoarsePathPlanningJob` 经服务、Map 快照、Hybrid A* 到 `PlanningResult` 的固定路径;明确地图失败不会启动搜索。
4. **状态、单位与安全边界**:集中说明 mm/m、deg/rad、车辆安全外扩、取消/超时和“非成功不发布部分路径”。
5. **最小调用示例**:沿用现有可编译门面调用,展示成功、地图失败和规划失败的处理方式。
6. **缓存与 SourceVersion**:解释长期持有服务、`Input`/`Occupancy`/`None` 缓存层级及版本递增责任。
7. **详细使用指南**:依次说明长期服务、准备地图请求、车辆和搜索参数、调用门面、消费路径与方向段。
8. **P1 测试与调试**:集中说明七个 MovementTest、AMR 手动终点单位边界、后台停止和 Painter 图例。
9. **常见错误**:用“现象 / 原因 / 处理”表格覆盖单位混用、遗漏 `SourceVersion`、隐式空图、错误处理失败结果、将粗路径当作控制轨迹等问题。
10. **第一版限制**:保留不属于 P0/P1 的能力清单。
## 内容约束
- 仅记录已实现且已验证的行为;不把 P1 计划或人工验收说成已完成能力。
- 固定使用 `CoarsePathPlanningService.Plan(job, cancellationToken)` 作为唯一业务调用示例;不鼓励 UI 直接组装搜索组件。
- 保留 `Map/README.md` 链接,避免复制地图障碍物和栅格化的详细说明。
- P1 手动终点必须明确是显式空图演示,不能代表现场无障碍;当前 AMR 位姿为车辆几何中心,输入在 UI 边界从 mm/deg 转为 m/rad。
- 使用中文说明、目录树、数据流图、参数表、代码示例和常见错误表,保持 Map README 的信息密度与顺序。
## 验证
- 扩展 `ClumsyPilot/tests/verify_coarse_path_ui.ps1`,以 ASCII 稳定标识检查 README 含有新的主要章节、核心门面、数据流、P1 入口、单位、停止语义、非部分路径和限制边界。
- 运行 README 的 P1 UI 检查,以及现有 Debug 构建和粗路径集成检查;文档改动不应影响生产代码或 P0 行为。
## 非目标
- 不重写或迁移 `Map/README.md`
- 不新增、删除或改名 C# 类型、场景、MovementTest 或测试脚本。
- 不恢复、清理或迁移 TrapMap 及其旧验证脚本。
@@ -0,0 +1,71 @@
# P1 手动障碍物输入设计
## 目标
扩展 `[MovementTest(name = "粗路径规划")]`,使操作者可在一次手动测试中输入多个圆形或轴对齐矩形障碍物的中心与尺寸。输入经纯场景工厂转换为 `ManualObstacleSource`,再由已有 `CoarsePathPlanningService` 创建地图和规划;不绕过门面,也不添加任何底盘控制。
## 输入流程
1. 读取一次 `DetourInterface.getCartLocation()`,冻结 AMR 车身几何中心的世界 `X/Y(mm)``th(deg)`
2. 输入目标世界 `X(mm)``Y(mm)` 与航向 `deg`
3. 输入障碍物数量,允许范围为 `0``20`
4. 对每个障碍物输入类型:`1` 为圆形,`2` 为矩形。
5. 输入障碍物几何中心世界 `X(mm)``Y(mm)`
- 圆形再输入半径 `r(mm)`
- 矩形再输入 X 方向长度与 Y 方向宽度(均为 mm)。矩形不提供旋转角,始终与世界坐标轴平行。
6. 将已冻结的 AMR 位姿、目标和障碍物集合提交给现有后台执行器。
输入必须是有限数字。数量、类型、半径、长度和宽度不合法时拒绝启动规划并显示输入失败信息;不会产生不完整的规划请求。
## 工厂契约
`CoarsePathScenarioFactory` 新增面向手动测试的纯数据类型与工厂方法:
```csharp
public enum ManualCoarsePathObstacleKind
{
Circle,
AxisAlignedRectangle,
}
public sealed class ManualCoarsePathObstacle
{
public static ManualCoarsePathObstacle Circle(
double centerXMillimeters, double centerYMillimeters, double radiusMillimeters);
public static ManualCoarsePathObstacle AxisAlignedRectangle(
double centerXMillimeters, double centerYMillimeters,
double lengthXMillimeters, double widthYMillimeters);
}
public static CoarsePathPlanningJob CreateManualObstacleDemo(
double startXMillimeters, double startYMillimeters, double startHeadingDegrees,
double goalXMillimeters, double goalYMillimeters, double goalHeadingDegrees,
IReadOnlyList<ManualCoarsePathObstacle> obstacles, long obstacleSnapshotVersion);
```
原有 `CreateManualGoalDemo` 保留不变,并委托到相同的边界/位姿转换逻辑和空障碍物路径,因此既有调用方与验证不受破坏。
工厂将中心/尺寸转换为 `CircleObstacle``AxisAlignedRectangleObstacle`。有障碍物时请求使用必需的 `ManualObstacleSource("manual-user-input", obstacleSnapshotVersion, true, ...)``AllowExplicitEmptyMap=false`;没有障碍物时使用空来源数组和 `AllowExplicitEmptyMap=true`。工厂校验障碍物列表、版本、几何数值与正尺寸,避免将缓存版本或无效几何交给 Map。
## 边界和缓存
手动地图边界的候选范围由起点、终点和每个障碍物的完整外轮廓共同决定:圆形使用中心 ± 半径,矩形使用中心 ± 半长/半宽。候选范围的每侧保留 2000 mm,随后按既有 50 mm 分辨率向外取整。
后台执行器为每次包含障碍物的手动提交生成单调递增的 `obstacleSnapshotVersion`,并在 UI 线程完成输入后冻结它。这样新输入绝不会复用旧障碍物地图;固定场景的缓存命中测试保持原样。空障碍物演示不需要障碍物来源版本。
## 可视化与停止
不新增 Painter 专用绘图分支。现有结果绘制已从 `PlanningGridMap.IsOccupied(row, col)` 消费占据格,因此新的手动障碍物会自动在同一 `CoarsePathPlanningV1` 图层显示为实际栅格快照。起点、终点、路径、换向、扩大车体检查框、状态和 `TestStop` 取消语义均保持不变。
## 验证与文档
- `verify_coarse_path_integration.ps1` 增加工厂反射与行为断言:新类型/方法存在;圆形和矩形输入生成非空手动来源、关闭显式空图、边界覆盖外轮廓;空障碍物仍保留显式空图;无效尺寸被拒绝。
- `verify_coarse_path_ui.ps1` 检查手动入口读取障碍物数量、类型、中心和尺寸,并调用 `CreateManualObstacleDemo`,同时仍不直接创建地图或搜索器。
- `CoarsePath/README.md` 的 P1 节说明输入顺序、单位、20 个上限、矩形无旋转、空图仅限零障碍物演示,以及可视化仍基于最终规划快照。
## 非目标
- 不支持旋转矩形、多边形、导入文件、拖拽编辑或实时编辑已运行任务。
- 不让手动障碍物直接跳过 `ManualObstacleSource`、Map 缓存或 `CoarsePathPlanningService`
- 不改变已有固定场景、车辆安全参数、搜索算法、P1 Painter 颜色或任何底盘控制边界。
@@ -0,0 +1,38 @@
# Path Smoothing Six-Figure Report Design
## Goal
Replace the current one-file, three-panel path-smoothing report with six focused, independent point-plot figures for every scenario. Preserve both SVG and 600 dpi PNG export, retain one CSV metrics file, and never connect trajectory samples with lines.
## Output contract
Every scenario directory contains exactly these six figures in both `.svg` and `.png` form:
1. `01-coarse-path-overview`: raw Hybrid A* samples, map obstacles, start and coarse-path endpoint.
2. `02-all-paths-comparison`: raw, B-spline, Bézier, and quintic samples together; no obstacles, start, or goal marker.
3. `03-cubic-bspline-overview`: faded raw samples, B-spline samples, relevant obstacles, start and endpoint.
4. `04-local-cubic-bezier-overview`: faded raw samples, Bézier samples, relevant obstacles, start and endpoint.
5. `05-piecewise-quintic-overview`: faded raw samples, quintic samples, relevant obstacles, start and endpoint.
6. `06-curvature-comparison`: raw and every available smoother's vehicle-curvature samples against arc length.
`comparison.csv` remains the single numerical report. The legacy composite `comparison.svg` and `comparison.png` are no longer emitted.
## Point-only rendering
Each `SmoothingFigurePoint` in a displayed series becomes one circular marker. SVG must not emit a trajectory polyline/path for any figure; PNG must not call a line-drawing API for trajectory samples. Marker size is fixed in report points so 0.025 m samples remain individually visible at 600 dpi. Start and endpoint remain distinct point markers only in figures 1, 3, 4, and 5.
Raw samples are dark gray, B-spline samples blue, Bézier samples orange, and quintic samples green. A method with no geometry has no markers but remains represented by an `Infeasible` or `Failed` status in that figure's legend.
## Framing and annotation
Every overhead figure derives its world bounds from the displayed path samples, then adds a fixed 10% padding with a 0.25 m minimum. The X/Y scales are equal. Obstacles are clipped by the panel rather than expanding the camera away from the path. Overhead axes show numeric ticks plus `X (m)` and `Y (m)` labels.
The curvature figure uses `s (m)` horizontally and `κ (m⁻¹)` vertically, with numeric ticks, zero axis, and displayed curvature limits. Every figure owns a compact legend describing its visible series and statuses.
## Export and compatibility
The existing shared, immutable comparison data remains the source of all six figures. SVG continues to use SimSun/Times New Roman family references. PNG continues to require exact SimSun and Times New Roman and returns `FontUnavailable` rather than falling back. The report exporter publishes all image files atomically and cleans any temporary files if one fails.
## Validation
Regression coverage verifies the six stable file stems, absence of trajectory line commands/styles, presence of all expected point markers and units, correct legends/statuses, valid PNG signature/CRC/600 dpi metadata, and no leftover `.tmp` files. Visual inspection covers straight, rectangle-detour, forward-reverse-switch, and an infeasible scenario.
@@ -0,0 +1,62 @@
# Local G2 日报式报告交付设计
## 目标
在仓库根目录新增 `dailywork_report/`,交付两份中文主报告及各自独立、可直接打开的 HTML 可视化附录。内容面向研发人员和需要快速理解进度/风险的项目协作者。
## 交付物
```text
dailywork_report/
├── Map_rep/ # 预留:地图模块报告
├── coarsepath_rep/ # 预留:粗路径模块报告
└── pathsmoothing_rep/ # 本次 Local G2 报告
├── 01-local-g2-quintic-hermite-algorithm-report.md
├── 01-local-g2-quintic-hermite-algorithm-visualization.html
├── 02-local-g2-issues-and-next-actions-report.md
└── 02-local-g2-issues-and-next-actions-visualization.html
```
HTML 文件为单文件附件:内嵌 CSS、SVG 和少量原生 JavaScript,不依赖网络、第三方 CDN 或构建步骤。
本次只在 `pathsmoothing_rep/` 中创建内容;`Map_rep/``coarsepath_rep/` 仅建立目录结构,供后续对应模块的日报式报告使用。
## 报告一:算法说明
主题为“Local G2 五次 Hermite 路径平滑算法说明”。内容按以下顺序组织:
1. 目标、适用位置与非目标:说明它位于 Hybrid A* 与后续 SQP 之间,只生成空间路径初值,不涉及速度、加速度或 SQP 求解。
2. 输入:成功的粗路径、方向段、地图、车辆参数、G2 配置和取消令牌;明确坐标/单位与有效性前提。
3. 模块架构:预处理、曲率跳变检测、窗口规划、五次 Hermite 候选构造、路径拼接、统一几何分析、安全/质量评价,以及计划中的专用发布流水线。
4. 数据流:以“输入 → 检测 → 局部候选 → 安全筛选 → 输出”的线性流程说明每一步的职责和边界。
5. 输出:说明路径点、方向段、曲率、曲率导数、区域报告、状态和诊断;明确当前任务 8 尚未接入,不能把候选层能力描述为已发布的主路径功能。
6. 约束与安全门:不跨换向点、窗口/偏移/净空/曲率限制、候选数量上限和确定性排序。
配套 HTML 使用模块卡片、输入/输出栏和 SVG 数据流箭头,分别标明“已实现”和“待接入”模块。
## 报告二:问题分析与后续措施
主题为“Local G2 路径平滑问题分析与后续措施”。开头先给出证据边界:专项测试通过不等于端到端功能已经完成。随后按固定结构分别描述三个问题:
1. **已复现故障**`RectangleDetour` 原始基线在平滑前的复验中变为 `InvalidInput`
2. **已确认的逻辑缺口**:窗口合并包络与候选总长度上限不一致,可能将本可分开处理的事件合并为没有合法候选的区域。
3. **待验证的集成风险**:连续处理同一方向段多个区域时,前一处替换重算弧长可能让后一处继续使用旧的窗口坐标。
每个问题都包含:现象、通俗例子、技术成因、影响范围、证据等级、建议验证/修复措施和进入任务 8 前的验收条件。报告不把风险说成已经发生的运行时故障。
配套 HTML 使用状态徽章、场景示意 SVG、因果链和“问题 → 验证 → 措施”流程,突出已复现故障与待验证风险的区别。
## 写作与证据原则
- 以中文撰写,术语首次出现时同时给出白话解释。
- 明确区分“已通过的专项测试”“已复现的失败”和“静态分析发现的风险”。
- 引用现有实现计划、测试脚本、关键实现文件和本次实际测试结果;不宣称尚未实现的任务 8/9 已完成。
- HTML 与 Markdown 的事实、术语和问题分级必须一致。
## 验收标准
- 四个文件均位于 `dailywork_report/`,命名稳定、无需外部资源即可阅读。
- 两份 Markdown 报告结构完整,能够单独解释算法和问题。
- 两份 HTML 附录在本地直接打开时内容可读、层级清楚、与主报告一致。
- 报告二准确表达三个问题的证据等级和下一步,不给出未经验证的结论。
- 本次工作只创建报告,不修改路径平滑算法或测试逻辑。
@@ -0,0 +1,295 @@
# Daily Summary Job 领域算法可视化升级设计
## 1. 目标
升级个人技能 `daily-summary-job`,使开发日报不只用文字和通用流程框陈述工作,而是先理解当天函数或算法的真实业务目标,再生成与该任务匹配的领域可视化。
页面必须让读者通过点击直接理解:
1. 原函数或算法解决什么问题、正常情况下如何工作;
2. 当前问题发生在哪个对象、区域或算法阶段;
3. 出错原因是什么,或当前有哪些待验证假设;
4. 问题会沿什么路径传播并导致什么结果;
5. 纠正方案会改变哪些对象、约束或处理步骤;
6. 纠正后的预期效果是什么;
7. 哪些内容是实际观测、静态分析、概念预演或已验证结果。
路径规划只是示例。技能必须根据当前任务选择合适的可视化,而不能把所有算法都硬编码成轨迹图或普通流程图。
## 2. 已确认的设计决策
- 使用混合生成策略:涉及函数或算法时自动生成基础领域视图;出现复杂问题时再生成深入诊断和修正前后对比。
- 使用“双层算法地图”:正常算法效果作为稳定底图,问题与修正方案作为可切换叠加层。
- 使用混合粒度:主图展示业务对象和算法阶段,点击后下钻到函数、源码、输入输出与约束。
- 严格区分三类变化状态:当前故障、候选修正预演、已验证修正结果。
- 使用“统一诊断外壳 + 领域可视化适配器”架构。
- 测试与验证场景由当前任务、算法约束和问题类型动态决定,不使用预设的固定领域清单代替任务匹配。
- 当前实施范围只交付通用声明式领域画布和统一诊断交互;路径规划、数值曲线、状态机等专用适配器在真实使用出现明确需求后再逐步增加。
## 3. 核心原则
### 3.1 领域效果优先
领域可视化必须展示算法实际处理的业务对象或结果:
- 空间或规划任务展示地图、边界、障碍物、姿态、搜索空间、候选路径或几何结果;
- 数值任务展示真实曲线、阈值、异常区间、收敛过程或误差变化;
- 搜索任务展示搜索空间、扩展顺序、代价变化、剪枝与最终路径;
- 状态相关任务展示状态、迁移、触发条件、错误跳转和恢复路径;
- 数据处理任务展示输入样本、中间变换、异常字段、影响传播与输出结果;
- 其他任务展示最能表达其业务对象和正确性约束的视图。
当算法天然具有空间、数值、时间、状态或数据结构语义时,通用流程图只能作为辅助导航,不能代替主领域效果图。
### 3.2 先理解,后选择图形
技能不能仅根据目录名或函数名选择模板。生成可视化前必须回答:
- 当前函数或算法的业务目的是什么;
- 输入、核心处理与输出是什么;
- 用户需要直接观察的业务对象是什么;
- 哪些约束决定结果是否正确;
- 当前问题与哪个对象、区域或阶段关联;
- 当前证据能支持展示哪些真实数据。
### 3.3 证据边界不可被动画掩盖
动画和交互只负责解释证据,不得制造证据。候选方案的预测画面必须明确标记为“概念预演”或“尚未验证”,不能显示成已经发生的修正结果。
## 4. 总体架构
```text
当天对话、Agent 汇报、源码、测试和运行证据
任务与算法理解
Algorithm Visualization Brief
┌────────────┴────────────┐
▼ ▼
领域适配器选择 问题诊断关系构建
│ │
└────────────┬────────────┘
领域主视图 + 统一诊断叠加层
Markdown / 交互式 HTML
任务匹配验证与证据一致性检查
```
架构由五个逻辑组件组成。
### 4.1 任务与算法理解器
这是写入 `SKILL.md` 的 Agent 工作流程,不是只依赖关键词的确定性分类器。它负责:
1. 从当天证据中识别真正相关的函数、算法和业务任务;
2. 合并主 Agent 与子 Agent 的成果、问题、原因、验证和遗留事项;
3. 确定算法输入、输出、处理阶段、正确性约束和可观察对象;
4. 区分实际数据、源码静态重建、对话结论与方案推演;
5. 生成内部使用的可视化说明。
### 4.2 Algorithm Visualization Brief
可视化说明是技能内部生成的结构化事实,不要求用户手工填写。至少包含:
- 算法标识、名称、目的和领域语义;
- 输入、输出、处理阶段与关键约束;
- 适合的主视图类型和选择理由;
- 可用的真实样本、运行数据及其来源;
- 可视化对象与源码函数之间的关联;
- 问题、影响、修正和预期结果关联到哪些图形对象;
- 当前视图属于实际观测、静态重建、概念预演还是已验证结果。
### 4.3 领域可视化适配器
适配器负责把统一说明转换成领域主视图。适配器是可扩展能力,不是固定领域白名单。
当前版本提供可组合的声明式图元:
- 点、线、折线、曲线、区域、坐标轴和阈值;
- 节点、边、树、图和搜索空间;
- 状态、迁移、触发条件和时间线;
- 网格、边界、障碍物、姿态和空间对象;
- 输入输出样本、字段、数据块和转换关系;
- 标注、告警、影响范围和证据引用。
当前版本由 Agent 根据可视化说明组合图元,形成符合任务语义的视图;适配器注册表只保留扩展接口。路径规划、数值曲线、状态机等专用适配器不属于本轮实施范围。若证据不足以形成可信领域视图,必须明确显示缺失信息,不得退化为伪装成实际效果的通用图。
### 4.4 统一诊断叠加层
所有领域视图共享相同的诊断交互协议。每个问题通过稳定问题 ID 和目标对象 ID 关联到主视图。
点击异常对象后必须展示:
- 目前状况;
- 正常预期;
- 出错位置;
- 出错原因或待验证假设;
- 影响传播路径;
- 会导致的结果;
- 纠正方案及步骤;
- 纠正后的预期结果;
- 实施和验证状态;
- 函数、源码、测试和证据等级。
### 4.5 验证器
验证器继续检查 Markdown 与 HTML 的事实一致性和离线自包含性,并新增领域视图约束:
- 问题引用的算法、阶段和可视化对象必须存在;
- 实际数值或几何结果必须具有证据引用;
- 候选预演不能被标记成已验证结果;
- 修正前后比较必须具有相同场景、单位和比较条件;
- 每个可交互问题必须具有原因、影响、方案和预期结果;
- 所有视图必须具有证据状态和必要的“概念示意”标签。
## 5. 报告事实结构扩展
现有 `achievements``issues``validations``next_steps``sources` 保持兼容,新增顶层 `algorithm_views`
```json
{
"algorithm_views": [
{
"id": "planner-main",
"name": "泊车路径规划",
"purpose": "从起始姿态生成满足碰撞和运动学约束的可执行轨迹。",
"domain": "spatial-planning",
"adapter": "spatial-scene",
"evidence_state": "actual",
"inputs": [],
"outputs": [],
"constraints": [],
"stages": [],
"scene": {},
"source_refs": []
}
]
}
```
`algorithm_views[].scene` 使用声明式数据,不直接嵌入任意脚本。具体适配器解释该字段并渲染 SVG、Canvas 或 DOM 图形。
每个 `issue` 新增:
- `algorithm_view_id`:关联的算法视图;
- `target_ids`:主视图中需要高亮的对象;
- `effect_target_ids`:影响传播涉及的对象;
- `solution_preview`:候选修正会改变的对象和预期状态;
- `verified_result`:存在真实修正验证时的结果引用。
旧报告缺少 `algorithm_views` 时仍可使用现有问题诊断页面,不得导致更新失败。
## 6. 页面交互结构
### 6.1 页面区域
```text
┌──────────────────────────────────────────────────────────┐
│ 算法选择器 / 问题选择器 / 证据状态 │
├───────────┬──────────────────────────┬───────────────────┤
│ 查看模式 │ 领域效果主画面 │ 对象诊断卡 │
│ │ │ │
│ 正常机制 │ 路径、曲线、状态、搜索树 │ 目前状况 │
│ 当前问题 │ 或其他任务匹配视图 │ 原因与影响 │
│ 修正预演 │ │ 方案与预期 │
│ 验证结果 │ │ 源码与测试证据 │
├───────────┴──────────────────────────┴───────────────────┤
│ 算法阶段导航 / 方案步骤 / 验证门 / 下一步 │
└──────────────────────────────────────────────────────────┘
```
### 6.2 四种查看模式
1. **正常机制**:展示算法原本的输入、处理、输出和正确性约束。
2. **当前问题**:在正常底图上高亮异常对象、实际状态和影响传播。
3. **修正预演**:逐步展示候选方案会改变什么,并明确标记尚未验证。
4. **验证结果**:仅在存在修正后测试或运行证据时启用,展示真实结果及证据。
### 6.3 一次完整交互
```text
选择算法
→ 查看正常领域效果
→ 选择问题或点击异常对象
→ 播放原因与影响传播
→ 点击纠正步骤查看候选变化
→ 对比当前状态与预期状态
→ 有验证证据时切换到已验证结果
→ 下钻源码、测试和证据引用
```
## 7. 任务匹配验证
报告生成时不得运行或引用与当前任务无关的固定测试场景。技能必须动态构建验证清单:
1. 识别当前任务和算法目标;
2. 从源码、设计、测试和运行证据中提取正确性约束;
3. 确定当前问题的复现条件和失败判据;
4. 查找与这些条件直接匹配的现有测试或运行证据;
5. 只在安全且成本合理时运行针对性验证;
6. 将已运行、未运行和仍缺失的验证严格分开;
7. 为没有匹配测试的结论生成任务专属验证建议。
示例:泊车规划任务可以匹配碰撞、安全间距、可达性、曲率和车辆运动学约束;并发缓存任务可以匹配竞争、重复写入、超时和一致性约束。这些示例用于说明匹配原则,不是固定覆盖列表。
## 8. 证据状态与视觉语义
| 状态 | 含义 | 页面表达 |
| --- | --- | --- |
| 实际观测 | 来自测试、运行或可复核数据 | 实线、明确数值和证据引用 |
| 静态重建 | 根据源码控制流或公式重建 | 静态分析标签,不声称运行复现 |
| 概念预演 | 候选方案的预测效果 | 虚线或半透明,并显示尚未验证 |
| 已验证结果 | 修正后经过匹配测试确认 | 已验证标签和测试证据 |
| 结论冲突 | 多个可信证据不一致 | 同时保留视图与结论,显示冲突状态 |
颜色不能成为唯一状态区分方式;同时使用文字、线型、图标和可访问标签。
## 9. 降级与错误处理
- 当天没有函数或算法工作:生成普通开发日报,不强制创建算法视图。
- 找到算法但缺少运行数据:允许静态重建或概念示意,并明确证据状态。
- 无法确认算法业务目的:列出缺失证据,不生成伪领域效果。
- 多个算法同时出现:提供算法选择器,分别维护视图与问题关联。
- 数据量过大:允许抽样、聚合或简化,页面必须说明简化规则并保留原始证据位置。
- 适配器无法渲染某个图元:显示可读的局部错误卡,其他报告内容仍可访问。
- 修正方案没有验证:禁用“已验证结果”模式,而不是复制候选预演内容。
## 10. 文件和组件变化
预计修改:
- `SKILL.md`:增加任务理解、可视化说明、领域适配器选择和任务匹配验证流程。
- `references/report-schema.md`:增加 `algorithm_views`、问题对象关联和证据状态字段。
- `scripts/prepare_report.py`:验证新结构、保持旧结构兼容、向模板注入领域视图数据。
- `assets/interactive-report-template.html`:重构为统一诊断外壳和适配器注册表。
- `scripts/test_prepare_report.py`:增加结构、适配器协议、交互和证据边界测试。
可以按复杂度把适配器拆入 `assets/visual-adapters/`,但最终报告仍必须是一个无外部依赖的 HTML 文件。
## 11. 验收标准
- 技能先识别任务目的和业务对象,再选择可视化,不按固定目录名盲选。
- 路径、数值、状态、搜索或其他算法能够呈现各自真实领域效果,而不是统一文字流程框。
- 点击图中异常对象可查看目前状况、原因、后果、纠正方案和预期结果。
- 正常机制、当前问题、候选预演和已验证结果可以明确切换。
- 候选方案在没有验证证据时不会显示为已修复。
- 问题、图形对象、源码和测试证据能够互相追踪。
- 验证清单根据当前任务动态生成,不以固定领域测试替代任务匹配。
- 无法形成可信领域视图时诚实降级,不编造运行数据。
- Markdown 与 HTML 保持事实一致,HTML 离线可用并支持键盘和移动端。
- 旧日报数据仍可生成和更新。
## 12. 非目标
- 不在生成日报时自动修改业务代码。
- 不为了可视化而运行昂贵、破坏性或未经授权的测试。
- 不要求每种算法预先拥有专用硬编码模板。
- 不把动画效果当作算法正确性的证明。
- 不自动暂存、提交或推送 Git 变更。
@@ -0,0 +1,188 @@
# Daily Summary Job 个人技能设计
## 目标
创建个人技能 `daily-summary-job`,在用户按需要求记录进展、生成今日日报或更新今日日报时,整理当前开发工作的成果、问题发现、改善措施、验证状态和下一步,并生成事实一致的 Markdown 主报告与单文件交互式 HTML。
技能面向任意本地项目。项目缺少日报目录、模块目录或日期目录时,技能只初始化报告归档结构,不创建或修改业务代码目录。
## 名称与安装范围
- 规范技能名:`daily-summary-job`
- 界面显示名:`Daily Summary Job`
- 安装范围:个人技能目录 `$CODEX_HOME/skills/daily-summary-job``CODEX_HOME` 未设置时使用 `~/.codex/skills/daily-summary-job`
- `dailySummary_job` 只作为用户原始名称保留在说明中,不作为目录名或 YAML 名称。
## 按需触发
技能不后台运行,也不自动监听 Agent。以下是意图示例,不是固定口令:
- “记录当前进展”“把刚才的问题加入今日记录”进入检查点模式。
- “生成今日日报”“汇总今天的开发工作”进入生成模式。
- “更新今天的日报”“把刚解决的问题补充进去”进入更新模式。
- 显式使用 `$daily-summary-job` 时最可靠;自然语言明确表达日报、今日问题整理或进展记录意图时也应触发。
## 工作流
```text
当前对话与 Agent 汇报 ─┐
Git 提交、改动与文档 ──┼─→ 结构化事实源 ─→ Markdown 主报告
已有测试与构建结果 ────┘ └→ 交互式 HTML
```
1. 确定项目根目录和项目机器的本地日期;用户可以覆盖日期。
2. 从当前对话、主 Agent 与子 Agent 汇报中提取成果、问题、调查结论、改善和遗留事项。
3. 用当天 Git 提交、未提交改动、设计/计划文档以及已有测试结果交叉核对。
4. 将信息压缩为结构化事实源,并根据稳定问题标识去重。
5. 自动识别单模块、多模块或无法分类的工作范围。
6. 初始化缺失的报告、模块和日期目录。
7. 由同一结构化事实源生成 Markdown 与 HTML,避免两者事实漂移。
8. 更新模式合并新证据并重新生成原有文件对,不重复创建相同主题。
默认不重新运行耗时构建或测试。已有证据不足时标记“待验证”;完全没有有效开发证据时不生成空日报。
## 上下文预算
技能采用渐进式读取:
- `SKILL.md` 只保留核心流程和路由规则。
- 先读取当天检查点索引,再加载相关模块的必要记录。
- 不读取历史日期的日报,除非用户明确要求比较。
- 检查点不复制完整对话或完整日志,只保存结论和证据引用。
- 每次检查点最多记录 5 条成果、5 个问题和 3 个下一步;单条说明尽量不超过 120 个汉字。
- 长日志只记录命令、文件路径、提交号、结果摘要和原始证据位置。
磁盘上的历史文件不会自动进入上下文;只有本次任务选中的文件才会读取。
## 证据模型
每条问题至少包含:
- 稳定标识、标题和所属模块;
- 问题如何被发现、实际现象和正确预期;
- 原因或当前假设、影响范围;
- 已采取的改善、验证结果和下一步;
- 证据引用与证据等级。
证据等级固定为:
| 等级 | 含义 |
| --- | --- |
| 已验证 | 有测试、构建、运行输出或可复核改动支持。 |
| 静态分析 | 可从当前代码和控制流确认,但尚无运行复现。 |
| 对话发现 | Agent 或用户在讨论中提出,尚未完成独立核验。 |
| 待验证风险 | 合理推断,仍需要专门实验或回归。 |
多个 Agent 给出冲突结论时,不擅自合并为单一事实。结构化事实源保留冲突双方、各自证据和待验证动作,报告明确显示“结论冲突”。
## 自动分类与目录初始化
项目根目录优先使用 Git 根;没有 Git 时使用当前工作目录。分类顺序如下:
1. 用户明确指定的模块。
2. 当天改动路径与既有 `dailywork_report/*_rep` 的匹配结果。
3. 代码、测试和文档中占主导的业务目录。
4. 涉及多个独立模块时使用 `cross-module_rep`
5. 无法可靠判断或项目没有代码目录时使用 `general_rep`
既有项目命名优先,例如已有 `pathsmoothing_rep` 时不另建语义重复目录。新模块名只允许安全的小写字母、数字和连字符,再追加 `_rep`
```text
dailywork_report/
├── .daily-summary-job/
│ └── YYYY-MM-DD/
│ └── checkpoints/
│ └── HHmmss-<module>.json
└── <module>_rep/
└── YYYY-MM-DD/
├── NN-<topic>-daily-summary-report.md
└── NN-<topic>-daily-summary-visualization.html
```
- 同日同主题更新原文件对。
- 同日新主题从 `01` 开始递增编号。
- 检查点目录保存精简、机器可读的中间证据;最终日报目录只保留交付文件。
- 所有目录均按需创建;技能不创建任何业务源码目录。
## Markdown 报告
Markdown 使用固定主结构,但允许没有内容的非关键小节省略:
1. 今日结论摘要。
2. 今日完成的工作。
3. 今日发现的问题。
4. 问题如何被发现及证据等级。
5. 已采取的改善和验证结果。
6. 尚未解决的风险与下一步。
7. 变更、测试和资料证据索引。
问题描述采用“发现 → 现象 → 原因/假设 → 影响 → 改善 → 验证 → 下一步”的顺序。不得把未运行的测试写成通过,也不得把候选方案写成已完成修复。
## 交互式 HTML
每份 Markdown 对应一个单文件离线 HTML。HTML 内嵌 CSS、结构化数据和原生 JavaScript,不使用 CDN、网络请求、第三方库或外部图片。
页面信息流为:
```text
今日总览
↓ 选择问题
现象与正确预期对照
↓ 展开因果节点
发现过程 → 证据 → 根因/风险
↓ 切换改善步骤
修改前 → 改善措施 → 修改后
↓ 查看验证门
测试结果 → 遗留风险 → 下一步
```
交互组件包括:
- 成果、问题、验证和待办总览;
- 问题选择器与证据等级筛选;
- “实际发生 / 正确预期”对照;
- 可展开的发现与因果链;
- 改善方案步骤导航和修改前后切换;
- 构建、测试、安全约束等验证门漏斗;
- 按优先级和模块筛选的下一步路线图;
- 键盘导航、移动端布局与 `prefers-reduced-motion` 支持。
没有数值证据时只使用明确标注的概念图,不伪造曲线、比例或指标。HTML 与 Markdown 必须由同一份规范化 JSON 生成。
## 技能组成
```text
daily-summary-job/
├── SKILL.md
├── agents/openai.yaml
├── scripts/prepare_report.py
├── scripts/test_prepare_report.py
├── references/report-schema.md
└── assets/interactive-report-template.html
```
- `SKILL.md`:触发、取证、分类、生成和更新流程。
- `agents/openai.yaml`:显示名、简短说明和默认提示。
- `scripts/prepare_report.py`:安全规范化名称、选择输出路径、生成 Markdown/HTML 并执行一致性校验。
- `scripts/test_prepare_report.py`:使用 Python 标准库验证路径、分类、预算、生成和更新行为。
- `references/report-schema.md`:结构化事实源字段、证据等级和内容约束。
- `assets/interactive-report-template.html`:响应式、无外部依赖的交互页面模板。
## 异常与安全边界
- 目标文件存在且无法确认同一主题时,创建新编号,不覆盖未知内容。
- 更新前校验结构化事实源和目标文件配对关系。
- 生成先写入临时文件并校验,成功后再替换文件对;失败时保留已有有效版本。
- 路径、模块和主题统一安全规范化,拒绝目录穿越。
- HTML 中的所有项目文本进行转义,避免把代码或对话内容解释为页面脚本。
- 技能只整理和生成日报;不修复业务代码、不放宽测试或安全门,也不执行 Git 提交。
## 验证标准
- 技能目录通过 `quick_validate.py`
- 路径脚本覆盖 Git/非 Git、单模块、多模块、无模块、非法名称、同主题更新和连续编号。
- 模拟项目完成一次“记录 → 生成 → 更新”流程。
- Markdown 与 HTML 包含相同问题标识、证据等级、改善和下一步。
- HTML 不包含外部 URL、外部脚本或第三方依赖。
- 交互控件、键盘操作、响应式规则和减少动画规则均存在。
- 全部验证不修改 ParkingRobot 的业务源码,也不暂存或提交任何文件。