Hardware-in-the-loop development platform for the Commodore 64.
ultimate-tool docs ARCHITECTURE.md
8.2 kB
Markdown
at main

Architecture #

Ultimate Tool is one deterministic development core with several thin surfaces on top of it. The CLI, the loopback service (with its embedded dashboard), and the stdio MCP adapter all call the same versioned core operations; none of them implements build, run, diagnostics, or evidence logic of its own.

Workspace layout #

The Cargo workspace (Cargo.toml) contains five crates:

Crate Role
ultimate-tool-core The development core: manifests, build/run orchestration, targets, evidence, reports.
ultimate-tool-cli The ultimate-tool binary. Parses commands and calls core operations.
ultimate-tool-server The loopback HTTP service and embedded dashboard assets.
ultimate-tool-mcp The stdio MCP adapter that talks to a running service.
ultimate-tool-c64 The no_std C64 SDK statically linked into C64 programs.

The C64 fixtures (fixtures/rust-mos-smoke, fixtures/rust-mos-timeline, fixtures/checkpoint-demo) are excluded from the workspace because they are built by their own toolchains (Rust-MOS via the pinned Docker image, and the RISC-V fixtures via GNU RISC-V Embedded GCC).

The core #

ultimate-tool-core (crates/ultimate-tool-core/src/) is organized by responsibility:

  • Configuration and inputs — config (manifest c64dev.toml parsing and schema versions), model (shared value types such as TargetKind, TargetIdentity, RunOutcome, FailureRecord, HeartbeatSummary).
  • Build and artifact — build executes the declared build command and captures outputs; artifact validates and hashes the primary PRG plus any declared supporting artifacts.
  • Orchestration — runner drives the build-to-evidence path for a test request and a test matrix, lease provides bounded exclusive target access, progress emits structured progress events to an observer.
  • Targets — target holds the Target trait and its adapters.
  • Observation and analysis — diagnostic parses the versioned mailbox events, crash and crash_analysis handle crash state and site resolution, analysis snapshots and normalizes declared map/ELF symbol sources, insight evaluates build budgets and run comparisons, timeline and trace cover the 6510 debug-stream timeline.
  • Evidence and records — evidence writes run records and their files, integrity verifies a finished run against its SHA256SUMS, runs lists and pages immutable run history.
  • Read-only readers — doctor inspects local prerequisites without launching commands, report assembles a diagnostic report from an existing run record.
  • Trusted-native paths — ue2, native, native_runner, riscv, and riscv_runner implement UE2 packaging, launch intents, and the RISC-V runtime.

The Target trait #

Every execution target implements target::Target. The trait is the single boundary the runner uses, so the same test request can run against any adapter:

pub trait Target {
    fn identity_key(&self) -> String;
    fn probe(&mut self) -> Result<TargetIdentity>;
    fn deploy_reu_image(&mut self, artifact: &SupportingArtifact) -> Result<()>;
    fn deploy_and_start(&mut self, artifact: &Artifact) -> Result<()>;
    fn read_memory(&mut self, address: u16, length: usize) -> Result<Vec<u8>>;
    fn write_memory_coordinated(&mut self, address: u16, bytes: &[u8]) -> Result<()>;
    fn sample_screen(&mut self, screen_address: u16, color_address: u16) -> Result<ScreenCapture> {
        self.capture_screen(screen_address, color_address)
    }
    fn capture_screen(&mut self, screen_address: u16, color_address: u16) -> Result<ScreenCapture>;
    fn finish(&mut self) -> Result<()>;
    fn mutations(&self) -> Vec<Mutation>;
    fn retry_count(&self) -> u32;
    fn recovery_count(&self) -> u32;
    fn cpu_resumed(&self) -> Option<bool>;
}

The adapters are:

  • target::fake — a deterministic target used by fast core tests. It does not execute C64 instructions; it produces reproducible observations so the orchestration, classification, and evidence path can be tested without hardware, Docker, or VICE.
  • target::vice — drives VICE x64sc 3.8, using coordinated binary-monitor writes for the versioned startup command.
  • target::ultimate — the C64 Ultimate over its REST/DMA interfaces, with target::ultimate_dma handling the DMA-service REU writes. Mutation is gated by a fresh identity probe and explicit reset/REU-write confirmations.

Target selection is by TargetKind (Fake, Vice, Ultimate) rather than product-name assumptions, and target actions are gated by discovered capabilities.

Execution planes #

A run records which execution plane produced it (ExecutionPlane in evidence): the ordinary C64 plane (C64), the volatile U64-II RISC-V updater (UltimateRiscvUpdater), or the 6510 debug-stream capture (UltimateDebugStream). This keeps emulator, physical-C64, RISC-V, and bus-capture results distinguishable in one history.

Data flow #

  1. A surface loads a manifest (config) and builds a TestRequest.
  2. runner executes the declared build command (build) and validates the artifacts (artifact).
  3. analysis snapshots and normalizes any declared symbol sources; insight evaluates declared build budgets before any target mutation.
  4. The selected Target adapter probes identity, performs any authorized mutations, and observes the program.
  5. diagnostic / crash / crash_analysis classify the outcome.
  6. evidence writes one immutable run directory under .ultimate-tool/runs/<RUN_ID>/ and its SHA256SUMS; integrity later verifies it.

A run directory is never updated in place. Rerunning a test produces a new run ID, and readers reject unsupported future schemas explicitly.

Surfaces #

        +-----------------------------------------------------------+
        |                    ultimate-tool-core                     |
        |  manifest (c64dev.toml) -> build -> artifact -> runner    |
        |  runs & evidence   analysis/budgets   reports   doctor    |
        +----------------+------------------------------+-----------+
                         |                              |
              Target trait adapters                 Surfaces
              - fake    (deterministic, no execution)  - CLI    ultimate-tool
              - vice    (x64sc 3.8)                    - serve  loopback dashboard
              - ultimate (C64 Ultimate, gated)         - MCP    stdio adapter
  • CLI (ultimate-tool-cli) exposes every operation directly. The outer result envelope is schema 1.
  • Service (ultimate-tool-server) binds only to 127.0.0.1, owns one manifest per process, and serves the embedded dashboard. Mutating HTTP requests require a per-process token and same browser origin. Its outer envelope is schema 13. See HTTP_API.md.
  • MCP (ultimate-tool-mcp) wraps the running service over stdio. Its outer envelope is schema 12. See MCP.md.

The dashboard is plain embedded HTML/JavaScript/CSS with no frontend build step. It requests only GET diagnostics against the same core-backed routes the CLI uses; the report computation happens in the core, not in the browser.

Reporting #

runs report and runs report-items (and their service and MCP equivalents) assemble a diagnostic report from a single existing run record. The inner report is schema 1 on every surface, while the outer envelopes differ (CLI 1, HTTP 13, MCP 12). A finalized run verifies its registered evidence before any detail is presented, and a provisional run exposes only its in-record observations. Detail rows are paged with cursors bound to the run, the section, and the source evidence hash. See DIAGNOSTICS.md and CONTRACTS.md.

Contracts and versioning #

Manifests, run records, evidence bundles, and the service/CLI/MCP operations are versioned independently from their first implementation. See CONTRACTS.md for the current schema versions and error codes.