npx 한 줄이면 내 JS·TS 코드에서 고칠 자리를 파일과 줄로 짚어준다. 유명 오픈소스 74개를 같이 재놔서 내 코드가 어디쯤인지도 나온다.
이 규칙들은 남의 저장소에 PR 로 내봤다 — 머지된 것도, 거절당한 것도 그대로 공개한다.
One npx command points at the file and line to fix in your JS/TS code. We graded 74 well-known open-source repos the same way, so you also see where yours lands.
These rules have been opened as PRs on strangers' repos — the merges and the rejections are both published as they are.
구조 지표만 잰다 — 설계·테스트·문서·보안·성능·커뮤니티, 그 소프트웨어가 준 가치는 재지 않는다. 오래 살아남아 기능을 많이 감당한 코드일수록 이 지표는 불리하다. 낮은 등급이 나쁜 프로젝트라는 뜻이 아니다.
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
규칙 후보를 오픈소스 74곳에 걸고 걸린 자리를 하나씩 손으로 연다. 쓰기만 하는 컬렉션 은 15건 전부 진짜여서 올렸고, 해제할 수 없는 등록 은 16건 중 1건만 진짜라 떨어뜨렸다.
Candidate rules run against 74 open-source repos; every hit gets opened by hand. write-only collection was 15/15 real and shipped; unremovable registration was 1/16 and got dropped.
머지된 성능 PR 909건을 긁어 형태로 묶는다. 무엇이 실제로 고쳐지는지가 거기 있다 — 그리고 기계로 잡을 수 있는 건 5%뿐이라는 것도.
We mine 909 merged performance PRs and group them by shape — what actually gets fixed, and that only 5% is mechanizable.
규칙이 짚은 자리를 PR 로 낸다. 남의 저장소 메인테이너가 받아주거나 거절한다. 점수는 자기가 자기를 채점하지만 머지는 못 조작한다.
We open PRs for what the rules flag. A stranger's maintainer merges it or turns it down. A score grades itself; a merge can't be faked.
아래는 ③의 전체 기록이다. 닫힌 것도 사유와 함께 그대로 싣는다 — 거절이 통과보다 정보가 많다. 끝난 것은 접어두기만 했으니 버튼으로 편다.
Below is the full record of ③. Closed ones stay, with the reason verbatim — a rejection carries more information than a pass. Finished ones are only folded away; the buttons unfold them.
머지된 14건은 0~20일 만에 들어갔다.
The 14 merged PRs went in within 0–20 days.
머지됨 14 · 승인 · 머지 대기 2 · 아무도 안 봄 1 · 보류 1 · 닫힘 20 — 전체 기록은 IMPACT.md. 상태는 GitHub 에서 자동 갱신된다. 기준선은 각 저장소의 최근 닫힌 PR 60건 중 외부 기여자 머지의 중앙값이다 — 평균은 묵은 몇 건이 통째로 끌고 간다(next.js 는 평균 13.6일인데 중앙값 16.1일). 초록불은 그 안, 빨간불은 초과, 보라불은 +7일을 넘긴 보류다. 옆의 수락률은 그 저장소가 최근 닫힌 외부 PR 을 얼마나 머지했는지다 — 늦은 게 우리 것만 밀린 건지, 원래 대부분 거절인 곳인지를 가른다. 보류는 GitHub 에 없는 상태다 — 닫히지도 머지되지도 않은 채 그 저장소의 평소 속도를 크게 지난 것을 우리가 따로 센다.
14 merged · 2 approved · 1 nobody has looked · 1 stalled · 20 closed — full log in IMPACT.md, refreshed from GitHub. The baseline is the median of external-contributor merges among each repository's latest 60 closed PRs — a mean gets dragged by a few stale PRs (next.js averages 13.6 days but its median is 16.1). Green is within it; red is over; violet is stalled — past it by 7 days. The percentage next to it is how many recently closed external PRs that repository merged — it separates "ours is being skipped" from "this repo turns most outsiders down". Stalled is not a GitHub state: it is neither merged nor closed, just well past how fast that repository usually moves, and we count it separately.
크기 지표가 원리상 못 보는 결함을 따로 짚는다. 점수엔 넣지 않는다 — 이건 채점이 아니라 작업 목록이다.
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). 나머지 19종은 등급을 건드리지 않고 파일·줄만 짚는다.
Caps by axis: complexity 37, function length 23, duplication 9, file size 8, sequential I/O 5, O(n²) 5 — 87 total. The other nineteen 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.