Skip to content

feat(core): add geometry fields to element predicates - #1635

Open
CloudCorpRecords wants to merge 1 commit into
reticlehq:mainfrom
CloudCorpRecords:feat/element-geometry-predicates
Open

CloudCorpRecords wants to merge 1 commit into
reticlehq:mainfrom
CloudCorpRecords:feat/element-geometry-predicates

Conversation

@CloudCorpRecords

Copy link
Copy Markdown
Contributor

Closes #1539

An element predicate could ask that some of an element shows (state: "inViewport") and nothing else about its geometry, so responsive-layout claims had no verdict: fully on screen versus half off the bottom, two elements overlapping, a label whose text is cut off.

This change adds the three fields the report names, measured in the page from the box reticle_look already reports:

  • fullyInViewport: boolean — the whole box is inside the viewport and no clipping ancestor cuts any part of it. It reuses the clipping walk visibleBounds already does, so it composes with the Text clipped by overflow: hidden / line-clamp counts as visible #1237 clipping work instead of re-deriving it.
  • textClipped: boolean — content beyond the client box under overflow: hidden | clip, or text-overflow: ellipsis in effect, with the overflow amounts (pixels per axis) carried on the evidence.
  • overlaps: { <query> } — another element as a query; the boxes must intersect with positive area. Assert the negative with the not combinator around the predicate.

How it flows: core declares the fields on the element predicate and the ElementGeometry evidence on the descriptor. The browser measures only when a MATCH carries the ask (measureGeometry in a11y.ts, stamped in matchQuery, including the second box for overlaps). The engine grades it in predicate-element.ts: one element must satisfy every ask, a page that reports no geometry is inconclusive rather than a guess, and failures print the measured box, the viewport, and the overflow amounts. Because the fields live in the schema, they appear in the derived predicate grammar automatically.

Proof: the new tests failed before this change and pass after it. The browser cases (query.geometry.test.ts) cover two viewport widths, a box cut by a clipping ancestor, pixel-exact text clipping (120px), and intersection both ways. The engine cases cover pass, fail with the measured numbers in the message, and the no-geometry inconclusive. Full suites pass locally: browser 2,159 tests, engine 1,578 tests, core 881 tests.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@github-actions

Copy link
Copy Markdown

Thanks for your first pull request to Reticle! A maintainer aims to review within two days. Before then, pnpm verify locally catches most of what CI will, and CONTRIBUTING.md explains the rest. If CI does not start, a maintainer needs to approve it for first-time contributors; that is normal.

@github-actions github-actions Bot added area/core The wire contract and schemas: core/ area/browser The browser SDK: adapters/realm/browser area/docs Documentation and docs site labels Oct 15, 2026
@CloudCorpRecords

Copy link
Copy Markdown
Contributor Author

CI caught a real gap in the new commit's predecessor, fixed in 7b27d6c: the geometry fields were added to the zod PredicateSchema but not to the hand-written Predicate type the engine compiles against, so the workspace build failed with TS2339 in engine's predicate.ts. The type now declares fullyInViewport, textClipped, and overlaps with the same docs as the schema, and the engine builds its geometry asks with conditional spreads so no explicitly-undefined property reaches ElementGeometryAsks under exactOptionalPropertyTypes.

Verified locally before pushing: full workspace tsc -b clean, engine predicate suites 569/569, core verdict suites 196/196 (core dist built with the repo's own post-steps; pnpm verify itself cannot run in my sandbox, so the parts were run separately, as before).

@github-actions github-actions Bot added the area/server The daemon, MCP server and CLI: server/ label Oct 15, 2026
@CloudCorpRecords

Copy link
Copy Markdown
Contributor Author

Follow-up on the latest red verify run: the build and typecheck were fine this time; lint failed on two errors from this change. a11y.ts compared against null with the literal on the right (the repo's yoda rule wants it on the left), and query.ts crossed the 1000-line maximum at 1022 lines once the geometry plumbing landed in it. Both are fixed in the newest commits: the comparison now reads null === clipped, and the overlap helper plus the geometry attachment moved out of query.ts into their own module, adapters/realm/browser/src/dom/geometry.ts, bringing query.ts to 997 lines. The flat-directory guard's record for the dom directory rises to 14 for the new module, with the reason noted beside the entry. Verified locally: eslint clean across the browser package, prettier clean on the touched files, the geometry and a11y suites pass 110/110, the flat-directory guard passes, and the workspace typecheck is clean.

@divshekhar divshekhar 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.

Thank you for this. The browser measurement and the engine grading are a good shape, and the inconclusive answer for an older SDK is right. Some problems block the merge. The first three can produce a wrong pass.

  1. A missing overlaps target is a plain failure. Under not, it becomes a pass: not { element: card, overlaps: { testid: "typo" } } passes when the second element does not exist. Please make "the overlaps query matched no element" inconclusive. The ElementGeometry comment already says that this is a different fact from "the boxes do not intersect".
  2. textClipped reads only the element's own box. Overflow does not apply to an inline element, and a browser reports scrollWidth and clientWidth as 0 for it. So a <span> inside a truncated <div> reads textClipped: false, and that clause passes while the text is cut off. The test sets overflow-x: hidden on a <span> with mocked widths, which a real browser does not do. Please measure the nearest box that clips (the element if it is a block, else the nearest ancestor that clips), or answer inconclusive for an inline element. Please change the fixture to a block element.
  3. With absent: true, the geometry fields are ignored, because the absent branch returns before checkGeometry. Please refuse that combination with an inconclusive answer or a schema error, so the caller knows.
  4. The overlap target is findCandidates(...).candidates[0]: the first candidate in document order, hidden or not. Please use the first visible match, or name in the evidence which element was measured.

Also:

  • The branch has merge conflicts with main. Please rebase (do not merge main in).
  • This changes how a verdict is reached. Please run pnpm gate:conformance and report the result. CI skips that job for this PR.
  • geometry and overlapsQuery are new MATCH arguments. Please define their names in core and use the constants in the engine and the browser, as the wire rule in AGENTS.md says.
  • #1634 also adds a file to adapters/realm/browser/src/dom and adds lines to query.ts. Together the two PRs take query.ts over 1,000 lines and the directory count to 15. Please coordinate with that PR.

@CloudCorpRecords
CloudCorpRecords force-pushed the feat/element-geometry-predicates branch from 3b40320 to 0d19a86 Compare October 18, 2026 17:05
CloudCorpRecords added a commit to CloudCorpRecords/reticle that referenced this pull request Oct 18, 2026
divshekhar (re-review on reticlehq#1635):

- A missing overlaps target is inconclusive, not a failure: under not a
  plain failure became a pass. A query that matched no visible element can
  no longer manufacture a verified verdict.
- textClipped measures the nearest box that clips: overflow does not apply
  to inline boxes, so a span inside a truncated div is read at the div.
  The pixel fixture is now a block element, as a real browser requires.
- absent:true with geometry asks is refused as inconclusive instead of
  silently ignoring the asks.
- The overlap target is the first visible match, not the first candidate
  in document order, hidden or not.
- MATCH arg names live in core as MatchArg (types.ts: constants.ts is at
  the 1000-line cap) and both sides use the constants.
- Rebased onto main (no merge); measureGeometry moved to geometry.ts and
  the overlap resolver with it, keeping a11y.ts and query.ts under the
  1000-line lint cap.

Guards: core/engine/browser unit suites green (902/1606/2202);
gate:conformance green (self/desktop/cli exit 0, self score identical to
main baseline). Coupling ceiling 65->66 recorded with the argument.
Note for merge order: reticlehq#1634 also adds a dom/ file, so the second PR to
merge bumps the recorded dom count 14->15.
@CloudCorpRecords

Copy link
Copy Markdown
Contributor Author

Re-review addressed and rebased onto main (no merge): missing overlaps target is now inconclusive (no more wrong pass under not); textClipped measures the nearest clipping box with the fixture moved to a block element; absent+geometry is refused as inconclusive; the overlap target is the first visible match; and the MATCH arg names live in core as MatchArg used by both sides. measureGeometry moved to geometry.ts to keep a11y.ts/query.ts under the lint cap. pnpm gate:conformance result: self/desktop/cli all exit 0, self score identical to the main baseline. Full suites green (core 902, engine 1606, browser 2202). Note for merge order: #1634 also adds a dom/ file, so whichever merges second bumps the recorded count 14→15.

CloudCorpRecords added a commit to CloudCorpRecords/reticle that referenced this pull request Oct 18, 2026
divshekhar (re-review on reticlehq#1635):

- A missing overlaps target is inconclusive, not a failure: under not a
  plain failure became a pass. A query that matched no visible element can
  no longer manufacture a verified verdict.
- textClipped measures the nearest box that clips: overflow does not apply
  to inline boxes, so a span inside a truncated div is read at the div.
  The pixel fixture is now a block element, as a real browser requires.
- absent:true with geometry asks is refused as inconclusive instead of
  silently ignoring the asks.
- The overlap target is the first visible match, not the first candidate
  in document order, hidden or not.
- MATCH arg names live in core as MatchArg (types.ts: constants.ts is at
  the 1000-line cap) and both sides use the constants.
- Rebased onto main (no merge); measureGeometry moved to geometry.ts and
  the overlap resolver with it, keeping a11y.ts and query.ts under the
  1000-line lint cap.

Guards: core/engine/browser unit suites green (902/1606/2202);
gate:conformance green (self/desktop/cli exit 0, self score identical to
main baseline). Coupling ceiling 65->66 recorded with the argument.
Note for merge order: reticlehq#1634 also adds a dom/ file, so the second PR to
merge bumps the recorded dom count 14->15.

Signed-off-by: Rene Turcios <95189389+CloudCorpRecords@users.noreply.github.com>
@CloudCorpRecords
CloudCorpRecords force-pushed the feat/element-geometry-predicates branch from 0d19a86 to 3c5bcfc Compare October 18, 2026 18:20

@divshekhar divshekhar 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.

Thank you. You fixed all four points, and the MatchArg constants and the rebase are correct. Three new problems can still give a wrong answer.

  1. checkGeometry reads only match.elements, which is capped. match.count can be larger. Main refuses this case for the other fields it checks after the match ("only M were described … narrow the locator"). Give the same inconclusive answer when count is larger than elements.length. Otherwise a not can pass while an element that was not described satisfies the ask.
  2. nearestClippingBox stops at an inline element whose computed overflow is hidden, for example a Tailwind truncate span. Overflow does not apply to that box, so its widths read 0. Skip boxes whose computed display is inline or contents, and add a test.
  3. If the overlaps query also matches the measured element, intersects is always true. Exclude the measured element from the target.

Minor: text-overflow: ellipsis has no effect without overflow: hidden. Treat it as a clip only when overflow also clips.

#1634 is queued. After it merges, query.ts goes over 1,000 lines with this change, and the dom count must be 15. Rebase after it merges, and move code out of query.ts to stay under the limit.

CloudCorpRecords added a commit to CloudCorpRecords/reticle that referenced this pull request Oct 19, 2026
divshekhar (re-review on reticlehq#1635):

- A missing overlaps target is inconclusive, not a failure: under not a
  plain failure became a pass. A query that matched no visible element can
  no longer manufacture a verified verdict.
- textClipped measures the nearest box that clips: overflow does not apply
  to inline boxes, so a span inside a truncated div is read at the div.
  The pixel fixture is now a block element, as a real browser requires.
- absent:true with geometry asks is refused as inconclusive instead of
  silently ignoring the asks.
- The overlap target is the first visible match, not the first candidate
  in document order, hidden or not.
- MATCH arg names live in core as MatchArg (types.ts: constants.ts is at
  the 1000-line cap) and both sides use the constants.
- Rebased onto main (no merge); measureGeometry moved to geometry.ts and
  the overlap resolver with it, keeping a11y.ts and query.ts under the
  1000-line lint cap.

Guards: core/engine/browser unit suites green (902/1606/2202);
gate:conformance green (self/desktop/cli exit 0, self score identical to
main baseline). Coupling ceiling 65->66 recorded with the argument.
Note for merge order: reticlehq#1634 also adds a dom/ file, so the second PR to
merge bumps the recorded dom count 14->15.

Signed-off-by: Rene Turcios <95189389+CloudCorpRecords@users.noreply.github.com>
CloudCorpRecords added a commit to CloudCorpRecords/reticle that referenced this pull request Oct 19, 2026
divshekhar (re-review on reticlehq#1635):

- A missing overlaps target is inconclusive, not a failure: under not a
  plain failure became a pass. A query that matched no visible element can
  no longer manufacture a verified verdict.
- textClipped measures the nearest box that clips: overflow does not apply
  to inline boxes, so a span inside a truncated div is read at the div.
  The pixel fixture is now a block element, as a real browser requires.
- absent:true with geometry asks is refused as inconclusive instead of
  silently ignoring the asks.
- The overlap target is the first visible match, not the first candidate
  in document order, hidden or not.
- MATCH arg names live in core as MatchArg (types.ts: constants.ts is at
  the 1000-line cap) and both sides use the constants.
- Rebased onto main (no merge); measureGeometry moved to geometry.ts and
  the overlap resolver with it, keeping a11y.ts and query.ts under the
  1000-line lint cap.

Guards: core/engine/browser unit suites green (902/1606/2202);
gate:conformance green (self/desktop/cli exit 0, self score identical to
main baseline). Coupling ceiling 65->66 recorded with the argument.
Note for merge order: reticlehq#1634 also adds a dom/ file, so the second PR to
merge bumps the recorded dom count 14->15.

Signed-off-by: Rene Turcios <95189389+CloudCorpRecords@users.noreply.github.com>
@CloudCorpRecords
CloudCorpRecords force-pushed the feat/element-geometry-predicates branch from 3c5bcfc to 2c7a18a Compare October 19, 2026 16:46

@divshekhar divshekhar 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.

Thank you. You fixed all four points from my last review, and each fix has a test.

Two problems can still give a wrong verdict:

  1. A missing element passes under not. The geometry check runs only if (geometry !== undefined && match.matched), so a locator that matches nothing stays a plain miss. not turns a plain miss into a pass (engine/src/question/predicate/predicate.ts:476-478). So not { element: { testid: "typo" }, overlaps: … } passes. When geometry asks are present and the locator matched nothing, return inconclusive.
  2. textClipped can read the wrong box. nearestClippingBox walks past a block element that does not clip, up to any ancestor that clips. A short <li> in an overflow-x: hidden carousel then reports the overflow of the whole list. Stop at the element when it is a block, and walk up only for inline content. Add a test: a block that fits, inside a clipping parent that overflows, must not read as clipped.

Also:

  • Remove the ### Fixed section from the .changes file. Those bugs were in this unreleased feature, so users never had them. Keep only the Added entry.
  • Run pnpm gate:conformance again on the final commit, and put the result in the PR.

Rebuilt cleanly on current main. The conflicts in query.ts,
predicate-element.ts and the two guard tests were resolved by hand,
keeping both sides: main's framework-dialog coverage and text-match
split, and this branch's geometry module and MatchArg symbol.

Latest review fixes:
- Geometry asks are inconclusive when the locator matched nothing,
  so a missing element cannot pass under not.
- textClipped reads the element's own box when it is a block and
  walks up only for inline content. New test: a block that fits
  inside a clipping parent that overflows is not clipped.
- The .changes file keeps only the Added entry. The Fixed section
  described bugs in this unreleased feature, so it is gone.

Signed-off-by: Rene Turcios <95189389+CloudCorpRecords@users.noreply.github.com>
@CloudCorpRecords
CloudCorpRecords force-pushed the feat/element-geometry-predicates branch from 2c7a18a to 2b080ef Compare October 20, 2026 16:00

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/browser The browser SDK: adapters/realm/browser area/core The wire contract and schemas: core/ area/docs Documentation and docs site area/server The daemon, MCP server and CLI: server/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No way to assert that an element is fully on screen, doesn't overlap another, or has unclipped text

2 participants