Skip to content

fix(interaction): measure the moving-target gate by adoption, not bounds (#3471) - #3548

Merged
thymikee merged 5 commits into
feat/3471-moving-target-judgementfrom
fix/3471-gate-measurements
Oct 13, 2026
Merged

thymikee merged 5 commits into
feat/3471-moving-target-judgementfrom
fix/3471-gate-measurements

Conversation

@thymikee

@thymikee thymikee commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Summary

Part of #3471 (layer 2 of 3), on top of #3538. Judgement defects in the moving-target gate found in #3538's review, plus the disclosure and hint fixes they imply.

  • A @ref look measured the wrong node. measured accepted any capture whose rect agreed with the dispatched one. ADR 0014 keeps the authorized frame node when a captured node's identity differs, so a replacement control that took over the ref at the frame's own coordinates produced two agreeing "observations" while the dispatch went to the stale point. A look now counts only when the reconciliation of the frame model says it read the dispatched node out of the observation (see the provenance bullet below) — the ADR 0014 adoption question, reported rather than re-derived.
  • Rest was granted across surfaces. Screening for stationary compared positions without comparing surfaceBundleId, so a same-identity node on a newly arrived iOS system sheet could excuse both re-observations. It now requires one surface, the line post-action-surface.ts already draws.
  • The two looks that certify rest repeated the same hole. A sheet presented during the sample gap carries the same control at the same rect, and the pair agreed. A measured look now carries its surface, and a look taken on another presentation is demoted to the shape that already means "this look measured nothing": it cannot certify agreement and cannot contribute a reported position.
  • One tolerance comparison, in one place. movingTargetRectsStable and MOVING_TARGET_GATE_RECT_TOLERANCE_PX were a second copy of the 4-field abs-compare against 1 px that rectsWithinTolerance already ran for the post-gesture surface checks. areCapturedRectsAtSamePosition is that comparison once, exported from @agent-device/kernel/rect-center beside normalizeRect and areRectsApproximatelyEqual: it takes a rect pair, normalizes first, and owns the 1 px band. rectsWithinTolerance's five call sites call it directly and the gate calls it too; both local constants and both loops are deleted. A third copy then turned up in review: stableCaptureSignalsEqual, the settle loop's quiet-window comparison, held its own GEOMETRY_TOLERANCE_PX = 1 and its own loop, and it now calls the same helper, so the rule covers the three places that ask the question and no others. That loop rounded each field before comparing, and the rounding was load-bearing: raw geometry compared against 1 px is stricter than what that loop has always run, since a rect quantized to the pixel can sit 1.9 px apart raw while being one pixel apart in the unit the band measures. With no device in this pass to re-tune a timing rule that decides how long every iOS settle waits, the quantization moved to stableCaptureNodeSignal — the one place a signal mints a rect — and the shared comparison runs over integers, which is the old band, kept as a test rather than as a claim. areRectsApproximatelyEqual keeps its 0.5 px band — it asks whether two values denote one point, which is a different question.
  • Ref adoption is reported, not re-derived. reconcileFreshObservation now states what it chose on every branch — adoptedFrom: 'observation' | 'authorized-frame' — riding the ref's ResolvedInteractionTarget, and measureRefObservation reads that value instead of matching node and rect identity a second time. The look's surface also comes from the same capture through surfaceScopedNodes, the way the selector branch reads its resolution's preAction, instead of reading snapshot.iosSystemSurfaceBundleId directly.
  • The suggested recovery could not run. The hint interpolated a raw selector inside single quotes, so label="It's done" taught a broken command. It goes through the kernel's shellQuoteIfNeeded, which also moves the hint beside the refusal that publishes it.

One unmeasured look ends the recheck, so a refusal reports no movedToRect rather than a stale one, and the reason's disclosure says which fields are optional. The screening tier's docblock now states that its claim is an inference from one tree and that the re-observations are the tier carrying evidence. Tests name the outcomes they were accepting-any-rejection about: refNotFound with dispatched: 'no', the withheld movedToRect, and the @ref branch of the hint.

13 files, 781 gross lines. Behaviour is still unreachable in production: no writer of movingTargetBaseline exists until #3539.

Where this layer's evidence is

This layer carries the code and its regression tests. The two evidence classes #3471 asks for live on #3539, and cannot live here:

Validation

Tested at d1e251aa6.

  • pnpm check:affected --run --base feat/3471-moving-target-judgement — result recorded in a comment on this PR.
  • typecheck, lint, format:check, check:layering — clean.
  • vitest run packages/kernel packages/contracts src/daemon src/commands/interaction test/integration packages/selectors packages/ad-script packages/maestro packages/replay — pass.
  • Every new test fails when its guard is reverted, verified by mutation:
    • narrowing the band to 0.5 px reddens a one-pixel shift is capture rounding, not motion;
    • reporting adoptedFrom: 'authorized-frame' on the adopt branch reddens the @ref path takes its own observations, because its resolution spends none;
    • reporting 'observation' on either keep-the-frame branch reddens a ref the second observation cannot re-measure is refused, never self-agreed and a replacement that took the ref at the frame coordinates is refused, not agreed;
    • dropping the screening surface comparison reddens a target newly arrived on a different surface is re-observed, not certified stationary;
    • dropping the between-looks surface rule, or reporting the cross-surface rect, reddens a surface that arrives between the two looks is not rest;
    • substituting the selector text for the @ref recovery reddens a ref the second observation cannot re-measure is refused, never self-agreed;
    • passing the rect through unrounded reddens sub-pixel geometry is quantized before the band, so the band stays in whole pixels.
  • Device evidence lives on fix(daemon): let a finishing mutation record the tree it acted through (#3471) #3539, where the refusal is reachable.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.19 MB 5.19 MB +408 B
Package (unpacked) 5.19 MB 5.19 MB +408 B
Package (download) 1.56 MB 1.56 MB +44 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 21.1 ms 20.7 ms -0.4 ms
CLI --help 52.7 ms 54.4 ms +1.7 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/commands/interaction/runtime/moving-target-gate.test.ts
Comment thread src/commands/interaction/runtime/moving-target-gate.ts
Comment thread packages/contracts/src/moving-target-gate.test.ts
@thymikee

Copy link
Copy Markdown
Member Author

Gate evidence at this layer's head

pnpm check:affected --run on 40edc7841 — exit code 0.

$ pnpm check:affected --run
check:affected — diff origin/main...HEAD
Fail-open: selecting the full check set.
  ! packages/contracts/package.json [workflow-tooling] — workflow/tooling change can alter any gate
  ...
Selected 63 check(s):
...
[run] mutation-model: pnpm run mutation:test

check:affected: all runnable checks passed.
$ echo $?
0
  • Fail-open again, so this is the full 63-check set: the stack touches packages/contracts/package.json (layer 1's export), which rewires package resolution and can alter any gate.
  • The run took pnpm install --frozen-lockfile and pnpm build first, then the gate, holding /tmp/agent-device-full-gate.lock for this layer's slot only: acquired 23:44:41, released 23:53:00 local after both queued layers finished. One full gate per host; this Mac runs other workers' gates too, so queue time is expected and is not part of the result.
  • The captured log contains check:affected: lint failed. several times. That is check:affected's own unit tests, which drive runChecks with a fake executor that returns non-zero — those tests are themselves a step of the gate. The real invocation's [run] sequence ends in all runnable checks passed.
  • Mutation-sensitive evidence for each guard this layer adds is listed in the PR body; each was verified by reverting the guard and watching the named test redden.

@thymikee

Copy link
Copy Markdown
Member Author

I found no code problems at 40edc78, but the PR still lacks the live and contract evidence the issue asks for. CI is green with 14/14 checks passing, and there are no conflicts. I did not run the tests or the mutations; I judged that the new tests fail on the old code by reading the pre-change code. I also reasoned through the Android family (ADB helper and Appium) without running it. The surface guard does nothing there because surfaceBundleId is iOS-only, and identity and reconciliation are platform-neutral. The ADR 0011 targetStability cells are not in packages/contracts/src/interaction-guarantees.ts at this head, so the Contract requirement of #3471 is not met by this layer alone.

Not blocking: measureRefObservation re-derives the adoption rule that reconcileFreshObservation already applies, and the ref look reads snapshot.iosSystemSurfaceBundleId directly while the selector look uses surfaceScopedNodes(snapshot).surfaceBundleId. Take or leave both. The first is the sharper one: if reconciliation gains a rule later, the gate could call a look measured when reconciliation did not adopt it. Could reconcileFreshObservation report what it chose (adopted from the observation, or kept the frame node) on the ref resolution, so the gate reads that value instead of matching identity and rect again? That would also let the gate compare surfaces through resolvePostActionSurfaceChange(first.preAction, second.preAction) === undefined. What would change first is a provenance field on ResolvedRefNode and the ref ResolvedInteractionTarget in packages/contracts/src/interaction.ts.

The three cubic-dev-ai threads are fixed at this head, so please resolve them: movedToRect assertions, cross-surface second look, and real @ref refusal assertions.

Before merge, decide whether ref adoption should come from reconcileFreshObservation. #3539 must then add the targetStability cells and live Android evidence, with at least 16 trials on both ADB and Appium, or a scoped claim naming the backend it covers. A writer of movingTargetBaseline in #3539 makes the gate reachable, so that is where the live run belongs.

@thymikee

Copy link
Copy Markdown
Member Author

Gate evidence at this layer's head

pnpm check:affected --run --base feat/3471-moving-target-judgement on 19089eee0 — passed (exit 0, full run). pnpm format first; the worktree was clean at the end of the run.

Rebased onto this stack's layer 1 at 3e6c29f44. git range-diff against the previous head 40edc7841: the two commits the first review produced are unchanged in content (9299300a5→c16f7e4b5, 40edc7841→d149f2a2a) and two commits are new on top — 3d8e3fff6 moves the tolerance comparison to one place, 19089eee0 makes the frame model report a ref's provenance. Both are open items from review.

Reply to the review at 40edc78

The shared helper. Landed here rather than in #3538, as your note allowed. areCapturedRectsAtSamePosition(a, b) is now the 4-field abs-compare against 1 px, exported from @agent-device/kernel/rect-center beside normalizeRect and areRectsApproximatelyEqual; it normalizes first and owns CAPTURE_RECT_TOLERANCE_PX = 1. rectsWithinTolerance's five call sites call it directly and the gate calls it too. MOVING_TARGET_GATE_RECT_TOLERANCE_PX, movingTargetRectsStable and both loops are deleted — neither name exists anywhere in the repo at this head. areRectsApproximatelyEqual keeps its own 0.5 px band, which asks a different question; a comment in the kernel states which callers use which band, so the two bands are not a silent disagreement.

The MovingTargetBaseline.tree doc you asked to widen once the writer existed: done on #3539 at 2c6a17794. The type doc now reads "The tree the action acted through: its resolution's own observation, or the settled one its --settle published as rest", with the same sentence in snapshot-post-action-mutation.ts and interaction-http-api.ts.

Your other two items here, the ref measuring the wrong node and rest granted across surfaces, are now decided by reported facts rather than by matching: the ref branch asks the reconciliation which tree it read the node from (adoptedFrom), and rest is not granted when the two captures disagree about the surface.

The live and targetStability evidence you said this PR lacks: that is on #3539 and cannot be here, because no press can be refused until the writer of movingTargetBaseline exists. The ADR 0011 cells and the contract scenarios that fill them, and every device trial, are collected there; the body's "Where this layer's evidence is" section states the split.

The mutation list in the body names which test reddens when each new guard is reverted.

@thymikee

Copy link
Copy Markdown
Member Author

The PR is ready at 19089ee. The earlier evidence request is now satisfied, and the code looks right to me.

Not blocking, and you can take or leave it: in interaction-surface-signature.ts, rectsWithinTolerance now goes through normalizeRect, which rejects a negative width or height. isFiniteRect does not filter those, so such an entry would compare unequal to itself and the surface checks would read "changed". Either say in the PR that the rule is now wider, or filter negative-size rects in buildInteractionSurfaceEntry so entries already satisfy normalizeRect.

The three cubic-dev-ai P2 threads are fixed at this head, so you can resolve them: the unmeasured @ref refusal assertion, the surface used for the ref look, and the real @ref refusal assertions.

I read the code and the earlier-review changes. I did not run the test suite or the claimed mutations, and I did not check whether focus or gesture responses, which spread resolved, put adoptedFrom into public output. I did not verify live Android adoption (ADB and Appium) here.

Only the Smoke Tests are not passing, and they are queued or in progress, not failed. The diff touches the @ref path in ref-target-resolution.ts that the smoke ref presses use. The added field and the post-gesture rect comparison keep behavior there, so a failure would need a check against that overlap.

Before the gate is fully proven, #3539 needs to add the ADR 0011 targetStability cells and live Android trials (ADB and Appium, guarded @ref press and --settle baseline). They must run on the route where a refusal is reachable.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 13, 2026
…nds (#3471)

The gate's `measured` flag accepted any capture whose rect agreed with the
dispatched one. ADR 0014 keeps the authorized frame node when a captured node's
identity differs, so a replacement control that took over the ref at the frame's
own coordinates produced two agreeing "observations" while the dispatch went to
the stale point. A look now counts only when the captured node matches the
dispatched node's rect AND its local identity — the ADR 0014 adoption question in
the value form that survives the session store's clone. One unmeasured look ends
the recheck, so the refusal reports no `movedToRect` rather than a stale one,
which is what its published shape now says.

Screening for `stationary` compared positions across surfaces: a same-identity
node on a newly arrived iOS system sheet could excuse both re-observations. The
baseline's `surfaceBundleId` must now match the resolved surface's, the same line
`post-action-surface.ts` draws.

The hint interpolated a raw selector inside single quotes, so a label carrying an
apostrophe taught a `wait` command that cannot run; it goes through the kernel's
`shellQuoteIfNeeded` like the daemon's other recovery hints. That also moves the
hint beside the refusal that publishes it, since contracts may not reach
`device-shell` without growing its own eager closure.

The lost-target test asserted only "not the motion refusal" and now names
`ref_not_found` with `dispatched: 'no'`.
The re-observations that certify a resting position agreed on the rect alone, while the screening
that decides whether to recheck at all already compares surfaces. The same line has to hold in
both: an iOS system sheet presented during the sample gap carries the same control at the same
rect, and the gate would have read that as rest and dispatched onto the sheet (#2438's transition,
guarding the other half of the same decision). A measured look now carries the surface it was
measured on, and two looks agree only on one surface.

The unmeasured refusals also pin what they withhold — no `movedToRect` when no look measured a
position — and the `@ref` branch of the published recovery is now asserted on a real refusal rather
than in the contracts suite the hint left.
…3471)

`movingTargetRectsStable` and `MOVING_TARGET_GATE_RECT_TOLERANCE_PX` copied the
4-field abs-compare against `RECT_TOLERANCE_PX = 1` that `rectsWithinTolerance`
already ran for the post-gesture surface checks, and its own comment named that
file as the source of the band. Two copies of one rule: change either band and
the gate and the no-effect check disagree about the same pair of rects.

`areCapturedRectsAtSamePosition` is that comparison, exported once from
`@agent-device/kernel/rect-center` beside `normalizeRect` and
`areRectsApproximatelyEqual`. It normalizes first, so a rect that reports no
position never reports one position with another, and it owns the 1 px band
while `areRectsApproximatelyEqual` keeps its 0.5 px band for "do these two
values denote one point" — a different question, with a different reason to be
strict. Both call sites now delegate: `rectsWithinTolerance`'s five surface
comparisons call the helper directly, and the gate imports it in place of the
contracts copy. The contracts module keeps the decision and the window; the
band was never its fact to hold, so its constant and loop are gone along with
the tests that pinned the constant's value.

The band's assertions moved to `rect-center.test.ts` with the rule, including
the per-axis 1 px / 2 px boundary and the asymmetry check the old suite made on
the moved-to values rather than on the rule.

Verified by mutating the band: at 0.5 px `a one-pixel shift is capture rounding,
not motion` reddens, and relaxing the comparison to ignore the `width` axis
reddens `the declared window is the 500 ms transition budget`'s neighbour,
`one axis is enough to move`.
… from (#3471)

`measureRefObservation` re-derived the adoption rule that
`reconcileFreshObservation` had already applied: it looked the ref up in the
capture again and required the dispatched node to match it on rect and local
identity. Two implementations of one ADR 0014 step 5 decision, so the day
reconciliation gains or drops a rule, the gate can call a look "measured" on an
adoption the rule no longer grants — or refuse one it does grant.

`reconcileFreshObservation` now states what it chose, on every branch:
`adoptedFrom: 'observation' | 'authorized-frame'`. The value rides the ref's
`ResolvedInteractionTarget`, which is where a caller reads the node, rect and
`preAction` it dispatched, so the provenance travels with the evidence it
describes rather than being inferred from it afterwards. The gate reads that
field: a look measured the target exactly when the resolution says it read the
dispatched node out of the observation, and the look's surface now comes from
that same capture through `surfaceScopedNodes` — the way the selector branch
reads its resolution's `preAction` — instead of reaching for
`snapshot.iosSystemSurfaceBundleId` directly.

The two refusal tests are the ones that pay for this, and they are the reason
rect agreement is still not enough: a different control that took over the ref
at the frame's own coordinates produces two agreeing rects, and only the
reported provenance says the dispatch read them from the authorized frame.

Verified by mutating the reported value on each branch: reporting
'authorized-frame' where the observation was adopted reddens `the @ref path
takes its own observations, because its resolution spends none`; reporting
'observation' on either keep-the-frame branch reddens `a ref the second
observation cannot re-measure is refused, never self-agreed` and `a replacement
that took the ref at the frame coordinates is refused, not agreed`.
…3471)

`stableCaptureSignalsEqual` kept the repository's third copy of the "did this
rect move" band: a 4-field abs-compare against a local `GEOMETRY_TOLERANCE_PX = 1`
that rounded each field first. `areCapturedRectsAtSamePosition` is now the one
place that decides it, so the quiet window, the post-gesture surface check and the
moving-target gate cannot drift apart when the band changes.

The rounding was load-bearing rather than cosmetic: comparing raw backend geometry
against 1 px would restart the quiet window on pairs this loop has always called at
rest, because a rect quantized to the pixel can sit up to 2 px apart raw and still
be one pixel apart in the unit the band is measured in. So the quantization moves to
`stableCaptureNodeSignal`, the one place a signal mints a rect, and the comparison
becomes the shared rule over integers. The band is therefore unchanged, stated as a
test rather than as a claim: 0.51 against 2.4 is rest, 0.51 against 3.4 is not, and
reverting the quantization reddens it.

`GEOMETRY_TOLERANCE_PX` and the local loop are deleted.
@thymikee
thymikee force-pushed the fix/3471-gate-measurements branch from 19089ee to 0d8bf9f Compare October 13, 2026 16:21
@thymikee

Copy link
Copy Markdown
Member Author

Gate evidence and replies at this head (0d8bf9f0d)

Rebased onto main with the rest of the stack: origin/main at f70224ad8 → #3538 c4d517f60 → this layer at 0d8bf9f0d. The five commits are the four you reviewed plus one (below), git range-diff reports all five as identical patches, and no file here conflicted — the stack's only conflict was #3538's, against #3495's covering-node additions in packages/selectors/src/interaction-error.ts. pnpm install --frozen-lockfile, pnpm build and pnpm typecheck are clean on the new base, which also picks up main's vitest 5 upgrade.

pnpm check:affected --run --base feat/3471-moving-target-judgement — result recorded above. pnpm format first; worktree clean at the end of the run. typecheck, lint, format:check, check:layering and eager-closure-budgets (842 cases) clean; the settle suites (stable-capture-signal, stable-capture, post-gesture-stabilization, post-gesture-no-effect-claim) pass, 65 tests.

The take-or-leave item about normalizeRect widening the rule

The premise does not hold on the code as it stands, so I changed nothing there. isFiniteRect — interaction-surface-signature.ts:353, the private helper, not kernel's — reads values.every(Number.isFinite) && rect.width > 0 && rect.height > 0. A negative-size rect is therefore already refused at construction, inside buildInteractionSurfaceEntry, before any entry reaches a signature; and since the entry stores Math.rounded fields, normalizeRect's Number() conversions are identity on it. So no entry can compare unequal to itself, and the rule is not wider at this head. I did not add the filter you offered, because the two filters would then be a pair of sources of truth for one question, which is what the shared-helper finding was about.

The take-or-leave item you left unchecked: does adoptedFrom reach public output?

It does not, and the reason is that the response layer never spreads the resolution:

  • interactionResultExtra (interaction-touch-targets.ts:158) enumerates the ref branch's fields by name — ref, refLabel, selectorChain, targetHittable, hint, evidence, settle, resolution. adoptedFrom is not among them, and neither is preAction, node or point, which the same variant carries.
  • The gestures/focus path is the same shape: gestureResponseData (interaction-gesture-response.ts:16) lists kind, durationMs, pointerCount, from, to, targets, message and spreads only backendResult and its own extra.
  • The corroborated-tap path spreads { ...params.resolvedTarget, warning }, but that object is the builder's result, and the builder reads it through interactionResultExtra — the spread feeds the named-field reader, not the payload.
  • The result that is returned to a caller is built from publicData (the transformed backend data) plus that extra, not from the resolution object.

I did not add a test asserting the absence: there is no builder that could leak it, so a pin would be an assertion about an enum of response keys in a file that already owns that list.

The item I took from the same review, one layer down

stableCaptureSignalsEqual in src/commands/interaction/runtime/stable-capture-signal.ts kept the repository's third copy of the band: a local GEOMETRY_TOLERANCE_PX = 1 and its own 4-field loop, which #3538's review also pointed at. It now calls areCapturedRectsAtSamePosition, and the constant and loop are deleted, so the body's claim that one rule decides capture position now covers the settle quiet window as well as the gate and the post-gesture check.

Its rounding was load-bearing, so it moved rather than vanished. Comparing raw backend geometry against 1 px is stricter than what that loop has always run: a rect quantized to the pixel sits up to 1.9 px apart raw while being one pixel apart in the unit the band is measured in, and the quiet window would restart on those pairs. With no device in this pass I was not willing to retighten a timing rule that decides how long every iOS settle waits, so the quantization now happens where a signal mints its rect and the shared comparison runs over integers — the old band, stated as a test (0.51 against 2.4 is rest, against 3.4 is not; reverting the quantization reddens it). Mutation-checked both ways: widening the band reddens the kernel band test, dropping the quantization reddens this one.

The targetStability cells and the live Android trials

The cells exist at this head, in #3539: every dispatch path has a targetStability cell, enforced by Record<InteractionGuarantee, GuaranteeEnforcement> on the guarantee union plus the Layer-3 scenario gate, which refuses an enforced cell with no scenario. They could not sit here — a cell names a guarantee the shipped path provides, and until #3539 there is no writer of movingTargetBaseline, so the gate cannot fire and the cell would be a claim with no code behind it. #3539's body carries the cells, the scenarios, and the device numbers.

The Android gap is a scoped claim, not a silent one, and it is stated in #3539 rather than implied: 16 trials on the ADB-backed helper and the iOS runner route are measured; no Appium-backed Android run was made. The backend-neutrality it needs rests on two checkable things instead of a run: the gate sits in the shared runtime path in front of runtime.backend.tap with no backend branch in it, and the baseline writer is pinned on a non-ADB backend by the daemon-route scenario that drives a real iOS runner conversation. The claim that is backend-specific — reporting frames in flight — names ADB inside the cell's own text. The two open rows are named too: a guarded @ref press has not been re-verified since this layer changed how a look is measured, and the table has no delayed-start-animation case. Both are owed by whoever next holds a device.

@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready. Since the earlier review at 19089ee, the moving-target gate now compares bounds through the shared rect normalizer, and I found nothing that needs a change in 0d8bf9f. Not blocking, and you can take or leave it: the settle signal in https://github.com/callstack/agent-device/blob/0d8bf9f/src/commands/interaction/runtime/stable-capture-signal.ts#L48 now goes through normalizeRect, which rejects negative width or height, while the old loop compared those sizes numerically. A signal node with a negative rounded size would then never equal itself and would restart the quiet window on every poll. Android parseBounds clamps sizes to 0 and I know of no producer of negative sizes, so a simple fix is to leave a node's rect out of the signal when it is negative or non-finite, or to note the widening in the commit message. I read the delta and worked out by hand that the new test fails on the old behavior. I did not run the test or your two-way mutation checks. I only confirmed the clamp for Android, so the iOS runner, macOS AX and web producers are unchecked for negative sizes. I did not re-check that adoptedFrom stays out of public output in this delta. CI is still running at 0d8bf9f and no job has failed. Smoke Tests and Lint & Format are queued, and Repo Guards and Compatibility & Provenance are in progress. This delta touches the settle quiet window, which the smoke settle and press routes exercise, so a smoke failure would need a look at that overlap. Before merge, those four checks need to go green. The live Android guarded @ref trials stay owed in #3539.

@thymikee
thymikee merged commit 6dc1e34 into main Oct 13, 2026
19 of 20 checks passed
@thymikee
thymikee deleted the fix/3471-gate-measurements branch October 13, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant