diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 6f2c92f..895e16e 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -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)."}} diff --git a/CHANGELOG.md b/CHANGELOG.md index abc79f3..f8d1670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/apps/web/e2e/execute.spec.ts b/apps/web/e2e/execute.spec.ts index 3707845..6503283 100644 --- a/apps/web/e2e/execute.spec.ts +++ b/apps/web/e2e/execute.spec.ts @@ -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(); +}); diff --git a/apps/web/src/execute/useExecutionSession.ts b/apps/web/src/execute/useExecutionSession.ts index b61caf6..2f6ac85 100644 --- a/apps/web/src/execute/useExecutionSession.ts +++ b/apps/web/src/execute/useExecutionSession.ts @@ -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') @@ -98,6 +101,7 @@ export function useExecutionSession({ const [progress, setProgress] = useState(0); const [args, setArgs] = useState>({}); const [trace, setTrace] = useState(null); + const [traceOrigin, setTraceOrigin] = useState(null); const [cursor, setCursor] = useState(0); const [callLog, setCallLog] = useState([]); const [callCount, setCallCount] = useState(0); @@ -148,6 +152,7 @@ export function useExecutionSession({ setDeployedArtifactId(null); setOutcome(null); setTrace(null); + setTraceOrigin(null); clientRef.current?.reset(); } }, [artifact?.id, deployedArtifactId]); @@ -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'}`); @@ -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); @@ -250,6 +275,7 @@ export function useExecutionSession({ observations: result.observations, logs: result.logs ?? [], }); + setTraceOrigin('call'); setCursor(result.trace.stepCount - 1); } if (result.success) { @@ -292,6 +318,7 @@ export function useExecutionSession({ setDeployedArtifactId(null); setOutcome(null); setTrace(null); + setTraceOrigin(null); void replaced.catch(() => {}); } } @@ -309,6 +336,7 @@ export function useExecutionSession({ args, setArgs, trace, + traceOrigin, cursor, setCursor, resolutions, diff --git a/apps/web/src/probe/ProbePane.tsx b/apps/web/src/probe/ProbePane.tsx index 954009a..af1901b 100644 --- a/apps/web/src/probe/ProbePane.tsx +++ b/apps/web/src/probe/ProbePane.tsx @@ -42,7 +42,7 @@ export function ProbePane({ ownerClass: ReadonlyMap; 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); @@ -92,6 +92,9 @@ export function ProbePane({ CALL #{Math.max(callCount, 1)} · {outcome.fnName}() )} + {outcome === null && traceOrigin === 'constructor' && ( + DEPLOY · constructor + )} {effectiveMode === 'storage' && ( @@ -102,7 +105,7 @@ export function ProbePane({ declaringContracts={result.declaringContracts} ownerSuffixByContract={ownerSuffixByContract} /> - + )} diff --git a/apps/web/src/storage/RuntimeStorage.tsx b/apps/web/src/storage/RuntimeStorage.tsx index 3773147..2507781 100644 --- a/apps/web/src/storage/RuntimeStorage.tsx +++ b/apps/web/src/storage/RuntimeStorage.tsx @@ -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; @@ -21,7 +31,9 @@ export function RuntimeStorage({ resolutions }: { resolutions: readonly Semantic return (
-

Runtime storage (last traced call)

+

+ Runtime storage ({origin === 'constructor' ? 'from the constructor' : 'last traced call'}) +

{[...groups.entries()].map(([groupName, byLabel]) => (
{groupName}