유명 오픈소스 74개를 같은 자로, 체급을 나눠 재서 기준선을 만들었다. 네 저장소가 그 안에서 어디쯤인지, 어느 줄을 고치면 올라가는지 알려준다. 묻는 건 코드가 예쁜가가 아니라 다음에 이걸 고칠 때 얼마가 드는가다. 같은 진단으로 낸 PR 은 그 저장소들에 머지됐다.
We built a baseline by grading 74 well-known open-source repos with one ruler, split by weight class. You get where your repo lands in it, and which line to change to move. The question isn't whether the code is pretty; it's what the next change will cost. Findings from the same checks have been merged into those repos.
구조 지표만 잰다 — 설계·테스트·문서·보안·성능·커뮤니티, 그 소프트웨어가 준 가치는 재지 않는다. 오래 살아남아 기능을 많이 감당한 코드일수록 이 지표는 불리하다. 낮은 등급이 나쁜 프로젝트라는 뜻이 아니다.
Structural signals only — not design, tests, docs, security, performance, community, or the value the software delivered. Code that survived long enough to carry many features scores worse here by construction. A lower grade is not a worse project.
빌드 산출물·테스트·벤더 파일은 알아서 제외한다. 빈 파일로 점수를 부풀리는 것도 막는다.
Build output, tests, and vendored files are excluded automatically — and padding with empty files won't inflate the score.
$ npx fixearly --dir=packages/vite/src git 추적 파일만 분석 — 빌드산출물 0개 제외 (382개 대상) 비-프로덕션·벤더 파일 249개 제외 (133개 분석) 자잘 파일(코드 <5줄) 1개 제외 — 패딩 게이밍 방지 파일: 132개 코드: 36,996줄 등급: C+ (66점) — 함수 2,378개, cognitive 평균 3.1·최대 195, cog15+ 106개·cog25+ 56개, 중복 0.5%, 200줄+ 42개 복잡: generateBundle cog 195 — node/plugins/importAnalysisBuild.ts:281 복잡: ssrTransformScript cog 101 — node/ssr/ssrTransform.ts:59 복잡: resolveConfig cog 98 — node/config.ts:1460
크기 지표가 원리상 못 보는 결함을 따로 짚는다. 점수엔 넣지 않는다 — 이건 채점이 아니라 작업 목록이다.
Defects a size metric can't see, listed separately. They never touch the score — this is a work list, not a grade.
$ npx fixearly --dir=src --mine O(n²) 배열 조회: 71곳 (PR후보 67곳 · backend 67 / frontend 0 / test 4) 루프 안 선형 탐색 — Map/Set으로 O(n). 점수 미반영. [★backend] enrichedRows.find() — row/staticFormula.ts:114 (loop: rows) [★backend] baseUsers.find() — field-handler/user.general.handler.ts:153 [★backend] targetList.some() — services/data-table.service.ts:1491 ✓ 후보 목록 저장 → public/quadratic-candidates.json
git 이력을 읽어 복잡도 × 변경빈도로 정렬한다. 제일 복잡한 파일이 아니라, 복잡한데 매주 만지는 파일이 답이다. 안 건드리는 파일은 놔두라고 말해준다.
Reads git history and ranks by complexity × churn. The answer isn't the most complex file — it's the complex one you touch every week. It also tells you what to leave alone.
$ npx fixearly --dir=src --hotspots ── 복잡 × 변경빈도 = 먼저 고칠 파일 ── maxCog churn worst함수 · 파일 148 28 handleTranscriptLine():395 qa-runner.claude-runner.ts 88 110 processFileAsync():978 file/service/file.service.ts 62 201 streamAgent():255 agent/agent.service.ts 놔둘 것 (복잡하지만 안정 — 리팩터 ROI 낮음): cog 248 churn 3 expandExternalLinks() chain-collector/external-links.ts
--report는 HTML 한 장을 뽑는다 — 같은 체급 오픈소스 안에서 내 위치, 축별 좌표, 무엇부터 고칠지 파일·줄, 그리고 각 항목마다 코딩 에이전트에 그대로 붙여넣는 작업 지시문. 측정할 때마다 이력이 쌓여서 지난번 대비 변화도 같이 나온다. 외부 요청 0 — 파일 하나로 끝난다.
--report writes a single HTML page: where you sit inside your weight class, your position on each axis, what to fix with file and line, and a ready-to-paste work order for each item. Every run appends to a local history, so you also get the delta since last time. Zero external requests.
$ npx fixearly --dir=src --report 등급: B (73점) — 함수 11,000개, cog15+ 293개, 중복 5.5% 지난 측정(2026-07-20 09:00) 대비: C 69 → B 73 (+4점) ✓ 리포트 → fixearly-report.html ── 리포트 안에 ── 어디쯤인가 헤비급 11개 중 6위 (체급 중앙 73점) 무엇부터 processFileAsync() 354줄 file.service.ts:978 · churn 110회 [지시문] ← 눌러서 전문 확인 후 복사 지난번 대비 긴 함수 661개 → 623개 (−38)
3천 줄 유틸과 20만 줄 백엔드를 같은 링에 세우면 큰 게 늘 진다. 그래서 코드줄로 체급을 나눈다(파일 수로 나누면 파일을 잘게 쪼갠 쪽이 체급만 올라간다). 체급이 크기를 지운다면 종류는 성격을 지운다 — 엔진·컴파일러 11개는 중앙 65점으로 툴체인(85)·라이브러리(85)·프레임워크(83)보다 구조적으로 낮고, 앱은 75점이다. 엔진은 성능·번들 크기 때문에 추상화를 포기하고, 앱은 화면 단위 함수가 길어지는 관습을 따른다 — 둘 다 이 축이 깎는다. 그래서 행을 열면 동종 기준선을 같이 보여준다.
Pit a 3k-line util against a 200k-line backend and the big one always loses — so we split by lines of code, not file count. Weight class cancels size; kind cancels temperament — the 11 engines and compilers sit at a median of 65 — below toolchains (85), libraries (85) and frameworks (83) — and apps land at 75. Engines trade abstraction for speed and bundle size; apps grow long screen-level functions. These axes penalise both. So every row shows its peer baseline.
규칙이 다르면 점수를 나란히 놓을 수 없다. 아래는 무엇이 바뀌어서 점수가 움직였는지이지, 저장소가 좋아졌다·나빠졌다는 뜻이 아니다. 보드를 다시 발행한 판만 싣는다 — 발행 없이 지나간 판은 아무도 그 점수를 본 적이 없다. 현재 판은 v12다.
Scores from different rulesets are not comparable. This lists what changed in the ruler, not whether a project improved. Only rulesets the board was republished under are listed — the board is on v12.
감점 비중은 인지 복잡도 37 · 함수 길이 23 · 중복 9 · 파일 크기 8 · 순차 I/O 5 · O(n²) 5(캡 기준, 합 87). 나머지 18종은 등급을 건드리지 않고 파일·줄만 짚는다.
Caps by axis: complexity 37, function length 23, duplication 9, file size 8, sequential I/O 5, O(n²) 5 — 87 total. The other eighteen never touch the grade; they name the file and line.
한 번에 읽어야 하는 양. 중첩 함수·주석을 뺀 자기 코드 줄 기준, 40줄 초과(JSX 60줄) 비율. 파일을 쪼개도 안 변한다 — 그래서 조작이 안 된다.
How much you must read at once — a function's own code lines, nested functions and comments removed. Splitting files doesn't move it, so it can't be gamed.
함수를 머리로 따라가는 부담. SonarSource S3776 스펙과 정본값까지 일치 검증.
How hard a function is to follow. Matches SonarSource S3776, verified to canonical values.
토큰 단위 복사·붙여넣기 밀도. 74개 실측에서 점수와 상관 −0.11이라 비중을 16→9로 줄였다 — 대부분에겐 0점, 소수에게만 큰 항목이다.
Token-level copy-paste density. Correlates −0.11 with score across 74 repos, so its cap was cut 16 → 9: zero for most, heavy for a few.
평균 줄 수 + 대형 파일 비중. 보조 항으로 강등(27→8) — 같은 코드를 6파일로 쪼개기만 해도 옛 공식은 +27점을 줬다. 함수 길이와 상관 +0.16이라 버리진 않았다.
Average lines + oversized-file share, demoted to a minor term (27 → 8): splitting identical code into six files used to gain +27. Kept, because it correlates only +0.16 with function length — different information.
루프 안 행마다 조회(N+1)와, 서로 안 엮인 await 를 줄 세우는 것. 같은 결함이라 한 축으로 본다. 37개 재측정에서 기존 점수와 rho=−0.18 로 독립이고 11곳(30%)에서 발동한다 — N+1 만 봤을 땐 1곳뿐이라 축이 되지 못했다. 3곳 미만은 안 센다(재시도·커서 페이지네이션은 순차가 맞다). 유예 3.0/1000파일, 캡 5.
A query per row (N+1), plus independent awaits queued one by one — the same defect, so one axis. Across 37 re-measured repos it correlates rho=−0.18 with the existing score and fires in 11 of them (30%); N+1 alone fired in one, too few to be an axis. Under three sites is ignored — retries and cursor pagination are meant to be serial. Free below 3.0 per 1,000 files, capped at 5.
행 루프 안에서 바깥 배열을 .find/.some 으로 훑는 자리 — Map/Set 이면 O(1)이다. 이 도구가 PR 을 가장 많이 만든 축이다(11건 제출·머지 1·승인 1). 등급을 안 움직이면서 고칠 줄만 주고 있었다. 테스트·프론트 구역, 정적 외곽, n 이 잘린 자리는 세지 않고 3곳 미만은 면제한다. 10k줄당으로 재고 유예 1.0, 캡 5(제출한 PR 중 판정 난 6건에서 4건이 닫혔다 — 오탐이 등급을 뒤집으면 안 된다).
Scanning an outer array with .find/.some inside a row loop, where a Map/Set would be O(1). This axis produced more PRs than any other — 11 filed, 1 merged, 1 approved — yet it never moved the grade. Test and frontend zones, static outers, and capped-n sites are excluded, and under three sites is free. Measured per 10k lines, free below 1.0, capped at 5: of six decided PRs, four were closed, so a false positive must not flip a grade.
import cycle서로를 import 해 한 덩어리가 된 모듈. 따로 읽지도 테스트하지도 교체하지도 못한다. 소스 기준이며 import type 은 세지 않는다.Modules that import each other, so none can be read, tested, or replaced alone. Type-only imports are not counted.
uncached read in loop캐시 없는 리더를 루프에서 호출 — 호출 1번이 파일을 N번 읽는다.An uncached reader called inside a loop: one call, N file reads.
render gatefetch 하나를 기다리느라 그 데이터와 무관한 UI까지 못 그린다.One pending fetch blocks UI that does not depend on it.
loop-invariant index루프 밖 값으로 매 회 new Set/Map을 다시 만든다.Rebuilds a Set/Map from invariant data each pass.
spread accumulatoracc = [...acc, x] · acc.concat(x) · Object.assign({}, acc, …) — 매 회 전체 복사 = O(n²).acc = [...acc, x], acc.concat(x), Object.assign({}, acc, …) — copies everything each pass.
regex in loop루프마다 new RegExp() 재컴파일.Recompiles new RegExp() every iteration.
unawaited asyncasync 함수를 await 없이 호출하고 결과를 버린다.Calls an async fn without await, drops the result.
ignored promiseforEach는 콜백의 프라미스를 무시한다 — 안 기다린다.forEach ignores the callback promise — it never waits.
shared-ref fillArray(n).fill([]) — 한 참조를 모든 칸이 공유.Array(n).fill([]) shares one reference everywhere.
sort no comparator숫자 배열을 비교자 없이 sort() → 사전순.Numeric sort() with no comparator sorts lexically.
stateful /g regex/g 정규식 .test()의 lastIndex 누수.A /g regex .test() leaks lastIndex between calls.
empty catchcatch {} — 에러를 조용히 삼킨다.catch {} swallows the error silently.
for-in over array인덱스가 문자열이고 상속 속성까지 돈다.Yields string keys and inherited props.
any annotations타입 주석 중 any 비율. 경계 밖에서 잦으면 타입이 사실상 꺼진 것.Share of annotations that are any — off-boundary means types are effectively off.
as any assertion타입을 강제로 지운 자리. 컴파일러가 못 지켜준다.Erases the type on purpose; the compiler can no longer help.
non-null assertion"여기 null 아님"을 사람이 장담한 자리. 틀리면 런타임 크래시.A human vouching for non-null — wrong means a runtime crash.
ts-ignore / nocheck타입 검사를 아예 끈 줄. 남으면 조용히 썩는다.Type checking switched off for that line.
dead code (knip)미사용 export·파일. 진단은 항상, 점수 반영은 --dead 옵트인(라이브러리 공개 API 오탐 때문).Unused exports and files. Always diagnosed; scored only with --dead (public APIs would false-positive).
체급으로 나눠 같은 덩치끼리 겨루게 한다.
Weight classes let each compete with its own.
감이 아니라 git 버그픽스 이력과의 상관으로 맞췄다. 측정해보니 파일 크기 > 인지 복잡도여서 — 직감과 반대라 — 그 순서로 재조정했다.
Set by correlation with real git bug-fix history. The data said file size > complexity — the opposite of our instinct — so we rebalanced.
제일 무서운 함수 하나만 고쳐도 점수가 튀던 것을 막았다. 이제 상위 10% 분포가 점수를 몬다.
Fixing the single scariest function used to swing the grade. Now the top-10% distribution drives it.
점수는 자기가 자기를 채점한다. 머지는 못 조작한다 — 낯선 저장소의 메인테이너가 눌러야 한다.
A score grades itself. A merge can't be faked — a stranger's maintainer has to press it.
여기 있는 건 fixearly 가 남의 코드에서 찾아 실제로 PR 을 낸 것들이다. 등급을 매기는 것과 고칠 줄을 짚는 것은 다르고, 후자는 남이 받아줘야 증명된다. 닫힌 것도 같이 둔다 — 실측해보니 이득이 작아 접은 건도 결과다.
These are issues fixearly found in other people's code and actually opened PRs for. Grading is one thing; naming the line to fix is another, and only someone else merging it proves the second. Closed ones stay on the list — a patch we measured and dropped is a result too.