Context
VDomUpdate.getAdjustedUpdateDepth() widens an owner's update depth to cover every child that merged into its cycle, poisoning to -1 when any child needs a full subtree. Its dispatch-site call was commented out on 2026-01-20 in 9e2a82ee57 (a WIP batching commit that shipped the allowlist half and disabled the depth half). Seven months, no design record, and the function's only live callers were four assertions that invoke it directly — so it stayed green while nothing in production applied its answer.
This restores the call and adds the dispatch-path coverage that would have caught its removal.
Evidence: MergedUpdateDepth.spec.mjs:172 is red on unmodified dev at fd742cdd71 and green after the fix; full unit suite 15192 passed, with 12 pre-existing [unit-brain] reds carrying a named cause and a base-commit control (both below).
The mechanism, corrected
The ticket attributed the loss to TreeBuilder pruning alone. Measured, it needs both halves, and neither is sufficient on its own:
- The owner's tree prunes at its own finite depth, so the merged child's subtree is not expanded.
executeVdomUpdate()'s collision filter then deletes the merged child's own payload as "covered by the parent" — parentDepth > distance — and that payload was the thing that would have carried the subtree.
The compensation is what makes the coverage claim in step 2 true. Without it the filter deletes a payload nothing replaced. Probe on dev, owner at depth 2 with a child that merged at -1:
after merge {"ownerDepth":2,"adjusted":-1,"mergedChildren":["p-branch"]}
batch keys ["p-owner"] <- branch payload deleted
owner payload ... "cn":[{"componentId":"p-twig","neoIgnore":true}] <- pruned at distance 2
deltas [{"style":{"color":"green"},"id":"p-owner"}] <- p-leaf's text change is GONEcanMergeUpdate() returns true unconditionally, so the merge is never refused on depth grounds — sound only if the compensation runs.
grid/Body.mjs:832 (me.updateDepth = -1; me.update()) is the production registrant this protects.
A finding that changes the fixture contract
beforeSetUpdateDepth() (src/component/Base.mjs:1104) only ever widens:
return oldValue === -1 || value === -1 ? -1 : Math.max(value, oldValue)
Mounting leaves components at -1, so a spec that assigns updateDepth = 2 after mount is silently clamped back to -1 and ends up testing an owner that already requested the whole tree. My first fixture did exactly that and was green in all three arms for a reason unrelated to merged depth. getVdomUpdatePayload() resets _updateDepth to the class default after each payload build, so a warm-up flush is the only way to reach a finite depth. This is documented on createTree() because the next author will hit it.
AC Evidence
| AC |
Proof |
| AC-1 |
src/mixin/VdomLifecycle.mjs:1030-1040 — getAdjustedUpdateDepth(me.id) is applied before the payload is built, and the commented-out block is replaced rather than left beside the live call. |
| AC-2 |
MergedUpdateDepth.spec.mjs:172 drives update() -> updateVdom() -> executeVdomUpdate() and reads deltas. Red on unmodified dev at fd742cdd71 (git diff --stat src/ empty) at expect(twigNode?.tag).toBeDefined(); green after the fix. Both runs under Test Evidence. |
| AC-3 |
MergedUpdateDepth.spec.mjs:216 — a child merging at depth 1 leaves max(2, 1+1) = 2 unchanged; asserts the twig is still a pruned placeholder (componentId set, tag undefined) and the leaf stays absent. Green before and after, so the fix cannot silently widen every merged update into a full-tree update. |
| AC-4 |
Satisfied by the AC's own "or an added sibling does": MergedUpdateDepth.spec.mjs is that sibling. The four direct-call arms keep their value as unit coverage of the manager's arithmetic; AsymmetricUpdates.spec.mjs:43 now carries a describe-level docblock stating they deliberately do not cover the wiring and naming where that half lives, so the false-coverage reading is not re-derived. |
| AC-5 |
Payload table below, measured on a 122-component grid.Body-shaped tree — not asserted. |
Payload size
Measured on a grid.Body-shaped tree (owner -> body -> 20 rows x 5 cells = 122 components), one cell's text changed, bytes of the batch handed to the VDOM worker:
| owner reach |
bytes |
carries the change |
| depth 2 (pre-fix) |
2,893 |
no |
-1 (post-fix) |
32,206 |
yes |
| finite depth 4 (tightest bound that reaches the cell) |
32,206 |
yes |
The 11.1x is against a payload that was cheap because it was wrong. Against the tightest correct alternative the fix costs 1.00x — depth is a uniform radius, not a path, so any bound reaching distance-3 cells expands all 20 rows. So -1 poisoning carries no payload penalty over a correct finite bound here, and the sparse saving that remains comes from the mergedChildIds allowlist, not the depth term.
Scope of that claim: measured for this shape, and it is the shape #17427 is about. Per the ticket's Probe A the compensation is inert for finite registrants, so no other case changes.
Test Evidence
test/playwright/unit/vdom/MergedUpdateDepth.spec.mjs — 3 arms.
- On unmodified
dev (fd742cdd71): 1 failed, 2 passed. The failure is arm 2, at expect(twigNode?.tag).toBeDefined() — the twig pruned.
- With the fix: 3 passed.
Both controls are load-bearing and both pass on dev: the positive control (an owner declaring -1 itself) proves the oracle can observe a distance-3 leaf delta at all, and arm 3 proves it goes to "absent" when the bound genuinely cannot reach. Without the first, "no leaf delta" would be indistinguishable from a fixture that never produces one.
test/playwright/unit/vdom/ — 159 passed, 0 failed.
Full unit suite (--workers=1 --retries=0) — 15192 passed, 12 failed, 11 skipped (12.7m). All 12 reds are [unit-brain], all under ai/, none in vdom/ or anywhere this diff reaches — and they are not mine, with a named cause and a control:
authorityLeaseBoot.spec.mjs:25 (1 arm)
HostEdgePosture.spec.mjs:303,311 (2, one of them a POSITIVE CONTROL)
genesisProbe.spec.mjs:567 (1)
scriptPlaneClosure.spec.mjs:887,899,917,978 (4)
hostBarrelRuntimeReach.spec.mjs:105,121,170 (3, one of them a CONTROL)
HealthService.providerReady.spec.mjs:741 (1)
That three of them are control arms is the tell: controls fail when the harness cannot reach the code under test. Cause, from the subprocess stderr rather than inferred:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '<worktree>/ai/config.mjs'
imported from '<worktree>/ai/daemons/orchestrator/daemon.mjs'
These arms spawnSync a real boot with cwd: REPO_ROOT, and REPO_ROOT = process.cwd() (HostEdgePosture.spec.mjs:35) is the linked worktree, which by design does not carry the gitignored operator overlay ai/config.mjs (ADR-0019 §2.1). The spawned process cannot resolve it, so the controls cannot reach the code under test — exactly what they exist to report.
Control: a detached worktree at the same base commit fd742cdd71, zero modifications (git status clean), nothing else running, reproduces all 12 — same specs, same line numbers. So they are independent of this diff, and the one that worried me (genesisProbe:567 binds a port, and I had run concurrent suites) fails there in isolation too, ruling out my own interference.
CI runs from a full checkout and observes the real behaviour, so there is no unobserved residual here and nothing to file. I am not fabricating the overlay to make them green — its absence is the design.
Deltas
src/mixin/VdomLifecycle.mjs — restore the widening at the dispatch site; the comment states the invariant (the collision filter's coverage claim) rather than the incident.
test/playwright/unit/vdom/MergedUpdateDepth.spec.mjs — new, 3 arms plus the warm-up contract.
test/playwright/unit/vdom/AsymmetricUpdates.spec.mjs — describe-level docblock only; no assertion changed.
Post-Merge Validation
- Watch
grid/ and list/ suites on dev: grid.Body is the only production -1 registrant today, so it is the only surface whose payloads change shape.
- The payload table above is the baseline for #17427's finite-bound work: if
Body.mjs:832 later computes a real bound, the correct comparison is against 32,206 bytes, not against the 2,893-byte broken payload.
Resolves #17589
Authored by ⚖️ Ada · @neo-opus-ada · Claude Opus 5 · Claude Code
Context
VDomUpdate.getAdjustedUpdateDepth()widens an owner's update depth to cover every child that merged into its cycle, poisoning to-1when any child needs a full subtree. Its dispatch-site call was commented out on 2026-01-20 in9e2a82ee57(a WIP batching commit that shipped the allowlist half and disabled the depth half). Seven months, no design record, and the function's only live callers were four assertions that invoke it directly — so it stayed green while nothing in production applied its answer.This restores the call and adds the dispatch-path coverage that would have caught its removal.
Evidence:
MergedUpdateDepth.spec.mjs:172is red on unmodifieddevatfd742cdd71and green after the fix; full unit suite 15192 passed, with 12 pre-existing[unit-brain]reds carrying a named cause and a base-commit control (both below).The mechanism, corrected
The ticket attributed the loss to
TreeBuilderpruning alone. Measured, it needs both halves, and neither is sufficient on its own:executeVdomUpdate()'s collision filter then deletes the merged child's own payload as "covered by the parent" —parentDepth > distance— and that payload was the thing that would have carried the subtree.The compensation is what makes the coverage claim in step 2 true. Without it the filter deletes a payload nothing replaced. Probe on
dev, owner at depth 2 with a child that merged at-1:after merge {"ownerDepth":2,"adjusted":-1,"mergedChildren":["p-branch"]} batch keys ["p-owner"] <- branch payload deleted owner payload ... "cn":[{"componentId":"p-twig","neoIgnore":true}] <- pruned at distance 2 deltas [{"style":{"color":"green"},"id":"p-owner"}] <- p-leaf's text change is GONEcanMergeUpdate()returnstrueunconditionally, so the merge is never refused on depth grounds — sound only if the compensation runs.grid/Body.mjs:832(me.updateDepth = -1; me.update()) is the production registrant this protects.A finding that changes the fixture contract
beforeSetUpdateDepth()(src/component/Base.mjs:1104) only ever widens:return oldValue === -1 || value === -1 ? -1 : Math.max(value, oldValue)Mounting leaves components at
-1, so a spec that assignsupdateDepth = 2after mount is silently clamped back to-1and ends up testing an owner that already requested the whole tree. My first fixture did exactly that and was green in all three arms for a reason unrelated to merged depth.getVdomUpdatePayload()resets_updateDepthto the class default after each payload build, so a warm-up flush is the only way to reach a finite depth. This is documented oncreateTree()because the next author will hit it.AC Evidence
src/mixin/VdomLifecycle.mjs:1030-1040—getAdjustedUpdateDepth(me.id)is applied before the payload is built, and the commented-out block is replaced rather than left beside the live call.MergedUpdateDepth.spec.mjs:172drivesupdate()->updateVdom()->executeVdomUpdate()and reads deltas. Red on unmodifieddevatfd742cdd71(git diff --stat src/empty) atexpect(twigNode?.tag).toBeDefined(); green after the fix. Both runs under Test Evidence.MergedUpdateDepth.spec.mjs:216— a child merging at depth 1 leavesmax(2, 1+1) = 2unchanged; asserts the twig is still a pruned placeholder (componentIdset,tagundefined) and the leaf stays absent. Green before and after, so the fix cannot silently widen every merged update into a full-tree update.MergedUpdateDepth.spec.mjsis that sibling. The four direct-call arms keep their value as unit coverage of the manager's arithmetic;AsymmetricUpdates.spec.mjs:43now carries a describe-level docblock stating they deliberately do not cover the wiring and naming where that half lives, so the false-coverage reading is not re-derived.grid.Body-shaped tree — not asserted.Payload size
Measured on a
grid.Body-shaped tree (owner -> body -> 20 rows x 5 cells = 122 components), one cell's text changed, bytes of the batch handed to the VDOM worker:-1(post-fix)The 11.1x is against a payload that was cheap because it was wrong. Against the tightest correct alternative the fix costs 1.00x — depth is a uniform radius, not a path, so any bound reaching distance-3 cells expands all 20 rows. So
-1poisoning carries no payload penalty over a correct finite bound here, and the sparse saving that remains comes from themergedChildIdsallowlist, not the depth term.Scope of that claim: measured for this shape, and it is the shape #17427 is about. Per the ticket's Probe A the compensation is inert for finite registrants, so no other case changes.
Test Evidence
test/playwright/unit/vdom/MergedUpdateDepth.spec.mjs— 3 arms.dev(fd742cdd71): 1 failed, 2 passed. The failure is arm 2, atexpect(twigNode?.tag).toBeDefined()— the twig pruned.Both controls are load-bearing and both pass on
dev: the positive control (an owner declaring-1itself) proves the oracle can observe a distance-3 leaf delta at all, and arm 3 proves it goes to "absent" when the bound genuinely cannot reach. Without the first, "no leaf delta" would be indistinguishable from a fixture that never produces one.test/playwright/unit/vdom/— 159 passed, 0 failed.Full unit suite (
--workers=1 --retries=0) — 15192 passed, 12 failed, 11 skipped (12.7m). All 12 reds are[unit-brain], all underai/, none invdom/or anywhere this diff reaches — and they are not mine, with a named cause and a control:That three of them are control arms is the tell: controls fail when the harness cannot reach the code under test. Cause, from the subprocess stderr rather than inferred:
These arms
spawnSynca real boot withcwd: REPO_ROOT, andREPO_ROOT = process.cwd()(HostEdgePosture.spec.mjs:35) is the linked worktree, which by design does not carry the gitignored operator overlayai/config.mjs(ADR-0019 §2.1). The spawned process cannot resolve it, so the controls cannot reach the code under test — exactly what they exist to report.Control: a detached worktree at the same base commit
fd742cdd71, zero modifications (git statusclean), nothing else running, reproduces all 12 — same specs, same line numbers. So they are independent of this diff, and the one that worried me (genesisProbe:567binds a port, and I had run concurrent suites) fails there in isolation too, ruling out my own interference.CI runs from a full checkout and observes the real behaviour, so there is no unobserved residual here and nothing to file. I am not fabricating the overlay to make them green — its absence is the design.
Deltas
src/mixin/VdomLifecycle.mjs— restore the widening at the dispatch site; the comment states the invariant (the collision filter's coverage claim) rather than the incident.test/playwright/unit/vdom/MergedUpdateDepth.spec.mjs— new, 3 arms plus the warm-up contract.test/playwright/unit/vdom/AsymmetricUpdates.spec.mjs— describe-level docblock only; no assertion changed.Post-Merge Validation
grid/andlist/suites ondev:grid.Bodyis the only production-1registrant today, so it is the only surface whose payloads change shape.Body.mjs:832later computes a real bound, the correct comparison is against 32,206 bytes, not against the 2,893-byte broken payload.Resolves #17589
Authored by ⚖️ Ada ·
@neo-opus-ada· Claude Opus 5 · Claude Code