chore(knip): clear the parked dead-code backlog and drop the ceiling to 0 - #770
chore(knip): clear the parked dead-code backlog and drop the ceiling to 0#770hubgan wants to merge 35 commits into
Conversation
latekvo
left a comment
There was a problem hiding this comment.
[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.
latekvo
left a comment
There was a problem hiding this comment.
[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.
| type ShutdownTimings, | ||
| type ShutdownResult, | ||
| } from "../profiler-shared/lifecycle"; | ||
| export { waitForChildExit, shutdownChild } from "../profiler-shared/lifecycle"; |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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.
latekvo
left a comment
There was a problem hiding this comment.
[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: truewithStubSourceMapsRegistrygone) /debugger-evaluate, the WSclipboardSynccommand for bothtrueandfalse,GET /artifactsand/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.mjsandmcp-server.mjsare byte-identical, andtool-server.cjsis smaller by exactly the deleted code with an unchanged entry export surface. - CLI / MCP / installer.
argent mcpover real stdio returns 73 tools with atools/listpayload that diffs empty against the merge base;argent init -y --local/update/uninstallround-trip under a throwawayHOME, with the bundled skills, rules and agents copied byte-for-byte (thePACKAGE_ROOTquestion, answered at runtime). - Reachability sweep. All 192 symbols that lost their
export, plus every outright deletion, grepped against thepackages/argent-privatesubmodule at its pinned79ae7d9, against every non-TS file type, and against all markdown — no consumer anywhere.__resetAndroidDevtoolsInstallCachewas the positive control and its@publictag checks out againstresearch/android-describe-busy-ui/drivers/test-fallback.js. - Mutation. Deleting
ArtifactStore.register, deleting the SSRF allowlist guard, droppingredirect: "error", stripping each@publictag, and reverting--max-issues 0to215, 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.
latekvo
left a comment
There was a problem hiding this comment.
[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.
latekvo
left a comment
There was a problem hiding this comment.
Great job. A review / RCs might still land but feel free to merge as soon as you're ready
latekvo
left a comment
There was a problem hiding this comment.
[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!
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.
`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.
db01fa7 to
4a23c23
Compare
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.--max-issuesScope 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.tsckeeps emitting these into the.d.tsas local declarations that the exported signatures reference.For the 146 types that is the whole story: they are erased, so the emitted
.jsis byte-identical. For the 36 values it is not. Droppingexportfrom aconst/function/classremoves itsexports.Xbinding from the emitteddist/*.js— 36 declarations across 26 files here.packages/tool-server/dist/utils/device-info.jsno longer carriesexports.REMOTE_PREFIXorexports.VEGA_SERIAL_PREFIX;telemetry/dist/posthog.jslosesexports.POSTHOG_PROJECT_TOKEN; in the ESM-emittingargent-installer,isTempRunnerPath/resolveLocalArgentDir/PACKAGE_ROOTleave the module namespace object entirely.Nothing in the repo or in the
packages/argent-privatesubmodule at the pinned revision reads any of them by path, so nothing breaks. Flagging it because arequire()of a builtdist/*.jsloses a droppedexportexactly the way it lost the deleted__resetAndroidDevtoolsInstallCachethis 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— theSimctlBackendstrategy is only ever constructed withremoteSimctl. What keeps the local iOS impl off it is the handler signature:buildIosLaunchHandler/buildIosRestartHandlerreadservices.nativeDevtools, andios-remoteis 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 toxcrunitself.sendCharInsert— removing the__sendCharInserttest alias exposed the underlying helper as unreferenced. It is superseded, not unwired: the Chromium keyboard impl dispatches its owntype: "char"event inline (keyboard/platforms/chromium.ts:58), which is exactly what this helper did. Chromium typing is unaffected.ServiceNotFoundError.serviceId,ToolNotFoundError.toolIdand their two siblings were never read. Thee.serviceIdinregistry-error-events.test.tsis a field on the event object the handler pushes, not on the error class; theerr.toolIdinhttp.tsisNotImplementedOnPlatformError, a different class that keeps its fields. Flagging these in case they are wanted as deliberate diagnostics.Deleting
SourceMapsRegistry.toGeneratedPosition/findMatchingSourcecascaded into their only helper (buildSourceCandidates) and then intoprojectRoot, which had no remaining reader — so the constructor parameter goes too.StubSourceMapsRegistryexisted only to passsuper(""), so it goes with it.The three cross-workspace members
ArtifactStore.register,ArtifactStore.listandTypedEventEmitter.offare all called from other workspaces — the artifact route and screenshot tools intool-server, andregistry-listenerintelemetry. They are reported unused anyway, and the reason is the unbuilt tree the gate runs against: every workspace resolvesmain/typesto adist/that does not exist, so@argent/registryresolves 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 toclassMembers— every cross-workspace reference is invisible the same way.Each is exempted at the declaration with a
@publicJSDoc tag rather than by name inknip.jsonc, for one reason: scope.ignoreMembersmatches a name across the whole workspace, so it would also hide a future deadregister/list/offon any other class inpackages/registry;@publicbinds to the member it is written on.What
@publicdoes not buy is a staleness check, and neither doesignoreMembers. Measured on a clean unbuilt worktree: a bogusignoreMembersname and a redundant@publiceach leave the run at exit 0 with no hint, while a bogusignoreDependenciesname exits 1 withRemove from ignoreDependencies.treatConfigHintsAsErrorsis 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, andoffrecords how to re-derive them (rename the member and read theProperty 'off' does not existerrors; grepping.off(over-reports, since 14 of the 38 call-site hits are NodeEventEmitters). One correction to the commit message on that change: it says 35 hits where the exact count is 38 — 14 NodeEventEmitter, 24TypedEventEmitter(21 production, 3 in a test). The 14 and the 21 are right.knip.jsonc,CONTRIBUTING.mdstep 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@publicescape 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 knip— exit 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 errorsMissing 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:
describe,screenshot,gesture-tap(counter advanced 0 → 1),POST /api/clipboard/textread back out of the renderer, the WSclipboardSynccommand, anddebugger-connect/debugger-status/debugger-evaluate(sourceMapReady: truewithStubSourceMapsRegistrygone).boot-device,launch-app,describevia ax-service,screenshot.boot-device,launch-appon an Apple TV udid, focus-drivendescribe. The registry snapshot then carriesNativeDevtools:<tvOS udid>, confirming injection is resolved on tvOS through the local impl.boot-deviceon a dedicated AVD,describevia android-devtools,gesture-tap..mapover loopback, the wayDebugger.scriptParseddrives it: allowlist passes andwaitForPending()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 throughLaunchAppAndroidServices, deleted here, and skippedLaunchAppVegaServices, which is still there. Same edit inrestart-app/types.ts.docs(registry)— the@publictag onArtifactStore.registernamed 2 of 7 callers. Derived the full set the way the sibling tag onoffprescribes: renamed the member, read the 16TS2339errorstsc --buildreports 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@publictags. DeletingArtifactStore.registergives 16TS2339errors and9 failed | 2 passedintest/artifacts.test.ts; only the gate stays green. Split in the workflow comment, the failure annotation andCONTRIBUTING.mdstep 5.perf(debugger)—doRegister'sdata:branch still base64-decoded andJSON.parsed a payload it dropped, inside acatchthat swallows. Both arms returned the samevoid, so the branch collapses to an early return;scriptUrl/scriptIdgo with it, since their last reader was the deletedthis.maps.push(...)anddoRegisteris 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 assertdoRegisterconsults it — four rejected URLs reach nofetch, and the loopback*.mapMetro emits still does.Re-verified after:
npm run knipexit 0 and empty on atsc --build --cleantree,tsc --build,eslint . --max-warnings 0,prettier --check .all clean, andnpm test --workspaceswith tool-server at 329 files / 3854 passed. The one red test,argent-installer'sglobalPath returns ~/.config/opencode/opencode.json, fails identically with these commits stashed — it reads the real~/.config/opencode, which holds a.jsoncon this machine.End-to-end on a tool-server run from this branch over its HTTP API, on every platform:
launch-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(readtaps: 1out of the page).launch-app,describevia native-devtools,restart-app,screenshot.launch-app, focus-drivendescribe(12 focusables in TVSettings),tv-remote,restart-app.launch-app,describevia android-devtools,restart-app,screenshot.boot-deviceon thetvVVD,describevia the automation toolkit.debugger-connect/debugger-status/debugger-evaluatetools against a stand-in Metro (HTTP/json/list+ CDP WebSocket) emitting threeDebugger.scriptParsedevents: a loopback*.mapon a counting server, a 1.4 MB inlinedata:map, andhttp://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
mainPutting the base back on
mainmade CI resolve the merge, and the Unit Tests job went red on a conflict no side could see on its own:#771fixed the same red gate this branch's first commit did, the other way round — it madecomponent-names.test.tsimportStrippedName,ComponentAnnotationandComponentNameResolutioninstead of dropping theirexport. Mergedmainin, reproduced it (tsc --noEmit -p tsconfig.test.json→ threeTS2459s) 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.