LearnNewsExamplesServices
Frontmatter
title>-
authorneo-opus-grace
stateClosed
createdAtAug 23, 2026, 3:05 AM
updatedAtAug 23, 2026, 4:06 AM
closedAtAug 23, 2026, 4:06 AM
mergedAt
branchesdev ← feat/17427-ac4-derived-flush-bound
urlhttps://github.com/neomjs/neo/pull/17592
contentTrust
projected
quarantined0
signals[]
Closed
neo-opus-grace
neo-opus-grace commented on Aug 23, 2026, 3:05 AM

grid.Body's createViewData flush set updateDepth = -1. Per the config's own contract, that means "include the full tree of any depth" — the entire Body subtree is sent every flush — and hasUpdateCollision additionally treats -1 as colliding with every distance, absorbing any pending descendant update in flight. This replaces it with the depth TreeBuilder's ancestor contract derives for exactly what the flush rebuilds.

⚠️ Framing corrected after an operator challenge — read this before the rest. An earlier revision of this body called -1 a defect that "dragged unrelated pending child updates into its cycle". I could not support that, and the challenge was fair: is -1 a bug, or a batching optimisation that speeds updates up? What I actually had was grid/View.mjs's comment asserting it "destabilises the TreeGrid" — inherited framing I never tested — plus a reach contract that says what a bound should be if one is wanted, not that -1 is wrong. So I measured instead of arguing.

Measurement — 15-step scripted scroll of examples/grid/bigData, counting real executeVdomUpdate entries:

bound Body cycles Row cycles end state
-1 (current dev) 30 0 scrollTop 7500, mountedRows [229,260]
ROW_REACH + maxCellDepth 30 0 scrollTop 7500, mountedRows [229,260]

Identical. So -1 is not buying batching here, and the honest reason is structural: createViewData writes every row with silent: true, so the flush generates no pending child updates of its own — the row cycle count is 0 in both arms across 31 pooled rows. An infinite collision radius has nothing to absorb on this path. What it can still absorb is unrelated work that happens to be in flight, which is a timing coupling rather than a speedup.

So the defensible claim is narrower than "fixes a bug", and this PR now makes only that claim: equal in cycles, narrower in payload and in absorption, and derived from a documented contract rather than chosen. For flat cells the two bounds send the same tree, which is why the cycle counts match; for a grid whose cells nest app content, -1 would send that content every scroll frame while the derived bound stops at the cell chain.

Scope of the measurement, stated so it is not over-read: one example, flat cells (maxCellDepth 1), no concurrent app-driven updates during the scroll, and it does not measure the merge-bypass risk under #17589. A grid with nested cell containers or concurrent updates could differ, and I have not measured that.

This closes the split-out leaf #17591, deliberately NOT #17427. The ticket has five ACs and three of them are @neo-opus-ada's neo-side reproduction, with AC-2 ("the repro fails on dev before the fix and passes after") structurally unmeetable by either half alone. A Resolves here would close a ticket whose reproduction does not exist yet. We agreed the shape in A2A: the bound lands now because it is finished and correct, the repro lands when it has a mechanism to drive, and #17427 stays open honestly until then.

Resolves #17591

Refs #17427 Refs #17589

Evidence: L2 achieved (unit suites at exact head; the invariant has no CI-reachable behavioural witness — see below) → L3 required (AC-1/AC-3 rendered remap witness). Residual: the repro that would make AC-2 checkable, and the -1 unmasking risk, Residual-Owner: #17589.

Deltas

Body.mjs — the flush derives a finite bound.

me.updateDepth = ROW_REACH + (me.gridContainer?.maxCellDepth ?? 1);

The bound must reach cells, and that is provable rather than analogous. Every row is written with silent: true — both the populated pass and the record: null hide pass — so no Row queues an update of its own and this flush is the sole committer of the row and cell changes just made. A Row-only bound would drop cell content silently, which is the bad direction of error.

Container.mjs — maxCellDepth moves here. It is a property of the column set, not of whichever component measures. View and Body have no shared ancestor (container/Base vs component/Base), so the alternative was a duplicated getter — and two consumers bounding against one derivation from different distances is exactly where a second copy drifts silently. View now delegates.

The landmark is cited, not re-derived. TreeBuilder#getComponentDepth already documents the envelope an ancestor caller needs — distanceToComponent + getComponentDepth(component) — and warns that a literal "would silently become a snapshot of whatever nesting happened to ship". A Row is Body's direct child, so distanceToComponent is 1 and getComponentDepth(row) is 1 + maxCellDepth, giving 2 + maxCellDepth. The same contract resolves to 3 for View one level higher; neither ports the other's arithmetic. Named ROW_REACH rather than View's ROW_DISTANCE because the value is not a distance — it is distance-to-row + 1 under a strict <.

Surfaced by @neo-gpt-emmy's #17581: list.Buffered violates the same contract in the opposite direction, passing getComponentDepth(component) without distanceToComponent, and her live 500-record 2 → 3 falsifier is this arithmetic observed in a sibling component. Three consumers reached one rule; it deserved citing once rather than re-deriving three times.

AC Evidence

AC proof
AC-1 The flush carries a finite bound; -1 is gone from this path. MET, scoped precisely. Body.mjs sets ROW_REACH + (gridContainer?.maxCellDepth ?? 1), and createViewData's flush no longer sets -1. To be exact rather than flattering: git grep -- 'updateDepth = -1' src/grid/ is not empty — header/Toolbar.mjs and plugin/AnimateRows.mjs each still carry one. Both are outside createViewData's flush and outside this ticket, which scopes the AC to this path; neither is touched here, and whether they warrant the same treatment is unexamined and deliberately not claimed.
AC-2 The bound states what it is derived FROM, and no number is chosen by widening until green. MET. It resolves TreeBuilder's documented distanceToComponent + getComponentDepth(component) against Body's own distance to the row (1), giving 2 + maxCellDepth; the constant's docblock carries that derivation and the silent: true sole-committer reason for cell reach. No value in this diff was obtained by raising it until a suite passed — and the suite could not have told me either way (see Test Evidence).
AC-3 maxCellDepth has exactly one definition; View and Body consume it. MET. Defined once on grid.Container; View's accessor delegates; Body reads it through its own gridContainer. git grep 'get maxCellDepth' returns two hits — the definition and the delegating accessor — and no second derivation.
AC-4 Suites stay green, and the PR states plainly that this green does not certify the bound. MET. 71 grid / 219 grid+tree at exact head, with the both-directions mutation measured and reported below rather than assumed. The blindness is stated as the headline of that section, not a footnote.

On #17427, from which this was split: its AC-1/AC-3 (@neo-opus-ada's neo-side reproduction) and AC-2 (fails on dev before the fix, passes after) are not delivered here and cannot be — AC-2 needs both halves in one tree, and the repro lost its candidate mechanism to #17589 and is back to discovery. #17427 stays open for exactly that. Its AC-4/AC-5 are the substance this PR carries, and AC-5 is now met by measurement rather than argument: #17589's Probe B measured what -1 actually does through the merge path, so "the bound must stay finite" is a citation instead of a reading of the predicate's source.

Test Evidence

Evidence: exact-head unit suites —

npm run test-unit -- grid        → 71 passed
npm run test-unit -- grid tree   → 219 passed

⚠️ That green certifies the swap, not the defect, and I measured which. Mutating in both directions leaves the suite unmoved:

bound grid specs
ROW_REACH + maxCellDepth (shipped) 71 passed
ROW_REACH alone — cell reach dropped 71 passed
-1 — the original defect restored 71 passed

The suite is blind in both directions: it cannot see the defect being fixed, and it cannot see an under-reach that would leave cells stale. So neither the 71 nor the 219 is evidence the bound is correct — they are evidence nothing regressed. The bound's correctness rests on the cited contract and on #17581's independent live falsifier in a sibling component, not on this suite. Stating that plainly because a reviewer reading "219 passed" would otherwise reasonably infer more than it supports.

Post-Merge Validation

  • The repro (AC-1/AC-3) lands separately when it has a mechanism; #17427 stays open until then and is not closed by this PR.
  • Unmasking risk, owned by #17589: today's -1 forces full expansion regardless of merge accounting, so replacing it with a finite bound removes that mask. If #17589's merge-bypass is real, stale cells it was hiding could surface — a green-to-red that would look like this PR caused a regression when it exposed one. That is why the residual names #17589 rather than this PR's close target.

Authored by @neo-opus-grace (Claude Opus 5, Claude Code)

Origin Session ID: 1b0d28eb-3461-40b6-bb35-88d6bf09ec94

Closing — I have no working oracle for the property this change could break

@tobiu challenged whether updateDepth = -1 is a defect or a feature, and named the mechanism I had never established: for component-based cells, if they do not update on scroll, the content goes stale. That is what -1 guarantees on this path, and I changed it without demonstrating the guarantee was unnecessary.

The measurements, including the one that invalidates the others

examples/grid/bigData carries two component cells — a Button (firstname + ' ++') and a NestedCell one boundary deeper — and states its own invariant in a comment: "Both are fed from firstname, so the two columns and the plain firstname cell must agree at every scroll position." That is a staleness oracle, so I used it across five scroll positions.

arm bound mismatches
A -1 (current dev) 0
B ROW_REACH + maxCellDepth (this PR) 0
C ROW_REACH alone — cell reach deliberately dropped 0

Arm C is the finding. A bound that cannot reach the cells at all produces no mismatch either, so the oracle does not discriminate — and Arms A and B therefore establish nothing about this change. I would have shipped on B's green had I not run C.

An earlier cycle-count measurement (30 update cycles under either bound) is equally uninformative: it counts how many cycles ran, never whether their payload reached the component cells.

Why this closes rather than continues

I do not have evidence that this change is harmful. I have no evidence that it is safe either, and the difference matters on a flagship widget's scroll path. The grounding I actually had was: grid/View.mjs's comment asserting -1 "destabilises the TreeGrid" — inherited framing I never tested — plus TreeBuilder's reach contract, which says what a bound should be if one is wanted, not that the existing mechanism is wrong. My own mutation table already showed the unit suite cannot distinguish the three variants, and now the e2e oracle cannot either.

cellDepth is published once per cell component at creation (grid/column/Component.mjs:181, Math.max(…, TreeBuilder.getComponentDepth(component))), so 2 + maxCellDepth is sufficient only while every component cell has been measured and its subtree was complete at measurement time. -1 — "include the full tree of any depth" per the config contract — carries no such precondition. I did not establish that the precondition holds before replacing the mechanism that does not need it.

Disposition

PR closed, feat/17427-ac4-derived-flush-bound abandoned, nothing landed, dev untouched. #17591 closed as not-planned: its premise — that -1 here is a defect to be replaced by a derived bound — is unsupported, and the component-cell staleness guarantee is a positive reason it is correct where it is.

#17427 is unaffected and keeps all five ACs, AC-4/AC-5 included; they remain open work rather than something this PR delivered.

What the next attempt needs before touching this path

A discriminating oracle, first — one demonstrated to go red when the bound cannot reach a component cell. Arm C is the control it must pass. Until such a witness exists, no bound change on this path can be evaluated, including a repeat of this one. Beyond that: when cellDepth is published relative to a cell's subtree being built; whether grid.View's existing finite bound carries the same exposure and simply has not been hit; and what the scroll transaction actually sends for a component cell versus a flat one.

Origin Session ID: 1b0d28eb-3461-40b6-bb35-88d6bf09ec94


neo-opus-grace
neo-opus-grace commented on Aug 23, 2026, 4:06 AM