# Trajectory Planning Visualization Library 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:** Build a reusable `netstandard2.0` library that publishes immutable planning snapshots through a bounded, non-blocking loopback HTTP/SSE service and renders a self-contained Chinese scientific-style dashboard.

**Architecture:** The library owns generic visualization contracts, an atomic latest-frame exchange, bounded cycle summaries, a minimal `TcpListener` HTTP/1.1 server, embedded web assets, and one `PlanningVisualizationSession` facade. It has no dependency on EMPlanner, MDCS, MovementTest, ClumsyCore, Painter, ASP.NET Core, Node.js, or an external CDN.

**Tech Stack:** C# 10, `netstandard2.0`, `TcpListener`, Server-Sent Events, Newtonsoft.Json 13.0.4, embedded HTML/CSS/JavaScript, a `net10.0-windows` console verification host.

## Global Constraints

- Bind only `IPAddress.Loopback`; never bind `IPAddress.Any`, `0.0.0.0`, a LAN address, or a hostname prefix.
- Accept only HTTP `GET`; cap request headers at `16 KiB` and request reads at `2 s`.
- Require the per-session random token on the page, assets, bootstrap, and event endpoints.
- `Publish` may only perform validation plus `Interlocked.Exchange`; it must not serialize, write a socket, wait for a client, or mutate caller-owned collections.
- Latest dynamic-frame capacity is exactly `1`; full trajectory data exists only in that latest frame.
- Cycle history is bounded by `HistoryCycleLimit` and stores summaries only.
- Default refresh rate is `10 Hz`, default history limit is `60`, default port is `0`, and maximum clients is fixed at `2`.
- Web assets are embedded and self-contained; no CDN, package manager, web build, or runtime file lookup.
- Chinese is used for descriptions and state text; axis variables, SI units, and mathematical symbols retain scientific notation.
- Preserve all unrelated dirty-worktree changes and stage only files named by each task.

---

## File Structure

| File | Responsibility |
| --- | --- |
| `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj` | Standalone class library, Newtonsoft dependency, and (from Task 4 onward) embedded assets. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationPrimitives.cs` | Points, poses, bounds, key/value data and defensive collection-copy helpers. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs` | World polylines, markers, segments and static world snapshot. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs` | Charts, axes, series and line-style contracts. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/PlanningVisualizationSnapshots.cs` | Static snapshot, dynamic snapshot, cycle summary and session status. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/PlanningVisualizationOptions.cs` | Validated transport and retention options. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LatestVisualizationFrameStore.cs` | Atomic capacity-one dynamic-frame exchange. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/BoundedCycleHistory.cs` | Service-side deduplicated cycle-summary ring. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/VisualizationJson.cs` | Stable camel-case invariant JSON serialization. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackHttpRequestReader.cs` | Bounded GET request parser. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/SseClientConnection.cs` | Per-client capacity-one outbound payload and bounded writer. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs` | TCP accept loop, route authorization and SSE clients. |
| `ClumsyPilot/TrajectoryPlanningVisualization/PlanningVisualizationSession.cs` | Public `Start/Publish/Stop` facade and fault isolation. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html` | Chinese dashboard structure. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css` | Thin-line scientific visual system. |
| `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js` | Snapshot consumption, SVG charts, tabs and stale-state handling. |
| `ClumsyPilot/TrajectoryPlanningVisualization/README.md` | Public API, endpoints, resource guarantees and integration example. |
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj` | Independent executable verification host. |
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs` | Adds each check group as its task lands and provides the final timed smoke mode. |
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs` | Deterministic synthetic dashboard sample for browser smoke testing. |
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs` | Minimal assertion helpers. |
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs` | Contract immutability and validation checks. |
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs` | Latest-frame, history and JSON checks. |
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs` | Real loopback HTTP/SSE lifecycle checks. |
| `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs` | Embedded Chinese scientific dashboard checks. |
| `ClumsyPilot/ClumsyPilot.csproj` | Excludes the new library/test sources from default recursive compilation and references the library project. |

---

### Task 1: Standalone project and immutable visualization contracts

**Files:**

- Create: `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationPrimitives.cs`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationGeometry.cs`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/VisualizationCharts.cs`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Contracts/PlanningVisualizationSnapshots.cs`
- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj`
- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs`
- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Verification.cs`
- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs`
- Modify: `ClumsyPilot/ClumsyPilot.csproj`

**Interfaces:**

- Produces: `VisualizationPoint`, `VisualizationPose`, `VisualizationBounds`, `VisualizationValue`, `VisualizationConfigurationGroup`, `VisualizationPolyline`, `VisualizationMarker`, `VisualizationDirectionSegment`, `VisualizationOccupancyGrid`, `VisualizationChart`, `VisualizationSeries`, `PlanningVisualizationStaticSnapshot`, `PlanningVisualizationDynamicSnapshot`, and `VisualizationCycleSummary`.
- Collection-bearing constructors must reject null elements and copy source collections into `ReadOnlyCollection<T>`.
- Numeric constructors must reject NaN/infinity. Timestamps must be carried as `DateTimeOffset`.

- [ ] **Step 1: Create the independent verification host and failing contract checks**

Add a host that initially calls only the contract group and returns `1` on exceptions:

```csharp
internal static class Program
{
    private static int Main()
    {
        try
        {
            ContractChecks.Run();
            Console.WriteLine("PASS trajectory-planning-visualization");
            return 0;
        }
        catch (Exception exception)
        {
            Console.Error.WriteLine(exception);
            return 1;
        }
    }
}
```

Start `ContractChecks.Run()` with a defensive-copy test:

```csharp
var points = new List<VisualizationPoint> { new VisualizationPoint(1d, 2d) };
var series = new VisualizationSeries("current", "当前轨迹", VisualizationLineStyle.Solid, points);
points[0] = new VisualizationPoint(9d, 9d);
Verification.NearlyEqual(1d, series.Points[0].X, "series copies points");
Verification.Throws<ArgumentOutOfRangeException>(
    () => new VisualizationPoint(double.NaN, 0d), "point rejects NaN");

byte[] bits = { 0x01 };
var grid = new VisualizationOccupancyGrid(new VisualizationBounds(0d, 1d, 0d, 1d),
    0.5d, rows: 2, columns: 2, bits);
bits[0] = 0x00;
Verification.Equal("AQ==", grid.OccupancyBitsBase64, "occupancy grid copies compact bits");
```

The verification host project must reference only the standalone visualization library:

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <LangVersion>10</LangVersion>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\..\TrajectoryPlanningVisualization\TrajectoryPlanningVisualization.csproj" />
  </ItemGroup>
</Project>
```

- [ ] **Step 2: Run the host to verify RED**

Run:

```powershell
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj
```

Expected: compilation fails because `TrajectoryPlanningVisualization` contracts do not exist.

- [ ] **Step 3: Add project boundaries and contract implementations**

The library project must contain:

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>10</LangVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
  </ItemGroup>
</Project>
```

Update the main project so nested project/test sources are not compiled twice:

```xml
<Compile Remove="TrajectoryPlanningVisualization\**\*.cs" />
<Compile Remove="tests\TrajectoryPlanningVisualizationVerificationHost\**\*.cs" />
<ProjectReference Include="TrajectoryPlanningVisualization\TrajectoryPlanningVisualization.csproj" />
```

Define the stable chart contract exactly as follows:

```csharp
public enum VisualizationLineStyle { Solid, Dashed, Limit }

public sealed class VisualizationSeries
{
    public VisualizationSeries(string id, string legend, VisualizationLineStyle lineStyle,
        IReadOnlyList<VisualizationPoint> points);
    public string Id { get; }
    public string Legend { get; }
    public VisualizationLineStyle LineStyle { get; }
    public IReadOnlyList<VisualizationPoint> Points { get; }
}

public sealed class VisualizationChart
{
    public VisualizationChart(string id, string chineseTitle, string xAxisLabel, string yAxisLabel,
        IReadOnlyList<VisualizationSeries> series, string noteChinese = "");
    public string Id { get; }
    public string ChineseTitle { get; }
    public string XAxisLabel { get; }
    public string YAxisLabel { get; }
    public IReadOnlyList<VisualizationSeries> Series { get; }
    public string NoteChinese { get; }
}
```

The two top-level snapshots must expose no setters:

```csharp
public sealed class PlanningVisualizationStaticSnapshot
{
    public PlanningVisualizationStaticSnapshot(string sessionNameChinese, VisualizationBounds worldBounds,
        VisualizationOccupancyGrid occupancyGrid,
        IReadOnlyList<VisualizationPolyline> staticPolylines,
        IReadOnlyList<VisualizationMarker> staticMarkers,
        IReadOnlyList<VisualizationDirectionSegment> directionSegments,
        IReadOnlyList<VisualizationConfigurationGroup> configurationGroups);
    public string SessionNameChinese { get; }
    public VisualizationBounds WorldBounds { get; }
    public VisualizationOccupancyGrid OccupancyGrid { get; }
    public IReadOnlyList<VisualizationPolyline> StaticPolylines { get; }
    public IReadOnlyList<VisualizationMarker> StaticMarkers { get; }
    public IReadOnlyList<VisualizationDirectionSegment> DirectionSegments { get; }
    public IReadOnlyList<VisualizationConfigurationGroup> ConfigurationGroups { get; }
}

public sealed class PlanningVisualizationDynamicSnapshot
{
    public PlanningVisualizationDynamicSnapshot(long sequence, DateTimeOffset observedAtUtc,
        string sessionStateChinese, int activeSegmentIndex, string activeDirection,
        VisualizationPose vehiclePose, IReadOnlyList<VisualizationPolyline> dynamicPolylines,
        IReadOnlyList<VisualizationMarker> dynamicMarkers, IReadOnlyList<VisualizationChart> charts,
        IReadOnlyList<VisualizationValue> statusValues, VisualizationCycleSummary cycleSummary);
    public long Sequence { get; }
    public DateTimeOffset ObservedAtUtc { get; }
    public string SessionStateChinese { get; }
    public int ActiveSegmentIndex { get; }
    public string ActiveDirection { get; }
    public VisualizationPose VehiclePose { get; }
    public IReadOnlyList<VisualizationPolyline> DynamicPolylines { get; }
    public IReadOnlyList<VisualizationMarker> DynamicMarkers { get; }
    public IReadOnlyList<VisualizationChart> Charts { get; }
    public IReadOnlyList<VisualizationValue> StatusValues { get; }
    public VisualizationCycleSummary CycleSummary { get; }
}
```

Use these exact primitive/geometry fields so later adapters do not invent parallel DTOs:

```text
VisualizationPoint: X, Y
VisualizationPose: X, Y, HeadingRadians
VisualizationBounds: XMin, XMax, YMin, YMax
VisualizationValue: ChineseName, RawName, Value, Unit, Severity
VisualizationConfigurationGroup: ChineseTitle, Entries
VisualizationPolyline: Id, LegendChinese, Kind, LineStyle, Points
VisualizationMarker: Id, Kind, LabelChinese, Position
VisualizationDirectionSegment: SegmentIndex, Direction, StartsAtGearSwitch, EndsAtGearSwitch, Points
VisualizationOccupancyGrid: Bounds, ResolutionMeters, Rows, Columns, OccupancyBitsBase64
VisualizationCycleSummary: CycleVersion, OccurredAtUtc, Status, Published,
    PlanningElapsedMilliseconds, SegmentIndex, Direction, LongitudinalMode,
    TerminalType, TerminalVelocity, TerminalAcceleration, FailureReason
```

`TerminalVelocity` and `TerminalAcceleration` are nullable doubles; all other numeric fields are non-null. `Severity` is one of `normal`, `notice`, or `failure`, validated as an exact string so the generic library does not depend on an EM enum.

`VisualizationOccupancyGrid` accepts a row-major bit array of exactly `ceil(Rows * Columns / 8)` bytes, makes a defensive copy, and exposes it as Base64 JSON through `OccupancyBitsBase64`. Bit index `row * Columns + column` uses the least-significant bit first within each byte. A null occupancy grid is allowed for non-map visualizations.

- [ ] **Step 4: Run the focused contract host GREEN**

Run:

```powershell
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj
dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true
```

Expected: both exit `0`; the host prints `PASS trajectory-planning-visualization` and the main build has no duplicate-type errors.

- [ ] **Step 5: Commit the project and contracts**

```powershell
git add -- ClumsyPilot/TrajectoryPlanningVisualization ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost ClumsyPilot/ClumsyPilot.csproj
git commit -m "feat: add planning visualization contracts"
```

---

### Task 2: Capacity-one frame exchange, bounded history, and JSON

**Files:**

- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/PlanningVisualizationOptions.cs`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LatestVisualizationFrameStore.cs`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/BoundedCycleHistory.cs`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/VisualizationJson.cs`
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs`
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs`

**Interfaces:**

- Produces: `PlanningVisualizationOptions.CreateValidatedSnapshot()`, `LatestVisualizationFrameStore.Publish(...)`, `TryReadAfter(...)`, `BoundedCycleHistory.Add(...)`, `Snapshot()`, and `VisualizationJson.Serialize(...)`.
- `VisualizationFrame` contains a monotonically increasing store version and one dynamic snapshot.

- [ ] **Step 1: Write failing store, history, and JSON checks**

Create `RuntimeChecks`, add `RuntimeChecks.Run()` immediately after `ContractChecks.Run()` in `Program`, and add these behaviors:

```csharp
var store = new LatestVisualizationFrameStore();
store.Publish(Snap(1));
store.Publish(Snap(2));
Verification.True(store.TryReadAfter(0, out VisualizationFrame frame), "latest frame exists");
Verification.Equal(2L, frame.Snapshot.Sequence, "latest frame replaces old frame");
Verification.True(!store.TryReadAfter(frame.Version, out _), "same frame is not replayed");

var history = new BoundedCycleHistory(2);
history.Add(Cycle(1)); history.Add(Cycle(2)); history.Add(Cycle(3)); history.Add(Cycle(3));
Verification.Equal("2|3", string.Join("|", history.Snapshot().Select(x => x.CycleVersion)),
    "history is bounded and deduplicated");

string json = VisualizationJson.Serialize(Snap(2));
Verification.True(json.Contains("\"sessionStateChinese\"") && json.Contains("\"observedAtUtc\""),
    "JSON uses stable camel case names");
```

Also verify invalid options: negative port, refresh rate `0`, history `0`, and port `65536` throw.

- [ ] **Step 2: Run to verify RED**

Run the visualization host. Expected: compilation fails because runtime types do not exist.

- [ ] **Step 3: Implement the non-blocking stores and options**

`LatestVisualizationFrameStore.Publish` must contain no lock or callback:

```csharp
public void Publish(PlanningVisualizationDynamicSnapshot snapshot)
{
    if (snapshot == null) throw new ArgumentNullException(nameof(snapshot));
    long version = Interlocked.Increment(ref nextVersion);
    Interlocked.Exchange(ref latest, new VisualizationFrame(version, snapshot));
}
```

`TryReadAfter` reads with `Volatile.Read`, and the history uses a private lock only on the service-consumer thread. `BoundedCycleHistory.Add` ignores a repeated `CycleVersion`, removes from the head while count exceeds capacity, and `Snapshot` returns a fresh read-only copy.

Use Newtonsoft settings with `CamelCasePropertyNamesContractResolver`, `InvariantCulture`, `DateFormatHandling.IsoDateFormat`, and `Formatting.None`.

- [ ] **Step 4: Run to verify GREEN**

Run the visualization host twice. Expected: both runs print the PASS line and never depend on working-directory files.

- [ ] **Step 5: Commit runtime data handling**

```powershell
git add -- ClumsyPilot/TrajectoryPlanningVisualization/Runtime ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/RuntimeChecks.cs
git commit -m "feat: add bounded visualization snapshots"
```

---

### Task 3: Restricted loopback HTTP/SSE server and public facade

**Files:**

- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackHttpRequestReader.cs`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/SseClientConnection.cs`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/PlanningVisualizationSession.cs`
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs`
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs`

**Interfaces:**

- Produces: `PlanningVisualizationSession.Start(PlanningVisualizationStaticSnapshot)`, `Publish(PlanningVisualizationDynamicSnapshot)`, `Stop()`, `IsRunning`, and `SessionInfo.Uri`.
- This task produces `/api/bootstrap` and `/api/events`; Task 4 adds `/`, `/app.css`, and `/app.js`. Every available route requires `token=<session-token>`.
- Statuses: bad token `403`, unknown route `404`, non-GET `405`, oversized/malformed request `400`, third live SSE client `503`.

- [ ] **Step 1: Write failing real-socket checks**

Create `ServerChecks`, call it after `RuntimeChecks`, and use `TcpClient` rather than mocks:

```csharp
using (var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0 }))
{
    PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot());
    Verification.Equal("127.0.0.1", info.Uri.Host, "server binds loopback");
    Verification.Equal(403, SendStatus(info.Uri.Port, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"),
        "missing token is forbidden");
    Verification.Equal(405, SendStatus(info.Uri.Port,
        "POST /?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\n\r\n"),
        "POST is rejected");
    Verification.Equal(200, SendStatus(info.Uri.Port,
        "GET /api/bootstrap?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\n\r\n"),
        "authorized bootstrap succeeds");
    session.Stop();
}
Verification.True(CanBindReleasedPort(port), "stop releases port");
```

Also send an unknown authorized route, a malformed request line, and a header larger than `16 KiB`; assert `404`, `400`, and `400`. Hold two authorized SSE connections open and assert a third receives `503`. Read one normal JSON response as bytes and assert its `Content-Length` equals the UTF-8 body length.

Add a slow SSE client that stops reading, publish 10,000 tiny snapshots on a task, and assert the publish task completes within one second. The assertion is about `Publish`, not socket delivery.

Before connecting any SSE client, publish one distinct cycle summary, wait two refresh periods for the dispatcher, and repeat for three summaries. Then connect and assert the first event contains all three summaries. This respects the capacity-one latest-frame contract while proving history collection is service-owned and does not depend on an open browser.

- [ ] **Step 2: Run to verify RED**

Run the visualization host. Expected: compilation fails because the server and facade do not exist.

- [ ] **Step 3: Implement bounded request parsing and routing**

Use `TcpListener(IPAddress.Loopback, validated.Port)`. For port `0`, read the assigned port from `LocalEndpoint`. The request reader must:

```csharp
const int MaximumHeaderBytes = 16 * 1024;
static readonly TimeSpan RequestReadTimeout = TimeSpan.FromSeconds(2d);
```

Read until `\r\n\r\n`, reject a buffer that reaches the cap, split the first line into exactly method/target/version, allow only origin-form paths, and URL-decode only the `token` query value. Never accept a caller-provided filesystem path.

The server owns one dispatcher task in addition to the accept loop. At `1 / RefreshRateHz`, the dispatcher reads `LatestVisualizationFrameStore.TryReadAfter`, appends a new non-null cycle summary to `BoundedCycleHistory` even when no browser is connected, and serializes only when at least one SSE client exists. It offers the resulting payload to each client's capacity-one outbound slot with `Interlocked.Exchange`.

Each `SseClientConnection` owns its socket writer. A slow socket can block only its own writer; newer dispatcher payloads overwrite that client's unsent slot. Apply a one-second write timeout and disconnect the client on timeout. The writer emits:

```text
event: frame
data: {camelCase JSON containing snapshot and bounded history}

```

The SSE response uses `Content-Type: text/event-stream; charset=utf-8`, `Cache-Control: no-cache`, and `Connection: keep-alive`. Normal JSON responses use `application/json; charset=utf-8`; HTML/CSS/JavaScript use explicit UTF-8 content types and byte-accurate `Content-Length`.

All socket exceptions are caught inside the client task. A server-fatal exception sets `FaultReason`, cancels the server, and never escapes through `Publish`.

On normal `Stop`, offer one `event: end` payload with Chinese reason `会话已结束` to every client, allow at most `100 ms` for best-effort flush, then close sockets and the listener. Shutdown must never wait indefinitely for a browser.

- [ ] **Step 4: Implement idempotent facade lifecycle**

`PlanningVisualizationSession.Start` validates options and creates a 256-bit token using APIs available in `netstandard2.0`:

```csharp
byte[] tokenBytes = new byte[32];
using (RandomNumberGenerator random = RandomNumberGenerator.Create())
    random.GetBytes(tokenBytes);
string token = BitConverter.ToString(tokenBytes).Replace("-", string.Empty).ToLowerInvariant();
```

It then starts the server and returns:

```csharp
new PlanningVisualizationSessionInfo(
    new Uri("http://127.0.0.1:" + port + "/?token=" + token), token);
```

`Stop` swaps the server field to null under a lifecycle lock, then cancels and disposes outside the lock. `Dispose` calls `Stop`. Calling `Publish` before start or after stop is a no-op; passing null still throws.

- [ ] **Step 5: Run server checks GREEN**

Run:

```powershell
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj
```

Expected: exit `0`, PASS line, loopback ports released, no unobserved task exception.

- [ ] **Step 6: Commit the transport**

```powershell
git add -- ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackHttpRequestReader.cs ClumsyPilot/TrajectoryPlanningVisualization/Runtime/SseClientConnection.cs ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs ClumsyPilot/TrajectoryPlanningVisualization/PlanningVisualizationSession.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ServerChecks.cs
git commit -m "feat: serve planning snapshots on loopback"
```

---

### Task 4: Embedded Chinese scientific dashboard

**Files:**

- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Web/index.html`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js`
- Create: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/EmbeddedWebAssets.cs`
- Create: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs`
- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj`
- Modify: `ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs`
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs`
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs`

**Interfaces:**

- Produces: four tabs with stable IDs `overview`, `ls-st`, `kinematics`, `history-config`.
- Required chart IDs: `ls`, `st`, `curvature-s`, `curvature-t`, `velocity-t`, `acceleration-t`, `jerk-t`, and `yaw-rate-t`.
- The client gets `/api/bootstrap` once and consumes `/api/events` through `EventSource`.

- [ ] **Step 1: Write failing embedded-resource and content checks**

Create `WebAssetChecks`, call it after `ServerChecks`, read resources through `EmbeddedWebAssets`, and assert:

```csharp
Verification.Contains(html, "路径总览", "overview Chinese title");
Verification.Contains(html, "LS / ST", "LS/ST tab");
Verification.Contains(html, "曲率与运动学", "kinematics tab");
Verification.Contains(html, "周期历史", "history tab");
Verification.Contains(html, "生效配置", "configuration panel");
Verification.Contains(css, "--current-trajectory: #1769aa", "scientific current color");
Verification.Contains(css, "stroke-width: 1.1", "thin scientific line");
Verification.Contains(js, "末点后无时间区间", "jerk terminal explanation");
Verification.Contains(html, "occupancy-grid", "single occupancy canvas exists");
Verification.Contains(html, "world-overlay", "SVG trajectory overlay exists");
Verification.Contains(js, "atob", "compact occupancy bitset is decoded in browser");
Verification.True(!html.Contains("http://") && !html.Contains("https://"), "page has no CDN URL");
```

- [ ] **Step 2: Run to verify RED**

Run the visualization host. Expected: failure because resources are absent.

- [ ] **Step 3: Implement page structure and scientific CSS**

Add the three web files to the library as embedded resources:

```xml
<ItemGroup>
  <EmbeddedResource Include="Web\index.html" LogicalName="TrajectoryPlanningVisualization.Web.index.html" />
  <EmbeddedResource Include="Web\app.css" LogicalName="TrajectoryPlanningVisualization.Web.app.css" />
  <EmbeddedResource Include="Web\app.js" LogicalName="TrajectoryPlanningVisualization.Web.app.js" />
</ItemGroup>
```

The page must include:

```html
<header><h1>EM 轨迹规划观察台</h1><div id="live-state">等待数据</div></header>
<nav aria-label="图表页签">
  <button data-tab="overview">路径总览</button>
  <button data-tab="ls-st">LS / ST</button>
  <button data-tab="kinematics">曲率与运动学</button>
  <button data-tab="history-config">周期历史与生效配置</button>
</nav>
<main>
  <section id="overview">
    <div class="world-stack">
      <canvas id="occupancy-grid" aria-label="占用栅格底图"></canvas>
      <svg id="world-overlay" role="img" aria-label="全局路径与当前规划段"></svg>
    </div>
  </section>
  <section id="ls-st" hidden></section>
  <section id="kinematics" hidden></section>
  <section id="history-config" hidden></section>
</main>
```

The HTML references `/app.css?token=__SESSION_TOKEN__` and `/app.js?token=__SESSION_TOKEN__`. When serving only `index.html`, replace that exact placeholder with the lowercase hexadecimal session token; the token alphabet requires no HTML escaping. `app.js` reads the token from `window.location.search` and appends `encodeURIComponent(token)` to `/api/bootstrap` and `/api/events`. This keeps every route authorized without cookies or custom EventSource headers.

CSS uses a white canvas, `#20252b` text, `#d9dde1` grid, `#1769aa` current line, `#8d959d` previous dashed line, `#d87918` handoff/gear marker, and `#b42318` only for failures/limits. SVG data lines are `1.1px`; axes are `0.8px`; grids are `0.55px`.

- [ ] **Step 4: Implement SVG rendering and stale-state behavior**

`app.js` must:

- preserve equal X/Y scale in the overhead plot;
- decode the row-major occupancy bitset once and draw occupied cells on one Canvas below the SVG overlay, without one DOM node per grid cell;
- derive plot bounds from finite values and add a 5% pad;
- render series as SVG paths without thick strokes or point-per-sample DOM nodes;
- label axes exactly from snapshot `xAxisLabel/yAxisLabel`;
- render configuration as Chinese name, raw field, invariant value, unit;
- render the active segment and current horizon above faded global segments;
- show “末点后无时间区间” beside `jerk-t` rather than adding a final sample;
- show “数据已过期” when no frame arrives for `2 / refreshRateHz` seconds;
- show “会话已结束” only after the explicit terminal `event: end`; on an ordinary `EventSource` error show “连接中断，正在重连” while the independent stale timer may show “数据已过期”;
- install `window.onerror` and `unhandledrejection` handlers that show “页面绘图异常” and stop only that tab's redraw loop;
- never evaluate HTML from snapshot strings; use `textContent` for labels and values.

- [ ] **Step 5: Run GREEN and build from a non-repository working directory**

Run the host from repository root, then:

```powershell
$visualizationHost = (Resolve-Path 'ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj').Path
Push-Location $env:TEMP
try {
  dotnet run --project $visualizationHost
} finally { Pop-Location }
```

Expected: both invocations print PASS, proving assets come from the assembly rather than current directory.

- [ ] **Step 6: Add a deterministic timed smoke mode**

Add `SampleSnapshotFactory` that creates two direction segments, a gear marker, effective configuration groups and all required charts without referencing EMPlanner. Extend `Program` so no arguments runs checks, while `--smoke-seconds N` starts a session, publishes the sample at 10 Hz for exactly `N` seconds, prints the full URI once, then stops.

```csharp
if (args.Length == 2 && args[0] == "--smoke-seconds" &&
    int.TryParse(args[1], out int seconds) && seconds > 0)
    return RunSmoke(seconds);
```

Verify with:

```powershell
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj -- --smoke-seconds 1
```

Expected: exit `0`, one `http://127.0.0.1:<port>/?token=<token>` line, and clean shutdown after one second.

- [ ] **Step 7: Commit the dashboard**

```powershell
git add -- ClumsyPilot/TrajectoryPlanningVisualization/Web ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj ClumsyPilot/TrajectoryPlanningVisualization/Runtime/EmbeddedWebAssets.cs ClumsyPilot/TrajectoryPlanningVisualization/Runtime/LoopbackVisualizationServer.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/Program.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs
git commit -m "feat: add scientific planning dashboard"
```

---

### Task 5: Library documentation and final standalone verification

**Files:**

- Create: `ClumsyPilot/TrajectoryPlanningVisualization/README.md`
- Modify: `ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs`

**Interfaces:**

- Documents the exact facade, route/security boundary, performance model, known `localhost` scope, chart conventions, and adapter example.

- [ ] **Step 1: Add a failing documentation check**

Assert the README contains `OBSERVE_ONLY`, `127.0.0.1`, `capacity 1`, `60`, `10 Hz`, `j[i]`, `末点后无时间区间`, and the exact `Start/Publish/Stop` example.

- [ ] **Step 2: Run to verify RED**

Expected: host fails because README is absent.

- [ ] **Step 3: Write the README**

Include this minimal use sequence:

```csharp
using var visualization = new PlanningVisualizationSession(
    new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 10d, HistoryCycleLimit = 60 });
PlanningVisualizationSessionInfo info = visualization.Start(staticSnapshot);
visualization.Publish(dynamicSnapshot);
visualization.Stop();
```

Explicitly state that callers build all domain-specific charts, the server is read-only, `Publish` drops old frames, and no browser event can flow back into planning.

- [ ] **Step 4: Run complete standalone verification**

```powershell
dotnet run --project ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/TrajectoryPlanningVisualizationVerificationHost.csproj
dotnet build ClumsyPilot/TrajectoryPlanningVisualization/TrajectoryPlanningVisualization.csproj
dotnet build ClumsyPilot/ClumsyPilot.csproj -p:ExcludeLegacyAutoAvoidance=true
git diff --check
```

Expected: all commands exit `0`; host prints one PASS line; builds report zero errors; `git diff --check` reports no whitespace errors in task files.

- [ ] **Step 5: Commit documentation**

```powershell
git add -- ClumsyPilot/TrajectoryPlanningVisualization/README.md ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/ContractChecks.cs
git commit -m "docs: document planning visualization library"
```

Plan 1 is complete when this standalone verification is green. Do not begin EM-specific data conversion before this boundary is reviewed.
