Skip to content

refactor: move selector engine into workspace package - #1589

Merged
thymikee merged 7 commits into
mainfrom
agent/selectors-package-cutover
Aug 4, 2026
Merged

refactor: move selector engine into workspace package#1589
thymikee merged 7 commits into
mainfrom
agent/selectors-package-cutover

Conversation

@thymikee

@thymikee thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Deletions / remaining debt

Item Status / evidence
src/selectors/ Deleted; implementation and selector tests moved under packages/selectors/src/internal/.
createDaemonReplaySelectorPort and src/daemon/replay-selector-port.ts Deleted; daemon replay routes call direct package operations.
In-memory replay selector adapter Deleted from test utilities.
Dual adapter/port contract and parity suites Deleted; direct @agent-device/selectors engine and façade tests cover the behavior.
src/sdk/selectors.ts and root ./selectors export Retained (repointed at @agent-device/selectors/ast). The . façade still does not export Selector, SelectorChain, SelectorTerm, SelectorKey, or grammar internals; ./ast carries exactly the ten symbols v0.20.5 published, pinned in facade-symbols.ts and reachable from that one file only.
Remaining selector-adapter debt None; replay, healing, target verification, CLI, and Maestro paths use the package engine directly.

Validation

  • pnpm check — full repository gate green: 598 unit files / 5280 tests; smoke 35 passed and 3 live-device/web cases skipped.
  • pnpm check:affected --run — all runnable checks passed; affected coverage ran 393 files / 3670 tests, and changed-line coverage was 44/47 (93.62%) with 30/31 changed branches covered.
  • pnpm mutation:test — 45/45.
  • pnpm check:layering — 71/71; 1016 source files; 10 workspace packages / 31 exported subpaths; zero root back-imports.
  • pnpm depgraph:test — 22/22.
  • Clean-install package smoke — publint and attw passed; dependency closure verified (@limrun/api, yaml); all 12 published entry points imported; packaged CLI 0.20.5 ran on Node 26.1.0; agent-device/selectors resolves and its parse/match/format surface behaves as v0.20.5 did.
  • Focused selector and production-route tests — 7 selector package files / 56 tests, plus 11 runtime-route files / 115 tests.
  • Counterfactual red evidence — disabling replay uniqueness caused 2 selector-package failures; temporarily exporting SelectorChain caused both exact-façade and AST-privacy gates to fail. Both mutations were restored.
  • Independent in-thread reviewer — no actionable findings confirmed before push.
  • Device evidence — not required for this package/public-CLI cutover; no live device was used. Native/device lanes remain GitHub-authoritative.

Do not release or merge as part of this change.


Review follow-ups (post-3806720c)

Three review passes landed on top of the cutover commit:

  • 6cd73ecb5 — trimmed 15 façade symbols with no consumer, deleted the unread selectorExpression capture-policy parameter and the two tests that asserted it had no effect, returned the Maestro export vocabulary to packages/maestro (MAESTRO_SELECTOR_PROJECTION) instead of hardcoding its key lists at the CLI call site, deduped the twice-declared resolution types, and removed three identity wrappers.
  • e93c7df50 — split the 708-line engine aggregation test into six files that mirror source topology.
  • a792415ac + this commit — reverted the public-subpath removal (see above) and corrected the two stale expectations that still classified the subpath as gone (installed-package-metro.test.ts, owner-files-no-leak.test.ts).

Two of the 15 trimmed symbols (isNodeVisible, isNodeEditable) were only unused because the public subpath had been removed; they are exported from ./ast again.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-04 17:07 UTC

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.96 MB 1.96 MB -1.0 kB
JS gzip 627.7 kB 626.8 kB -874 B
npm tarball 749.1 kB 749.0 kB -119 B
npm unpacked 2.62 MB 2.62 MB +590 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 20.9 ms 20.4 ms -0.5 ms
CLI --help 49.4 ms 48.4 ms -1.0 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/sdk-batch-runner.js +19.8 kB +6.0 kB
dist/src/session.js -2.1 kB -670 B
dist/src/cli.js -760 B -228 B
dist/src/internal/daemon.js -789 B -194 B
dist/src/selector-runtime.js -311 B -93 B

@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Review of 3806720c: not ready.

[P2] Remove the unused internal barrel packages/selectors/src/internal/index.ts and split internal/index.test.ts by the source concepts it covers, importing those modules directly. The barrel has no production consumer and violates the repo’s internal-barrel rule; the 712-line aggregation test exceeds the 500-line tripwire and does not mirror source topology.

[P2] .fallowrc.json still lists the deleted src/sdk/selectors.ts entry; remove that stale configuration in the tightening pass.

Owner-action CI: exact-head iOS Smoke is red at the initial wait text "Agent Device Tester" with wait_capture_stalled / zero readable captures. This may be simulator infrastructure, but because capture/wait consumers are part of the cutover, rerun it and compare on current main if it repeats before readiness.

The core cutover otherwise checks out: all four intended deletions are realized, the DAG is acyclic, production uses one selector engine, AST stays behind the new façade, and package/other gates are green.

Residual risk: no separately authorized cross-vendor review was run.

@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Release/API triage addendum: agent-device/selectors shipped as a public v0.20.5 subpath and exported parser/resolution symbols. The S1 dossier also described the SDK façade as mechanically repointable; this PR intentionally removes it.

Before merge, please record an explicit maintainer decision that this is targeted for a breaking 0.21 release, with migration notes. If it is intended for a 0.20.x patch release, retain a deprecated compatibility façade instead. This is a human release-boundary decision, not an assertion that the selector-package cutover is technically wrong.

@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Re-review of a792415a: the prior structural findings are fixed, iOS failure is cleared, and restoring agent-device/selectors through the pinned @agent-device/selectors/ast compatibility boundary resolves the shipped-API/release concern without widening the normal string-only façade. Not ready because two deterministic stale expectations now fail exact-head CI: [P1] installed-package-metro.test.ts still classifies agent-device/selectors as removed, so Integration Tests fails; smoke the restored subpath instead. [P1] owner-files-no-leak.test.ts still asserts dist/src/sdk-selectors.js is absent, so Coverage fails; require the stable named compatibility chunk while continuing to reject an auto-numbered fallback. Also update the PR body, which still claims the public subpath is intentionally removed. Rerun the full exact-head gates after these corrections. No other code regression found in the changed delta. Residual risk: no separately authorized cross-vendor review was run.

thymikee added a commit that referenced this pull request Aug 4, 2026
`ignorePatterns: ["packages/**"]` landed in #1494 W0 with the recorded
reason "its resolver cannot follow workspace specifiers". That was either
wrong at the time or never re-checked: the fallow version has not moved
(^2.95.0 then and now) and it resolves @agent-device/* through each
package's exports map today. packages/kernel alone exposes 8 subpaths and
~110 exports reachable only via workspace specifiers, and scanning it
reports zero findings — a resolver that could not follow the specifier
would report all of them.

The cost of the ignore is that every package extraction silently removes
its code from dead-code analysis. #1589 moved the selector engine into
packages/selectors/ and shipped a façade with 15 zero-consumer exports,
including `selectorUsesKey`, written in that PR and never called. A
follow-up commit removed them by hand; nothing would have caught them.

Removing the pattern surfaced 43 findings, driven to zero by deleting the
dead code rather than by baselining or excluding it (fallow-baselines/*.json
are empty on purpose — the posture is fix-or-document-the-exemption, so a
first baseline entry would be a policy change):

- 38 are deleted. 24 façade type re-exports whose only claim was that a
  consumer might one day want to name them — typecheck is green without
  every one, so the claim was theoretical; 5 façade value re-exports; 9
  `export` keywords on symbols used only inside their own file. Every
  deleted façade symbol comes off scripts/layering/facade-symbols.ts (and
  ad-replay's inline pin in package-boundaries.test.ts) in the same change,
  so R11 is narrowed with the façade, never weakened around it.
- 4 stale suppressions in src/provider-limrun-runtime.ts existed only
  because packages/ was invisible.
- 5 have consumers analysis genuinely cannot see, and get an
  `ignoreExports` entry naming the consumer per the existing `comment`
  convention: four test-tree importers that --production does not walk, and
  `LimrunIosCommandExecution`, which src/sdk/limrun.ts republishes as
  agent-device/limrun — its only importer compiles in a temp checkout, so
  no static edge reaches it. test/integration/limrun-public-types.test.ts
  is the standing proof that one is real API.

Three doc comments named types their façade no longer exports and are
corrected rather than left asserting something false — including #1555's
claim in session-replay-target-verification.ts that the daemon imports
`AdReplayVerifiedTargetGuard` directly. It does not; it reaches that shape
through `AdReplayTargetClassification`/`AdReplayDispatchGuard`, which is
why the name read as dead.

`scripts/maestro-conformance/**` was ignored wholesale to cover its corpus
data. Narrowed to `corpus/**`, which un-hides the tooling beside it and
turned up one more file-local export (`buildManifest`); regenerate.mjs's
importer of `fixtureContentHash` becomes visible, so that needs no
exemption at all.

scripts/check-affected/model.ts deliberately did not select the `fallow`
check for packages/*/src/**, carrying the same stale rationale as a
comment. Without that selection the new scope would never run in the
affected-driven lane, so the ignore removal would have bought nothing.
model.test.ts now pins the selection.

Verified: check:fallow and check:production-exports green with packages in
scope; full-repo `fallow dead-code` back to its one pre-existing finding;
typecheck, layering (R11), lint, format, build, check:package, and the
limrun published-types integration test all pass. Probed by adding a fresh
zero-consumer export to the xml façade — check:production-exports reports
it, so the #1589 case now fails the gate.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Follow-up to the selector-package cutover, from a structural review of it.

- Drop 15 façade symbols with no consumer anywhere in the repo:
  selectorUsesKey (added by the cutover, never called), isNodeVisible /
  isNodeEditable (the real helpers are contracts/snapshot's), normalizeText,
  splitIsSelectorArgs, IS_PREDICATE_REQUIRED_MESSAGE, four nested Replay
  types, SelectorDisambiguationDisclosure, and the four kernel type
  re-exports every consumer already imports from kernel directly.
- Delete SelectorCapturePolicyInput.selectorExpression, which
  deriveSelectorCapturePolicy never read; the policy varies only by
  predicate, so it takes one now. Two of the four tests asserted that the
  unread parameter had no effect and could not fail; they go with it.
- Return the Maestro export vocabulary to the maestro package. The cutover
  inlined MAESTRO_TEXT/STATE_SELECTOR_KEYS' values into the CLI call site,
  leaving both constants dead in the package that owns the concept and no
  gate over the two copies. MAESTRO_SELECTOR_PROJECTION is now the one
  statement of it.
- Dedupe SelectorDiagnostics and SelectorDisambiguationDisclosure, declared
  character-for-character twice across the AST/string seam, and name the two
  shared option shapes once instead of five inline copies. The parser-side
  resolution types take an Ast prefix so the twins read as twins.
- Delete three identity wrappers: parsePrivateSelector,
  selectorExpressionToMaestro, and the formatSelectorFailure forwarder —
  nothing passes it a chain any more, so the SelectorChain | string union
  and its branch go too.
- Delete internal/index.ts, an AST barrel whose only consumer was one test
  in the same directory (renamed to engine.test.ts), and the match.ts
  pass-through that existed to feed it.
- ReplaySelectorGrammar had three variants for two behaviors; 'wait' and
  'ordinary' were the same path. It is 'is' | 'positional' now.
- Drop the deleted src/sdk/selectors.ts from .fallowrc.json's entry list.

Behavior unchanged. pnpm check green: 598 unit files / 5278 tests, smoke
35 passed / 3 live skipped, layering 71/71, depgraph 22/22, mutation config
45/45, fallow clean, package smoke sound. Counterfactual: pointing
MAESTRO_SELECTOR_PROJECTION.textKeys at the state keys turns three
replay-maestro-export cells red; restored before commit.
`internal/index.test.ts` (renamed `engine.test.ts` when its barrel went away)
was a 708-line aggregation over the whole engine — past the 500-line tripwire
and mirroring no source module, so it also ran as one serial unit.

It becomes five files that each mirror what they test, plus the parser cells
folded into the existing parse test:

  resolve.test.ts                 alternative fallback, strict uniqueness,
                                  first-match existence
  resolve-disambiguation.test.ts  ADR 0012 ranking: deepest, smallest-area,
                                  winner-vs-challenger disclosure, tie fallback
  resolve-viewport.test.ts        the visibility half: on-screen beats
                                  off-screen, including inside an off-screen
                                  scroll container
  match.test.ts                   per-key matching semantics (text, role,
                                  focused, appname/windowtitle, decoded
                                  newline labels)
  arguments.test.ts               where the selector ends and the command's
                                  positionals begin, both grammars
  parse.test.ts                   +6 grammar/escape cells beside the existing
                                  property tests

The login-form tree shared by resolve.test.ts and match.test.ts moves to
`__tests__/login-form-nodes.ts` rather than being copied into both.

All 27 cells are carried over unchanged and still pass; no file now exceeds
224 lines. pnpm check green: 602 unit files / 5278 tests, layering 71/71,
depgraph 22/22, mutation config 45/45, fallow clean over 127 changed files.
… subpath

The cutover removed the `agent-device/selectors` public subpath as part of
tightening the API. It is in use, so the removal is reverted: the subpath ships
the same ten symbols v0.20.5 shipped, with the same signatures.

That has to coexist with the reason the package façade is string-only, so the
AST leaves through one named door instead of the main one:

  @agent-device/selectors        string-in/string-out; every in-repo consumer
  @agent-device/selectors/ast    the published parser surface; one consumer,
                                 src/sdk/selectors.ts

`packages/selectors/src/ast.ts` re-exports parseSelectorChain,
tryParseSelectorChain, isSelectorToken, the AST-taking findSelectorChainMatch
and resolveSelectorChain, isNodeVisible, isNodeEditable, and types
SelectorChain / SelectorDiagnostics. `formatSelectorFailure` keeps its
published `SelectorChain | string` first parameter as a shim here rather than
widening internal/resolve.ts back to a union — the compatibility obligation
sits at the boundary that owes it.

This is strictly narrower than main, where the AST was reachable from anywhere
in src/ via src/selectors/*. Two gates hold it there: facade-symbols.ts pins
./ast to exactly the v0.20.5 list, and package-boundaries.test.ts asserts
src/sdk/selectors.ts is the only file outside the package that imports it.

Restored alongside: the ./selectors export and tsdown entry/chunk group, the
.fallowrc.json entry, the package-exports supported-subpath list, and both
client-api.md sections. No CHANGELOG entry — nothing is removed any more.

pnpm check green: 602 unit files / 5278 tests, smoke 35 passed / 3 live
skipped, layering 71/71 (10 packages, 32 subpaths), depgraph 22/22, mutation
config 45/45, fallow clean over 129 changed files, package smoke imported all
12 published entry points with publint and attw passing. Verified functionally
against the built dist: the doc's parse -> findSelectorChainMatch example
returns the same shapes as before, resolveSelectorChain still returns an AST
`selector`, and formatSelectorFailure still accepts a chain.
…moval

Review P1s on a792415: restoring the public subpath left two gates asserting
it was gone.

- installed-package-metro.test.ts moved `agent-device/selectors` into the
  blocked-specifier list. It goes back to the subpath smoke set, running the
  same `isSelectorToken('||')` + `parseSelectorChain` check it ran before the
  removal, so the file's only remaining delta from main is a formatter reflow.
- owner-files-no-leak.test.ts asserted `dist/src/sdk-selectors.js` was absent.
  It requires the stable named chunk again, and still rejects an auto-numbered
  `selectors2.js` fallback — the pair is what proves the restored tsdown chunk
  group is doing its job, verified against a clean build.

PR body corrected: the removal is no longer described as intentional API
tightening.
main's #1591 (the follow-up filed from this review) removed `packages/**` from
.fallowrc.json's ignorePatterns, so the new package is audited for the first
time. Everything below is a finding fallow could not previously see.

Dead surface, all confirmed consumer-free:

- 12 type re-exports from the `.` façade whose shapes consumers only ever
  reach structurally.
- MAESTRO_TEXT_SELECTOR_KEYS / MAESTRO_STATE_SELECTOR_KEYS, orphaned by this
  branch's own MAESTRO_SELECTOR_PROJECTION change, and the test-util
  SELECTOR_VALUE_HAZARDS. All three are module-local now.
- IS_PREDICATE_USAGE_HINT fails --production because its only consumer is the
  is-argument-surface parity test. It gets a commented `ignoreExports` entry
  rather than deletion: the constant is what makes the daemon and CLI raise
  ONE hint instead of two copied strings (ADR 0010), so the test asserting
  that is the point, not an accident.

`fast-check` is now declared by the package that imports it.

Duplication, split by what could be proven:

- `isUsefulVisibilityAnchor` existed character-for-character in both
  packages/selectors and packages/maestro. Moved to
  @agent-device/contracts/snapshot, which both already depend on and which
  already owns this vocabulary. Safe because the `normalizeType` each copy
  called is itself character-identical to the contracts one — checked before
  moving, since a different normalizer would have silently changed which
  nodes anchor.
- maestro additionally reimplemented `normalizeType`, `buildSnapshotNodeMap`
  (as `buildSnapshotNodeByIndex`) and `findSnapshotAncestor`, all
  character-identical to contracts'. Deleted in favour of the shared ones.
- The three scroll-ancestor walks are NOT deduped. They are structurally the
  same walk but each uses a different scrollable predicate, and I have no
  evidence the three agree; collapsing them would be a Maestro-conformance
  change, not a cleanup. Both maestro sites now say so, and the work is filed
  separately.

`projectSelectorExpression` (15 cyclomatic / 22 cognitive, written by the
cutover) splits into a dispatcher plus `readAgreedTextValue` and
`projectSelectorTerms`; all three are under threshold.

Rebase note: the one conflict, in package-boundaries.test.ts, resolved to
NEITHER side — #1591 had already deleted `AdReplayVerifiedTargetGuard` as an
unused export, and this branch deletes the seven ReplaySelectorPort names, so
the conflicting block is empty.
@thymikee
thymikee force-pushed the agent/selectors-package-cutover branch from a792415 to 4572fb1 Compare August 4, 2026 16:47
Declaring the dependency in packages/selectors/package.json without
regenerating pnpm-lock.yaml made every CI job fail in its install step with
ERR_PNPM_OUTDATED_LOCKFILE. My local `pnpm install --frozen-lockfile` printed
"+ 1 dependencies were added: fast-check@^4.9.0" and exited 0, which read as
success but was the same mismatch CI refuses.

Regenerated with the pinned pnpm 11.17.0, not the 11.5.3 on this machine:
11.5.3 rewrites peer-dependency resolution keys repo-wide (dropping
`(supports-color@7.2.0)` suffixes) and produced a 222-line diff. With the
pinned version the diff is the 4 lines this change actually needs, plus
pnpm's alphabetical re-sort of the root selectors entry.
@thymikee
thymikee merged commit 4f8dc3f into main Aug 4, 2026
42 checks passed
@thymikee
thymikee deleted the agent/selectors-package-cutover branch August 4, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant