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 @@ -87,3 +87,4 @@
{"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."}}
{"id":"int-d4e8aa7f76623df676964dd1a4c2a317","kind":"field_change","created_at":"2026-07-30T14:22:32.928557923Z","actor":"halaprix","issue_id":"slotscope-02a","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"buildStaticIndex now indexes nested reference heads from the decoded model's collapsed spans. Verified end to end: a write to a mapping inside a struct resolves exact as nest.inner[0xaa..] where it previously reported unresolved (test added to chains.test.ts, confirmed failing first). 236/236 unit, 61/61 e2e, fixtures no drift, release:verify passed. Committed on fix/nested-reference-heads; PR held until #4 merges, since the fix reads CollapsedStorageSpan.encoding which #4 introduces."}}
24 changes: 24 additions & 0 deletions packages/semantics/src/chains.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ contract Corpus {
uint256[] internal arr; // slot 2
uint64[] internal packedArr; // slot 3
mapping(bytes4 => uint256) internal mb; // slot 4
Nested internal nest; // slot 5..6

struct Pair { uint256 a; uint256 b; }
struct Nested { uint256 head; mapping(address => uint256) inner; }

function approve(address owner_, address spender, uint256 value) external {
allowance[owner_][spender] = value;
Expand All @@ -24,6 +26,7 @@ contract Corpus {
function setArr(uint256 i, uint256 v) external { arr[i] = v; }
function pushPacked(uint64 v) external { packedArr.push(v); }
function setMb(bytes4 k, uint256 v) external { mb[k] = v; }
function setNested(address k, uint256 v) external { nest.inner[k] = v; }
}
`;

Expand Down Expand Up @@ -140,3 +143,24 @@ describe('keccak-chain resolution (S2, roadmap acceptance)', () => {
expect(dataWrite!.decodedValues[0]!.path).toBe('packedArr[0]');
});
});

describe('reference heads nested inside a struct', () => {
it('grounds a mapping declared inside a struct, instead of giving up', async () => {
const { executor, address, layout } = await deployedCorpus();
const result = await executor.call(
{
toHex: address,
dataHex: encodeCall(fn('setNested', ['address', 'uint256']), [OWNER, '42']),
},
{ captureTrace: true },
);
expect(result.success).toBe(true);
const write = resolveAccesses(result.observations!, layout).find((r) => r.kind === 'sstore')!;
expect(write.status).toBe('exact');
expect(write.label).toBe(`nest.inner[${OWNER}]`);
expect(write.baseVariable).toBe('nest.inner');
expect(write.decodedValues).toEqual([
{ path: write.label, typeLabel: 'uint256', display: '42' },
]);
});
});
8 changes: 8 additions & 0 deletions packages/semantics/src/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ export function buildStaticIndex(layout: RawStorageLayout | null): StaticLayoutI
try {
const model = decodeStorageLayout(layout, () => null);
for (const [key, cells] of groupCellsBySlot(model.items)) cellsBySlot.set(key, cells);
// Reference heads nested inside a struct or an array element are not
// top-level layout entries, so `layout.storage` alone cannot name them and
// their keccak chains resolve as unresolved. The decoded model carries
// every one of them with its exact base slot and dotted path.
for (const item of model.items) {
if (item.kind !== 'collapsed' || item.encoding === 'inplace') continue;
variableBaseSlots.set(item.path, { slot: item.startSlot, typeId: item.typeId });
}
} catch (error) {
if (!(error instanceof LayoutDecodeError)) throw error;
// A malformed layout resolves nothing; accesses degrade to unresolved.
Expand Down