Use when asked whether a codebase's size is justified — "is 800k lines reasonable?", "audit our LOC", "are we bloated?", "值不值" — or when a LOC number is being used in an argument and nobody agrees what it measures. Produces a defensible per-sub-product accounting: strict line counts, a product-surface inventory, every line attributed to exactly one sub-product, and a worth verdict per block.
Resources
2Install
npx skillscat add innei/skill/codebase-value-audit Install via the SkillsCat registry.
codebase-value-audit
Turns "our repo is N lines, is that bad?" from a vibes debate into an
audit. The core move: a raw LOC number is meaningless until (a) the
measurement lens is pinned, (b) the product surface it bought is
inventoried, and (c) every line is attributed to one sub-product and
judged against what a standalone alternative would cost. The output is a
report where the verdict is per-block, not global — because the honest
answer is always "these 4 blocks are gold, these 4 deserve a discount."
When to use
- Someone quotes a repo-wide LOC figure and asks if it's reasonable.
- Two people quote different LOC figures for the same repo and both are
"right" (different lenses) — reconcile before arguing. - Deciding where to cut: which subsystems dilute maintenance attention.
Not for: per-PR review, performance work, or dead-code sweeps alone
(knip/depcheck do that); this skill tells you which blocks deserve a
sweep.
Inputs
- Repo root (git-tracked; works on monorepos).
- Optional: the number the requester believes ("we have 800k lines") —
you must reconcile your count with theirs, not just assert your own.
Files provided
scripts/count-pure-loc.sh— strict source-file list + pure-code line
count (3 lenses printed side by side).scripts/bucketize.py— attributes every file to the first matching
bucket prefix and sums pure-code lines per bucket.scripts/drill-bucket.py— aggregates one bucket's lines per
directory at a chosen depth; mandatory before verdicts (step 4).scripts/render-report.py— renders the final report as a standalone
HTML file (light/dark, verdict chips, share bars) from a JSON data
file; schema inreferences/report-data.example.json.
Workflow
[1] Pin the lens 3 counts: tracked-total / non-test / pure-code
[2] Inventory surface parallel read-only agents, one per layer
[3] Attribute lines bucketize.py: every line -> exactly one bucket
[4] Verdict per block 5 tiers, judged vs standalone-alternative cost
[5] Report JSON data -> render-report.py -> HTML + open[1] Pin the lens
Run scripts/count-pure-loc.sh <repo-root>. It prints three numbers:
- Tracked total — all git-tracked TS/JS lines.
- Non-test physical — minus tests/fixtures/e2e/scripts/mocks/
stories/.d.ts. - Pure code — minus blank and comment-only lines.
These differ by 2x+ on a healthy repo (1.84M / 1.06M / 836k on the repo
this skill came from). When the requester's number differs from yours,
find which lens produces their number and say so explicitly — "your 800k
is lens 3, my 1.84M is lens 1, both correct" ends the argument; a
counter-assertion restarts it. All later steps use lens 3.
Also record: test:code ratio, and the largest data-as-code files (model
catalogs, locale tables, generated schemas) — they surface in step 4.
[2] Inventory the product surface
Dispatch parallel read-only agents (Explore-type), one per layer, e.g.:
web client (routes/stores), server (routers/services/modules), desktop +
CLI, shared packages, and docs/changelog (the marketed feature list —
this one cross-checks the code-derived inventory against what the product
claims to ship; discrepancies are findings). Each agent returns a breadth
inventory: surfaces, one-liners, and a "full sub-products" shortlist —
no code review, no line counts.
Merge into a sub-product list (typically 15–25 entries). A sub-product is
something that could be a standalone company/tool: an eval platform, a
bot gateway, a document editor — not a feature like "dark mode".
[3] Attribute every line
Copy scripts/bucketize.py, fill in the bucket → path-prefix table from
step 2's inventory, run it against the file list from step 1. Rules:
- First match wins; order buckets specific → general.
- One catch-all
infrabucket with prefix""must be last; inspect its
breakdown output — anything large hiding in it means a missing prefix. - Cross-layer buckets are the point: one sub-product's bucket lists its
frontend routes, its stores, its server services, its packages, and its
CLI commands together. - Expect ±10% precision; say so in the report. Sum must equal lens-3
exactly (the script guarantees it — every file lands somewhere).
[4] Verdict per block
No verdict without a drill-down. Before judging any bucket ≥ ~15k
lines, run scripts/drill-bucket.py <file-list> <depth> <prefix...> and
scan its top directories for the five water categories:
| Category | What it looks like |
|---|---|
| DEAD-ROUTED | deprecated pages/components still imported or routed — verify with grep against router configs and import sites, never by name alone |
| DATA-AS-CODE | large TS files that are pure declarations (catalogs, menus, prompt text) |
| SQUATTER | content semantically belonging to another domain living under this bucket's paths (feature pages under a settings route) |
| PARALLEL-DUP | two generations/implementations of the same thing coexisting |
| OVERSIZED | files past the repo's own size convention — count and total them |
| FIXTURE-IN-PROD | test fixtures / dev harnesses living outside test dirs, counted as product code |
Bucket-level verdicts issued without this pass systematically miss mixed
buckets: a "settings" bucket can be 60% provider forms + squatting
feature pages + dead routes, and the whole-bucket verdict ("necessary
cost") will be confidently wrong. Parallelize: one read-only subagent
per 2–3 buckets, each returning findings with line estimates and
file-path evidence; require them to verify DEAD claims and to say
"clean" when a category is empty — invented water is worse than missed
water.
Then refute before publishing. Every water claim from the first pass
gets attacked in a second adversarial round: prompt agents to disprove
each claim with import graphs, normalized diffs, and actual-wiring
traces. Expect casualties — in the audit this skill came from, 5 of 8
first-round claims fell (a "duplicate subsystem" was a dependency; "two
workflow generations" were layers; an unused-keys report was 43% false
positives). Publish only claims that survive, and record the refuted
ones in the report as corrections — an audit that shows its own
overturned accusations is more credible, not less.
Then judge each sub-product's pure-code count against what acquiring
that capability standalone would cost (build or adopt), not against
zero. Five tiers:
| Tier | Criteria |
|---|---|
| core asset | Directly implements the product's differentiation; rebuilding or adopting would cost more; high leverage per line |
| market price | Table-stakes capability at a normal size for what it does |
| risk bet | Coherent and well-built, but its value depends on an unproven product thesis — flag "watch usage data" |
| discount | Competes head-on with dedicated products without an edge, or carries measurable waste (data-as-code, duplicated generations, copy-paste variants) |
| necessary cost | Settings, auth, base layer — nobody loves it, nothing works without it; judge only its share (base layer ≤ ~20% is healthy) |
Also assemble the squeeze list: concrete removable waste with line
estimates (coexisting old/new generations of a subsystem, overlapping
sibling services, data-as-code to externalize, convention violations,
unused i18n keys, stale packages). This is the actionable output.
[5] Report
Lead with the global verdict and its split (e.g. "70% worth it / 13%
core / 18% discounted / 8–10% pure squeeze"). Then: lens reconciliation,
the ranked table (lines, share, verdict chip), tier commentary, squeeze
list, and a closing that answers the requester's actual question. If they
invoked an "AI era = fewer lines" thesis, engage it honestly: writing
cost fell, review/maintenance cost didn't; the metric that matters is
features per maintenance unit, and a ~1:1 test ratio is agent-era
scaffolding, not bloat.
To ship it as a page: fill a JSON data file followingreferences/report-data.example.json (in prose fields [[…]] renders
bold, ((…)) renders accent-highlight), then
python3 scripts/render-report.py report-data.json report.html && open report.htmlOutput is self-contained HTML (inline CSS, system fonts, light/dark).
When a verdict gets challenged and revised, keep the row, annotate it
(REV note in eyebrow/footer + a breakdown in its sub), and re-render —
an audit that silently rewrites itself loses its authority.
Common pitfalls
| Mistake | Fix |
|---|---|
xargs wc -l | tail -1 as the total |
xargs batches into multiple wc calls; tail -1 returns the last batch only. Sum with awk '$2 != "total" {s+=$1}'. |
| Counting merge-conflicted files 3x | git ls-files lists each unmerged file once per stage. Always sort -u the file list. |
| Arguing totals before pinning the lens | Both parties are usually right under different lenses. Reconcile first (step 1), then argue. |
| Running the counting script from the wrong cwd | Relative paths silently open()-fail and count 0 per file; totals become 0 without an error. Run from repo root; treat an all-zero bucket table as this bug, not as truth. |
| Treating a high test share as waste | ~1:1 test:code is the agent-era signature — it's the guardrail that lets agents keep writing. Cut discipline debt, not tests. |
| Data-as-code inflating "logic" | Model catalogs, locale tables, generated schemas are declarations. Measure them separately and recommend externalizing, or the density math is wrong. |
| Inventorying from code only | Also sweep docs/changelog/README with an agent — the marketed surface reveals sub-products the directory names hide, and mismatches are findings. |
| Catch-all bucket swallowing a subsystem | Order prefixes specific → general; always print the infra-bucket breakdown and chase anything unexpectedly large. |
| Verdict by size alone | A 10k-line block can be the highest-leverage asset (device gateway) and an 80k block half-waste (provider variants). Judge vs standalone-alternative cost. |
| Global verdict only | "Worth it overall" helps nobody decide anything. The per-block table with tiers is the deliverable; the global number is its summary row. |
| Defending a challenged verdict without drilling down | When a block's size surprises the requester ("why is settings 20k?"), re-aggregate that bucket one directory level deeper before answering. Route-named buckets often hide squatters: provider forms, feature pages living under a settings route, deprecated-but-routed dead pages. The challenge is usually a real finding. |
| Trusting a static "unused" report | An unused-i18n-keys report was 43% false positives: dynamic key construction, string-literal keys inside config objects, and plural suffixes all defeat static detection. Sample-verify against those three mechanisms before acting; a report whose own statistics are self-contradicting (usage rate > 100%) is telling you something. |
| Convicting by name or shape | "workflows/ vs workflows-hono/" looked like two generations — it was business logic + HTTP adapter, layered. "PluginDevModal" looked legacy — it was misnamed MCP UI. Prove duplication with import graphs and normalized diffs; naming is not evidence. |
| Treating a registry/identifier file as the wiring | An identifiers array turned out cosmetic (tools wired via register.ts and server runtimes); a desktop controllers registry.ts turned out type-only (runtime uses import.meta.glob). Find the mechanism that actually loads things before declaring anything unregistered/dead. |
| Publishing the first round's water list | Run the refutation round (step 4). First-pass water claims are hypotheses; in practice a large fraction die under import-graph scrutiny, and shipping them unverified torches the audit's credibility on the claims that were right. |
Verification
- The three lens counts are internally consistent (lens1 > lens2 >
lens3) and the requester's number is mapped to one of them. - Bucket sums equal the lens-3 total exactly; file list was
sort -u'd. - Infra/catch-all breakdown inspected; nothing unexplained > ~5%.
- Every bucket ≥ ~15k lines went through the drill-down pass; each
water finding carries a category, a line estimate, and verified
evidence paths. - Every sub-product has: lines, share, tier verdict, and a one-line
justification referencing a standalone alternative or a concrete
waste observation. - Squeeze list items each carry a line estimate and a concrete fix.
- Report states the ±10% attribution caveat and any excluded scopes
(e.g. closed-source stubs). -
render-report.pyoutput opens cleanly in a browser; row count,
total, and share bars match the bucketize output.