Skip to content

chore: remove 328 unreferenced files — 22% of the tracked tree — after a skeptic pass rescued 62 of the candidates - #252

Merged
houko merged 8 commits into
mainfrom
chore/remove-dead-code-and-assets
Sep 19, 2026
Merged

houko merged 8 commits into
mainfrom
chore/remove-dead-code-and-assets

Conversation

@houko

@houko houko commented Sep 18, 2026

Copy link
Copy Markdown
Member

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.ts was 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 outside packages/services. Deleting it takes down @pace/services and 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 dropping export * from "./base-layouts". That line is the only path by which the live gantt layout reaches GANTT_TIMELINE_TYPE. It breaks the build immediately.
  • Eleven route redirect targets appear only as string arguments in apps/web/app/routes/core.ts (including the /inbox → /intake legacy redirect), so no symbol search sees them.
  • trend-piece.tsx and sticky-item-drag-handle.tsx are referenced from commented-out JSX — temporarily disabled features, not dead code. TrendPiece also uses export 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. 2e3185ec is 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" and components/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, and base-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

  • Code refactoring
  • Bug fix (non-breaking change which fixes an issue)

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).
  • Assets, the reverse check rather than the forward 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 collisions — empty-state/label.svg scores "referenced" under a basename search purely because the live string empty_label.svg contains it. A wrong deletion here is a hard Vite build error, not a silent 404, so the green build is meaningful.
  • Assets are reachable only through static Vite ?url imports with literal paths: no import.meta.glob, no require.context, no new URL( in any of the three apps or in packages. Re-verified rather than taken from the audit.

Honest limits on what green proves:

  • Go does not error on an unused package-level func, so the Go build passing shows only that nothing broke — not that the two deleted helpers were dead. The evidence for that is the search: zero references in a single-module repo, no build tags, no dot-imports, both symbols unexported so neither reflection nor interface satisfaction can reach them.
  • No browser was used. Nothing here should be visible, but "should" is the operative word for the 256 assets.

References

Needs one decision before or after merge: apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/integrations/page.tsx exists on disk and appears in no route() call. Its two dependencies are provably unreachable at runtime, but apps/web/tsconfig.json has "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, delete page.tsx and the three together; if it is, all four stay and the sibling integration-card / select-repository / select-channel files removed here were the genuinely dead half.

Found while doing this, deliberately not fixed here:

  • Italic text in exported PDFs renders upright. apps/web/core/components/editor/pdf/document.tsx:195-212 registers all nine fontStyle: "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.
  • A latent crash at apps/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx:172: if (!sourceId || !groupedFavorites[sourceId].parent) return; has no optional chaining, so any drop whose sourceId is absent throws inside the drag-and-drop onDrop. It is an unfinished move-favorite-out-of-folder handler.
  • Three packages/propel build-config defects, all additions rather than deletions: tsdown.config.ts lists entry src/emoji-reaction-picker/index.ts for a directory that does not exist; src/separator is in neither the entry list nor package.json exports, so no app can import it; and @pace/propel/utils/placement is imported by the apps but absent from the exports map.
  • Three more assets look dead by the same standard and were left for a skeptic pass: empty-state/label.svg, empty-state/estimates/dark.svg and estimates/light.svg (32,211 bytes). They scored "alive" only through substring collision.
  • apps/space/app/assets/robots.txt is in the wrong place; it belongs in apps/space/public/.
  • Six exports in packages/utils/src/url.ts (isValidIPv4, isValidIPv6, validateIPAddress, isLocalhost, extractHostname, formatURLForDisplay) now have only url.test.ts as 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:90 logs "Error in uploading user asset" from the removal path; the sibling workspace modal gets it right.
  • Five catch blocks in apps/space/core/store/issue-detail.store.ts bind the error as _error and 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-state exports is unreliable — IFilterInstance is imported by seven apps/web files — and its claim that no-projects-* assets are unreferenced is wrong; settings/projects/page.tsx imports 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.

@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

React Doctor found 2 new issues in 2 files · 2 warnings · score 83 / 100 (Needs work) · 24 fixed · vs main

2 warnings

core/components/profile/user-image-upload-modal.tsx

  • ⚠️ L30 React function has high control-flow complexity no-high-complexity-react-function

core/components/workspace/workspace-image-upload-modal.tsx

  • ⚠️ L35 React function has high control-flow complexity no-high-complexity-react-function

Reviewed by React Doctor for commit ad4021b. See inline comments for fixes.

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
houko force-pushed the chore/remove-dead-code-and-assets branch from 5717c2d to ad4021b Compare September 19, 2026 13:35
@houko
houko merged commit be07dbe into main Sep 19, 2026
14 checks passed
@houko
houko deleted the chore/remove-dead-code-and-assets branch September 19, 2026 13:40
houko added a commit that referenced this pull request Sep 19, 2026
…nt (#262)

.setver.sh and .rebase-pr.sh are throwaway helpers for rebasing the P3 branch
batch onto a moving main. They reached the repository root through a `git add
-A` during the last of those rebases and went to main with #252. Neither is
referenced by anything and neither belongs in the tree.
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.

1 participant