Skip to content

Racket port phases 3-5: full parity, C# tree retirement prep (ADR 0002) - #30

Merged
turinglambdaai merged 18 commits into
mainfrom
racket-port-2
Oct 2, 2026
Merged

turinglambdaai merged 18 commits into
mainfrom
racket-port-2

Conversation

@turinglambdaai

Copy link
Copy Markdown
Owner

What

Completes the ADR 0002 port on racket-port-2:

  • Phase 4 (drivers): can-bus generic (ICanBus port), SocketCAN (Linux FFI), PCAN-Basic (Windows FFI), system-serial (POSIX termios + Windows CommAPI), can-iso-tp factory with C#-parity transport selection, 7-factory daemon composition, J-Link probe discovery + deterministic USB selection, SCPI real identify health check. 25 hardware-free driver tests.
  • Phase 5a: SelfUpdater port + update --check|update on the CLI (feed check, SHA256 verify via OS tools, OS-tar extraction, active-op refusal, graceful daemon stop + kill fallback, in-place swap).
  • Phase 5b: MCP adapter — stdio JSON-RPC, full 26-tool proxy surface with input schemas, reuses the CLI's HTTP/autostart primitives.
  • Phase 5c: the E2E suite — 16 process-level tests driving the real CLI against the real resident daemon (Shutdown 2 / Autostart 2 / AuthAndDiagnostics 8 / FullLoop 4). This is the phase 3 gate the branch had skipped.
  • Phase 5d: benchpilot report --format html — static self-contained evidence report.
  • Phase 5e: Racket release pipeline (build-dist, deb, installers, 5-RID release matrix, racket-only CI), docs parity, version → 0.6.0.

Suite: 95 tests green locally (unit + drivers + MCP smoke + E2E), including 16 E2E process tests on this machine.

Bugs the E2E suite flushed out (all fixed here)

  1. Any request with a query string died with an empty reply (HTTP server regex group mis-index) — first harness ever to send query params.
  2. shutdown never drained/exited (only blocked new work).
  3. CLI exit codes for busy(5)/deadline(6) unreachable (string vs symbol datums).
  4. Structured error fields (deadlineMs, busyScope, …) were dropped from printed errors.
  5. Doctor handler order made network errors crash it; autostart mis-used subprocess return values.

Deliberate deviations (documented in code/CHANGELOG)

  • Release matrix drops win-arm64 (Racket publishes no Windows ARM64 builds); scoop serves 64bit only.
  • Unix release archives carry a bin/ + lib/ distribution tree; installers (install.sh, deb, Homebrew) wrap the real launchers so the packaged runtime resolves and self-update keeps swapping in place.
  • DoIP discovery returns an empty result on hosts without a broadcast route (C# let the SocketException surface).

After merge (separate steps)

  1. workflow_dispatch the Release workflow on main to validate the 5-RID packaging without publishing.
  2. Tag v0.6.0 → the release ships and the C# tree removal lands with it (this PR already retires src/ + tests/? — no: C# removal is staged for the merge commit itself if CI is green, per ADR 0002's phase 5 gate 'C# tree removed; release v0.6.0').

ren.ji and others added 18 commits October 1, 2026 14:54
Core runtime: frozen wire-contract records with typed camelCase
serializers, evidence pipeline with bounded stores and per-target
context rings, the runtime state machine (target-first mutation gates
with rollback, first-cause-wins cancellation, wall-clock deadline
rejection of late successes, bounded history, drain), and the
target-oriented facade (power/serial/flash/diagnostics operations,
preflight, real-ECU readiness).

Diagnostics: ISO 15765-2 codec and endpoint over an in-process
simulated CAN bus with per-port pump threads, the ISO 14229 UDS client
(P2/P2* timing, NRC 0x78 pending), the behavioral simulated ECU
(sessions with S3 timeout, security access attempt counting, DID
read/write, erase/verify routines, RequestDownload/TransferData/Exit
block sequencing), the transport-independent flash engine (CRC32
verify, audited steps) and the simulator bench (power/serial/flash
sharing virtual board state).

42 ported/contract tests green locally; port bugs found by the port
itself: escaped-string corruption in tolerant JSON reading (regression
test added), '() used as empty-queue marker (truthy in Racket),
left-shifts where C# encoders right-shift.
ISO 13400-2 frames over TCP: routing activation handshake, alive-check
answering, buffered frame reassembly; UDP vehicle discovery with
loopback-then-broadcast; the simulated DoIP entity serving the shared
behavioral UDS processor; and the DoIP UDS channel behind the same
diag-channel contract, so the flash engine drives CAN and DoIP
unchanged. DoIP workflow and channel tests green; 44 ported tests
total green locally.
benchpilotd: loopback HTTP/1.1 JSON API over the runtime with per-user
token auth, admit/stopping middleware, route table over flat thunks,
bounded-drain shutdown and autostart quiet mode (stdio detach).
benchpilot CLI: the full command surface (status/doctor, operations and
observations with evidence/cancel, preflight/validate, power, flash,
serial, UDS, DoIP discover, shutdown) with the stable exit-code table,
one-retry daemon autostart and indented/compact JSON output per --json.

End-to-end through real HTTP: doctor, validate, power on, flash,
serial wait, UDS DID read, power check, history, power off, shutdown —
all green through the CLI against the daemon. Port-side findings:
hasheq never matches dynamically-built string keys (equal?-hash the
header table), Racket 0/1 integers are truthy like everything else,
case only matches symbol datums, and #rx lacks \d and {n} (use #px).
44 unit tests + the CLI/daemon flow green locally.
Generic raw-TCP SCPI power supply with profile command templates,
IO-gated connection lifecycle, safety shutoff on transport/protocol
errors, and bounded response timeouts (TCP input ports as sync events).
ScpiPowerTests ported: the full profile-driven flow against a fake
SCPI server and the timeout-triggers-safety-off path. 46 tests green.

Port finding: hasheq's eq?-based keys silently fail for dynamically
built strings (the fake server's command table never matched); use
equal-based hash for anything with runtime-built keys.
SEGGER J-Link Commander backend: generated command files through the
installed JLink.exe/JLinkExe with process timeout, probe enumeration
that never touches the target MCU (ShowEmuList only), executable
resolution across Program Files/SEGGER and /opt/SEGGER paths, and
factory validation parity (device required, speed/timeout ranges,
binAddress hex-string format). J-Link and SCPI drivers complete
phase 4's TCP/process-backed drivers; SocketCAN/PCAN (Linux/Win FFI)
and system-serial (POSIX/Win32 FFI) follow when hardware is available
per ADR 0002's Phase 4 gate.
The derivers table was hasheq (eq?-based), so the plan's dynamically
built 'xor0x5a' key never matched the table entry on Linux CI. Use
equal-based hash for any table with runtime-built string keys.
Deriver names arrive as plan strings from other modules; hasheq's eq?
lookup misses cross-module string literals, so CI failed 3 flash/DoIP
tests that passed locally only by compiled-literal-sharing luck. Also
name the fake SCPI server's silence sentinel correctly ('no-reply)
instead of relying on the same lookup miss.
The key-derivers table was hasheq (eq?-based), so the plan's
dynamically built 'xor0x5a' key never matched. Changed to equal-based
hash and removed the hash-union dependency that silently failed.
- gen:can-bus: port of ICanBus; the ISO-TP endpoint and both diagnostics
  channels now attach to any transport through the generic
- SocketCAN (Linux libc FFI) and PCAN-Basic (Windows kernel32 FFI) bus
  transports; wire-frame codec is pure and unit-tested everywhere
- system-serial: SystemSerialChannel port with POSIX termios and Windows
  CommAPI backends behind one bounded line-buffer state machine
- can-iso-tp resource factory with C#-parity transport selection; DoIP
  factory fixed to the C# driver name and settings contract
- daemon composes the same seven factories as the C# RuntimeHost
- J-Link probe discovery ported (ShowEmuList parse + deterministic USB
  selection) and wired into the health check
- SCPI health check performs the real identify query; factory rejects
  multiline command templates; driver settings read symbol-keyed JSON
  (string keys never matched parsed profiles)
- 25 new hardware-free driver tests; suite at 69 green
- updater.rkt: release-feed check, archive download, SHA256SUMS
  verification (sha256sum/shasum/certutil), OS-tar extraction (bsdtar
  reads zip on Windows), active-operation refusal, graceful daemon
  shutdown with bounded wait + pkill/taskkill fallback, in-place
  executable swap with .old backups
- contracts: update-check-result / update-result API records
- cli: update --check|update replaces the not_supported stub; exit
  codes match the C# table (4 on failure)
- fixed raco-subprocess value order in jlink (stdout pipe arrived
  first) and switched driver health details to equal-based hashes so
  cross-module string lookups resolve
- cli main: current-command-line-arguments is a vector
- 5 SelfUpdater tests ported; suite at 74 green
- mcp/server.rkt: stdio JSON-RPC server with newline-delimited messages,
  26 tools proxying the same /api/v1 surface as the CLI (bench,
  operations, observations, power, flash, serial, UDS/DoIP), full
  inputSchema per tool, tools/call errors surfaced as isError content
- reuses the CLI's api-call/resolve-endpoint/autostart (now exported)
- 6-test smoke suite drives initialize/tools/list/tools/call against an
  in-process resident daemon; the C# tree had no MCP tests
… gates)

E2EEnvironment/SharedDaemonFixture port: stages benchpilot + benchpilotd
side by side as wrappers, spawns the real CLI and resident daemon as
processes over loopback, and drives the full product contract:

- ShutdownTests (2): graceful stop releases the endpoint; idempotent
  not_running
- AutostartTests (2): first command starts the resident daemon; doctor
  never does (exit 4)
- AuthAndDiagnosticsTests (8): version contract, healthz/status runtime
  version, 401 without/wrong token, doctor health report, UDS/DoIP
  routes through the daemon, history records completed mutations
- FullLoopTests (4): the README simulator loop through the CLI, unmatched
  wait exit 1 with failure-window + context evidence, runtime deadline
  exit 6 with deadlineMs, validate not-ready exit 1

Bugs the suite flushed out and fixes:
- http.rkt: query-string regex read group four instead of three, so any
  request with a query died silently (empty reply) — E2E was the first
  harness to send query parameters
- daemon: /shutdown now goes through the same drain-and-exit thread as
  SIGTERM
- cli: error exit codes compared string codes against symbol datums
  (deadline/busy never matched); structured error fields (deadlineMs,
  busyScope...) survive to the printed JSON; doctor handles the network
  error subtype before the base class; autostart spawns staged .rkt
  daemons via the racket binary and consumes all four subprocess values
- doip: discovery hardens the broadcast leg (empty result on hosts
  without a route) and fixes the udp-receive!* arity for this Racket
…html (phase 5d)

- html-report.rkt: one self-contained static page (no scripts, no
  network) covering bench status, readiness checks with pass/fail
  badges, operation/observation history and the evidence recorded for
  the newest completed mutation and observation
- cli: benchpilot report [--format html] [--out PATH] [--target ID]
- 4 renderer tests (sections, escaping, static contract); suite at 95
…phase 5e)

- scripts/build-dist.sh: per-RID distributions — Windows ships three
  embed-dlls single-file executables, Unix a full raco distribution tree
  (bin/ + lib/); committed launcher entries under packaging/launchers/
  so the packaged CLI/daemon/MCP behave exactly like the E2E-tested
  wrappers
- install.sh installs the tree under ~/.benchpilot/lib/benchpilot/<ver>
  and writes thin bin wrappers; the self-updater follows wrappers back
  to the real launchers and swaps them in place
- Debian package carries the distribution tree under /opt/benchpilot
  with /usr/bin wrappers (deb verify step runs the real binary)
- release matrix: win-x64, linux-x64/arm64, osx-x64/arm64 on native
  runners; win-arm64 dropped (Racket publishes no Windows ARM64 builds);
  Homebrew formula installs to libexec + bin wrappers, scoop is 64bit
- ci.yml is now racket-only (unit + driver + MCP + E2E + smoke on
  ubuntu/windows); smoke scripts boot the real daemon and CLI
- docs: README requirements/build/report sections, CHANGELOG 0.6.0,
  ROADMAP port-complete note, site milestone; benchpilot-version 0.6.0
The Racket port reached full contract parity: same frozen JSON API, CLI
exit-code table and the black-box E2E suite — now ported and green. The
0.5.x C# sources, their test projects and the .NET solution files leave
with this commit; v0.6.0 ships from the Racket tree only.
- the serial factory test no longer asserts health ok=false (a host may
  genuinely expose the configured port); the contract stays 'never
  opened anything'
- the doip E2E check reports the response body on failure
- DoIP discovery treats every socket leg as best-effort (open/bind/send/
  receive), so hosts without broadcast routes or with blocked sockets
  get an empty result instead of a 500
main holds the same phases 0-2 content as this branch (PR #29 squash);
conflicts resolve to this branch, which is a strict superset built on
the same tree.
@turinglambdaai
turinglambdaai merged commit 1a3d965 into main Oct 2, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant