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
2 changes: 2 additions & 0 deletions .beads/interactions.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,5 @@
{"id":"int-70a07e495b05b829ee58102fb0b5cba5","kind":"field_change","created_at":"2026-07-29T00:05:10.765764819Z","actor":"halaprix","issue_id":"slotscope-pe7.13","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Codex reviewed v1.0 (9 findings), fixes verified in two follow-up rounds; final residual (prose pins) closed — all runtime items confirmed FIXED by the external verifier."}}
{"id":"int-74d2e61abae11dd74f5866942307a3e0","kind":"field_change","created_at":"2026-07-29T05:47:53.640204834Z","actor":"halaprix","issue_id":"slotscope-zhv","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Stale copy replaced with per-reason honest explanations; e2e asserts the mapping wording and the absence of version claims."}}
{"id":"int-78d92a08ad438bc04c0142e555c53902","kind":"field_change","created_at":"2026-07-29T07:12:36.373056725Z","actor":"halaprix","issue_id":"slotscope-tkd","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"v2 identity applied (band+record, resolution treatment on runtime surfaces, contrast promotions, chassis lock restored); inventory doc written for the next design round; 209 unit + 57 e2e green."}}
{"id":"int-508bcad5d22f7e38c6348ba64ec7e735","kind":"field_change","created_at":"2026-07-29T09:17:25.835199235Z","actor":"halaprix","issue_id":"slotscope-bnq","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"PR #1 merged into master (83fd429); Variant B cockpit-bar workbench live, review fixes (reserved-hue violations, call index) landed before merge."}}
{"id":"int-5f6e4259a9ea3d7c490343f6a09f79cf","kind":"field_change","created_at":"2026-07-29T09:27:34.871060006Z","actor":"halaprix","issue_id":"slotscope-p1t","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented: deploy captures constructor trace via captureTrace:true, resolveAccesses runs over it identically to a call trace, RuntimeStorage labels the group 'from the constructor'. New e2e test covers non-zero + zero-valued initializers. All gates green (209 unit, 58 e2e, lint/format/typecheck/build/fixtures/deps-policy/token-discipline)."}}
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,51 @@ All notable changes to SlotScope. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow the
[roadmap](.resources/slotscope-final-scope-roadmap-v2.0.md) release plan.

## [1.1.0] — 2026-07-29 · Cockpit workbench

A post-launch UX pass: the full measure loop — compile → deploy → call →
step the trace → read named accesses — now works on one screen at ≥75rem,
and initialized storage values are visible the moment you deploy.

### Added

- **Variant B cockpit-bar workbench.** The three-pane face (Source ·
Assembly · the new Probe pane) drops to 56dvh to fund a full-width
cockpit bar (steppers, scrubber, cursor facts, outcome) and a slim
execute strip, both fixed height and visible in both probe modes. The
Probe pane switches between `Storage` and `Trace`; Trace auto-enters
when a call completes and auto-selects Forensics on a revert.
- **Live storage strip.** Pinned above the trace detail tabs at every
cursor position: one mini 32-byte band per slot the call touched,
owner-hued static cells, hatched padding, and the bytes actually
written up to the cursor filled amber with a caption naming the slot,
the proven changed byte range (from the old→new diff — a first-touch
write honestly claims the whole word), the resolved label, and the new
value. Keccak-derived slots render as labeled derived bands.
- **Initialized storage values are visible immediately after deploy.**
The constructor is an execution like any other: deploying now captures
its trace exactly like a call, so `Runtime storage` shows every
initializer's actual byte value the moment "Deploy locally" succeeds —
labeled "from the constructor" — with no source parsing or heuristics,
purely what the constructor measurably wrote. A zero-valued initializer
needs no `SSTORE` (storage is already zero) and honestly shows nothing.
- Diagnostics and the contract picker dock into the Source pane header
(`DIAG · n` toggle, auto-opens on an error-severity diagnostic); the
share URL field is now permanently rendered at a fixed height, closing
a layout-shift.
- Storage grid v2: the band is the map (cells carry text only when wide
enough), a per-slot record list beneath it names every field, and
resolution status is carried by border treatment and a mark
(`=`/`≈`/`?`) with a legend, never by hue.

### Fixed

- Two reserved-hue violations caught in design review: the `= exact`
pill wore the owner-b hue (read as ownership next to an owner-b chip)
and `COLD` access chips wore pin cyan (read as pinned). Both moved to
neutral tiers — status stays border-treatment-only, cyan stays
pin-only.

## [1.0.0] — 2026-07-29 · Launch

The instrument becomes a complete public teaching product (roadmap v1.0).
Expand Down
37 changes: 37 additions & 0 deletions apps/web/e2e/execute.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,40 @@ test('aborting a runaway execution leaves the executor reusable', async ({ page
await page.getByRole('button', { name: 'Deploy locally' }).click();
await expect(page.locator('[data-testid="deployed-address"]')).toBeVisible({ timeout: 60_000 });
});

test('initialized storage values are visible immediately after deploy (constructor trace)', async ({
page,
}) => {
await page.goto('/');
await expect(page.locator('[role="status"]').first()).toHaveAttribute('data-status', 'ready', {
timeout: 60_000,
});
await page
.locator('.cm-content')
.fill(
'pragma solidity 0.8.36;\ncontract Init { uint256 public total = 42; bool public paused = true; uint256 public unset; }',
);
await page.getByRole('button', { name: 'Compile' }).click();
await expect(page.locator('[data-exec-status]')).toHaveAttribute('data-exec-status', 'ready', {
timeout: 60_000,
});
await page.getByRole('button', { name: 'Deploy locally' }).click();
await expect(page.locator('[data-testid="deployed-address"]')).toBeVisible({ timeout: 60_000 });

// No call has been made yet — this is purely the constructor's own writes.
const runtime = page.getByRole('region', { name: 'Runtime storage' });
await expect(runtime).toBeVisible({ timeout: 60_000 });
await expect(runtime).toContainText('from the constructor');
await expect(runtime).toContainText('total');
await expect(runtime).toContainText('42');
await expect(runtime).toContainText('paused');
await expect(runtime).toContainText('true');
// A zero-initialized variable needs no SSTORE (storage is zero by
// default) — the constructor trace honestly has nothing to say about it.
await expect(runtime).not.toContainText('unset');

// The trace tab is available too, honestly labeled as the constructor.
await page.getByRole('tab', { name: 'Trace' }).click();
await expect(page.getByText('DEPLOY · constructor')).toBeVisible();
await expect(page.locator('[data-testid="live-storage-strip"]')).toBeVisible();
});
28 changes: 28 additions & 0 deletions apps/web/src/execute/useExecutionSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export interface TraceBundle {
logs: readonly LogObservation[];
}

/** What produced the current trace — the constructor, or a later call. */
export type TraceOrigin = 'constructor' | 'call';

export function abiFunctions(artifact: ContractArtifact): AbiFunction[] {
return artifact.abi
.filter((item) => item.type === 'function')
Expand Down Expand Up @@ -98,6 +101,7 @@ export function useExecutionSession({
const [progress, setProgress] = useState(0);
const [args, setArgs] = useState<Record<string, string>>({});
const [trace, setTrace] = useState<TraceBundle | null>(null);
const [traceOrigin, setTraceOrigin] = useState<TraceOrigin | null>(null);
const [cursor, setCursor] = useState(0);
const [callLog, setCallLog] = useState<SessionCall[]>([]);
const [callCount, setCallCount] = useState(0);
Expand Down Expand Up @@ -148,6 +152,7 @@ export function useExecutionSession({
setDeployedArtifactId(null);
setOutcome(null);
setTrace(null);
setTraceOrigin(null);
clientRef.current?.reset();
}
}, [artifact?.id, deployedArtifactId]);
Expand Down Expand Up @@ -200,16 +205,35 @@ export function useExecutionSession({
setErrorText(null);
setOutcome(null);
setProgress(0);
// Cleared unconditionally: a redeploy whose constructor writes nothing
// must not leave a previous deployment's trace lingering on screen.
setTrace(null);
setTraceOrigin(null);
try {
const result = await client.execute({
mode: 'deploy',
payloadHex: artifact.creationBytecodeHex,
captureTrace: true,
});
if (result.success && result.createdAddressHex !== null) {
setAddress(result.createdAddressHex);
setDeployedArtifactId(artifact.id);
setCallLog([]);
setCallCount(0);
// The constructor is an execution like any other: initializer
// values only exist once it has run, and this is the ONLY honest
// way to show them — never parsed from source, only measured.
if (result.trace !== null && result.trace.stepCount > 0) {
setTrace({
encoded: result.trace,
calldataHex: artifact.creationBytecodeHex,
returnDataHex: result.returnDataHex,
observations: result.observations,
logs: result.logs ?? [],
});
setTraceOrigin('constructor');
setCursor(result.trace.stepCount - 1);
}
return result.createdAddressHex;
}
setErrorText(`Deployment reverted: ${result.errorMessage ?? 'unknown error'}`);
Expand All @@ -233,6 +257,7 @@ export function useExecutionSession({
setErrorText(null);
setProgress(0);
setTrace(null);
setTraceOrigin(null);
try {
const values = explicitValues ?? fn.inputs.map((input, i) => args[`${fn.name}:${i}`] ?? '');
const dataHex = encodeCall(fn, values);
Expand All @@ -250,6 +275,7 @@ export function useExecutionSession({
observations: result.observations,
logs: result.logs ?? [],
});
setTraceOrigin('call');
setCursor(result.trace.stepCount - 1);
}
if (result.success) {
Expand Down Expand Up @@ -292,6 +318,7 @@ export function useExecutionSession({
setDeployedArtifactId(null);
setOutcome(null);
setTrace(null);
setTraceOrigin(null);
void replaced.catch(() => {});
}
}
Expand All @@ -309,6 +336,7 @@ export function useExecutionSession({
args,
setArgs,
trace,
traceOrigin,
cursor,
setCursor,
resolutions,
Expand Down
7 changes: 5 additions & 2 deletions apps/web/src/probe/ProbePane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function ProbePane({
ownerClass: ReadonlyMap<string, string>;
ownerChipText: (contract: string) => string;
}) {
const { trace, cursor, setCursor, resolutions, outcome, callCount } = session;
const { trace, traceOrigin, cursor, setCursor, resolutions, outcome, callCount } = session;
const effectiveMode: ProbeMode = trace === null ? 'storage' : mode;
const boundedCursor =
trace === null ? 0 : Math.min(Math.max(cursor, 0), trace.encoded.stepCount - 1);
Expand Down Expand Up @@ -92,6 +92,9 @@ export function ProbePane({
CALL #{Math.max(callCount, 1)} · {outcome.fnName}()
</span>
)}
{outcome === null && traceOrigin === 'constructor' && (
<span className="ss-pane-meta">DEPLOY · constructor</span>
)}
</div>

{effectiveMode === 'storage' && (
Expand All @@ -102,7 +105,7 @@ export function ProbePane({
declaringContracts={result.declaringContracts}
ownerSuffixByContract={ownerSuffixByContract}
/>
<RuntimeStorage resolutions={resolutions} />
<RuntimeStorage resolutions={resolutions} origin={traceOrigin ?? 'call'} />
</div>
)}

Expand Down
22 changes: 17 additions & 5 deletions apps/web/src/storage/RuntimeStorage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@ import type { SemanticResolution } from '@slotscope/domain';
import { MigrationBadge, StatusPill } from '@slotscope/ui';

/**
* Runtime storage grouping (S5): the slots the last traced call actually
* wrote, grouped under the static variable they derive from and carrying
* their resolved labels — the runtime companion to the static grid.
* Runtime storage grouping (S5): the slots the last traced execution
* actually wrote, grouped under the static variable they derive from and
* carrying their resolved labels — the runtime companion to the static
* grid. Also how initializer values become visible: a constructor is an
* execution like any other, so its writes appear here immediately after
* deploy — never parsed from source, only measured.
*/
export function RuntimeStorage({ resolutions }: { resolutions: readonly SemanticResolution[] }) {
export function RuntimeStorage({
resolutions,
origin = 'call',
}: {
resolutions: readonly SemanticResolution[];
/** What produced these writes, for an honest heading. */
origin?: 'constructor' | 'call';
}) {
const writes = resolutions.filter((r) => r.kind === 'sstore' || r.kind === 'tstore');
if (writes.length === 0) return null;

Expand All @@ -21,7 +31,9 @@ export function RuntimeStorage({ resolutions }: { resolutions: readonly Semantic

return (
<section aria-label="Runtime storage" className="ss-runtime-storage" tabIndex={0}>
<h3>Runtime storage (last traced call)</h3>
<h3>
Runtime storage ({origin === 'constructor' ? 'from the constructor' : 'last traced call'})
</h3>
{[...groups.entries()].map(([groupName, byLabel]) => (
<div key={groupName} className="ss-runtime-group" data-testid="runtime-group">
<span className="ss-runtime-var">{groupName}</span>
Expand Down