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

13 KiB

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:

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:

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:

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
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:

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:

Verification.True(finalTick.PlanningCompleted, "observer completion tick reports cycle completion");

In VerifiesObservationSourceUsesRequiredOperatorText, add:

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:

bool planningStarted, bool planningCompleted

public bool PlanningStarted { get; }

public bool PlanningCompleted { get; }

In TrajectoryObservationLoop.Tick, use:

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:

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:

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
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:

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:

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:

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:

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:

new TrajectoryObservationDiagnostic("bootstrap failed: " + bootstrap.FailureReason)
  • Step 4: Verify focused test, build, and diff

Run:

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