Every number below was produced by running the command in its row, twice: once
on a clean git clone --depth 20 of main before any edit, and once on the
same tree after. Where a tool does not fit this stack, the row says so instead
of guessing.
Baseline commit: f0b17fc ("Gate the Convex setup door on validity, not
presence"). Machine: Windows 11, Node 22, npm install --no-audit --no-fund.
The optimization target was concepts removed, not lines. This repository is about a million lines of production source; shaving lines off it would be theatre. What a new engineer actually pays for is the number of things they must hold in their head — dependencies, npm scripts, lifecycle hooks, files that appear in search results but are dead. Those are what moved.
| Measure | Before | After | Change | Evidence command |
|---|---|---|---|---|
| Production files | 3,241 | 3,227 | −14 | git ls-files 'apps/*' 'backend/*' 'shared/*' 'workers/*' 'packages/*' 'api/*' 'services/*' 'mcp-services/*' 'middleware.ts' | grep -E '\.(ts|tsx|js|jsx|mjs|cjs)$' | grep -vE '(\.test\.|\.spec\.|\.stories\.|/tests?/|/__tests__/|/__mocks__/)' | wc -l |
| Production source lines | 1,037,982 | 1,036,216 | −1,766 | same file list piped through xargs wc -l, summing the total rows |
| Direct dependencies | 246 | 232 | −14 | node -e "const p=require('./package.json');console.log(Object.keys(p.dependencies).length+Object.keys(p.devDependencies).length)" |
| — of which runtime deps | 185 | 172 | −13 | same, p.dependencies |
| — of which devDeps | 61 | 60 | −1 | same, p.devDependencies |
| npm scripts | 200 | 198 | −2 | node -e "console.log(Object.keys(require('./package.json').scripts).length)" |
| npm lifecycle hooks | 1 (postinstall) |
0 | −1 | node -e "console.log(Object.keys(require('./package.json').scripts).filter(s=>/^(pre|post)?(install|pack|publish|prepare)$/.test(s)))" |
| Unused files (Knip) | 926 | 912 | −14 | npx knip --no-exit-code --reporter json > k.json then node -e "console.log(require('./k.json').files.length)" |
| Unused exports (Knip) | 991 | 991 | 0 | same JSON, summing issues[].exports |
| Unused types (Knip) | 908 | 908 | 0 | same JSON, summing issues[].types |
| Unused dependencies (Knip) | 52 | 39 | −13 | same JSON, summing issues[].dependencies |
| Duplicate blocks (jscpd) | 2,486 | 2,480 | −6 | npx jscpd apps/web/src backend/convex shared workers packages --ignore "**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/node_modules/**,**/_generated/**" |
| Duplicate percentage (jscpd) | 3.76% | 3.76% | 0 | same command, Total row |
| Circular dependencies | 1,196 | 1,196 | 0 | npx madge --circular --extensions ts,tsx --ts-config tsconfig.json apps/web/src (602) + npx madge --circular --extensions ts --ts-config tsconfig.json backend/convex (594) |
| Circular dependencies (dependency-cruiser) | — | — | — | not applicable — see "dependency-cruiser does not fit this stack" below |
| Canonical workflow tests | exit 1 · app segment 22 failed / 1,426 passed / 20 skipped | exit 1 · app segment 22 failed / 1,426 passed / 20 skipped | 0 new failures | npm run test:run |
| Typecheck | not re-measured in this pass — promotion/PROMOTION_LOG.md records exit 2 / 5,383 errors at the Wave 1 baseline |
exit 2 · 5,378 errors | no delta claimed | npx tsc -p tsconfig.app.json --noEmit --pretty false 2>&1 | grep -c "error TS" |
| Browser workflow passes | not run — needs a Convex deployment | not run — same reason | — | npx playwright test evals/e2e/one-flow-regression.spec.ts (requires VITE_CONVEX_URL) |
| Production bundle size | exit 0 · PWA precache 338 entries / 22,522.18 KiB | exit 0 · PWA precache 338 entries / 22,522.06 KiB | −0.12 KiB | npm run build |
| CodeTour steps resolving | n/a (no tours existed) | 27 / 27 | +27 | node scripts/validate-tours.mjs |
| Additions/deletions | — | — | 34 files, +1,700 / −1,885 | git diff --shortstat |
On the unused-files arithmetic: 926 − 15 deleted + 1 added = 912.
The one added is scripts/validate-tours.mjs, the CodeTour validator, which
Knip reports as unused because nothing imports it — it is a command-line entry
point. It is named in docs/codebase/TESTING.md and in all three tour
descriptions, and it is deliberately not wired to a 199th npm script in a
repository whose script count is itself a documented problem (CONCERNS C5).
On the typecheck row specifically: the "before" figure is quoted from an
earlier wave, not produced by this pass, so the five-error difference is not
offered as a result — a number measured before the change it is used to justify
proves nothing. What is this pass's measurement is the after figure: exit 2,
5,378 errors, 4,637 of them TS2339, 4,361 error lines containing never,
first error apps/web/src/App.tsx(88,20). That profile matches the known
ApiFromModules cause exactly (CONCERNS C1), which is the useful conclusion.
The gate names dependency-cruiser for the circular-dependency row. It was
tried four ways and the row is recorded as not applicable for a specific,
reproducible reason rather than left blank:
- Given a directory (
apps/web/src), it reports "1 modules, 0 dependencies cruised" on Windows — it never walks the tree. - Given the entry file (
apps/web/src/main.tsx) with no config, it resolves 78 modules and then stops: every@/…import fails, because this repo's path aliases are declared invite.config.tsandtsconfig.json, and dependency-cruiser reads webpack or tsconfig, never a vite config. - Adding
tsConfig: { fileName: "tsconfig.json" }makes it worse (14 modules) — with a tsConfig set, even sibling./Appfails to resolve. - Its
enhancedResolveOptionsschema rejects analiaskey (data/options/enhancedResolveOptions must NOT have additional properties), so the aliases cannot be supplied directly.
Making it work would require adding a webpack config to the repository purely to
feed a measurement tool — a new config file, i.e. exactly the kind of concept
this pass exists to remove. madge --ts-config tsconfig.json resolves the
aliases correctly and walks 3,020 + 1,741 files, so it is used for that row and
the deviation is recorded here.
The finding it produced stands either way: 1,196 circular import chains, and nothing in CI forbids them. That is logged as CONCERNS C3, unresolved.
Thirteen runtime dependencies that nothing imports. Verified twice — Knip's
report, then a literal git grep for from "<pkg>", require("<pkg>") and
import("<pkg>") across every .ts/.tsx/.js/.jsx/.mjs/.cjs file, which returned
zero hits for each:
@ai-sdk/provider, @convex-dev/crons, @lexical/history, @lexical/html,
@lexical/utils, @remotion/player, ag-grid-community, ag-grid-react,
discord-interactions, install, react-data-grid, reactflow, tavily-mcp.
install is worth naming on its own: it is the npm package literally called
install, the classic residue of a mistyped npm install install.
discord-interactions appears only in two comments describing an approach the
code took instead.
One devDependency that this pass made dead: lighthouse. It existed for the
perf:lighthouse script, which was already broken — it ended in
&& node analyze-lighthouse.cjs, and that file does not exist in the
repository. Removing the broken script left the dependency with no consumer.
Pruning it removed 78 packages from node_modules.
The postinstall hook and the script it ran.
scripts/patch-crons-exports.mjs (71 lines) ran on every single npm install
to patch the exports field of @convex-dev/crons — a package that
backend/convex/convex.config.ts never registers and no module imports. A
lifecycle hook is the most expensive kind of concept in a repository, because it
runs for everyone, silently, forever. This one existed to maintain a dependency
that was not used.
Fourteen unreferenced source files under apps/web/src. Selection rule,
stated so it is checkable: a file qualified only if Knip listed it as unused
and a repo-wide git grep -F for its module basename returned no hit in any
other tracked file. Of Knip's 532 unused files under apps/web/src, exactly 20
passed both checks; six of those were kept because they are real —
vite-env.d.ts is an ambient TypeScript declaration and five *.stories.tsx
are discovered by Storybook's glob rather than by import. The other 14 went:
hooks/useAgentStateApi.ts, hooks/useMousePosition.ts,
hooks/useUnifiedItems.ts, lib/llmProvider.ts, types/wx-react-gantt.d.ts
(an ambient declaration for a package that is not installed),
shared/components/PillAction.tsx, shared/ui/AnimatedNumber.tsx,
shared/ui/AnimatedProgressBar.tsx,
features/chat/components/EntityDisambiguationCard.tsx,
features/entities/hooks/useCanonicalFastLane.ts,
features/graph/lib/graphTypes.ts,
features/onboarding/utils/dossierParser.ts,
features/redesign/hooks/useLongHorizonRetrospectiveSwr.ts,
features/research/components/MorningBriefingHeader.tsx.
One duplicated script. evals/linkupSmoke.js was a CommonJS transcription of
evals/linkupSmoke.mjs — same twenty lines, require instead of import. The
repository declares "type": "module", so the .js copy could not have run.
One accidentally committed file. A zero-byte file whose name was a mangled
Windows path (C:UsershshumAppDataLocalTempchat-diff.txt) sitting in the
repository root.
One, and it is the postinstall case above: a 74-line hand-written patch script
maintaining a dependency the platform never loaded. The replacement was not a
library — it was deletion. That is the correct top rung of the reuse ladder:
does this need to exist at all.
Nothing else was replaced. Two candidates were examined and rejected on purpose:
- The hand-rolled SSE parse loop in
chatRuns.ts(reader, decoder, manual\nscan) duplicates what the installedaiSDK does. Replacing it is a rewrite of the one path every answer flows through, on a repo whose test suite is already red — that is feature-shaped risk wearing a cleanup costume. Logged, not done. - The
*Swrhook pairs duplicate Convex's own reactiveuseQuery. Both variants have live callers, so this is a behavioural fork, not dead code. Merging them changes what renders during loading, which is externally observable. Logged as CONCERNS C6; it needs a characterization test on the loading states before anyone touches it.
| Finding | Size | Why it was left |
|---|---|---|
tsc -p tsconfig.app.json red |
5,378 errors | One upstream cause: Convex's ApiFromModules over ~1,537 backend modules exceeds a TypeScript type-level limit, api collapses to never, and 4,637 TS2339s cascade from it. The fix is a backend module-count or codegen change — a system change, out of scope for a reduction pass, and explicitly excluded by the brief. CONCERNS C1. |
| 912 files reported unused | 912 | Knip cannot see dynamic imports, string-built paths, or platform entry points (api/*.js are Vercel functions, Convex modules are invoked by the platform). Separating dead from platform-invoked needs per-file judgement; only the 14 that failed two independent checks were deleted. CONCERNS C2. |
| 991 unused exports, 908 unused types | 1,899 | Same reason, at higher volume and lower per-item value. Removing an export is a public-API change to modules that other packages in this repo consume; without a per-package API surface definition, a bulk pass would break consumers silently. CONCERNS C2. |
| 1,196 circular imports | 1,196 | Breaking a cycle means moving code, which is a structural refactor of paths with no characterization tests. Rule 2 of the gate says the test comes first. CONCERNS C3. |
| Test suite red in 3 of 4 segments | 27 assertion failures + 1 timeout | Pre-existing, and the app segment's failing set is byte-identical before and after this pass. Fixing them is triage work, not reduction work, and mixing the two would make it impossible to tell which change caused what. CONCERNS C4. |
| 198 npm scripts | 198 | Only two could be proven stale by static means (a referenced file that does not exist). A script that runs but does the wrong thing is undetectable without executing all 198, several of which deploy or call paid APIs. CONCERNS C5. |
package-lock.json is gitignored |
— | Committing a lockfile changes what every contributor's npm install resolves. That is a policy decision for the maintainer, not a cleanup. CONCERNS C5b. |
| 2,480 duplicate blocks (3.76%) | 3.76% | Below the threshold where mechanical deduplication pays for itself, and the blocks are spread across 3,636 files rather than concentrated. CONCERNS C7. |
| Product defects D1, D3 | — | D1 (no product route without a Convex cloud deployment) needs a backend, which this pass may not create. D3 (graph rail dies at zero viewport and never recovers) is a behaviour fix, not a structural one. Both stay in promotion/PROMOTION_LOG.md. |
-
Build.
npm run buildexits 0 before and after; PWA precache stays at 338 entries and moves 0.12 KiB, which is the deleted source, not a bundle change. The removed dependencies were never bundled, because nothing imported them. -
Dependency removal was verified against a pruned tree.
npm installwas re-run after editingpackage.jsonsonode_modulesactually lost the 22 + 78 packages before the build and tests were re-measured. Otherwise the build would have passed on packages that were still on disk. -
Tests.
npm run test:runbefore and after. Rather than compare counts, theFAILlines were extracted, ANSI-stripped, sorted and diffed:npm run test:run 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -o "FAIL.*" | sort -u
The app segment's 22 failures and the convex-mcp segment's 5 are byte-identical before and after. The
mcp-localsegment differs, and the honest reading of that is in CONCERNS C4: it is killed by a 300-second timeout, so its failure list is truncated at a different point each run. It was additionally checked directly — no test in that package asserts on anything this pass touched, and the three files that changed there (index.ts,metaTools.ts) were re-run green apart from three failures present in the baseline. -
The one behavioural edit was removing the
perf:lighthousenpm script. Three strings inpackages/mcp-localtold users to run it; all three were updated in the same change, so the MCP server no longer advertises a command that does not exist. That is a defect fix with its cause named, not a silent removal.
Reducing a million-line repository to a comprehensible one is not a one-pass job, and pretending otherwise would be the dishonest version of this document. What was done is the subset that is provably safe: things with zero references by two independent methods, and machinery maintaining something that does not exist. The large findings — 912 unused files, 1,899 unused exports, 1,196 cycles — are measured, reproducible, and left with their reasons, so the next person starts from a number rather than an impression.