# 固定粗路径案例使用实时 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) +
                  " mm，Y=" + 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"
  ```
