Skip to content

chore(knip): clear the parked dead-code backlog and drop the ceiling to 0 - #770

Open
hubgan wants to merge 35 commits into
mainfrom
chore/knip-clear-unused-exported-types
Open

chore(knip): clear the parked dead-code backlog and drop the ceiling to 0#770
hubgan wants to merge 35 commits into
mainfrom
chore/knip-clear-unused-exported-types

Conversation

@hubgan

@hubgan hubgan commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Built on top of #769, so this PR carries that commit too — the workflows only run for PRs targeting main, which is why it is based there rather than on the branch. #769 on its own is the minimal, low-risk fix for the currently-red job; if it merges first this shrinks to its own commit, and if this one merges first it closes #769 as well.

What this does

#572 landed the gate with 215 findings parked under --max-issues. That bounded the backlog but left it in place, and the first thing to touch it (#663) pushed the total to 218 and turned the job red. This clears the backlog and sets the ceiling to 0, so the next unused export fails the job instead of consuming headroom nobody knew was there.

before after
Unused exports 47 0
Unused exported types 156 0
Unused exported class members 12 0
--max-issues 215 0

Scope check first

Every workspace holding a finding is private: true. @swmansion/argent — the one published package — had zero, so nothing here moves a public API.

Two shapes of fix

Still referenced inside its own file → drop export (146 types, 36 exports). The declaration stays put; it just leaves the module's public surface. tsc keeps emitting these into the .d.ts as local declarations that the exported signatures reference.

For the 146 types that is the whole story: they are erased, so the emitted .js is byte-identical. For the 36 values it is not. Dropping export from a const/function/class removes its exports.X binding from the emitted dist/*.js — 36 declarations across 26 files here. packages/tool-server/dist/utils/device-info.js no longer carries exports.REMOTE_PREFIX or exports.VEGA_SERIAL_PREFIX; telemetry/dist/posthog.js loses exports.POSTHOG_PROJECT_TOKEN; in the ESM-emitting argent-installer, isTempRunnerPath / resolveLocalArgentDir / PACKAGE_ROOT leave the module namespace object entirely.

Nothing in the repo or in the packages/argent-private submodule at the pinned revision reads any of them by path, so nothing breaks. Flagging it because a require() of a built dist/*.js loses a dropped export exactly the way it lost the deleted __resetAndroidDevtoolsInstallCache this branch had to restore — so this half of the diff does deserve runtime scrutiny, not a skim.

Referenced nowhere → delete. Orphaned test seams (__test, _testValidators), stale compatibility re-exports, and a few real orphans. The ones worth a reviewer's eye:

  • localSimctl — the SimctlBackend strategy is only ever constructed with remoteSimctl. What keeps the local iOS impl off it is the handler signature: buildIosLaunchHandler / buildIosRestartHandler read services.nativeDevtools, and ios-remote is the only platform that declares that service, so the local impl — which resolves native-devtools per device inside its own handler — has nothing to hand them and shells out to xcrun itself.
  • sendCharInsert — removing the __sendCharInsert test alias exposed the underlying helper as unreferenced. It is superseded, not unwired: the Chromium keyboard impl dispatches its own type: "char" event inline (keyboard/platforms/chromium.ts:58), which is exactly what this helper did. Chromium typing is unaffected.
  • Error metadata fieldsServiceNotFoundError.serviceId, ToolNotFoundError.toolId and their two siblings were never read. The e.serviceId in registry-error-events.test.ts is a field on the event object the handler pushes, not on the error class; the err.toolId in http.ts is NotImplementedOnPlatformError, a different class that keeps its fields. Flagging these in case they are wanted as deliberate diagnostics.

Deleting SourceMapsRegistry.toGeneratedPosition / findMatchingSource cascaded into their only helper (buildSourceCandidates) and then into projectRoot, which had no remaining reader — so the constructor parameter goes too. StubSourceMapsRegistry existed only to pass super(""), so it goes with it.

The three cross-workspace members

ArtifactStore.register, ArtifactStore.list and TypedEventEmitter.off are all called from other workspaces — the artifact route and screenshot tools in tool-server, and registry-listener in telemetry. They are reported unused anyway, and the reason is the unbuilt tree the gate runs against: every workspace resolves main/types to a dist/ that does not exist, so @argent/registry resolves to nothing and the cross-workspace edge never forms. Build the tree and all three findings disappear on their own; a control member that really is dead is still reported in the built run, so the pass is live either way. Nothing about this is specific to classMembers — every cross-workspace reference is invisible the same way.

Each is exempted at the declaration with a @public JSDoc tag rather than by name in knip.jsonc, for one reason: scope. ignoreMembers matches a name across the whole workspace, so it would also hide a future dead register/list/off on any other class in packages/registry; @public binds to the member it is written on.

What @public does not buy is a staleness check, and neither does ignoreMembers. Measured on a clean unbuilt worktree: a bogus ignoreMembers name and a redundant @public each leave the run at exit 0 with no hint, while a bogus ignoreDependencies name exits 1 with Remove from ignoreDependencies. treatConfigHintsAsErrors is live and simply has no producer for either exemption style — so when one of the three stops being earned, nothing says so, and the JSDoc at each member is the only record a re-audit has. Each one names its callers, and off records how to re-derive them (rename the member and read the Property 'off' does not exist errors; grepping .off( over-reports, since 14 of the 38 call-site hits are Node EventEmitters). One correction to the commit message on that change: it says 35 hits where the exact count is 38 — 14 Node EventEmitter, 24 TypedEventEmitter (21 production, 3 in a test). The 14 and the 21 are right.

knip.jsonc, CONTRIBUTING.md step 5 and the workflow's failure-explanation step all described a parked backlog and a ceiling to stay under. All three are rewritten for a gate that must simply come back empty, and all three now name the @public escape hatch — the remedy for a symbol whose only caller is out of knip's reach.

Verification

Run against a clean worktree with no build output, the way CI counts:

  • npm run knipexit 0, prints nothing. Both passes empty, no config hints.
  • npx tsc --build — clean.
  • npm run lint (eslint . --max-warnings 0) — clean.
  • npx prettier --check . — clean.
  • npm test --workspaces — tool-server 329 files / 3857 passed; registry, telemetry, update-core, tools-client, mcp, cli green.
  • npm run typecheck:tests --workspaces — 13 workspaces define the script and all 13 are clean, but the command itself exits 1: npm errors Missing script: "typecheck:tests" for the three that do not define it rather than skipping them. Worth knowing before re-running this bullet and reading the exit code as a failure.

End-to-end on a tool-server built from this branch, driven over its HTTP API:

  • Chromium (Electron smoke app) — describe, screenshot, gesture-tap (counter advanced 0 → 1), POST /api/clipboard/text read back out of the renderer, the WS clipboardSync command, and debugger-connect / debugger-status / debugger-evaluate (sourceMapReady: true with StubSourceMapsRegistry gone).
  • iOSboot-device, launch-app, describe via ax-service, screenshot.
  • tvOSboot-device, launch-app on an Apple TV udid, focus-driven describe. The registry snapshot then carries NativeDevtools:<tvOS udid>, confirming injection is resolved on tvOS through the local impl.
  • Androidboot-device on a dedicated AVD, describe via android-devtools, gesture-tap.
  • Metro source maps — the registry driven against a real 12.5 MB Metro .map over loopback, the way Debugger.scriptParsed drives it: allowlist passes and waitForPending() resolves on both, while retained heap after gc drops from 17.3 MB to 3.6 MB.

Review follow-ups

Five commits on top, one per finding. The base is back on main, so the workflows run again.

  • docs(launch-app) — the services comment explained the empty shape through LaunchAppAndroidServices, deleted here, and skipped LaunchAppVegaServices, which is still there. Same edit in restart-app/types.ts.
  • docs(registry) — the @public tag on ArtifactStore.register named 2 of 7 callers. Derived the full set the way the sibling tag on off prescribes: renamed the member, read the 16 TS2339 errors tsc --build reports across flow-visual (5), screenshot (4), native-profiler-stop (2), screenshot-diff (2), native-profiler-analyze, react-profiler-analyze and screen-recording-stop. The re-derivation recipe is recorded too — .register( greps as badly as .off(.
  • docs(knip) — "the build, the tests and this gate all stay green on a wrong delete" held for argent-private and not for the cross-workspace case that justifies three of the four @public tags. Deleting ArtifactStore.register gives 16 TS2339 errors and 9 failed | 2 passed in test/artifacts.test.ts; only the gate stays green. Split in the workflow comment, the failure annotation and CONTRIBUTING.md step 5.
  • perf(debugger)doRegister's data: branch still base64-decoded and JSON.parsed a payload it dropped, inside a catch that swallows. Both arms returned the same void, so the branch collapses to an early return; scriptUrl / scriptId go with it, since their last reader was the deleted this.maps.push(...) and doRegister is private.
  • test(debugger) — the allowlist check moved to a top-level early return with nothing pinning it: delete the line and both SSRF files still pass 17/17. Two tests now assert doRegister consults it — four rejected URLs reach no fetch, and the loopback *.map Metro emits still does.

Re-verified after: npm run knip exit 0 and empty on a tsc --build --clean tree, tsc --build, eslint . --max-warnings 0, prettier --check . all clean, and npm test --workspaces with tool-server at 329 files / 3854 passed. The one red test, argent-installer's globalPath returns ~/.config/opencode/opencode.json, fails identically with these commits stashed — it reads the real ~/.config/opencode, which holds a .jsonc on this machine.

End-to-end on a tool-server run from this branch over its HTTP API, on every platform:

  • Chromiumlaunch-app, describe, screenshot (returned an artifact handle, GET /artifacts/<id> served the 137542-byte PNG back), gesture-tap (counter 0 → 1), debugger-connect / debugger-status (sourceMapReady: true) / debugger-evaluate (read taps: 1 out of the page).
  • iOSlaunch-app, describe via native-devtools, restart-app, screenshot.
  • tvOSlaunch-app, focus-driven describe (12 focusables in TVSettings), tv-remote, restart-app.
  • Androidlaunch-app, describe via android-devtools, restart-app, screenshot.
  • Vegaboot-device on the tv VVD, describe via the automation toolkit.
  • Metro — the real debugger-connect / debugger-status / debugger-evaluate tools against a stand-in Metro (HTTP /json/list + CDP WebSocket) emitting three Debugger.scriptParsed events: a loopback *.map on a counting server, a 1.4 MB inline data: map, and http://169.254.169.254/latest.map. sourceMapReady: true, loadedScripts: 3, evaluate 42, the loopback map fetched exactly once and the metadata URL never. A malformed inline payload gives byte-identical output, which is the finding.

Merge with main

Putting the base back on main made CI resolve the merge, and the Unit Tests job went red on a conflict no side could see on its own: #771 fixed the same red gate this branch's first commit did, the other way round — it made component-names.test.ts import StrippedName, ComponentAnnotation and ComponentNameResolution instead of dropping their export. Merged main in, reproduced it (tsc --noEmit -p tsconfig.test.json → three TS2459s) and took main's resolution: the three exports are back. The gate stays green because knip treats test files as entry points, so a type a test imports is not an unused export.

All nine workflows are green on the head commit.

@hubgan
hubgan changed the base branch from fix/knip-component-name-types-unexported to main August 11, 2026 12:02
@hubgan hubgan closed this Aug 11, 2026
@hubgan hubgan reopened this Aug 11, 2026
@hubgan
hubgan marked this pull request as ready for review August 11, 2026 12:08
@hubgan
hubgan requested a review from latekvo August 11, 2026 12:11

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[Diplomat]: Reviewed at c24b6ba1. Ran on a clean npm ci --ignore-scripts worktree: npm run knip (exit 0, empty), npx tsc --build, npm run typecheck:tests --workspaces (13 workspaces), eslint . --max-warnings 0, prettier --check ., npm test -w packages/tool-server (329 files / 3857 passed), and node packages/argent/scripts/bundle-tools.cjs (all four esbuild bundles built; only the unrelated trace-processor asset download is missing locally). Diffed every emitted dist/*.js against a build of f136c024. Booted the Electron smoke app against tool-servers built from both f136c024 and c24b6ba1 and compared describe, screenshot, gesture-tap, /artifacts/:id, debugger-connect/debugger-evaluate, keyboard and five error paths — identical output on both, and typing still lands real characters in a focused field. Also mutated the gate itself (unused export, unused type, unused class member, stale ignoreDependencies, stale ignoreMembers) to check what it does and does not catch. Findings inline.

Comment thread knip.jsonc Outdated
Comment thread knip.jsonc Outdated
Comment thread packages/tool-server/src/chromium-server/input.ts Outdated
Comment thread packages/telemetry/src/sanitize.ts Outdated
Comment thread packages/tool-server/src/utils/debugger/source-maps.ts Outdated
Comment thread packages/tool-server/src/utils/android-helper-install.ts
Comment thread packages/tool-server/src/utils/simctl-backend.ts Outdated
@hubgan
hubgan changed the base branch from main to fix/knip-component-name-types-unexported August 11, 2026 13:38
@hubgan
hubgan requested a review from latekvo August 11, 2026 13:39

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[Diplomat]: Reviewed at 671447d3, on top of the earlier round at c24b6ba1. Five of that round's seven findings land cleanly, each re-checked by re-running its repro across the boundary: both orphaned comments are gone, the ignoreMembers block is replaced by per-member tags, the discovery.ts rewrite holds clause by clause (readSourceFragment does return null on an empty projectRoot, source-resolver.ts:156), and __resetAndroidDevtoolsInstallCache is restored with a byte-identical body and now survives the gate on the @public tag alone - the submodule driver's require(dist).__resetAndroidDevtoolsInstallCache() succeeds at head and threw TypeError against a build of c24b6ba1. The two that are not resolved get replies on their existing threads rather than new comments.

What I ran on a clean npm ci --ignore-scripts worktree at this head: npm run knip (exit 0, empty, unbuilt and built), npx tsc --build, eslint . --max-warnings 0, prettier --check ., and the suites - tool-server 329 files / 3857 passed, telemetry 295, registry 90, argent-installer 554, argent-cli 459, argent-mcp 79, argent-tools-client 191. On npm run typecheck:tests --workspaces: 13 workspaces define the script, not six, and all 13 are clean - but the command itself exits 1, because npm errors Missing script: "typecheck:tests" for the three that do not define it rather than skipping them. Worth knowing if anyone re-runs that verification bullet and sees a nonzero exit. Built base and head and diffed all 432 emitted .js plus the four esbuild bundles from bundle-tools.cjs. Mutated the gate itself through 15 issue classes and confirmed each is caught in the pass the prose predicts, and that no knip issue type falls through both passes. Ran a live tool-server on a Chromium/Electron device against head and base and compared describe, gesture-tap, keyboard, gesture-scroll, screenshot, chromium-tabs, debugger-connect/status/evaluate, view-network-logs and five error paths - identical, apart from a pre-existing non-deterministic adb error-message race I reproduced on both builds.

Worth stating what came back clean, since most of it is the part that could have gone wrong. All 214 lost exports (183 unexported, 31 deleted) are unreferenced across every file type, scripts/, .github/, all 41 markdown files and the argent-private submodule at the pinned revision 79ae7d91 - __resetAndroidDevtoolsInstallCache was the only cross-boundary caller and it is fixed. Every one of the 183 unexported symbols still has an in-file use, so none of them became dead-but-invisible, which is the way this shape of change usually goes wrong. knip --production and --include-entry-exports, head against base with both trees unbuilt, report zero newly orphaned files and zero new issues at head. No barrel lost a symbol it still re-exports, and no value export became export type. Under noUnusedLocals/noUnusedParameters/exactOptionalPropertyTypes/noImplicitOverride the base-to-head delta across the monorepo is exactly one new error, the clipboard field below. And git merge-tree of this head into current origin/main is conflict-free, with the merged tree passing npm run knip unbuilt and tsc --build - so the zero headroom does not fire against what main has landed since the merge base.

One piece of context rather than a finding: the base was retargeted to fix/knip-component-name-types-unexported at 13:38:53Z, after all nine checks were created at 13:34:17Z, and every workflow filters pull_request: branches: [main] - so the green checks predate the retarget and nothing can re-run at the current base.

Comment thread package.json
Comment thread .github/workflows/knip.yml Outdated
Comment thread knip.jsonc Outdated
Comment thread packages/registry/src/event-emitter.ts Outdated
Comment thread packages/tool-server/src/utils/debugger/source-maps.ts Outdated
Comment thread packages/tool-server/src/chromium-server/index.ts Outdated
Comment thread packages/tool-server/src/chromium-server/clipboard.ts Outdated
Comment thread packages/tool-server/src/blueprints/chromium-js-runtime-debugger.ts Outdated
type ShutdownTimings,
type ShutdownResult,
} from "../profiler-shared/lifecycle";
export { waitForChildExit, shutdownChild } from "../profiler-shared/lifecycle";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[Diplomat]: This trims the shim's re-export list from four symbols to two, but the file has no production importer to be compatible with. The header directly above says it "is kept as a thin re-export for source compatibility with existing imports"; the only import of it anywhere is its own test:

$ grep -rn "ios-profiler/lifecycle" --include='*.ts' packages | grep -v /dist/
packages/tool-server/test/ios-instruments/lifecycle.test.ts:4:import { waitForChildExit, shutdownChild } from "../../src/utils/ios-profiler/lifecycle";

Same at base, and nothing in the argent-private submodule at 79ae7d91 reaches it either. The file therefore survives the files check only through the blind spot this PR documents at knip.jsonc:135-142 - test files are entry points, so a module nothing but its own test imports still counts as reached. Pointing that one test at ../profiler-shared/lifecycle would have removed the file, which is what the surrounding 135 files are doing. Flagging it because this is the change that edited the shim and would naturally have caught it, and because the header now states a reason that does not hold.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Leaving this one, and flagging why rather than acting on it.

The finding holds - packages/tool-server/test/ios-instruments/lifecycle.test.ts is the only importer, so the shim survives the files check purely through the test-files-are-entry-points blind spot, and the header's "source compatibility with existing imports" has nothing to be compatible with.

But it reads the same at the merge base: the file had no production importer at f136c024 either, and the header sentence is unchanged by this PR - the diff only trims two erased type re-exports off the list. So the dead shim predates this change, and pointing that one test at ../profiler-shared/lifecycle to delete the file is a change this PR did not make and should not smuggle in. Worth its own commit; happy to do it separately if you would rather it went in here.

Comment thread packages/tool-server/src/utils/device-info.ts
@hubgan
hubgan requested a review from latekvo August 12, 2026 10:28

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[Diplomat]: Reviewed at fec27a18, on top of the rounds at c24b6ba1 and 671447d3.

This PR's base is fix/knip-component-name-types-unexported, and every gate in this repo triggers on pull_request: branches: [main] — so none of them has run since 671447d3. I ran the equivalents locally on a clean npm ci --ignore-scripts worktree with packages/*/dist verified absent: npm run knip (exit 0, zero bytes of output), npx tsc --build, eslint . --max-warnings 0, npx prettier --check ., npm run typecheck:tests across the 13 workspaces that define it, typecheck:scripts, test:scripts (86), check-workspace-versions, and npm test --workspaces — tool-server 329 files / 3857 tests, matching the description. All green.

Beyond the gates:

  • Live tool-server built from this branch, driven over HTTP against an Electron app: boot-device, describe, screenshot (artifact handle returned across the workspace boundary), gesture-tap (counter 0 → 1), keyboard, debugger-connect / debugger-status (sourceMapReady: true with StubSourceMapsRegistry gone) / debugger-evaluate, the WS clipboardSync command for both true and false, GET /artifacts and /artifacts/:id (200, 9477-byte PNG), and the dispose cascade.
  • Published artifact. npm run pack:mcp (exit 0) and an md5 diff of the four emitted bundles against the merge base: cli-cmds.mjs, installer.mjs and mcp-server.mjs are byte-identical, and tool-server.cjs is smaller by exactly the deleted code with an unchanged entry export surface.
  • CLI / MCP / installer. argent mcp over real stdio returns 73 tools with a tools/list payload that diffs empty against the merge base; argent init -y --local / update / uninstall round-trip under a throwaway HOME, with the bundled skills, rules and agents copied byte-for-byte (the PACKAGE_ROOT question, answered at runtime).
  • Reachability sweep. All 192 symbols that lost their export, plus every outright deletion, grepped against the packages/argent-private submodule at its pinned 79ae7d9, against every non-TS file type, and against all markdown — no consumer anywhere. __resetAndroidDevtoolsInstallCache was the positive control and its @public tag checks out against research/android-describe-busy-ui/drivers/test-fallback.js.
  • Mutation. Deleting ArtifactStore.register, deleting the SSRF allowlist guard, dropping redirect: "error", stripping each @public tag, and reverting --max-issues 0 to 215, each with a control run.

Six findings below. Separately, I have replied on the earlier threads where the fix left something behind — including one where the mechanism I gave last round was wrong and is now in knip.jsonc.

Comment thread .github/workflows/knip.yml Outdated
Comment thread .github/workflows/knip.yml
Comment thread packages/tool-server/src/utils/debugger/source-maps.ts Outdated
Comment thread packages/tool-server/src/utils/debugger/source-maps.ts
Comment thread packages/registry/src/artifacts.ts Outdated
Comment thread packages/tool-server/src/tools/launch-app/types.ts
@hubgan
hubgan changed the base branch from fix/knip-component-name-types-unexported to main August 13, 2026 10:10
@hubgan hubgan closed this Aug 13, 2026
@hubgan hubgan reopened this Aug 13, 2026
@hubgan
hubgan requested a review from latekvo August 13, 2026 10:16

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[Diplomat]: Reviewed at c6d0d62d35cfb292a3282f3f89beba48809f674d. Ran the review moves over the whole 140-file diff — claims vs code, nearest twin, non-happy paths, inputs, reachability both ways, what outlives the call — plus the absence pass (sibling, prose, symmetry, mutation), with an adversarial round that tried to refute each finding before it went in.

What I ran: npm run knip on a dist-free npm ci worktree (exit 0, empty) and again with a planted instance of each of knip's 15 issue types; npx tsc --build, eslint . --max-warnings 0, prettier --check ., npm run typecheck:tests --workspaces --if-present, and npm test --workspaces (tool-server 332 files / 3884 passed, every other workspace green); the @swmansion/argent esbuild bundle (SourceMapConsumer gone from dist/tool-server.cjs, the allowlist still in it); the merge at 5af1b3d4 checked per-file against both parents; a sweep of every runtime binding the diff drops, against the repo, the argent-private submodule and the published tarball; and the branch's own tool-server driven over its HTTP API against a stand-in Metro and an Electron device (debugger-connect, debugger-status, launch-app, restart-app, keyboard, the WS clipboardSync route), with origin/main at fe597685 built as the control.

Inline comments below; four older threads have replies rather than new comments.

Comment thread packages/tool-server/src/utils/debugger/source-maps.ts Outdated
Comment thread packages/tool-server/src/utils/debugger/source-maps.ts Outdated
Comment thread knip.jsonc
Comment thread packages/tool-server/src/chromium-server/clipboard.ts
Comment thread .github/workflows/knip.yml Outdated
Comment thread packages/tool-server/src/chromium-server/index.ts Outdated
@hubgan
hubgan requested a review from latekvo August 13, 2026 14:20

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great job. A review / RCs might still land but feel free to merge as soon as you're ready

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[Diplomat, Deepseek V4 Flash 0731]: Ran the full sweep on this head (db01fa7): claims vs code, nearest twin, non-happy paths, inputs, reachability both ways, and what outlives the call, plus the absence pass (sibling/prose/symmetry/mutation) across the exports sweep, the source-map registry rewrite and the knip gate. Re-verified every prior round's finding at head and pushed a fresh four-agent pass over the move set. Gates all green on the unbuilt tree CI counts: npm run knip (exit 0, empty on both passes), npx tsc --build, eslint . --max-warnings 0, prettier --check, and the source-map SSRF suite 21/21. No HIGH or MEDIUM finding survived the sweep; the only LOW-shaped item (the shared launch-app/restart-app docstring twin) is out of this diff and already an accepted thread. Returned clean. Thank you for contributing!

hubgan added 11 commits August 20, 2026 11:26
The Dead Code gate has been red on main since f136c02: that commit added
StrippedName, ComponentAnnotation and ComponentNameResolution as exported
types, taking knip's parked backlog from 215 to 218 and past the
--max-issues 215 ceiling.

Nothing outside component-names.ts imports any of the three. Every
consumer -- profiler-commit-query, profiler-cpu-query,
react-profiler-analyze, react-profiler-component-source, 05-render and
the test file -- imports functions only, and callers already rely on
inference for the resolution type.

Drop the export keyword rather than raise the ceiling: knip.jsonc states
the pre-existing backlog is bounded, so the total goes back to the 215
the ceiling was set against. Declaration emit is unaffected -- tsc writes
the interfaces into the .d.ts as local declarations that the exported
signatures still reference -- and the emitted JS is byte-identical.
…to 0

The gate landed in #572 with 215 findings parked under --max-issues, which
bounded the backlog but let it sit. This clears all of it and sets the
ceiling to 0, so the next unused export fails the job instead of eating
headroom.

156 unused exported types and 47 unused exports, in the private workspaces
only -- @swmansion/argent, the one published package, had none, so no
public API moves. Two shapes of fix:

  - Still used inside its own file: drop the export keyword. 146 types and
    36 exports, no other change. tsc keeps emitting them into the .d.ts as
    local declarations, so the compiled output is unchanged.
  - Used nowhere at all: delete. Mostly orphaned test seams (__test,
    _testValidators, __resetAndroidDevtoolsInstallCache), stale
    compatibility re-exports, and localSimctl, whose strategy the local iOS
    impl never adopted because it shells out to xcrun itself to inject the
    native-devtools DYLD env first.

Removing the __sendCharInsert test alias exposed sendCharInsert underneath
it, unreferenced: the Chromium keyboard impl dispatches its own
`type: "char"` event, so the helper had been superseded rather than
unwired.

Of the 12 unused class members, 9 were real and are gone; the other 3 --
ArtifactStore.register/list and TypedEventEmitter.off -- are called from
tool-server. Knip's classMembers pass is workspace-local, so it cannot see
those call sites; adding a this.artifacts.list() call inside registry makes
the finding disappear on its own, which is what the new registry-scoped
ignoreMembers entry documents.

Verified on a tree with no build output, the way CI counts: npm run knip
exits 0 and prints nothing.
Removing `export const __test = { toCssPixels, clampPx, sleep }` kept the two
lines that introduced it, and they became the file's trailing content. The file
re-exports nothing for tests now: `toCssPixels` and `clampPx` are module-private
declarations with no exported handle, and `sleep` was deleted outright.
Removing `export const _testValidators = { oneOf, matches, finiteNonNeg, bool,
arrayOf }` kept its JSDoc as the file's last line. It asserts a test-facing
re-export the module does not have, and a JSDoc block binds to the next
declaration, so it would silently attach to whatever is appended here later.
… exist

Deleting `toGeneratedPosition` / `findMatchingSource` cascaded into
`buildSourceCandidates`, which was the sole home of all three strategies this
comment named: the `/[metro-project]/` push, the `projectRoot` push and the
suffix fallback. `SourceMapsRegistry` now exposes only `registerFromScriptParsed`
and `waitForPending`, and the production call site is `new SourceMapsRegistry()`,
so it has no candidate strategies left to lose.

The conclusion still holds, just for a different reason: `readSourceFragment`
bails on an empty project root, so a missing `X-React-Native-Project-Root` still
means "no location" and never a wrong one. That matters on the RN 0.72 /
Vega-Kepler path this comment exists to explain.
The replacement text blamed the native-devtools DYLD env, but both branches run
the same precheck: launch-app/platforms/shared.ts:17 and
restart-app/platforms/shared.ts:18 call `precheckNativeDevtools` before
`backend.launch`, and the local impl calls it too at launch-app/platforms/ios.ts:33.
So DYLD injection is not what separates the two paths.

The reason the local impl states for itself (ios.ts:20-23, and launch-app/index.ts:88)
is that it resolves native-devtools lazily through `registry` via
`nativeDevtoolsRef(device)` rather than as an eager declared service, so one impl
covers the iOS and tvOS slices and a tvOS udid never spins up the iOS-only
injection. Point at that instead, and name the wrong reason so it is not inferred
again.
…ve caller

The caller is one knip structurally cannot see: `packages/argent-private` is
listed under `ignoreWorkspaces` and the submodule is not checked out in the CI
job either, so neither the gate nor a repo grep reaches it. At the revision this
branch pins (79ae7d9, unchanged from the merge base),
research/android-describe-busy-ui/drivers/test-fallback.js requires this module
from dist/ and calls the helper at lines 24 and 31 - once to force the
install-fallback path, once to restore.

Reproduced against a build of this branch before the fix:
`typeof __resetAndroidDevtoolsInstallCache === "undefined"`, and calling it threw
`TypeError: ... is not a function`, so the driver died on line 24 before reaching
the measurement it exists for. After the fix both calls succeed.

Tagged `@public` rather than re-listed in knip.jsonc so the exemption travels
with the symbol and cannot outlive it. Control: stripping the tag alone puts the
finding straight back (`Unused exports (1)`).
… by name

`ignoreMembers` was the wrong instrument twice over, and the comment defending
it claimed it was the only one.

It is not the narrowest form knip offers. `graph/analyze.js:114` filters members
with `findMatch(workspace.ignoreMembers, member.identifier) || shouldIgnore(member.jsDocTags)`,
and that second handler exempts any member carrying `@public`, `@beta` or
`@alias`. Being per-member, it is strictly narrower than a workspace-wide name
match. Measured both ways: with a dead `list(): string[]` added to `Registry`,
the old config reports nothing while a control `zzzDefinitelyUnusedMember()` in
the same class is reported; under the `@public` tags both are reported. The
blind spot the comment accepted as unavoidable was avoidable.

It also cannot go stale visibly. `ConfigurationHintType` has no `ignoreMembers`
member and no producer emits a hint for it, so `treatConfigHintsAsErrors` never
sees one - contrary to what the PR description claimed. If `ArtifactStore.list`
were later deleted or a refactor dropped the cross-workspace call, the entry
would persist silently and keep hiding any future member of that name.

Tagging at the declaration fixes both: the exemption is scoped to the one member
and cannot outlive it.
Deleting `export { sendCharInsert as __sendCharInsert }` left the comment
that introduced it, so "Internal re-export so tests can stub these" ended up
governing `export type { CDPClient }` alone - one item, not "these", and a
type no test can stub because it is erased.

That re-export has no reader either: the barrel's three importers take
`NetworkRequestRecord`, `TabInfo` and the server types, and chromium-cdp.ts
imports `CDPClient` from `utils/debugger/cdp-client` directly. Drop the
comment, the re-export, and the type import that only fed it.
Deleting `isEnabled()` took the only reader of `private enabled` with it, so
`set()` was writing a field no code in the program could read back, while the
class docstring still promised it "records the desired state".

Adding `noUnusedLocals` to tsconfig.base.json and building the monorepo
reports exactly one TS6133 across every workspace - this field - and none
once it is gone. Knip stays quiet either way, because the assignment itself
counts as a reference.

Drop the field rather than invent a reader, and say what the class is for:
the seam that lets the WS `clipboardSync` route resolve without a
not-yet-implemented branch.
…e stub

The subclass existed to supply `super("")`. Removing `SourceMapsRegistry`'s
constructor took that reason away, and the `waitForPending` override that
remained cannot behave differently from the base: `registerFromScriptParsed`
is only ever called on the Metro registry (js-runtime-debugger.ts), so a
Chromium session's `pendingRegistrations` is permanently empty and the base
`await Promise.allSettled([])` already resolves at once.

Its single consumer, `debugger-status`, observes the same `sourceMapReady`
either way. Construct the base class and keep the explanation where the
decision is now made.
hubgan added 24 commits August 20, 2026 11:27
`toGeneratedPosition` and `findMatchingSource` were the only readers of the
private `maps` array. Deleting them left `doRegister` parsing a
`SourceMapConsumer` per script and pushing it onto an array no code in the
repo can reach - and `--max-issues 0` cannot see it, because the write
itself counts as a reference to the field.

Driving the registry against a 6 MB map served over loopback, the way
`Debugger.scriptParsed` does:

    before   blocked 226 ms, 9488 KB retained after gc, own fields [maps, pendingRegistrations]
    after    blocked  69 ms, 2943 KB retained after gc, own fields [pendingRegistrations]

Drop `interface RegisteredMap`, the field, the consumer parse that only fed
it, and the now-unused `source-map-js` dependency. The fetch stays: it is
what `waitForPending()` waits on and what `debugger-status` reports as
`sourceMapReady`, and it keeps the loopback allowlist, the `redirect:
"error"` guard and the 64 MiB cap - all still covered by the two SSRF
regression tests.
…nused

The `classMembers` pass is not workspace-local - it follows the import
graph. The three members are invisible only because CI analyses an unbuilt
tree, where every workspace resolves `main`/`types` to a `dist/` that does
not exist, so `@argent/registry` resolves to nothing and the tool-server
edge never forms. `knip.jsonc` already names that mechanism twenty lines
higher, so the file was giving two different causes for one behaviour.

Measured on a clean `npm ci --ignore-scripts` worktree at 671447d, with
only the three `@public` lines removed:

    unbuilt        exit 1, Unused exported class members (3)
    tsc --build    exit 0, 14 packages/*/dist created
    built          exit 0, no output

Same source, same command. A control `neverCalledProbe()` added to
`ArtifactStore` is still reported in the built run, so the pass is live
there and the empty result is real.

Correct the block and the three JSDoc copies of the sentence, and say that
the limitation covers every cross-workspace reference, not class members.
The tag's JSDoc named three tool-server files. Renaming the member and
reading every `Property 'off' does not exist` error `tsc` emits gives 21
production call sites across seven files in two workspaces:

    6  tool-server/src/index.ts
    4  tool-server/src/chromium-server/http-api.ts
    3  tool-server/src/preview.ts
    3  tool-server/src/blueprints/chromium-js-runtime-debugger.ts
    3  telemetry/src/registry-listener.ts
    1  tool-server/src/chromium-server/network.ts
    1  tool-server/src/blueprints/js-runtime-debugger.ts

The three listed covered 10 of them, and missed telemetry entirely - which
is the workspace that matters most, since being in another workspace is the
whole reason the tag is there. Nothing emits a hint when a `@public` stops
being earned, so this JSDoc is the only record a re-audit has.

Describe the callers by role rather than by path so the list does not rot,
and record the rename trick. Grepping `.off(` is not a substitute: 14 of the
35 hits in this repo are Node `EventEmitter`s.
The block gave two reasons to exempt at the declaration instead of by name.
The second one - that nothing emits a configuration hint for `ignoreMembers`,
so `treatConfigHintsAsErrors` could never report the entry as stale - is a
property both forms share, so it separates nothing and implies `@public`
does get reported.

Knip emits a tag hint only for tags in the exclude half of the `tags` option
(`graph/analyze.js`), and `tags` defaults to `[]`; this repo sets no `tags`
key and passes no `--tags`, so that code path can never fire. Measured on a
clean unbuilt worktree, knip 5.88.1:

    bogus "ignoreMembers": ["totallyNonexistentMemberXyz"]   exit 0, no hint
    redundant /** @public */ on ArtifactStore.get            exit 0, no hint
    bogus "this-dependency-does-not-exist"                   exit 1, hint

The control shows `treatConfigHintsAsErrors` is live and simply has no
producer for either exemption style. Keep per-member scope as the reason it
is, and say plainly that neither form is checked for staleness - so the
JSDoc at each member is what a re-audit has to go on.
The text split on file-local usage alone - drop the export keyword when the
symbol is used inside its own file, delete it when it is not - which sends a
contributor down the wrong branch whenever the only caller sits outside
knip's reach. It also left out the remedy the same change introduced and
uses four times: a `@public` tag at the declaration.

`__resetAndroidDevtoolsInstallCache` is exactly that case and it is in this
branch's history. Strip its tag on a clean unbuilt worktree and the gate
reddens under this very section, with no reference inside its own file - so
the sentence's delete branch is the one that fires. Following it leaves
`npm run knip`, `npm run build` and the tool-server suite all green while
the submodule's fallback driver dies on a `TypeError`. Every automated
backstop agrees with the wrong action, which is why the annotation has to
warn rather than the checks.

Give all three remedies, say where a caller can hide (another workspace
under the unbuilt tree, or the argent-private submodule), and say that
nothing here fails on a wrong delete.
Both causal clauses in the previous version are wrong.

"What stops a tvOS udid spinning up the iOS-only injection" - nothing does,
and the case is not vacuous. `classifyDevice` is pure UDID shape, so an
Apple TV sim UUID returns "ios" and reaches the local impl, which then
resolves native-devtools with no branch between handler entry and
`registry.resolveService`. The repo's own test pins the opposite of the
docstring: `test/launch-restart-tvos.test.ts` asserts `resolveService` is
called once for `TVOS_UDID` - "ensureEnv picks the TVOSSIMULATOR slice, so
injection is resolved on tvOS too". Mutating a tvOS early-return into the
local handler reddens that assertion, so it discriminates.

"That is what lets one impl cover the iOS and tvOS slices" - the slice is
picked by `await isTvOsSimulator(udid)` inside `setupNativeDevtoolsEnvLocal`,
whose signature carries nothing about how the service was resolved. The real
discriminator is `pickIosHost`: only the local host probes tvOS at all, and
`setupNativeDevtoolsEnvRemote` has no tvOS branch.

The reason is duller. The shared builders read `services.nativeDevtools`,
and ios-remote is the only platform that declares that service; the local
impl declares none, so it has nothing to hand them.
Step 5 still told contributors "raise the `--max-issues` number in the same
commit" for an export nothing imports yet. That regime is gone: the ceiling
is 0, and the workflow annotation now says there is no allowance to fall
back on. Of the three descriptions written together in #572 - knip.jsonc,
knip.yml and this step - the first two were rewritten and this one was not,
and it is the contributor-facing one.

Raising the number would still work, which is why leaving it is not
harmless. On a clean unbuilt worktree with one genuinely unused export
added:

    --max-issues 0   Unused exports (1) DEAD_CANARY_EXPORT ...   exit 1
    --max-issues 1   Unused exports (1) DEAD_CANARY_EXPORT ...   exit 0

The finding is still printed and the job passes, because the gate keys on
the exit code. The bump is global too, so it re-parks every future finding.

Replace that sentence with the escape hatch that does exist - a `@public`
tag when the only caller is out of knip's reach - which until now appeared
only in inline comments. The built-vs-unbuilt warning in the same step is
correct and stays as it is.
The comment above `LaunchAppIosServices` explained the empty services shape
through `LaunchAppAndroidServices`, which this branch deleted, and said
nothing about `LaunchAppVegaServices`, the identical empty shape sitting
right under it. Read cold, it introduced a type the file does not contain.

Point it at Vega, and say where Android's shape now lives — inline in
`platforms/android.ts`, which is why no second file names it. Same edit in
`restart-app/types.ts`, which carried the same comment against
`RestartAppAndroidServices`.
The `@public` tag said "every caller is in tool-server" and then listed two
of the seven files. knip.jsonc states that when one of these tags stops
being earned nothing reports it and the JSDoc is the only record a re-audit
has, so a partial list is the defect it warns about.

Derived the set the way the sibling tag on `TypedEventEmitter.off`
prescribes: renamed the member and read what `tsc --build` reports. Sixteen
TS2339 errors across flow-visual (5), screenshot (4), native-profiler-stop
(2), screenshot-diff (2), native-profiler-analyze, react-profiler-analyze
and screen-recording-stop. Record the recipe too — `.register(` is as noisy
a grep as `.off(`, with registerTool / registerBlueprint everywhere.
"The build, the tests and this gate all stay green on a wrong delete" holds
for one of the two cases the sentence names and not the other. Deleting a
symbol another workspace calls is caught loudly: removed
`ArtifactStore.register` on a clean worktree and `tsc --build` reported 16
TS2339 errors across 7 files, while `vitest run test/artifacts.test.ts` gave
9 failed | 2 passed. Only argent-private goes unnoticed — it `require()`s a
built `dist/*.js` from a submodule CI never checks out.

The claim mattered because it told a contributor the one backstop that does
work is not there, and this same branch sends a re-auditor straight to it in
`event-emitter.ts` ("rename the member and read the errors `tsc` reports").
Split the two cases in the workflow comment, the failure annotation and
CONTRIBUTING step 5.
Removing the `SourceMapConsumer` left `doRegister`'s `data:` branch
base64-decoding and parsing a payload it then dropped, inside a `catch` that
swallows the throw. Both `:113` and `:115` returned the same `void`, so the
branch collapses to an early return.

Drove it through the real tools against a stand-in Metro: a well-formed 1.4
MB inline map and a malformed one produce identical observable state —
`debugger-connect` 200, `debugger-status` `sourceMapReady: true`,
`loadedScripts: 3`, `debugger-evaluate` 42, and the loopback map fetched
exactly once in both. All the statement bought was time inside
`waitForPending()`, which every `debugger-status` blocks on: ~1.5 ms for a
1.4 MiB payload and ~25 ms for 20 MiB, with none of the size cap the fetch
path applies.

`scriptUrl` / `scriptId` go with it. Their last reader was the
`this.maps.push(...)` that went in the same sweep, and `doRegister` is
private, so no signature is preserved. `registerFromScriptParsed` keeps the
whole event shape, as its docstring says.
Moving the `isAllowedSourceMapURL` check out of the `else` branch and into a
top-level early return had nothing standing behind it: deleting the line
outright leaves `source-maps-ssrf.test.ts` and
`source-maps-ssrf-redirect.test.ts` at 17 passed. The suite exercises
`isAllowedSourceMapURL` as a pure function and drives the redirect test with
a URL that already passes it, so neither asserts `doRegister` consults it.

Add two tests that do. The first drives four rejected URLs — a non-loopback
127.0.0.0/8 host, the cloud-metadata endpoint this file's header names, a
public host and a loopback non-`.map` path — and asserts `fetch` is never
called. The second asserts the loopback `*.map` URL Metro emits still is, so
the guard cannot pass by rejecting everything.

Verified both directions: 19 passed at head, and deleting the guard fails
the first one.
`#771` landed on main and fixed the same red gate this branch's first
commit did, the other way round: instead of dropping `export` from
`StrippedName`, `ComponentAnnotation` and `ComponentNameResolution`, it made
`component-names.test.ts` import them and use the narrowing they enable.

The two fixes are mutually exclusive, and merging main in proves it — `tsc
--noEmit -p tsconfig.test.json` reports three `TS2459`s, "declares X
locally, but it is not exported". This is what the Unit Tests job caught
once the base went back to `main` and CI resolved the merge.

Main's resolution wins, so restore the three exports. The gate stays green
because knip treats test files as entry points, so a type a test imports is
no longer an unused export: `npm run knip` is exit 0 and empty on a
`tsc --build --clean` tree.
866d90c hoisted `sourceMapURL.startsWith("data:")` above the `try` that
used to contain it. That made it the first expression in the function to
touch the value, and the value is a bare cast over socket JSON:
cdp-client.ts reads `params.sourceMapURL as string | undefined` off a
`Debugger.scriptParsed` frame, js-runtime-debugger forwards it unchecked,
and registerFromScriptParsed only rejects falsy. A CDP peer that puts a
number there reaches `.startsWith` and throws.

Inside the `try` that throw was swallowed like any other malformed map.
Outside it escapes doRegister as a rejected promise that nothing awaits
before the next tick, so index.ts's `unhandledRejection` handler runs
crashShutdown and the tool-server exits with every device session it
owns. The debugger-connect caller only sees REGISTRY_SERVICE_TERMINATING.

Repro over the HTTP API against a stand-in Metro whose scriptParsed
carries `sourceMapURL: 12345`. Before: "Unhandled rejection: TypeError:
sourceMapURL.startsWith is not a function", then GET /tools stops
answering. After: connect returns `connected: true` and the server stays
up. The same message at the merge base leaves the server alive, so the
crash is this branch's. A string map still fetches (the .map route is
hit) and a data: URL still returns without one.
`readCappedJson`'s return value stopped having a reader when bb775a7
removed `new SourceMapConsumer(rawData)` and `this.maps.push(...)`. The
call stayed, so doRegister still buffered the whole body (up to 64 MiB)
and JSON.parsed it before dropping the result on the next line - the same
construct the commit seven lines above deletes the `data:` arm for.

Replaced with `drainCappedBody`, which reads the stream to the end and
counts bytes without accumulating or parsing them. The reasons to read at
all are unchanged: `waitForPending()` is only a defined moment if the
body has finished arriving, and the cap is what stops an unbounded body
holding it open. Not buffering also makes the memory bound
unconditional, so the cap's job is now time and bytes, not OOM - the
constant's comment says so.

Measured on the built code against a loopback responder, three runs each:
9.2 ms to buffer and parse a 9.4 MB map against 4.8 ms to drain it, 64 ms
against 22 ms at 59 MB. That sits inside the wait `debugger-connect` and
every `debugger-status` block on.

The class docstring also claimed `waitForPending()` marks the moment
"Metro has served every map the session asked for". It does not - it
awaits Promise.allSettled, so a 404 settles, and a `data:` or
allowlist-rejected URL settles with no fetch at all. Driving the branch's
tool-server against a stand-in Metro that answers 404 for the .map
returns `sourceMapReady: true`, matching what the tool's own description
already says ("always true"). Reworded to what it does mean.

E2E over the HTTP API: a 200 .map and a 404 .map both connect and leave
the server up, and the 200 case still hits the .map route.
f517e3d removed `ClipboardSyncState`'s `private enabled` field, so the
class now accepts the flag and drops it. The comment at the call site did
not move and still says "record intent so a future Chromium-side helper
can wire it up" — describing something that no longer happens, and
contradicting the class docstring the same commit wrote ("nothing can
read the desired state back").

That comment is the one someone landing the native bridge reads first, so
it is the one that has to be right. Says what the call actually buys —
the route resolves without a not-yet-implemented branch — and points at
the class as where a bridge would land.

Comment only. Verified the route still answers on a Chromium device
driven by a tool-server built from this branch: the WS `clipboardSync`
command returns {"status":"ok"} for enabled true, false and omitted.
Both lines were edited by this branch and both kept a dead sibling,
because knip pointed at the declaration rather than the barrel. It
reported SetCookieParams / DeleteCookieParams / StorageType at
storage.ts, and TargetDecision at install-targets.ts; un-exporting those
declarations forced the specifier edit here. `Cookie`, `TargetFlags` and
`DecideTargetsContext` still have live declarations, so nothing pointed
at them — but no one imports them through the barrel either.
chromium-cookies takes the cookie types from `chromium-server/storage`
directly, and install-targets.test.ts imports from `src/install-targets`.

Proved with the recipe this branch's own @public tags prescribe: rename
the exported name and read the errors. `Cookie as ZZZCookieProbe` and
`TargetFlags as ZZZFlagsProbe, DecideTargetsContext as ZZZCtxProbe` leave
`tsc --build` and `typecheck:tests` clean in both workspaces, so nothing
resolves them through these barrels. Neither name appears in
argent-private.

The gate cannot see either one. Naming classMembers turns isSkipLibs off,
which enables knip's hasExternalReferences guard, and for
`export { X } from "./y"` the declaration of X in ./y is itself such a
reference — so a same-workspace re-export specifier is unreportable
whether or not it is dead. knip.jsonc records that; this just stops the
two lines the branch touched from carrying it.

Chromium E2E on a tool-server built from this branch: chromium-cookies
and chromium-tabs both still answer through the trimmed barrel.
Three prose problems, all added or rewritten by this branch.

knip.jsonc said "--max-issues 0 means the whole report must be empty, and
a single new unused export fails the job". The ceiling is real, but the
report is not exhaustive, and the invocation is what narrows it. A dead
member of a referenced enum needs enumMembers and nsTypes in one run
(knip/dist/graph/analyze.js:72); pass 1 has enumMembers without nsTypes,
pass 2 the reverse. A dead specifier on a same-workspace `export … from`
is unreportable in pass 2, where naming classMembers turns isSkipLibs off
and enables the hasExternalReferences guard - the declaration in the
re-exported module counts as a reference - and pass 1 excludes exports
and types outright.

Measured on this tree with `tsc --build --clean` first, 0 emitted js
files: a ZZZ_DEAD_MEMBER on ServiceState leaves `npm run knip` at exit 0
while `--include enumMembers,nsTypes` reports it; re-adding a dead
`Cookie` type re-export and a dead `createTabsManager` value re-export
leaves it at exit 0 while the same lens without classMembers reports
both. Control on the same tree: a plain dead `export const` in
utils/secrets.ts fails the gate, so it is live.

knip.yml said "Knip reads source directly (mapping each package's dist
entry back to src), so no workspace build is required" - which reads as
"an unused-export finding for a symbol another workspace calls cannot
happen", the opposite of the annotation 15 lines below and of
knip.jsonc's own warning. The mapping resolves each workspace's own
entry; it does not form the cross-workspace edge. Says so.

knip.yml's annotation and CONTRIBUTING step 5 both scoped their three
remedies to "an unused export or type" and gave none for Unused exported
class members - the section this repo's three @public tags exist for, and
the one pass 2 raises the moment a tag comes off. Both now name it, and
note that dropping `export` is not available for a member.

Docs only. `npm run knip` still exits 0 with no output, and prettier is
clean on all three files.
15af0c1 rewrote this comment to point at LaunchAppVegaServices instead of
the deleted LaunchAppAndroidServices, and added two clauses that do not
hold. Same text in the restart-app twin.

"Android takes the same empty shape" - it does not. The empty shape is
Record<string, never>, which is what the Vega alias is and what the
deleted Android one was. Both android impls declare
PlatformImpl<Record<string, unknown>, ...>, which is strictly wider.
Setting launch-app's to Record<string, never> fails to compile: TS2322 at
index.ts(111,7), the `android: androidImpl` property, "Type 'unknown' is
not assignable to type 'never'".

"spelled inline in platforms/android.ts because no second file names it"
- two files write that shape and have to agree, platforms/android.ts and
the dispatchByPlatform generics in index.ts. That TS2322 is exactly the
disagreement. Nothing was spelled inline by the deletion either: the
android impl is byte-identical to origin/main, so the deleted alias had
no references on either side.

Comment only, so the emitted js is unchanged. `tsc --build` and prettier
clean.
a1bb8e6 replaced the DYLD reason with a handler-signature one, and that
reason is contradicted by the file it points at. It says the local impl
"declares none - it resolves native-devtools per device inside its own
handler - so it has nothing to hand the shared builder". Both halves
cannot hold: launch-app/platforms/ios.ts resolves a concrete
NativeDevtoolsApi out of the registry BEFORE its precheck, and
buildIosLaunchHandler takes (services: LaunchAppIosServices, params), so
`{ nativeDevtools }` would satisfy it at exactly that point.

The obstacle is one level up. launch-app/index.ts and restart-app/index.ts
return `{}` from `services()` for platform === "ios" - only ios-remote
declares the eager native-devtools service - so `services.nativeDevtools`
would be undefined at runtime. That is what anyone merging the two
branches has to change first, so that is what the header now says. The
lazy resolve is kept in the text as what it is: the reason a tvOS udid
never spins up the iOS-only injection.

The opening sentence also outlived localSimctl's deletion, promising a
strategy over two cases when one implementation and one caller remain.
Says one implementation, and why the seam is still where the shared
handler reads it.

Comment only. `tsc --build`, eslint and prettier clean.
f55ceb7 added "(SourceMapsRegistry no longer takes a project root at all
- it only registers maps from Debugger.scriptParsed.)" here. Two problems.

"only registers maps" disagrees with the class this branch left behind:
its own docstring says "Nothing keeps the map", and doRegister fetches the
body and drains it without keeping anything. Two comments this PR added
described the same object and said different things about it.

"no longer" is change narration in a comment that outlives the diff - it
compares against a state the reader cannot see. Gone, along with the
parenthetical's dependence on it.

Comment only. `tsc --build`, eslint and prettier clean.
Nothing failed when the `data:` test sat outside the try. The allowlist
tests all pass either way, and waitForPending() awaits allSettled, so it
resolves whether or not doRegister rejected - the damage was an unhandled
rejection reaching index.ts's handler, which no assertion looked at.

This asserts it directly: register a scriptParsed whose sourceMapURL is a
number, let a macrotask elapse (the gap production has, where the event
fires during CDP message handling and the wait comes much later), and
require that no unhandledRejection fired and no fetch was attempted.

Verified both ways. Against the fix, 19/19 pass. Against the hoisted
version re-applied, it fails with the received value being
[TypeError: sourceMapURL.startsWith is not a function], and the other 18
still pass - so this test alone carries the regression.
Rebasing onto main puts this branch's un-exporting under callers that did
not exist when it ran. #560 landed `restart_required` derivation, and with
it six `native-*` tools that each build their `Result` union on
`NativeDevtoolsPrecheckBlock`, plus a `native-devtools-remote-and-flow-tree`
test that imports `remoteIosHost` by name to drive `inspectRunningApp`.

Both symbols were file-local when the first commit dropped their `export`,
so the rebased tree is red twice over: `tsc --build` reports six `TS2459`s
for the type and `tsc --noEmit -p tsconfig.test.json` a seventh for the
host, all of them "declares X locally, but it is not exported". Main's
callers win, so restore both exports.

The dead-code gate stays green. The six tools are same-workspace importers,
and knip treats test files as entry points, so neither symbol reads as an
unused export -- `npm run knip` is exit 0 and empty on a tree with no build
output.
With the ceiling at 0 the gate has no headroom left to absorb what a rebase
brings in, and #560's `RunningAppProcess` and `RunningAppInspection` are the
first arrivals: `npm run knip` reports both as unused exported types on the
rebased tree, which is the whole point of the ceiling.

Neither leaves the file. `RunningAppInspection` is the return type of
`IosHost.inspectRunningApp` and `RunningAppProcess` is the field hanging off
it, and both are read only by the declarations and the two implementations
in `ios-host.ts` itself -- no other file in any workspace, and none in the
argent-private submodule, names either. So this is the first of the three
remedies the job's annotation lists: drop the `export`, keep the
declaration. `tsc` still emits both into the `.d.ts` as local declarations,
so `IosHost` stays as usable to a consumer as it was.
@hubgan
hubgan force-pushed the chore/knip-clear-unused-exported-types branch from db01fa7 to 4a23c23 Compare August 20, 2026 09:37
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.

2 participants