chore: remove 328 unreferenced files — 22% of the tracked tree — after a skeptic pass rescued 62 of the candidates - #252
Merged
Conversation
|
React Doctor found 2 new issues in 2 files · 2 warnings · score 83 / 100 (Needs work) · 24 fixed · vs 2 warnings
Reviewed by React Doctor for commit |
This was referenced Sep 18, 2026
houko
added a commit
that referenced
this pull request
Sep 19, 2026
…, shell, modals (#260) * refactor(web): merge core/components/core/theme into core/components/appearance Theme lived in two directories: core/components/appearance held the public entry point (theme-switcher.tsx, the only file the barrel re-exports and the only one settings reaches) while the six pieces it composes sat under the unnamed core/components/core bucket. A reader arriving at the switcher had to leave the feature directory to find the selector it renders. appearance/ is the home rather than a new top-level theme/ because it is feature-named and already owns the entry point. The four files reached only through relative ./ imports from custom-theme-selector.tsx move with it untouched, so the entire cross-directory cost is the two specifiers in theme-switcher.tsx, which become sibling-relative in line with how the rest of this directory already refers to its own files. * refactor(web): move the two non-loaders out of core/components/ui core/components/ui holds 24 files, 22 of which are skeleton loaders under loader/. The two exceptions are a label-count chip and an empty-state layout, and both have an obvious owning directory already: IssueLabelsList belongs with the other label components and EmptySpace/EmptySpaceItem with the other empty states. Moving them first means the directory that gets renamed next contains nothing but loaders, so the new name needs no qualification. Neither file is added to core/components/labels/index.ts: that barrel is hand-maintained and already omits several of its neighbours, so the single consumer keeps importing by path. * refactor(web): rename core/components/ui to core/components/skeletons The path lied twice over. apps/web imports the @pace/ui package on nearly every screen, so "@/components/ui" reads as a local copy of the design system when in fact every file under it is a loading skeleton. Naming the directory for what it contains removes the collision and tells a reader where skeletons live without opening the folder. Four files stay behind in core/components/ui because #252 (chore/remove-dead-code-and-assets) deletes them: markdown-to-component.tsx, profile-empty-state.tsx, loader/notification-loader.tsx and loader/pages-loader.tsx. All four are unreferenced anywhere in the repo, and none of them imports a sibling, so leaving them in place breaks nothing and keeps this rename from colliding with that deletion. core/components/ui disappears when #252 lands. * refactor(web): extract the route-shell primitives into core/components/shell PageTitle, ContentWrapper and AppHeader are the three highest-traffic residents of core/components/core, and unlike the rest of that bucket they share an obvious identity: they are the chrome every route renders around its content, consumed almost entirely by app/ layouts and pages. Ninety-eight of the directory's import lines point at these three files, so naming them is where the bucket's cost actually is. shell rather than app-shell keeps the import short, and rather than core/layouts because that directory already exists with a different meaning: it holds three route-level layout components, not primitives those layouts compose. The remaining 33 files in core/components/core are deliberately untouched. They have no single destination between them and each one wants a judgement call about its owning feature; emptying the directory is not what this change is for. * refactor(web): disperse core/components/core/modals into the owning features Every one of the eight files in this directory has an unambiguous owner, so the grouping by "is a modal" bought nothing: it only guaranteed that anyone working on work items, the workspace logo or the profile avatar had to look somewhere other than the feature directory. Fifteen import lines total, and the two relative imports inside the group (bulk-delete-issues-modal to its item, existing-issues-list-modal to its empty state) survive untouched because both pairs land in issues/. The two destinations that needed a judgement call: gpt-assistant-popover.tsx goes to editor/ rather than issues/ because it is a generic AI-completion popover over a RichTextEditor with no work-item concept in it, and its sole consumer imports it as an editor affordance. user-image-upload-modal.tsx goes to profile/ rather than account/ because it edits the profile avatar; account/ in this codebase holds authentication and account-lifecycle concerns, which is also why change-email-modal.tsx goes there. core/modals/ (the unrelated top-level directory) is not involved, and the remaining 25 files of core/components/core are left alone. * chore: bump the workspace version, which check-version requires of every PR
houko
added a commit
that referenced
this pull request
Sep 19, 2026
Generated by `pnpm --filter @pace/codemods run icons-to-pace-propel`; re-running it reproduces this commit byte for byte. 516 files, one changed line each — the module specifier and nothing else. No icon is renamed, added or removed, and because `@pace/propel/icons` re-exports the external set the rewrite is an identity at runtime. What it buys is that the choice of icon package stops being a decision made 516 times. 419 of the 689 files importing `@makeplane/propel` imported nothing from it but icons, so those files now name one component library instead of two, and the population of files juggling three or four of them drops from 159 to 82. Three hunks are not one line: `workspace-invitations/page.tsx`, `settings/workspace/sidebar/item-icon.tsx` and `workspace-notifications/.../menu-option/root.tsx` had multi-line specifier lists that fit within the print width once the specifier lost six characters, so oxfmt collapses them onto one line. Three scopes deliberately keep the vendor path. packages/propel's own 20 files are where the re-export lives, and a package importing its own public subpath would make its source depend on its build output and pull the whole 900-name barrel into every component that wants one icon — @makeplane/propel is propel's declared dependency, so importing it directly is the honest edge. apps/admin (23 files) and packages/utils (1) declare no `@pace/propel` dependency; admin was deliberately moved off it and carries shims that say so, and @pace/propel depends on @pace/utils, so pointing utils back at propel would close a package cycle. Moving either needs a package.json change that is a decision of its own, not a side effect of an icon sweep. 15 further files in apps/web still import the old path; every one is deleted by the dead-code removal in #252, so rewriting them would only produce modify/delete conflicts. Re-running the codemod after that lands finishes them if it does not.
houko
added a commit
that referenced
this pull request
Sep 19, 2026
) * feat(propel): re-export the external icon set from @pace/propel/icons Callers currently have to know which of two packages an icon lives in: 170 `*Icon` components come from `@pace/propel/icons` and 737 `*Outline`/`*Filled` ones from `@makeplane/propel/icons`, and 52 files in apps/web alone import from both. Re-exporting the external set here collapses that into one import path without drawing a single new SVG — @makeplane/propel stays the icon vendor, it just stops being something call sites have to name. The two name sets are disjoint, which is the property that makes a single barrel safe: comparing the exported symbols of both modules with the TypeScript checker gives 170 and 737 names with an empty intersection, so no name is ambiguous and the local exports below shadow nothing. The merged module resolves to 900 runtime values (the 170 local exports include 7 type-only ones). * chore(codemods): add the icons-to-pace-propel import rename The sweep this drives touches 516 files, which no one can review hunk by hunk — so the reviewable artefact has to be the transform and its tests, and the transform has to be provably incapable of changing anything but the module specifier. It uses the AST only to locate the specifier and then splices the replacement into the original text, rather than returning `root.toSource()`. That is not caution for its own sake: the first pass over these 531 files did go through recast, and recast rewrote unrelated JSX in 7 of them — wrapping elements in parentheses, folding a self-closing `<span />` onto the text node beside it, and stripping blank lines between siblings. recast reprints a whole subtree whenever it cannot reconcile comment attachment, and several of these files carry `// oxlint-disable-next-line` comments inside their JSX. Splicing keeps the property the sweep needs: every hunk is the single `from` line. The spec pins that by asserting full output equality both on a multi-line specifier list and on the exact JSX shape that recast mangled. Locating with the AST rather than a regex is what rules out matching the path inside a string or a comment, and it covers `export ... from` re-export sources. The other cases the spec pins are the ones easy to get wrong: type-only imports, and the sibling `@makeplane/propel/components/*` and `/hooks` subpaths, which must survive untouched because 55 form screens are expected to keep exactly one component import from the external package. It deliberately does not merge into an existing `@pace/propel/icons` import. Merging would reflow the specifier lists of 52 files for no behavioural gain, and two imports from one source is something the codebase already does — `issue-layouts/utils.tsx` splits its type and value icon imports that way, and both oxlint and oxfmt accept it. The script's target list names apps/web, apps/space, packages/editor and packages/ui explicitly instead of globbing `apps/*` and `packages/*`, because three scopes must keep the vendor path: packages/propel owns the re-export and must not import its own public subpath, and apps/admin and packages/utils declare no `@pace/propel` dependency at all. jscodeshift's `--ignore-pattern` does not filter these out when the targets are reached through `../..`, so an explicit list is both the working mechanism and the honest documentation of the exclusions. * refactor: import icons from @pace/propel/icons Generated by `pnpm --filter @pace/codemods run icons-to-pace-propel`; re-running it reproduces this commit byte for byte. 516 files, one changed line each — the module specifier and nothing else. No icon is renamed, added or removed, and because `@pace/propel/icons` re-exports the external set the rewrite is an identity at runtime. What it buys is that the choice of icon package stops being a decision made 516 times. 419 of the 689 files importing `@makeplane/propel` imported nothing from it but icons, so those files now name one component library instead of two, and the population of files juggling three or four of them drops from 159 to 82. Three hunks are not one line: `workspace-invitations/page.tsx`, `settings/workspace/sidebar/item-icon.tsx` and `workspace-notifications/.../menu-option/root.tsx` had multi-line specifier lists that fit within the print width once the specifier lost six characters, so oxfmt collapses them onto one line. Three scopes deliberately keep the vendor path. packages/propel's own 20 files are where the re-export lives, and a package importing its own public subpath would make its source depend on its build output and pull the whole 900-name barrel into every component that wants one icon — @makeplane/propel is propel's declared dependency, so importing it directly is the honest edge. apps/admin (23 files) and packages/utils (1) declare no `@pace/propel` dependency; admin was deliberately moved off it and carries shims that say so, and @pace/propel depends on @pace/utils, so pointing utils back at propel would close a package cycle. Moving either needs a package.json change that is a decision of its own, not a side effect of an icon sweep. 15 further files in apps/web still import the old path; every one is deleted by the dead-code removal in #252, so rewriting them would only produce modify/delete conflicts. Re-running the codemod after that lands finishes them if it does not. * chore: bump the workspace version, which check-version requires of every PR * chore(lint): keep apps/space on the one icon import path Now that apps/space imports every icon through @pace/propel/icons, an oxlint rule stops the old path coming back. It is an error rather than a warning deliberately: apps/space's script allows 676 warnings, so a warning there is invisible. Verified by introducing a violation and watching `oxlint --max-warnings=676` exit 1, then removing it and watching it exit 0. The scope is apps/space only, because that is the only app the sweep left clean. apps/web still has 15 users of the old path and every one of them is a file the dead-code branch deletes, so it joins this rule when that lands rather than being carved out file by file now. apps/admin cannot join at all while it declares no @pace/propel dependency.
houko
added a commit
that referenced
this pull request
Sep 19, 2026
…, shell, modals (#260) * refactor(web): merge core/components/core/theme into core/components/appearance Theme lived in two directories: core/components/appearance held the public entry point (theme-switcher.tsx, the only file the barrel re-exports and the only one settings reaches) while the six pieces it composes sat under the unnamed core/components/core bucket. A reader arriving at the switcher had to leave the feature directory to find the selector it renders. appearance/ is the home rather than a new top-level theme/ because it is feature-named and already owns the entry point. The four files reached only through relative ./ imports from custom-theme-selector.tsx move with it untouched, so the entire cross-directory cost is the two specifiers in theme-switcher.tsx, which become sibling-relative in line with how the rest of this directory already refers to its own files. * refactor(web): move the two non-loaders out of core/components/ui core/components/ui holds 24 files, 22 of which are skeleton loaders under loader/. The two exceptions are a label-count chip and an empty-state layout, and both have an obvious owning directory already: IssueLabelsList belongs with the other label components and EmptySpace/EmptySpaceItem with the other empty states. Moving them first means the directory that gets renamed next contains nothing but loaders, so the new name needs no qualification. Neither file is added to core/components/labels/index.ts: that barrel is hand-maintained and already omits several of its neighbours, so the single consumer keeps importing by path. * refactor(web): rename core/components/ui to core/components/skeletons The path lied twice over. apps/web imports the @pace/ui package on nearly every screen, so "@/components/ui" reads as a local copy of the design system when in fact every file under it is a loading skeleton. Naming the directory for what it contains removes the collision and tells a reader where skeletons live without opening the folder. Four files stay behind in core/components/ui because #252 (chore/remove-dead-code-and-assets) deletes them: markdown-to-component.tsx, profile-empty-state.tsx, loader/notification-loader.tsx and loader/pages-loader.tsx. All four are unreferenced anywhere in the repo, and none of them imports a sibling, so leaving them in place breaks nothing and keeps this rename from colliding with that deletion. core/components/ui disappears when #252 lands. * refactor(web): extract the route-shell primitives into core/components/shell PageTitle, ContentWrapper and AppHeader are the three highest-traffic residents of core/components/core, and unlike the rest of that bucket they share an obvious identity: they are the chrome every route renders around its content, consumed almost entirely by app/ layouts and pages. Ninety-eight of the directory's import lines point at these three files, so naming them is where the bucket's cost actually is. shell rather than app-shell keeps the import short, and rather than core/layouts because that directory already exists with a different meaning: it holds three route-level layout components, not primitives those layouts compose. The remaining 33 files in core/components/core are deliberately untouched. They have no single destination between them and each one wants a judgement call about its owning feature; emptying the directory is not what this change is for. * refactor(web): disperse core/components/core/modals into the owning features Every one of the eight files in this directory has an unambiguous owner, so the grouping by "is a modal" bought nothing: it only guaranteed that anyone working on work items, the workspace logo or the profile avatar had to look somewhere other than the feature directory. Fifteen import lines total, and the two relative imports inside the group (bulk-delete-issues-modal to its item, existing-issues-list-modal to its empty state) survive untouched because both pairs land in issues/. The two destinations that needed a judgement call: gpt-assistant-popover.tsx goes to editor/ rather than issues/ because it is a generic AI-completion popover over a RichTextEditor with no work-item concept in it, and its sole consumer imports it as an editor affordance. user-image-upload-modal.tsx goes to profile/ rather than account/ because it edits the profile avatar; account/ in this codebase holds authentication and account-lifecycle concerns, which is also why change-email-modal.tsx goes there. core/modals/ (the unrelated top-level directory) is not involved, and the remaining 25 files of core/components/core are left alone. * chore: bump the workspace version, which check-version requires of every PR
53 modules in apps/web and apps/space that nothing reaches: the pre-`steps/` onboarding flow superseded by components/onboarding/steps, an exporter modal, sidebar menus, and a scatter of components, hooks and stores. A skeptic tried to find a consumer for each first, by symbol, by path, through `export *` barrel chains, through dynamic `import(`/`lazy(`, through the route manifest, and against every git ref -- and rescued 21 candidates from the audit's list, so what is left is what survived that. Two of the rescues are worth recording because a mechanical sweep would have taken them. Eleven of them are route redirect targets, named only as string arguments in `apps/web/app/routes/core.ts` (including the `/inbox` -> `/intake` legacy redirect), so no symbol search sees them. And the audit proposed removing `export * from "./base-layouts"` at packages/types/src/index.ts:61, which is the only path by which the live gantt layout reaches GANTT_TIMELINE_TYPE -- that one line breaks the build. Held back deliberately, not deleted: `components/readonly/` and `components/base-layouts/` are provably unreachable but are coherent, complete, current-style slices that read as reserved surface, and the audit's argument for treating one as stale and the other as in-flight rested on file age this repo does not have (see the PR description). `trend-piece.tsx` and `sticky-item-drag-handle.tsx` are referenced from commented-out JSX, i.e. temporarily disabled rather than dead -- and TrendPiece uses `export default`, so a symbol search is not authoritative for it either way.
12.7 MB across apps/web, apps/admin and apps/space -- empty-state illustrations for screens that no longer use them, and one unused Inter weight set. Assets here are consumed only through static Vite `?url` imports with literal paths: there is no `import.meta.glob`, no `require.context` and no `new URL(` anywhere in the three apps or in packages, so a literal-path search is complete rather than a heuristic. The check that makes this safe is the reverse one: every asset path string remaining anywhere in the tree resolves to a file that still exists. That is stronger than the basename search the audit used, and it is what catches the collision cases -- `empty-state/label.svg` scores "referenced" against a literal basename search purely because the live string `empty_label.svg` contains it. The nine italic Inter faces are kept. They are unimported for a reason that is a bug rather than a cleanup: apps/web/core/components/editor/pdf/document.tsx registers every italic variant against the upright source, so italic text in an exported PDF renders upright today. Deleting the faces would cement that.
… no consumer 16 modules across packages/services, packages/propel and packages/shared-state, including three services whose endpoints the live WorkspaceService already implements, and the two unreachable scaffold stores in shared-state. The near miss here is the reason this needed a skeptic. packages/services/src/file/file-upload.service.ts has five consumers inside its own package and none outside it, and the audit's method searched only outside packages/services -- so it was on the list, and deleting it would have taken down @pace/services and all three apps. An unreferenced-export scan that excludes a package's own source is unsound for any package whose modules compose each other. apps/space also declared its own `TPublicCycle`, `TPublicMember` and `TPublicModule` in local `.d.ts` files while the services it calls are typed against @pace/types. The bodies were character-identical, so the shadows are gone and the four importers point at @pace/types: a new required field in packages/types would previously have gone undetected by tsc on the space side.
… reached Five confirmed-dead export groups across packages/utils, packages/constants and packages/types, including `extractURLComponents`/`processURL` and the 1,447-line TLD table that was reachable only from them. Removing those two orphaned five module-private regex and protocol constants that had no other reader; `LOCALHOST_ADDRESSES` stays because `isLocalhost` still uses it. The audit counted 335 dead exports. Most of that number does not survive checking: 152 are load-bearing names where only the `export` keyword is droppable, which is a larger and riskier change that wants a real type-check gate rather than a sweep, and the rest sit in the deployment-surface and theming areas the skeptic could not resolve either way. Only what was individually proven is removed here.
…ame the wrong operation 52 live `console.log` calls sat in catch blocks, so a failure printed at log level with no error level and, in several cases, nothing to identify what failed. They are now `console.error` following the convention already dominant in these exact files -- favorite.store.ts alone had ten. No toasts and no new dependency: the sites that fail silently to the user still do, which is a boundary for a hygiene change rather than an oversight. Seven messages named an operation other than the one that threw, which is worse than no message because it sends a reader to the wrong function: `deleteIssueComment` reporting an add, `removeProjectFromFavorites` reporting a fetch, and five more. Each was re-derived from its own enclosing function rather than taken on trust. The audit's count of 53 was off by one -- one of them was already commented out -- and two of the four it classified as debug leftovers were real error handlers, which are preserved rather than deleted.
`sidebarPreferenceJSON` in internal/workspace and `stringOrEmpty` in internal/project, neither with a caller anywhere in the module. The first is the one worth removing rather than leaving: the live GET builds the same shape inline further up the file, so the stale copy was a plausible-looking edit target that no longer matched the response. Note what the green build does and does not prove here. Go does not error on an unused package-level func, so `go build` and `go vet` passing shows only that nothing broke. The evidence for deadness is the search: zero references in a single-module repo, no build tags, no dot-imports, and both symbols unexported, so neither reflection nor interface satisfaction can reach them. apps/api/internal/server/proxy_config_test.go is deliberately untouched. The audit proposed deleting 1,955 lines of it as subsumed by the 223-line data-driven guard beside it, but cutover_test.go calls `proxiesToGoAPI` from that file so the literal recommendation does not compile, and apps/api/README.md names one of its tests as the compensating control for a blind spot in the fixture. Consolidating it is an additive, test-first refactor and does not belong in a deletion PR.
…wenty locales Adds an unused-key report built on the literal-resolution pass that `sync-check.ts` already uses, extracted into a shared `lib/source-scan.ts` so there is one resolver rather than two with different ideas of what counts as a reference. That matters here: keys reach `t()` indirectly often enough -- a label map, a password-strength table, a `labelKey` prop -- that matching call sites misses exactly the cases hardest to catch by eye, which is the reasoning the existing check was built on. It reports and exits 0 by default, with strict mode opt-in. A heuristic this fuzzy failing CI would block the next PR that adds a label map, and the point is to see the number, not to gate on it.
`react-markdown` lost its only consumer when markdown-to-component.tsx went, and DashboardService lost its when DashboardStore did -- it was reachable from the package barrel but from no component. Both go, along with the now-unused catalog entry.
houko
force-pushed
the
chore/remove-dead-code-and-assets
branch
from
September 19, 2026 13:35
5717c2d to
ad4021b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Removes 328 files — 12.7 MB, 22.4% of the tracked tree (56.8 MB → 44.0 MB, 5,271 files → 4,943) — plus the dead exports, swallowed errors and wrong error messages found alongside them.
Every deletion went through a skeptic first, whose job was the opposite of the audit's: prove each candidate is still alive. They rescued 62 candidates and left 18 as unprovable either way. Both groups are untouched here. That pass is the substance of this PR, not a formality:
packages/services/src/file/file-upload.service.tswas on the delete list. It has five consumers inside its own package (file.service.ts,sites-file.service.ts,issue-attachment,issue-comment,project-page) and zero outside it — and the audit's method searched only outsidepackages/services. Deleting it takes down@pace/servicesand all three apps. The general form: an unreferenced-export scan that excludes a package's own source is unsound for any package whose modules compose each other.packages/types/src/index.ts:61— the audit proposed droppingexport * from "./base-layouts". That line is the only path by which the live gantt layout reachesGANTT_TIMELINE_TYPE. It breaks the build immediately.apps/web/app/routes/core.ts(including the/inbox→/intakelegacy redirect), so no symbol search sees them.trend-piece.tsxandsticky-item-drag-handle.tsxare referenced from commented-out JSX — temporarily disabled features, not dead code.TrendPiecealso usesexport default, so a symbol search was never authoritative for it.One methodological finding that should outlive this PR
Git history carries no file-age information in this repo, so no argument may rest on it.
2e3185ecis the root commit: the entire fork was squashed into it on 2026-09-14, 5,235 files and 573,103 insertions. Every file looks exactly as old as every other file.The audit used that one commit to reach opposite conclusions about two directories —
components/readonly/was "predates the fork, inherited dead code, delete" andcomponents/base-layouts/was "two commits, first 2026-09-14, in-flight refactor, keep". Identical evidence, contradictory verdicts. Both are held back here, on the affirmative signals instead:readonly/is a complete, symmetric 8-component slice carrying a deliberate API comment and written in current house style, andbase-layouts/'s companion type tree has already been partly adopted by shipping code. Neither should be deleted without a word from whoever owns those views.Type of Change
Screenshots and Media (if applicable)
None — nothing user-facing changes. The 256 deleted assets are unreachable from any code path; the check that establishes that is described below.
Test Scenarios
pnpm turbo run build check:types check:lint check:format --force— 59/59, no cache, exit 0.apps/api:gofmt -l .no output,go vet ./...clean,go test ./...0 failures (non-Docker path; database-backed tests skip themselves by design).empty-state/label.svgscores "referenced" under a basename search purely because the live stringempty_label.svgcontains it. A wrong deletion here is a hard Vite build error, not a silent 404, so the green build is meaningful.?urlimports with literal paths: noimport.meta.glob, norequire.context, nonew URL(in any of the three apps or in packages. Re-verified rather than taken from the audit.Honest limits on what green proves:
References
Needs one decision before or after merge:
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/integrations/page.tsxexists on disk and appears in noroute()call. Its two dependencies are provably unreachable at runtime, butapps/web/tsconfig.jsonhas"include": ["**/*"], so deleting them leaves that page type-checking against unresolvable imports. All four files are kept. If the integrations settings page is not meant to ship, deletepage.tsxand the three together; if it is, all four stay and the siblingintegration-card/select-repository/select-channelfiles removed here were the genuinely dead half.Found while doing this, deliberately not fixed here:
apps/web/core/components/editor/pdf/document.tsx:195-212registers all ninefontStyle: "italic"variants against the upright source files. The nine italic Inter faces are therefore kept — they are unimported because of this bug, and deleting them would cement it.apps/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx:172:if (!sourceId || !groupedFavorites[sourceId].parent) return;has no optional chaining, so any drop whosesourceIdis absent throws inside the drag-and-droponDrop. It is an unfinished move-favorite-out-of-folder handler.packages/propelbuild-config defects, all additions rather than deletions:tsdown.config.tslists entrysrc/emoji-reaction-picker/index.tsfor a directory that does not exist;src/separatoris in neither the entry list norpackage.jsonexports, so no app can import it; and@pace/propel/utils/placementis imported by the apps but absent from the exports map.empty-state/label.svg,empty-state/estimates/dark.svgandestimates/light.svg(32,211 bytes). They scored "alive" only through substring collision.apps/space/app/assets/robots.txtis in the wrong place; it belongs inapps/space/public/.packages/utils/src/url.ts(isValidIPv4,isValidIPv6,validateIPAddress,isLocalhost,extractHostname,formatURLForDisplay) now have onlyurl.test.tsas a consumer. The skeptic correctly rescued them — the audit's evidence against them came from a broken prefix filter — but they are a test-only island that the barrel still re-exports. That is an API decision, not a sweep.user-image-upload-modal.tsx:90logs "Error in uploading user asset" from the removal path; the sibling workspace modal gets it right.apps/space/core/store/issue-detail.store.tsbind the error as_errorand never use it, so they now log a static string with no diagnostic payload. Renaming and passing it is the real fix; the underscore convention was left alone.Two corrections to the audit's own evidence, so nobody acts on them later: its finding-72 list of 13 unreferenced
packages/shared-stateexports is unreliable —IFilterInstanceis imported by sevenapps/webfiles — and its claim thatno-projects-*assets are unreferenced is wrong;settings/projects/page.tsximports both. Neither affected what was deleted.The i18n commit adds a report, not a gate: it prints unused English keys and exits 0, with strict mode opt-in. A heuristic this fuzzy failing CI would block the next PR that adds a label map, and no translation key or locale file is touched.