refactor: move selector engine into workspace package - #1589
Conversation
|
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Review of [P2] Remove the unused internal barrel [P2] Owner-action CI: exact-head iOS Smoke is red at the initial 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. |
|
Release/API triage addendum: 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. |
|
Re-review of |
`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.
a792415 to
4572fb1
Compare
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.
Summary
@agent-device/selectorsbetween@agent-device/ad-scriptand@agent-device/ad-replay, repoints production consumers, and keeps AST/parser internals private behind a narrow façade.agent-device/selectorssubpath exactly as v0.20.5 shipped it. An earlier revision of this PR removed it; that was outside the approved S1 plan, which called for the SDK shim to be repointed, and it is reverted. The AST now leaves the package through one pinned@agent-device/selectors/astsubpath whose only in-repo consumer issrc/sdk/selectors.ts, so the normal.façade stays string-only.Deletions / remaining debt
src/selectors/packages/selectors/src/internal/.createDaemonReplaySelectorPortandsrc/daemon/replay-selector-port.ts@agent-device/selectorsengine and façade tests cover the behavior.src/sdk/selectors.tsand root./selectorsexport@agent-device/selectors/ast). The.façade still does not exportSelector,SelectorChain,SelectorTerm,SelectorKey, or grammar internals;./astcarries exactly the ten symbols v0.20.5 published, pinned infacade-symbols.tsand reachable from that one file only.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.@limrun/api,yaml); all 12 published entry points imported; packaged CLI0.20.5ran on Node26.1.0;agent-device/selectorsresolves and its parse/match/format surface behaves as v0.20.5 did.SelectorChaincaused both exact-façade and AST-privacy gates to fail. Both mutations were restored.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 unreadselectorExpressioncapture-policy parameter and the two tests that asserted it had no effect, returned the Maestro export vocabulary topackages/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./astagain.