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
1 change: 1 addition & 0 deletions .beads/interactions.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@
{"id":"int-c41493d4db55c60870fbed5d3b4e195e","kind":"field_change","created_at":"2026-07-28T23:27:54.965894418Z","actor":"halaprix","issue_id":"slotscope-pe7.12","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Docs + evidence + gates green (release:verify, both benchmarks); deployed to production."}}
{"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."}}
Binary file added .playwright-screens/strip-closeup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-screens/variant-b.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion apps/web/e2e/a11y-v1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ test('axe: executed instrument with events, revert forensics, and challenges', a
// Success path: events panel + share size + settings visible.
await page.getByLabel('set argument v (uint256)').fill('5');
await page.getByRole('button', { name: 'Call', exact: true }).click();
await expect(page.locator('[data-testid="events-panel"]')).toBeVisible({ timeout: 60_000 });
await expect(page.locator('[data-testid="trace-pane"]')).toBeVisible({ timeout: 60_000 });
await page.getByRole('tab', { name: /Events · 1/ }).click();
await expect(page.locator('[data-testid="events-panel"]')).toBeVisible();
await expectClean(page, 'success path');

// Revert path: forensics panel visible.
Expand Down
11 changes: 6 additions & 5 deletions apps/web/e2e/compile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,20 @@ test('compiles the public fixture twice with a responsive main thread', async ({
});

await page.getByRole('button', { name: 'Compile' }).click();
await expect(page.getByRole('region', { name: 'Contracts' })).toContainText(
await expect(page.getByRole('combobox', { name: 'Contract' })).toContainText(
'Main.sol:PackedInheritance',
{ timeout: 60_000 },
);
// A clean compile keeps the diagnostics strip closed; opening it says so.
await page.getByRole('button', { name: 'DIAG · 0' }).click();
await expect(page.getByRole('region', { name: 'Diagnostics' })).toContainText('No diagnostics.');

const maxGap = await page.evaluate(() => Math.max(...window.__frameGaps.slice(1)));
expect(maxGap).toBeLessThan(500);

// Second compile must reuse the same worker generation.
await page.getByRole('button', { name: 'Compile' }).click();
await expect(page.getByRole('region', { name: 'Contracts' })).toContainText(
await expect(page.getByRole('combobox', { name: 'Contract' })).toContainText(
'Main.sol:PackedInheritance',
{ timeout: 60_000 },
);
Expand Down Expand Up @@ -83,9 +85,8 @@ test('invalid source returns solc diagnostics and no artifacts', async ({ page }
await page.locator('.cm-content').fill(invalidSource);
await page.getByRole('button', { name: 'Compile' }).click();

// Errors auto-open the diagnostics strip; no artifacts means no picker.
const diagnostics = page.getByRole('region', { name: 'Diagnostics' });
await expect(diagnostics).toContainText('error 9574', { timeout: 60_000 });
await expect(page.getByRole('region', { name: 'Contracts' })).toContainText(
'No contract artifacts.',
);
await expect(page.getByRole('combobox', { name: 'Contract' })).toHaveCount(0);
});
5 changes: 4 additions & 1 deletion apps/web/e2e/events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ test('emitted events decode in the panel and link to their step (V5)', async ({
await page.getByLabel('set argument v (uint256)').fill('9');
await page.getByRole('button', { name: 'Call', exact: true }).click();

// The probe pane auto-enters Trace mode; events live behind their tab.
await expect(page.locator('[data-testid="trace-pane"]')).toBeVisible({ timeout: 60_000 });
await page.getByRole('tab', { name: /Events · 1/ }).click();
const panel = page.locator('[data-testid="events-panel"]');
await expect(panel).toBeVisible({ timeout: 60_000 });
await expect(panel).toBeVisible();
await expect(panel).toContainText('Set(');
await expect(panel).toContainText('value = 9');
await expect(panel).toContainText('[indexed]');
Expand Down
3 changes: 2 additions & 1 deletion apps/web/e2e/lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ test('cancel during a long compile recovers to ready and compiles again', async
// The compiler subsystem is reusable: the public fixture compiles after.
await page.locator('.cm-content').fill(fixtureSource);
await page.getByRole('button', { name: 'Compile' }).click();
await expect(page.getByRole('region', { name: 'Contracts' })).toContainText(
await expect(page.getByRole('combobox', { name: 'Contract' })).toContainText(
'Main.sol:PackedInheritance',
{ timeout: 60_000 },
);
await page.getByRole('button', { name: 'DIAG · 0' }).click();
await expect(page.getByRole('region', { name: 'Diagnostics' })).toContainText('No diagnostics.');
});
3 changes: 2 additions & 1 deletion apps/web/e2e/semantics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ test('nested mappings show the collapsed KeccakFlow and the runtime storage grou
await expect(flow.locator('[data-depth="2"]').first()).toBeVisible();
await expect(write.locator('[data-testid="keccak-flow-collapsed"]')).toContainText('collapsed');

// The Storage section gains the labeled runtime group.
// The runtime group lives in the probe pane's Storage mode.
await page.getByRole('tab', { name: 'Storage' }).click();
const runtime = page.getByRole('region', { name: 'Runtime storage' });
await expect(runtime).toBeVisible();
await expect(runtime.locator('[data-testid="runtime-group"]')).toContainText('allowance');
Expand Down
2 changes: 2 additions & 0 deletions apps/web/e2e/trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ async function callPositionValue(page: Page) {

test('the trace pane shows machine state at the cursor (X5)', async ({ page }) => {
await callPositionValue(page);
await page.getByRole('tab', { name: 'State' }).click();

const facts = page.locator('[data-testid="trace-cursor-facts"]');
// Lands on the final step with the run-level return data visible.
Expand Down Expand Up @@ -120,6 +121,7 @@ test('storage observations show cold/warm and keccak provenance (X8)', async ({

test('replayed state is consistent while scrubbing back and forth', async ({ page }) => {
await callPositionValue(page);
await page.getByRole('tab', { name: 'State' }).click();
const facts = page.locator('[data-testid="trace-cursor-facts"]');

await page.getByLabel('Trace cursor').fill('20');
Expand Down
184 changes: 112 additions & 72 deletions apps/web/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
DEFAULT_COMPILE_SETTINGS,
type CompilationResult,
type CompileSettings,
type SemanticResolution,
} from '@slotscope/domain';
import {
CompileError,
Expand All @@ -29,9 +28,11 @@ import {
import { useEffectiveMotion } from '../accessibility/useEffectiveMotion';
import { AssemblyPane } from '../assembly/AssemblyPane';
import { SourceEditor } from '../editor/SourceEditor';
import { ExecutePane } from '../execute/ExecutePane';
import { RuntimeStorage } from '../storage/RuntimeStorage';
import { StorageGrid } from '../storage/StorageGrid';
import { ExecuteStrip } from '../execute/ExecuteStrip';
import { useExecutionSession } from '../execute/useExecutionSession';
import { CockpitBar } from '../cockpit/CockpitBar';
import { ProbePane, type ProbeMode, type ProbeTab } from '../probe/ProbePane';
import { ownerChipText } from '../storage/ownerChip';
import { InstrumentHeader } from './InstrumentHeader';
import { ChallengePane } from '../challenges/ChallengePane';
import { OnboardingTour } from '../onboarding/OnboardingTour';
Expand Down Expand Up @@ -115,7 +116,9 @@ export function App() {
const [announcement, setAnnouncement] = useState('');
const [solveTimeMs, setSolveTimeMs] = useState<number | null>(null);
const [tracePc, setTracePc] = useState<number | null>(null);
const [traceResolutions, setTraceResolutions] = useState<readonly SemanticResolution[]>([]);
const [probeMode, setProbeMode] = useState<ProbeMode>('storage');
const [probeTab, setProbeTab] = useState<ProbeTab>('accesses');
const [diagOpen, setDiagOpen] = useState(false);

useEffect(() => {
const client = createCompilerClient(() => new CompilerWorker());
Expand Down Expand Up @@ -324,6 +327,26 @@ export function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [result, selected, pinnedIndex, executeFacts]);

const session = useExecutionSession({
artifact: selected !== null && selected.deployable ? selected : null,
replay: pendingReplay,
onReplayDone: () => setPendingReplay(null),
onTraceCursorPc: setTracePc,
onSessionFacts: setExecuteFacts,
onCallCompleted: (success) => {
setProbeMode('trace');
setProbeTab(success ? 'accesses' : 'forensics');
},
});

// A fresh compile returns the probe to the static picture and surfaces
// diagnostics automatically when any of them is an error.
useEffect(() => {
setProbeMode('storage');
setProbeTab('accesses');
setDiagOpen(result !== null && result.diagnostics.some((d) => d.severity === 'error'));
}, [result]);

const relationIndex = useMemo(
() => (selected !== null && selected.deployable ? buildRelationIndex(selected) : null),
[selected],
Expand All @@ -332,6 +355,11 @@ export function App() {
// One contract, one hue — shared by the editor spine and the storage grid.
const { ownerSuffixByContract, ownerSpans } = useOwnerHues(result);

const ownerClassByContract = useMemo(
() => new Map([...ownerSuffixByContract].map(([name, suffix]) => [name, `owner-${suffix}`])),
[ownerSuffixByContract],
);

// Global shortcuts: Ctrl/Cmd+K palette, Ctrl/Cmd+Enter compile.
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
Expand Down Expand Up @@ -497,7 +525,58 @@ export function App() {
<span className="ss-pane-meta">
MAIN.SOL{result !== null ? ` · ${result.artifacts.length} UNITS` : ''}
</span>
{result !== null && result.artifacts.length > 0 && (
<label className="ss-source-contract">
contract{' '}
<select
aria-label="Contract"
value={selectedId ?? ''}
onChange={(event) => {
setSelectedId(event.target.value || null);
setPinnedIndex(null);
setHoverIndex(null);
}}
>
{result.artifacts.map((artifact) => (
<option key={artifact.id} value={artifact.id}>
{artifact.fullyQualifiedName}
{artifact.deployable ? '' : ' (not deployable)'}
</option>
))}
</select>
</label>
)}
{result !== null && (
<button
type="button"
className="ss-diag-toggle"
aria-expanded={diagOpen}
onClick={() => setDiagOpen(!diagOpen)}
>
DIAG · {result.diagnostics.length}
</button>
)}
</div>
{result !== null && diagOpen && (
<div role="region" aria-label="Diagnostics" className="ss-diag-strip">
{result.diagnostics.length === 0 ? (
<p>No diagnostics.</p>
) : (
<ol>
{result.diagnostics.map((diagnostic, index) => (
<li key={index}>
{diagnostic.severity} {diagnostic.code ?? ''}: {diagnostic.message}
{diagnostic.location !== null
? ` (bytes ${diagnostic.location.byteStart}–${
diagnostic.location.byteStart + diagnostic.location.byteLength
})`
: ' (whole file)'}
</li>
))}
</ol>
)}
</div>
)}
<SourceEditor
value={source}
onChange={setSource}
Expand All @@ -523,17 +602,33 @@ export function App() {
onHoverInstruction={setHoverIndex}
onTogglePin={togglePin}
/>
<StorageGrid
key={`sto-${result.jobId}`}
<ProbePane
result={result}
artifact={selected}
declaringContracts={result.declaringContracts}
session={session}
mode={probeMode}
onMode={setProbeMode}
tab={probeTab}
onTab={setProbeTab}
ownerSuffixByContract={ownerSuffixByContract}
ownerClass={ownerClassByContract}
ownerChipText={ownerChipText}
/>
<RuntimeStorage resolutions={traceResolutions} />
</>
)}
</div>

{result !== null && session.trace !== null && (
<CockpitBar
trace={session.trace}
outcome={session.outcome}
cursor={session.cursor}
onCursor={session.setCursor}
/>
)}

{result !== null && <ExecuteStrip artifact={selected} session={session} />}

{result === null && (
<p className="ss-empty-hint" data-testid="empty-hint">
Compile to light the instrument: the storage grid, execution, sharing, and challenges all
Expand All @@ -543,60 +638,6 @@ export function App() {

{result !== null && (
<div className="ss-lower">
<section aria-label="Diagnostics" className="ss-pane">
<h2>Diagnostics</h2>
{result.diagnostics.length === 0 ? (
<p>No diagnostics.</p>
) : (
<ol>
{result.diagnostics.map((diagnostic, index) => (
<li key={index}>
{diagnostic.severity} {diagnostic.code ?? ''}: {diagnostic.message}
{diagnostic.location !== null &&
` (bytes ${diagnostic.location.byteStart}–${
diagnostic.location.byteStart + diagnostic.location.byteLength
})`}
</li>
))}
</ol>
)}
</section>

<section aria-label="Contracts" className="ss-pane">
<h2>Contracts</h2>
{result.artifacts.length === 0 ? (
<p>No contract artifacts.</p>
) : (
<label>
Contract{' '}
<select
value={selectedId ?? ''}
onChange={(event) => {
setSelectedId(event.target.value || null);
setPinnedIndex(null);
setHoverIndex(null);
}}
>
{result.artifacts.map((artifact) => (
<option key={artifact.id} value={artifact.id}>
{artifact.fullyQualifiedName}
{artifact.deployable ? '' : ' (not deployable)'}
</option>
))}
</select>
</label>
)}
</section>

<ExecutePane
artifact={selected}
onTraceCursorPc={setTracePc}
onResolutions={setTraceResolutions}
onSessionFacts={setExecuteFacts}
replay={pendingReplay}
onReplayDone={() => setPendingReplay(null)}
/>

<section aria-label="Share" className="ss-pane">
<h2>Share</h2>
{shareSize !== null && (
Expand Down Expand Up @@ -630,15 +671,14 @@ export function App() {
}}
/>
</label>
{shareUrl !== null && (
<input
readOnly
value={shareUrl}
aria-label="Share URL"
data-testid="share-url"
size={80}
/>
)}
<input
readOnly
value={shareUrl ?? ''}
placeholder="Copy share link to mint a URL"
aria-label="Share URL"
data-testid="share-url"
size={80}
/>
</section>

<ChallengePane
Expand Down
Loading