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
7 changes: 7 additions & 0 deletions .beads/interactions.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,10 @@
{"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)."}}
{"id":"int-74ceaeac7b18e1d6d73229cf26040dce","kind":"field_change","created_at":"2026-07-30T10:09:27.063987887Z","actor":"halaprix","issue_id":"slotscope-73y","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed via EditorView.theme({...}, {dark: true}) in SourceEditor.tsx. Root cause: CodeMirror injects base styles as a StyleModule, so a plain .cm-tooltip rule in app.css lost the cascade at equal specificity and the widget kept its stock #f5f5f5 background under light text. A CSS-only fix was tried first and verifiably failed (received rgb(245,245,245)); the theme extension is the supported mechanism. Verified: new axe-gated e2e hovers the lint marker and asserts rgb(35,32,32); 59/59 e2e, 209/209 unit, lint and typecheck all pass. Commit pending owner approval."}}
{"id":"int-ad8880584f11af1a0add2fe779245102","kind":"field_change","created_at":"2026-07-30T10:23:23.09280563Z","actor":"halaprix","issue_id":"slotscope-zvd.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"StorageEncoding type + CollapsedStorageSpan.encoding populated at all five collapse sites; unknown encodings now throw LayoutDecodeError instead of being guessed. 16/16 layout tests, 212/212 unit, typecheck clean."}}
{"id":"int-cb5e2ee25b4426acfdea6a11226cad36","kind":"field_change","created_at":"2026-07-30T10:26:02.145821017Z","actor":"halaprix","issue_id":"slotscope-zvd.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"buildRows now walks a sorted claim list: reference heads get their own row, multi-slot inplace collapses become one elided range, unclaimed runs become an honest empty elided range. Gapless by construction, proved by a fast-check property suite (300 runs x2). 9/9 storage tests, 219/219 unit, lint+typecheck clean."}}
{"id":"int-ed1713e43409e336e1468796d1ae102a","kind":"field_change","created_at":"2026-07-30T10:31:04.596194724Z","actor":"halaprix","issue_id":"slotscope-zvd.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"runtimeView folds observed words (reads included, cursor-bounded, transient excluded) plus distinct derived-entry counts; referenceValue turns (span, word) into per-encoding copy and one measured value; StorageGrid renders reserved and elided rows with amber only on measured words, retires the 'Not expanded' list, and keeps roving focus/pin/zoom/axe green. 234/234 unit, 8/8 storage e2e, format+lint+typecheck clean."}}
{"id":"int-b276681d8d66b2c92c0ead9c38ffd11d","kind":"field_change","created_at":"2026-07-30T10:31:04.75317577Z","actor":"halaprix","issue_id":"slotscope-zvd.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"runtimeView folds observed words (reads included, cursor-bounded, transient excluded) plus distinct derived-entry counts; referenceValue turns (span, word) into per-encoding copy and one measured value; StorageGrid renders reserved and elided rows with amber only on measured words, retires the 'Not expanded' list, and keeps roving focus/pin/zoom/axe green. 234/234 unit, 8/8 storage e2e, format+lint+typecheck clean."}}
{"id":"int-51cd7e3fd904dfa823a0a73a0fb41156","kind":"field_change","created_at":"2026-07-30T10:36:21.003339065Z","actor":"halaprix","issue_id":"slotscope-zvd.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixture gains balances/history/note at slots 5-7 plus a constructor that fills the array and string, so the default document demonstrates reserved rows and their measured values on first compile and deploy. Declared after positionValue: the editor virtualizes lines and relations.spec.ts clicks one inside that function, so keeping earlier lines in place was required (caught by the full browser suite). Goldens regenerated via the sanctioned --update; verified every existing variable kept its exact slot and offset. sourceHash, expectedLayoutHash and expectedRelationHash moved; expectedInstructionHash did not."}}
{"id":"int-eaddbbe40fad713f478dda7fcb0c3278","kind":"field_change","created_at":"2026-07-30T10:36:21.314921434Z","actor":"halaprix","issue_id":"slotscope-zvd","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"All five tasks complete and verified: format+lint+typecheck clean, 235/235 unit (incl. Forge cross-check and a 300-run fast-check gaplessness property), fixtures:verify no drift, 61/61 Playwright, build+build:check (224 KB main entry), benchmark p95 225ms action-to-paint / 657ms cold start PASS. Not committed: conservative profile, awaiting owner approval."}}
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ 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.2.0] — unreleased

### Added

- **Reserved reference slots are visible.** The slot a mapping, dynamic array,
`bytes` or `string` reserves now gets its own labeled row instead of leaving a
silent hole in the slot sequence, and the row shows what the machine was
observed to put there: an array or long-string length, a short string's inline
data, and nothing at all for a mapping head — which solc never writes, so a
value there is flagged as an anomaly. Rows are gapless by construction; an
unexpanded multi-slot span becomes one honest elided range instead of thousands
of rows.

### Changed

- `CollapsedStorageSpan` carries solc's `encoding`, so the UI classifies spans by
compiler output instead of by parsing display labels. An encoding solc does not
emit now fails loudly rather than being treated as one of the four.
- The "Not expanded" prose list is retired; its teaching copy moved onto the rows
and the per-slot record.

### Fixed

- CodeMirror tooltips, diagnostics and the search panel were rendering with the
library's light-theme defaults under the app's light text, which made them
effectively unreadable. They are themed through an `EditorView.theme` marked
`dark`, so the cascade is correct by construction rather than by specificity.

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

A post-launch UX pass: the full measure loop — compile → deploy → call →
Expand Down
21 changes: 21 additions & 0 deletions apps/web/e2e/editor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';

const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
Expand Down Expand Up @@ -107,3 +108,23 @@ test('an abstract artifact is visibly unavailable, never empty-successful', asyn
'no deployed bytecode',
);
});

test('a diagnostic tooltip is readable on the dark chassis and axe-clean', async ({ page }) => {
await page.goto('/');
await waitReady(page);
await page.locator('.cm-content').fill(invalidSource);
await page.getByRole('button', { name: 'Compile' }).click();

const marker = page.locator('.cm-lint-marker-error');
await expect(marker).toHaveCount(1, { timeout: 60_000 });
await marker.hover();

const tooltip = page.locator('.cm-tooltip-lint');
await expect(tooltip).toBeVisible();
// CodeMirror's own tooltip background is light (#f5f5f5). The instrument theme
// must win, or the tooltip renders light text on a light surface.
await expect(tooltip).toHaveCSS('background-color', 'rgb(35, 32, 32)');

const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
74 changes: 65 additions & 9 deletions apps/web/e2e/storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,22 +128,78 @@ test('axe scan passes in both motion modes', async ({ page }) => {
expect(reduced.violations).toEqual([]);
});

test('a mapping base slot explains itself honestly, with no stale version claim', async ({
page,
}) => {
test('every reserved reference slot gets its own row, with no skipped slot', async ({ page }) => {
await page.goto('/');
await expect(page.locator('[role="status"]')).toHaveAttribute('data-status', 'ready', {
timeout: 60_000,
});
await page
.locator('.cm-content')
.fill(
'pragma solidity 0.8.36;\ncontract M {\n uint256 a;\n mapping(address => uint256) internal balances;\n uint256 b;\n uint256[] internal history;\n string internal note;\n}',
);
await page.getByRole('button', { name: 'Compile' }).click();
const grid = page.getByRole('grid', { name: 'Storage layout' });
await expect(grid).toBeVisible({ timeout: 60_000 });

// Slots 0..4 are all present: nothing is skipped.
for (const slot of [0, 1, 2, 3, 4]) {
await expect(grid.getByText(`slot ${slot}`, { exact: true })).toBeVisible();
}

const reserved = grid.locator('[data-testid="storage-reference"]');
await expect(reserved).toHaveCount(3);
await expect(reserved.nth(0)).toHaveAttribute('aria-label', /balances.*reserved base slot/);
await expect(reserved.nth(0)).toContainText('RESERVED · mapping');
await expect(reserved.nth(1)).toContainText('LENGTH · array');
await expect(reserved.nth(2)).toContainText('note');

// The old prose list is gone; the rows carry the teaching copy now.
await expect(page.locator('[data-testid="collapsed-spans"]')).toHaveCount(0);
await expect(grid).toContainText('keccak-derived slots that exist only at runtime');

const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});

test('a reserved slot shows the value the constructor put in it', async ({ page }) => {
await page.goto('/');
await expect(page.locator('[role="status"]')).toHaveAttribute('data-status', 'ready', {
timeout: 60_000,
});
await page
.locator('.cm-content')
.fill(
'pragma solidity 0.8.36;\ncontract M { uint256 a; mapping(bytes32 => address) internal klop; }',
'pragma solidity 0.8.36;\ncontract M {\n uint256[] internal history;\n string internal note;\n constructor() {\n history.push(7);\n note = "hi";\n }\n}',
);
await page.getByRole('button', { name: 'Compile' }).click();
const collapsed = page.locator('[data-testid="collapsed-spans"]');
await expect(collapsed).toBeVisible({ timeout: 60_000 });
await expect(collapsed).toContainText('klop (mapping(bytes32 => address)): base slot 1');
await expect(collapsed).toContainText('keccak-derived slots that exist only at runtime');
await expect(collapsed).not.toContainText('v0.1');
const grid = page.getByRole('grid', { name: 'Storage layout' });
await expect(grid).toBeVisible({ timeout: 60_000 });
await page.getByRole('button', { name: 'Deploy locally' }).click();

const reserved = grid.locator('[data-testid="storage-reference"]');
await expect(reserved.nth(0)).toContainText('length = 1', { timeout: 60_000 });
await expect(reserved.nth(1)).toContainText('"hi"');
});

test('the default source shows its reserved slots, then their values on deploy', async ({
page,
}) => {
await compileFixture(page);
const grid = page.getByRole('grid', { name: 'Storage layout' });

// Slots 0..4 keep their existing meaning; 5..7 are the new reference heads.
const reserved = grid.locator('[data-testid="storage-reference"]');
await expect(reserved).toHaveCount(3);
await expect(reserved.nth(0)).toContainText('balances');
await expect(reserved.nth(0)).toContainText('RESERVED · mapping');
await expect(reserved.nth(1)).toContainText('history');
await expect(reserved.nth(2)).toContainText('note');
await expect(grid.getByText('slot 7', { exact: true })).toBeVisible();

await page.getByRole('button', { name: 'Deploy locally' }).click();
await expect(reserved.nth(1)).toContainText('length = 1', { timeout: 60_000 });
await expect(reserved.nth(2)).toContainText('"slotscope"');
// solc never writes a mapping head, so it stays blank.
await expect(reserved.nth(0)).not.toContainText('0x');
});
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "6.0.4",
"axe-core": "4.12.1",
"fast-check": "4.9.0",
"typescript": "6.0.3",
"vite": "8.1.5",
"vitest": "4.1.10"
Expand Down
38 changes: 38 additions & 0 deletions apps/web/src/app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,44 @@ input[readonly] {
);
}

/* A reserved slot is not padding, so it never takes the hatch. Graphite while
only the compiler has spoken; the amber inside it is the measured word. */
.storage-cell.is-reference {
display: flex;
align-items: center;
gap: 0.5rem;
overflow: hidden;
padding-inline: 0.5rem;
border: 1px solid var(--ss-color-border-strong);
background: var(--ss-color-chip-bg);
}

.storage-reference-eyebrow {
color: var(--ss-color-fg-muted);
font-size: var(--ss-font-size-micro);
letter-spacing: var(--ss-tracking-label);
text-transform: uppercase;
white-space: nowrap;
}

/* solc never writes a mapping head: a value here is a finding, not a reading. */
.storage-cell.is-reference.is-anomaly {
border-color: var(--ss-color-warning);
}

.storage-cell.is-elided {
display: flex;
align-items: center;
padding-inline: 0.5rem;
border: 1px dashed var(--ss-color-border-strong);
background: transparent;
color: var(--ss-color-fg-muted);
}

.storage-record-note {
color: var(--ss-color-fg-muted);
}

.owner-a {
background: var(--ss-color-owner-a-surface);
border-color: var(--ss-color-owner-a-border);
Expand Down
63 changes: 63 additions & 0 deletions apps/web/src/editor/SourceEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,68 @@ import { solidity } from '@replit/codemirror-lang-solidity';
import { SOURCE_PATH } from '@slotscope/compiler';
import { Utf8IndexMap, type CompilerDiagnostic } from '@slotscope/domain';

/**
* Dark theming for CodeMirror's own floating widgets.
*
* CodeMirror injects its base styles as a StyleModule, so a plain `.cm-tooltip`
* rule in app.css loses the cascade at equal specificity and the widget keeps
* its stock light background under our light text. A theme extension is the
* supported fix: CodeMirror injects it after the base sheet, and `dark: true`
* makes it pick dark defaults for anything not named here.
*/
const instrumentWidgets = EditorView.theme(
{
'.cm-tooltip': {
background: 'var(--ss-color-raised)',
border: '1px solid var(--ss-color-border-strong)',
borderRadius: '2px',
color: 'var(--ss-color-fg)',
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'var(--ss-color-border-strong)',
borderBottomColor: 'var(--ss-color-border-strong)',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: 'var(--ss-color-raised)',
borderBottomColor: 'var(--ss-color-raised)',
},
'.cm-tooltip-lint': { fontSize: 'var(--ss-font-size-micro)', maxWidth: '32rem', padding: '0' },
'.cm-diagnostic': {
borderLeft: '3px solid var(--ss-color-border-strong)',
color: 'var(--ss-color-fg)',
padding: '0.35rem 0.6rem',
},
'.cm-diagnostic-error': { borderLeftColor: 'var(--ss-color-error)' },
'.cm-diagnostic-warning': { borderLeftColor: 'var(--ss-color-warning)' },
'.cm-diagnostic-info': { borderLeftColor: 'var(--ss-color-label)' },
'.cm-diagnosticAction': {
background: 'var(--ss-color-surface)',
border: '1px solid var(--ss-color-border-strong)',
color: 'var(--ss-color-fg)',
},
// The Ctrl-F panel and its controls.
'.cm-panels': {
background: 'var(--ss-color-raised)',
borderColor: 'var(--ss-color-border-strong)',
color: 'var(--ss-color-fg)',
},
'.cm-panels-bottom': { borderTop: '1px solid var(--ss-color-border-strong)' },
'.cm-panels-top': { borderBottom: '1px solid var(--ss-color-border-strong)' },
'.cm-textfield': {
background: 'var(--ss-color-surface)',
border: '1px solid var(--ss-color-border-strong)',
color: 'var(--ss-color-fg)',
},
'.cm-button': {
background: 'var(--ss-color-raised)',
backgroundImage: 'none',
border: '1px solid var(--ss-color-border-strong)',
color: 'var(--ss-color-fg)',
},
},
{ dark: true },
);

/**
* Presentation-only lexical highlighting in the instrument palette.
* The lezer grammar colors tokens and is NEVER a source of facts — solc
Expand Down Expand Up @@ -198,6 +260,7 @@ export function SourceEditor({
history(),
lintGutter(),
solidity,
instrumentWidgets,
syntaxHighlighting(instrumentHighlight),
highlightField,
spineField,
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/probe/ProbePane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { useExecutionSession } from '../execute/useExecutionSession';
import { EventsPanel } from '../execute/EventsPanel';
import { RuntimeStorage } from '../storage/RuntimeStorage';
import { StorageGrid } from '../storage/StorageGrid';
import { buildRuntimeView } from '../storage/runtimeView';
import { MachineStatePanes } from '../trace/MachineStatePanes';
import { ObservationList } from '../trace/ObservationList';
import { ForensicsView } from './ForensicsView';
Expand Down Expand Up @@ -52,6 +53,11 @@ export function ProbePane({
[trace, boundedCursor],
);

const storageRuntime = useMemo(
() => buildRuntimeView(trace, resolutions, boundedCursor),
[trace, resolutions, boundedCursor],
);

const accessCount = trace?.observations?.storage.length ?? 0;
const eventCount = trace?.logs.length ?? 0;

Expand Down Expand Up @@ -104,6 +110,7 @@ export function ProbePane({
artifact={artifact}
declaringContracts={result.declaringContracts}
ownerSuffixByContract={ownerSuffixByContract}
runtime={storageRuntime}
/>
<RuntimeStorage resolutions={resolutions} origin={traceOrigin ?? 'call'} />
</div>
Expand Down
Loading