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,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"
```