View.expand (compilers/openapi/internal/nodeview/nodeview.go) memoizes a mapping's merge expansion, and returns a cached entry as complete regardless of the depth the caller reached it at:
if cached, ok := v.pairs[n]; ok { return cached, true }
...
if depth > MergeDepthLimit { v.exhausted = true; return nil, false }
A node first expanded at a shallow depth is memoized; a later walk reaching that same node deeper than MergeDepthLimit allows is then served from the memo instead of truncating. So the answer depends on what was expanded before it, which makes a View unsafe to share across independent walks — the memo is not a pure cache of a pure function, because the function it caches is depth-sensitive and the key is not.
Why this is worth a root-cause issue
Two instances are already recorded, and both were found the hard way:
The reverted change is the cost side. declaresResourceIDAbove now rebuilds a View per call purely because sharing one is unsafe, which is the opposite of what the memo exists for. #338 asks for fewer rescans over the same nodes; that cannot be answered while a shared view can change an answer.
Reproducer
mergeBoundOrderSpec in compilers/openapi/internal/schema/schema_test.go writes the document: anchors c0..c50 form a 51-link chain, d0..d20 ride on c50 for 71 total, and two schemas walk it from different depths. TestDynamicRef_ResourceBoundaryVerdictIsOrderInvariant pins the current per-call behaviour; making the view outlive one walk reddens it.
Shape of a fix
The memo needs to record what it is valid for, not just what it produced — for example keying on (node, remaining depth budget), or storing the depth an entry was computed at and recomputing when a shallower budget would truncate. Alternatively expand could refuse to serve a cached entry to a caller whose remaining budget is smaller than the entry's own expansion consumed. Either makes a shared view safe and lets #338 proceed.
Found while probing #362.
View.expand(compilers/openapi/internal/nodeview/nodeview.go) memoizes a mapping's merge expansion, and returns a cached entry ascompleteregardless of the depth the caller reached it at:A node first expanded at a shallow depth is memoized; a later walk reaching that same node deeper than
MergeDepthLimitallows is then served from the memo instead of truncating. So the answer depends on what was expanded before it, which makes aViewunsafe to share across independent walks — the memo is not a pure cache of a pure function, because the function it caches is depth-sensitive and the key is not.Why this is worth a root-cause issue
Two instances are already recorded, and both were found the hard way:
$dynamicRefverbatim declared one way and expanded it across a resource boundary declared the other.The reverted change is the cost side.
declaresResourceIDAbovenow rebuilds aViewper call purely because sharing one is unsafe, which is the opposite of what the memo exists for. #338 asks for fewer rescans over the same nodes; that cannot be answered while a shared view can change an answer.Reproducer
mergeBoundOrderSpecincompilers/openapi/internal/schema/schema_test.gowrites the document: anchorsc0..c50form a 51-link chain,d0..d20ride onc50for 71 total, and two schemas walk it from different depths.TestDynamicRef_ResourceBoundaryVerdictIsOrderInvariantpins the current per-call behaviour; making the view outlive one walk reddens it.Shape of a fix
The memo needs to record what it is valid for, not just what it produced — for example keying on (node, remaining depth budget), or storing the depth an entry was computed at and recomputing when a shallower budget would truncate. Alternatively
expandcould refuse to serve a cached entry to a caller whose remaining budget is smaller than the entry's own expansion consumed. Either makes a shared view safe and lets #338 proceed.Found while probing #362.