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

