mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
03c3984066
* perf(contracts): granularize entry surfaces so hub importers stop evaluating the facade clump
`@agent-device/contracts/platform` unions 32 vocabulary modules and
`/interaction` another 18. A file that value-imports either evaluates the whole
union to reach one function, and because permanent hubs sat behind them —
`command-descriptor/registry.ts`, `core/capabilities.ts`,
`interactors/register-builtins.ts`, `command-descriptor/platform-execution-entry.ts` —
that union rode into roughly half the unit suite's test graphs.
Give every vocabulary module its own entry subpath and move all value-importers
onto the module that owns the symbol. Type-only importers are left alone: `import
type` is erased, so it already evaluated nothing.
Measured with the #1950 eager-import-closure walker over all 974 unit-core test
files, against base e5bfde3d1:
aggregate eager module evaluations 143,248 -> 129,738 (-9.4%)
facades/platform.ts carried by 466 -> 1 test graphs
facades/interaction.ts carried by 451 -> 0 test graphs
registry.ts 105 -> 66
capabilities.ts 113 -> 76
register-builtins.ts 111 -> 73
platform-execution-entry.ts 43 -> 3
dispatch.ts 134 -> 100
Three gate adjustments the split forces:
- R11's pinned contracts subpath list grows to the new entries, and the resolver
test's "must not resolve" example moves to `./clipboard`, since `./gesture-plan`
is now a real entry.
- R16 anchored the record-runtime join on the literal `contracts/platform`
specifier. It now accepts any contracts entry — the assertion's provenance is
what the rule pins, not which subpath carried it.
- `gesture-plan.ts` became an entry target, and the no-bare-star rule rejects the
`export * from './gesture-plan-types.ts'` it carried. Its one internal consumer
now imports the owning module directly.
Both facades keep their type re-exports for the ~490 type-only importers, so
every symbol on them now reads as value-unused; one fallow entry records that
and names retiring them as the follow-up.
Closes #1959
* test(contracts): text-filter the facade scan before parsing
The repo-wide scan parsed all ~3000 sources, which the coverage lane's
instrumentation pushed past both the 5s test timeout and the 2.5s slow-test
budget. A file that never names the specifier cannot import it, so filter on the
text first and parse only the ~490 candidates.
Non-vacuity moves with it: instead of counting narrow imports across every file,
require that the surviving type-only importers were seen and classified as
erased — which an empty scan cannot satisfy.
236 lines
13 KiB
JSON
236 lines
13 KiB
JSON
{
|
|
"$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
|
|
"entry": [
|
|
"tsdown.config.ts",
|
|
"vitest.mutation.config.ts",
|
|
"src/sdk/index.ts",
|
|
"src/sdk/io.ts",
|
|
"src/sdk/artifacts.ts",
|
|
"src/sdk/metro.ts",
|
|
"src/sdk/remote-config.ts",
|
|
"src/sdk/install-source.ts",
|
|
"src/sdk/android-adb.ts",
|
|
"src/sdk/contracts.ts",
|
|
"src/sdk/selectors.ts",
|
|
"src/sdk/finders.ts",
|
|
"src/ai-sdk/index.ts",
|
|
"src/bin.ts",
|
|
"src/client/companion-tunnel.ts",
|
|
"src/daemon.ts",
|
|
"src/utils/png-worker.ts",
|
|
"scripts/patch-xcuitest-runner-icon.ts",
|
|
"scripts/runner-request-count/run.ts",
|
|
// #1596 regression fixture: runs as a real `node --experimental-strip-types`
|
|
// subprocess (test/integration/daemon-replace-exit-flush.test.ts), so
|
|
// dependency analysis cannot follow the runCmdSync string path to it.
|
|
"test/integration/support/exit-after-flush.ts",
|
|
"src/utils/update-check-entry.ts",
|
|
"examples/sdk/client-session.ts",
|
|
"examples/sdk/metro-runtime.ts",
|
|
"examples/sdk/contracts-result.ts",
|
|
"examples/sdk/batch-orchestration.ts",
|
|
"examples/sdk/ai-sdk-tools.ts",
|
|
"test/scripts/metro-prepare-packaged-smoke.mjs",
|
|
"test/integration/*.test.ts",
|
|
"website/docs/404.mdx",
|
|
"website/rspress.config.ts"
|
|
],
|
|
"ignorePatterns": [
|
|
"examples/test-app/**",
|
|
"scripts/perf/**",
|
|
"scripts/layering/**",
|
|
"scripts/maestro-conformance/corpus/**",
|
|
"apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan",
|
|
"scripts/write-xcuitest-cache-metadata.mjs",
|
|
"scripts/help-conformance-sample-outputs.d.mts",
|
|
"scripts/help-conformance-runner-output.d.mts"
|
|
],
|
|
"ignoreDependencies": [
|
|
"@theme",
|
|
// @agent-device/maestro owns the source import, while the published root
|
|
// build externalizes yaml and retains runtime imports in packed chunks.
|
|
"yaml",
|
|
// @agent-device/provider-limrun owns the source import, while the published
|
|
// root build externalizes @limrun/api and retains runtime imports in packed chunks.
|
|
"@limrun/api",
|
|
// Packaging linters with no importable API surface here: scripts/check-package.ts runs their
|
|
// `node_modules/.bin` shims as subprocesses, which dependency analysis cannot follow to an import.
|
|
"publint",
|
|
"@arethetypeswrong/cli"
|
|
],
|
|
"ignoreExports": [
|
|
{
|
|
"comment": "Daemon route handlers are reached only through the dynamic `import()` table in request-handler-chain.ts, which --production analysis cannot follow to a consumer.",
|
|
"file": "src/daemon/handlers/{lease,session,snapshot,react-native,record-trace,find,interaction}.ts",
|
|
"exports": [
|
|
"handleLeaseCommands",
|
|
"handleSessionCommands",
|
|
"handleSnapshotCommands",
|
|
"handleReactNativeCommands",
|
|
"handleRecordTraceCommands",
|
|
"handleFindCommands",
|
|
"handleInteractionCommands"
|
|
]
|
|
},
|
|
{
|
|
"comment": "Dedicated CLI command handlers are reached only through the dynamic `import()` table `dedicatedCliCommandHandlerLoaders` in src/cli/commands/router.ts, which --production analysis cannot follow to a consumer. Same shape as the daemon route-handler entry above; that table is what enumerates this list, so add/remove here whenever a loader is added/removed.",
|
|
"file": "src/cli/commands/{auth,connection,daemon,device,proxy,replay,screenshot}.ts",
|
|
"exports": [
|
|
"authCommand",
|
|
"connectCommand",
|
|
"disconnectCommand",
|
|
"connectionCommand",
|
|
"daemonCommand",
|
|
"deviceCommand",
|
|
"proxyCommand",
|
|
"replayCommand",
|
|
"screenshotCommand",
|
|
"diffCommand"
|
|
]
|
|
},
|
|
{
|
|
"comment": "Test fixtures: every *.fixtures.ts builds doubles for co-located *.test.ts files (several import `vi` directly) and none is imported by production source, so --production analysis sees no consumer for any of them. Scoped to unused-exports rather than ignorePatterns so fixtures stay inside every other Fallow rule (health, dead-code, cycles); `*` rather than today's three symbols because the property being asserted is per-file, not per-symbol.",
|
|
"file": "**/*.fixtures.ts",
|
|
"exports": ["*"]
|
|
},
|
|
{
|
|
"comment": "Published package surface (see package.json#exports); consumed by SDK users, not internally.",
|
|
"file": "{src/sdk/*.ts,src/ai-sdk/index.ts}",
|
|
"exports": ["*"]
|
|
},
|
|
{
|
|
"comment": "#1642: its three consumers (src/__tests__/cli-device-status.test.ts, src/daemon/__tests__/device-claims.test.ts, src/daemon/__tests__/device-claim-prune.test.ts) all reach it the only way a Vitest module mock can — `vi.mock(path, async (importOriginal) => (await import('...host-process-mock.ts')).pinOwnProcessStartTime(importOriginal))`. Dependency analysis cannot follow that dynamic import to a consumer, the same limitation the daemon route-handler entry above records.",
|
|
"file": "src/__tests__/test-utils/host-process-mock.ts",
|
|
"exports": ["pinOwnProcessStartTime"]
|
|
},
|
|
{
|
|
"comment": "Tool config default exports, loaded by the tool rather than imported.",
|
|
"file": "{tsdown.config.ts,vitest.mutation.config.ts,website/rspress.config.ts}",
|
|
"exports": ["default"]
|
|
},
|
|
{
|
|
"comment": "Type-level `AssertTrue<...>` registry-totality guards. Exported only so `noUnusedLocals` keeps them alive; a consumer would defeat the point.",
|
|
"file": "{src/core/command-descriptor/registry.ts,src/core/interactors/register-builtins.ts,src/core/platform-descriptor/registry.ts,src/daemon/request-platform-providers.ts}",
|
|
"exports": [
|
|
"CommandOwnerFileClaimsAreComplete",
|
|
"BuiltinPluginsCoverAllPlatforms",
|
|
"PlatformDescriptorsAreTotal",
|
|
"GatedKeysAreResolverKeys"
|
|
]
|
|
},
|
|
{
|
|
"comment": "resolveVitestMaxWorkers is consumed by vitest.config.ts \u2014 a tool config outside --production analysis.",
|
|
"file": "scripts/lib/vitest-concurrency.ts",
|
|
"exports": ["resolveVitestMaxWorkers"]
|
|
},
|
|
{
|
|
"comment": "Mutation-lane seam: readTestScope is consumed by vitest.mutation.config.ts, a tool config outside --production analysis.",
|
|
"file": "scripts/mutation/test-scope.ts",
|
|
"exports": ["readTestScope"]
|
|
},
|
|
{
|
|
"comment": "Mutation-lane completeness gate: ownedTestFiles enumerates the whole derived ownership map, which only the assertion in ownership.test.ts consumes — a production consumer would defeat the point.",
|
|
"file": "scripts/mutation/ownership.ts",
|
|
"exports": ["ownedTestFiles"]
|
|
},
|
|
{
|
|
"comment": "ADR 0010 shared-hint pin: the facade re-export's only consumer is src/__tests__/is-argument-surface-parity.test.ts, which asserts the daemon and CLI `is` surfaces raise the SAME hint text. The constant is live inside the package (predicates.ts raises it); a production consumer of the re-export would defeat the point, since it exists so neither surface copies the string.",
|
|
"file": "packages/selectors/src/index.ts",
|
|
"exports": ["IS_PREDICATE_USAGE_HINT"]
|
|
},
|
|
{
|
|
"comment": "Help-benchmark conformance seams: helpTopicIds feeds the topic-coverage gate, and the *_SAMPLE constants feed the bench sample producers (scripts/__tests__/help-conformance-sample-producers.ts); every consumer is a test-tree file outside --production analysis.",
|
|
"file": "{src/cli/parser/cli-help.ts,scripts/help-conformance-sample-outputs.mjs}",
|
|
"exports": ["helpTopicIds", "PRIVATE_AX_RECOVERY_SAMPLE", "DEVICE_CLAIM_IN_USE_SAMPLE"]
|
|
},
|
|
{
|
|
"comment": "#1458: getSessionCommandKind's former production consumer (the session.ts if-chain) was replaced by SESSION_COMMAND_HANDLER_IMPLS, which encodes command-kind groupings directly via shared handler references instead of a kind lookup. The accessor and its SessionCommandKind classification remain covered by daemon-command-registry.test.ts, a test-tree file outside --production analysis.",
|
|
"file": "src/daemon/daemon-command-registry.ts",
|
|
"exports": ["getSessionCommandKind"]
|
|
},
|
|
{
|
|
"comment": "Workspace-package symbols whose only consumers are test-tree files, which --production analysis does not see. Each import is the package specifier, so the façade entry is load-bearing: parseTargetAnnotationV1Payload/TARGET_ANNOTATION_MAX_FIELD_BYTES from src/daemon/__tests__/session-target-evidence.test.ts, MAESTRO_COMPAT_* from src/cli/parser/__tests__/maestro-support-matrix.test.ts, createAppLogLiveHandle from src/__tests__/test-utils/app-log-live-handle.ts (its production sibling createAppLogLiveHandleFromFinish wraps it, and scripts/layering/platform-package-policy.test.ts pins the specifier form).",
|
|
"file": "packages/{ad-script,maestro,capture-kit}/src/index.ts",
|
|
"exports": [
|
|
"parseTargetAnnotationV1Payload",
|
|
"TARGET_ANNOTATION_MAX_FIELD_BYTES",
|
|
"MAESTRO_COMPAT_LIMITATIONS",
|
|
"MAESTRO_COMPAT_SUPPORTED_CAPABILITIES",
|
|
"createAppLogLiveHandle"
|
|
]
|
|
},
|
|
{
|
|
"comment": "Deliberately kept off the @agent-device/maestro façade (index.test.ts asserts its absence) and consumed only by the conformance harness under packages/maestro/test/.",
|
|
"file": "packages/maestro/src/internal/program-ir-command-parser.ts",
|
|
"exports": ["SUPPORTED_MAESTRO_COMMAND_NAMES"]
|
|
},
|
|
{
|
|
"comment": "Published type surface: re-exported by src/sdk/limrun.ts to agent-device/limrun, where test/integration/limrun-public-types.test.ts pins that consumers can import it. Its only importer compiles in a temp checkout, so no static in-repo edge reaches it — the same reason src/sdk/*.ts is exempt above.",
|
|
"file": "packages/provider-limrun/src/index.ts",
|
|
"exports": ["LimrunIosCommandExecution"]
|
|
},
|
|
{
|
|
"comment": "#1959 gave every contracts vocabulary module its own entry subpath and moved all ~230 value-importers onto them, so `./platform` and `./interaction` now carry zero value consumers — every re-export on them reads as unused. They stay as the compatibility surface for the ~490 type-only importers the granularization deliberately left alone (type imports are erased, so they cost nothing to load). Scoped to these two façades rather than listed symbol-by-symbol because the property is per-file: nothing on them is value-consumed any more. Retiring them entirely — moving the type-only importers too and deleting the files — is the follow-up that makes this entry unnecessary.",
|
|
"file": "packages/contracts/src/facades/{platform,interaction}.ts",
|
|
"exports": ["*"]
|
|
},
|
|
{
|
|
"comment": "Converting the contracts façades from `export *` to explicit named re-exports (the pin-table retirement) made these individually visible to --production analysis for the first time; a bare star previously hid them from this exact check. GESTURE_INITIAL_ANGLE_DEGREES/GESTURE_SAMPLE_INTERVAL_MS/INTERACTION_GUARANTEES/INTERACTION_PATH_IDS/parseAndroidInputMethodPackage/isRecordingExportQuality have same-package test-tree consumers (invisible to --production, mirroring the packages/{ad-script,maestro} entry above); the rest currently have none. Kept rather than narrowed here so the façade's re-export surface stays byte-identical to the symbol set the retired pin table asserted — narrowing the surface is a follow-up with its own review, not a side effect of this mechanical conversion. The last five (COORDINATE_GESTURE_KINDS, normalizePublicGesture, DEFAULT_DRAG_*) are #1567's drag vocabulary, surfaced the same way by the same conversion and kept for the same reason.",
|
|
"file": "packages/contracts/src/facades/{client,command,divergence,interaction,platform,recording}.ts",
|
|
"exports": [
|
|
"isRecord",
|
|
"IOS_SAFARI_BUNDLE_ID",
|
|
"REPLAY_DIVERGENCE_DEFAULT_REF_LIMIT",
|
|
"REPLAY_DIVERGENCE_DIGEST_REF_LIMIT",
|
|
"REPLAY_DIVERGENCE_LEVEL_BYTE_LIMITS",
|
|
"applyReplayDivergenceLevelCaps",
|
|
"measureReplayDivergenceBytes",
|
|
"sanitizeReplayDivergenceField",
|
|
"truncateUtf8Field",
|
|
"GESTURE_DURATION_MAX_MS",
|
|
"GESTURE_INITIAL_ANGLE_DEGREES",
|
|
"GESTURE_SAMPLE_INTERVAL_MS",
|
|
"INTERACTION_GUARANTEES",
|
|
"INTERACTION_PATH_IDS",
|
|
"buildSwipePresetGesturePlan",
|
|
"clampGestureCoordinate",
|
|
"gestureDirectionDelta",
|
|
"parseAndroidInputMethodPackage",
|
|
"isRecordingExportQuality",
|
|
"COORDINATE_GESTURE_KINDS",
|
|
"normalizePublicGesture",
|
|
"DEFAULT_DRAG_DESTINATION_HOLD_MS",
|
|
"DEFAULT_DRAG_MOVE_MS",
|
|
"DEFAULT_DRAG_SOURCE_HOLD_MS"
|
|
]
|
|
}
|
|
],
|
|
"usedClassMembers": ["name", "listActiveLeases", "delete", "values", "elapsedMs", "isExpired"],
|
|
"rules": {
|
|
"unused-types": "error",
|
|
"duplicate-exports": "off"
|
|
},
|
|
"overrides": [
|
|
{
|
|
"comment": "unused-types counts importers, not signature reachability. A façade names every type its re-exported functions/values put in a public signature, and callers use those types structurally without ever importing the name — the same false-positive shape already fixed for other workspace-package façades. unused-exports (a VALUE check) stays on for this glob; that is what would catch a real leak.",
|
|
"files": ["packages/contracts/src/facades/*.ts"],
|
|
"rules": {
|
|
"unused-types": "off"
|
|
}
|
|
}
|
|
],
|
|
"production": {
|
|
"dupes": true
|
|
},
|
|
"publicPackages": ["agent-device"],
|
|
"audit": {
|
|
"deadCodeBaseline": "fallow-baselines/dead-code.json",
|
|
"healthBaseline": "fallow-baselines/health.json"
|
|
},
|
|
"includeEntryExports": true,
|
|
"ignoreExportsUsedInFile": {
|
|
"type": true,
|
|
"interface": true
|
|
}
|
|
}
|