Files
ParkingRobot/docs/superpowers/plans/2026-08-05-em-observation-diagnostics.md
T

335 lines
13 KiB
Markdown

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