diff --git a/apps/web/e2e/storage.spec.ts b/apps/web/e2e/storage.spec.ts index e315b19..ddd3365 100644 --- a/apps/web/e2e/storage.spec.ts +++ b/apps/web/e2e/storage.spec.ts @@ -180,6 +180,15 @@ test('a reserved slot shows the value the constructor put in it', async ({ page 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"'); + + // A single push touches two slots (the length it rewrites, plus the new + // element) but names only one derived entry — the length slot is the + // reserved slot itself, not a derived one, and must not be double-counted. + const historyRecord = grid + .locator('[data-testid="storage-record"]') + .filter({ hasText: 'history' }); + await expect(historyRecord).toContainText('1 entry seen'); + await expect(historyRecord).not.toContainText('2 entries seen'); }); test('the default source shows its reserved slots, then their values on deploy', async ({ diff --git a/apps/web/src/storage/StorageGrid.tsx b/apps/web/src/storage/StorageGrid.tsx index a0e3663..1b42542 100644 --- a/apps/web/src/storage/StorageGrid.tsx +++ b/apps/web/src/storage/StorageGrid.tsx @@ -276,8 +276,11 @@ export function StorageGrid({ // prose that used to live under "Not expanded". const head = row.segments[0]; const referenceSpan = head?.kind === 'reference' ? head.span : null; - const derivedSeen = - referenceSpan === null ? 0 : (runtime?.derivedCountByBase.get(referenceSpan.path) ?? 0); + // The same count that drives the child rows below, so the chip and + // the rows can never disagree. Counting resolutions by baseVariable + // instead double-counted a dynamic array's own length slot (read on + // entry, written on exit) as if it were a second entry. + const derivedSeen = children.length; const recordCells = row.segments .filter((segment) => segment.kind === 'value') .map((segment) => (segment as Extract).cell) diff --git a/apps/web/src/storage/runtimeView.test.ts b/apps/web/src/storage/runtimeView.test.ts index 8d63e43..70c9e44 100644 --- a/apps/web/src/storage/runtimeView.test.ts +++ b/apps/web/src/storage/runtimeView.test.ts @@ -1,4 +1,4 @@ -import type { SemanticResolution, StorageAccessObservation } from '@slotscope/domain'; +import type { StorageAccessObservation } from '@slotscope/domain'; import { describe, expect, it } from 'vitest'; import { buildRuntimeView } from './runtimeView'; @@ -17,28 +17,10 @@ function trace(storage: StorageAccessObservation[]) { return { observations: { keccak: [], storage } }; } -function resolution(partial: Partial): SemanticResolution { - return { - accessId: 'step-0', - stepIndex: 0, - kind: 'sstore', - status: 'exact', - label: null, - decodedValues: [], - reasoning: [], - baseVariable: null, - accessCostNote: null, - migration: null, - previousValueHex: null, - ...partial, - }; -} - describe('buildRuntimeView', () => { it('is empty without a trace', () => { const view = buildRuntimeView(null, [], 0); expect(view.wordsBySlot.size).toBe(0); - expect(view.derivedCountByBase.size).toBe(0); }); it('keys words by decimal slot and lets the last observation win', () => { @@ -79,21 +61,6 @@ describe('buildRuntimeView', () => { ); expect(view.wordsBySlot.get('1')).toBe(`${'0'.repeat(63)}4`); }); - - it('counts distinct derived entries per base variable', () => { - const view = buildRuntimeView( - trace([]), - [ - resolution({ baseVariable: 'balances', label: 'balances[0x01]' }), - resolution({ baseVariable: 'balances', label: 'balances[0x01]' }), - resolution({ baseVariable: 'balances', label: 'balances[0x02]' }), - resolution({ baseVariable: null, label: 'raw' }), - ], - 9, - ); - expect(view.derivedCountByBase.get('balances')).toBe(2); - expect(view.derivedCountByBase.has('raw')).toBe(false); - }); }); describe('address-space filter (v1.5)', () => { diff --git a/apps/web/src/storage/runtimeView.ts b/apps/web/src/storage/runtimeView.ts index 4c9987e..cd63057 100644 --- a/apps/web/src/storage/runtimeView.ts +++ b/apps/web/src/storage/runtimeView.ts @@ -3,8 +3,6 @@ import type { SemanticResolution, TraceObservations } from '@slotscope/domain'; export interface StorageRuntimeView { /** Decimal slot → the last 32-byte word observed at or before the cursor. */ readonly wordsBySlot: ReadonlyMap; - /** Base variable → how many distinct derived entries this run has named. */ - readonly derivedCountByBase: ReadonlyMap; /** Raw Layer-1 facts, so the grid can reconstruct derived slots itself. It * needs the declared-slot set to do that, which only the grid knows. */ readonly observations: TraceObservations | null; @@ -13,7 +11,6 @@ export interface StorageRuntimeView { const EMPTY: StorageRuntimeView = { wordsBySlot: new Map(), - derivedCountByBase: new Map(), observations: null, resolutions: [], }; @@ -56,16 +53,5 @@ export function buildRuntimeView( } } - const derivedLabels = new Map>(); - for (const resolution of resolutions) { - const base = resolution.baseVariable; - if (base === null) continue; - const labels = derivedLabels.get(base) ?? new Set(); - labels.add(resolution.label ?? resolution.accessId); - derivedLabels.set(base, labels); - } - const derivedCountByBase = new Map(); - for (const [base, labels] of derivedLabels) derivedCountByBase.set(base, labels.size); - - return { wordsBySlot, derivedCountByBase, observations, resolutions }; + return { wordsBySlot, observations, resolutions }; }