See docs/adr/ for recorded decisions and docs/limitations-v1.0.md for
the honest cut-line.
A static React/Vite application. Compilation, execution, and normalization all happen locally in the browser; the release makes no application network request (asserted in e2e). The future-facing seam is the versioned URL session schema (ADR 0002, extended to v2 by ADR 0004).
domain <── compiler <── executor <── semantics
▲ ▲ ▲ ▲
│ │ │ │
layout │ lifecycle │
▲ │ ▲ │
└────────── web ─────────┴──────────────┤
▲ ▲ ▲ ▲ │
│ │ │ │ │
ui session challenges fixtures ─────┘
domain imports no product package. lifecycle is the generic worker-client
base both compiler and executor subclass. fixtures imports domain/compiler
and exercises layout/session/executor in tests. compiler, layout, executor,
semantics, session, challenges import no React. web composes packages and
owns no compiler/layout/execution/resolution algorithms. ui receives
explicit view state and never invents Solidity semantics.
| Package | Responsibility |
|---|---|
@slotscope/domain |
Locked environment constants, immutable normalized models, Utf8IndexMap (solc UTF-8 byte offsets ↔ editor UTF-16 code units), canonical JSON, deep-freeze, shared hex primitives |
@slotscope/compiler |
Worker protocol/client/worker (ADR 0001), settings-aware Standard JSON input, instruction decoder, source-map decoder, normalize() |
@slotscope/layout |
decodeStorageLayout: solc storageLayout → exact bigint slot/cell model with honest collapses |
@slotscope/executor |
In-browser EVM worker (EthereumJS, Cancun), keyframe/delta trace substrate (ADR 0003), keccak/storage observations, revert and event decode |
@slotscope/semantics |
Layer-3 resolver: pure keccak-chain grounding over the frozen trace observations → exact/inferred/unresolved |
@slotscope/lifecycle |
Generic worker-lifecycle client (generation/job ids, cancel-by-termination, crash recovery) that compiler and executor subclass |
@slotscope/session |
Share-URL schema v1/v2 + bounded codec, .slotscope.json (ADR 0002, ADR 0004) |
@slotscope/challenges |
Challenge schema, runtime-derived answer validator, local-first progress store |
@slotscope/ui |
Design tokens, focus ring, motion contract (tokens.css) |
@slotscope/fixtures |
Public fixture corpus, goldens, manifests, verify script |
@slotscope/web |
Composition: editor, assembly pane, probe pane, cockpit bar, execute strip, relation index, sharing |
Editor text → Compile (job id + settings + locked environment) → worker
(exact-version verified soljson, settings echoed back and verified against
the request) → raw Standard JSON output → pure normalize() → immutable
CompilationResult → assembly view + relation index + storage decode → UI
state (selection + one pin) + the deterministic call sequence + trace cursor
→ session schema v2 → #s= URL or .slotscope.json. Transient hover/focus/
pin state never mutates the compiled model.
Deployment and calls run in a second worker holding one long-lived EthereumJS executor per generation; the client mirrors the compiler lifecycle (generation/job ids, cancel-by-termination with verified replacement, one automatic crash recovery, then explicit retry). Termination drops executor state by design — the UI redeploys rather than pretending state survived.
Capture is opt-in per execution: full machine snapshots before each step are
encoded as keyframes (512-step cadence) plus deltas, so any cursor replays in
sub-millisecond time. A post-hoc pure analysis derives keccak and storage
observations plus shadow-stack SlotExpr provenance, degrading to unknown
wherever it cannot follow. All of these shapes are frozen — see
ADR 0003.
Compilation runs in one long-lived classic web worker per generation (ADR 0001). Because solc compiles synchronously inside its worker, active cancellation is termination of that generation plus a verified replacement.
uninitialized → starting → ready → compiling → ready
│ │ │
│ │ ├─ cancel → replacing → ready
│ │ └─ crash → replacing → ready
│ └─ fatal version mismatch → failed
└─ startup error → failed (Retry)
Rules (enforced by packages/compiler/src/client.ts, tested in
lifecycle.test.ts and apps/web/e2e/lifecycle.spec.ts):
- Every request carries a monotonically increasing
jobId; every worker has agenerationId. Only a message matching the current pair may enter app state — a terminated generation can never leak stale output, including in the completion race where a result lands just before termination. - Normal recompiles reuse the ready worker (
workersCreatedstays 1). - A cancelled job settles exactly once with typed
cancelled; it never becomes a diagnostic. - A replacement worker must verify the exact solc long version before status
returns to
ready; a mismatch parks the client infailed(fail closed). - One unexpected crash earns one automatic replacement. A second consecutive
crash parks the client in
failedwith a user-controlled Retry. A healthy compile result re-arms automatic recovery. - Starting a new compile while one is in flight supersedes the old job
(typed
superseded); stale output is never rendered for new source. - Unmount/disposal terminates the worker and rejects all outstanding
promises (
disposed); the pending-job registry must be empty afterwards.
| State | UI |
|---|---|
starting |
Compile disabled, "Starting compiler…" |
compiling |
Cancel button visible |
replacing |
"Cancelling — verifying replacement worker…" |
failed |
Retry button plus alert text; no silent retry loops |