Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,37 @@ be productive without rediscovering them.
## Project overview

OCPP DebugKit Studio is a native desktop debugger for OCPP charging sessions,
built in Zig on the Native SDK (native-rendered `.native` markup + a Zig
`Model` / `Msg` / `update` loop — no WebView, no web frontend). It is a fully
independent sibling of the TypeScript `@ocpp-debugkit/toolkit`; the two share a
specification contract, not code (see `docs/adr/0001-independent-implementation.md`).
built in Zig on the Native SDK (native-rendered UI + a Zig `Model` / `Msg` /
`update` loop — no WebView, no web frontend). The inspector view is a hand-written
`canvas.Ui` builder view rather than `.native` markup, because the event timeline
needs the builder-only windowed virtual list (see
`docs/adr/0006-inspector-builder-view.md`). It is a fully independent sibling of
the TypeScript `@ocpp-debugkit/toolkit`; the two share a specification contract,
not code (see `docs/adr/0001-independent-implementation.md`).

## Repository layout

```
studio/
├── app.zon # app manifest: identity, window, view, security policy
├── src/
│ ├── main.zig # Model, Msg, update, app wiring
│ ├── app.native # the view (declarative markup)
│ ├── main.zig # app wiring + CLI-arg trace loading (re-exports the TEA types)
│ ├── ui/ # the inspector: builder view + workspace state
│ │ ├── workspace.zig # Model, Msg, update — the bounded multi-trace workspace
│ │ ├── inspector.zig # the canvas.Ui builder view (ADR-0006)
│ │ └── ui.zig # aggregate root (test discovery)
│ ├── ocpp/ # pure, headless OCPP engine (types, parser, detection, …)
│ │ └── conformance/ # vendored shared-contract fixtures + goldens + harness
│ └── tests.zig # headless view/model tests
│ └── tests.zig # headless view/model integration tests
├── scripts/smoke.sh # portable automation smoke test (Xvfb in CI)
├── docs/adr/ # architecture decision records
└── .github/workflows/ci.yml # CI: verify (macOS+Linux) + Linux Xvfb smoke
```

As the engine lands, source grows under `src/ocpp/` (the pure, headless-testable
engine), `src/ui/` (views), `src/capture/` (live proxy), and `src/cli.zig`
(headless mode). Keep the engine free of UI and platform dependencies.
Source grows under `src/ocpp/` (the pure, headless-testable engine), `src/ui/`
(the inspector view + state), and later `src/capture/` (live proxy) and
`src/cli.zig` (headless mode). Keep the engine free of UI and platform
dependencies — `src/ui/` consumes it through the workspace Model.

## Build commands

Expand All @@ -41,7 +48,7 @@ native dev # build Debug + run, with markup hot reload
native test # run the test suite
native test -Dplatform=null # headless tests (what CI runs)
native build # ReleaseFast binary → zig-out/bin/studio
native check --strict # validate src/*.native + app.zon
native check --strict # validate app.zon (and any .native views)
native doctor --strict # toolchain / environment health
```

Expand All @@ -51,8 +58,9 @@ Prerequisites: Zig `0.16.0` and `@native-sdk/cli` (`npm install -g @native-sdk/c

- **Engine is pure Zig, UI-free.** Everything under `src/ocpp/` must be testable
headlessly with `native test`. The UI consumes it through the Model.
- **TEA discipline.** `.native` views are declarative; all side effects (spawn,
fetch, clipboard, sockets) go through the update-side effects channel.
- **TEA discipline.** The view is a pure function of the model (`ui/inspector.zig`
builds the tree; it never mutates state); all side effects (spawn, fetch,
clipboard, sockets) go through the update-side effects channel.
- **Version-tagged protocol boundary.** The decoder keys off an OCPP version tag
so 2.0.1 can be added later without reworking 1.6J.
- **Conformance over duplication.** Studio mirrors the toolkit's *behavior* via a
Expand Down
24 changes: 17 additions & 7 deletions CURRENT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

## Active milestone

**S2Detection + conformance (0.1.0): complete.** Next up: **S3 — Inspector UI**
(see [ROADMAP.md](ROADMAP.md)).
**S3Inspector UI (0.2.0): in progress.** The engine (S0–S2) is complete; the
native inspector is now being built (see [ROADMAP.md](ROADMAP.md)).

## What's done

Expand Down Expand Up @@ -72,13 +72,23 @@ toolkit's:

## What's in progress

- Nothing in flight — S2 is closed; S3 is next.
**S3 — Inspector UI (0.2.0).** The inspector shell has landed (#27): the
placeholder counter is replaced by a Zig `canvas.Ui` builder view (ADR-0006), a
bounded multi-trace workspace `Model` / `Msg` / `update` (`src/ui/`), and trace
loading from command-line path arguments (read unbounded in `main` via
`init.io`) plus a built-in embedded sample. The window opens on an empty state or
the loaded trace's overview.

Still ahead in S3: the virtualized event timeline (#28), the trusted-ingestion
capacity path for 500k-event / 100 MB traces (#29), the message inspector +
session panel (#30), the failure panel (#31), and search / filter (#32).
Interactive open (native dialog + drag-drop) is deferred to #33 — it needs an
ejected runner (see ADR-0006).

## What's next

**S3 — Inspector UI (0.2.0):** the native inspector — open / drag-drop traces, a
virtualized event timeline, the per-message inspector, the failure panel, and
search / filter, handling traces far larger than a browser tab can hold.
After S3: **S4 — Analysis parity+ (0.3.0)** — reports, anonymize, diff,
wall-clock replay, and a headless CLI mode.

## Known blockers / decisions pending

Expand All @@ -91,7 +101,7 @@ search / filter, handling traces far larger than a browser tab can hold.
| `repo` (tooling, CI) | ✅ done for S0 |
| `docs` (docs, ADRs) | ✅ done for S0 |
| `ocpp` (engine) | ✅ done for S2 |
| `ui` (native views) | ⬜ placeholder (S3) |
| `ui` (native views) | 🚧 shell landed (S3, #27); timeline + panes next |
| `capture` (live proxy) | ⬜ not started (S5) |
| `cli` (headless) | ⬜ not started (S4) |
| `conformance` | ✅ done for S2 (15/15, `contract-v1`) |
6 changes: 4 additions & 2 deletions app.zon
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
.{
.label = "main",
.title = "OCPP DebugKit Studio",
.width = 480,
.height = 320,
.width = 1200,
.height = 800,
.min_width = 900,
.min_height = 560,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
Expand Down
59 changes: 59 additions & 0 deletions docs/adr/0006-inspector-builder-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# ADR-0006 — The inspector is a Zig builder view, not `.native` markup

- **Status:** Accepted
- **Date:** 2026-07-11

## Context

The Native SDK's default and recommended way to author a view is declarative
`.native` markup (`ADR-0003`), with the Model / Msg / update logic in Zig. The
S0 scaffold followed that: `src/app.native` held the placeholder counter view.
Markup buys real advantages — hot reload in dev, `native check` binding
validation against the model contract, and a smaller, parser-free release binary.

S3 builds the inspector, whose centerpiece is the **event timeline** (issue #28).
A trace can hold hundreds of thousands of events, and every canvas view has a
hard budget of **1024 retained widget nodes**. The only primitive that renders a
dataset-scale list within that budget is the **windowed virtual list**
(`ui.virtualWindow` + `ui.virtualList`): the runtime owns the scroll and viewport
math, the model owns the data, and the view builds only the visible window.

Per the native-ui contract, the windowed virtual list is **builder-only**. The
closed markup grammar has no channel for a `for` binding to receive the runtime's
requested index range, so markup can only offer a bounded `<list virtualized>`
that still walks every item on each build — fine for hundreds of rows, not for
the 500 000-event capacity target (M8 / issue #29). Markup and a Zig
`canvas.Ui(Msg)` builder view produce the *identical* widget tree (same
structural ids, same typed handler table); the difference is only which
primitives are reachable.

## Decision

**Author the inspector's main view as a hand-written `canvas.Ui(Msg)` builder
view (`UiApp` `Options.view`), not a `.native` markup file.** `src/app.native` is
removed; `src/ui/inspector.zig` is the view, `src/ui/workspace.zig` the Model /
Msg / update.

## Consequences

- The timeline can use the windowed virtual list from the start, so the view
scales to dataset-size traces within the node budget — the capability the
milestone is defined around — instead of being rebuilt later.
- The full builder surface is available uniformly: `ui.split`, `ui.tree`,
`ui.virtualList`, per-element `ElementOptions`, and dynamic child slices, with
no "not markup-expressible" gaps to work around.
- The view is tested exactly like a markup view would be — build the tree with
`canvas.Ui`, assert on widgets, and drive `msgForPointer` / `msgForKeyboard` —
so headless coverage under `-Dplatform=null` is unchanged (`src/tests.zig`).
- Costs accepted: no markup hot reload for the main view (a data inspector is not
a design-iteration surface), and the `native check` binding-path check does not
apply to Zig view reads — model correctness is carried by the headless view
tests instead. The release binary still needs no markup interpreter.
- Simpler secondary surfaces may still be authored in markup later
(`CompiledMarkupView`) where the virtual list is not needed; this ADR governs
the main inspector view, not a blanket ban on markup.
- Trace loading uses command-line path arguments read in `main` via `init.io`
(unbounded). Interactive open — a native dialog and drag-drop — is deferred
(issue #33): the zero-config `UiApp` exposes no view/effect path to
`showOpenDialog` or file-drop routing, so it would require ejecting the build to
own a custom runner, a standing decision left to its own issue and ADR.
26 changes: 15 additions & 11 deletions scripts/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ set -euo pipefail

BIN=zig-out/bin/studio
AUTO_DIR=.zig-cache/native-sdk-automation
# Launch with the vendored sample as a command-line trace argument, so the smoke
# test exercises the real CLI load path and lands on the loaded overview.
SAMPLE=src/ocpp/testdata/normal-session.json

if [ ! -x "$BIN" ]; then
echo "smoke: $BIN not found — build first: native build -Dautomation=true" >&2
Expand All @@ -28,12 +31,12 @@ export NO_AT_BRIDGE=1
launch() {
if command -v xvfb-run >/dev/null 2>&1; then
if command -v dbus-run-session >/dev/null 2>&1; then
dbus-run-session -- xvfb-run -a "$BIN"
dbus-run-session -- xvfb-run -a "$BIN" "$SAMPLE"
else
xvfb-run -a "$BIN"
xvfb-run -a "$BIN" "$SAMPLE"
fi
else
"$BIN"
"$BIN" "$SAMPLE"
fi
}

Expand All @@ -44,15 +47,16 @@ trap 'kill "$APP_PID" >/dev/null 2>&1 || true' EXIT
# Block until the runtime publishes ready=true (or fail loudly on timeout).
native automate wait --timeout-ms 60000

# The window must exist with the expected widget tree. These come from the
# semantics snapshot (GPU-independent), so they hold under software GL too.
# The window must exist with the loaded trace rendered. The app was launched
# with the sample trace as a command-line argument, so it opens straight on the
# overview — proving the CLI load → parse → render path works end to end. These
# assertions read the semantics snapshot (GPU-independent), so they hold under
# software GL too.
native automate assert --timeout-ms 30000 \
'ready=true' \
'role=button name="Reset"' \
'role=button name="\+"' \
'role=button name="-"' \
'OCPP DebugKit Studio' \
'count: 0'
'normal-session.json' \
'22 events' \
'OCPP DebugKit Studio'

# No runtime/dispatch errors surfaced during startup.
native automate assert --absent 'error event='
Expand All @@ -61,4 +65,4 @@ native automate assert --absent 'error event='
native automate screenshot main-canvas
test -s "$AUTO_DIR/screenshot-main-canvas.png"

echo "smoke: ok — window rendered and automation drove the widget tree"
echo "smoke: ok — window rendered and automation drove the inspector"
15 changes: 0 additions & 15 deletions src/app.native

This file was deleted.

Loading
Loading