npx 한 줄이면 내 JS·TS 코드에서 고칠 자리를 파일과 줄로 짚어준다.
그 규칙은 전부 남의 저장소에 PR 로 내서 검증한다. 메인테이너가 머지하거나 거절하고, 거절 사유까지 그대로 공개한다. 점수는 스스로 매기지만 머지는 꾸밀 수 없다.
One npx command points at the file and line to fix in your JS/TS code.
Every rule is verified by opening a PR on a stranger's repo. A maintainer merges it or turns it down, and the rejections are published verbatim. A score grades itself; a merge can't be faked.
쓰기만 하는 컬렉션 15O(n²) 조회 12불필요한 순차 I/O 4버린 반환값 3버려진 Promise 2전역 정규식 상태 1Write-only collection 15O(n²) lookup 12Needless sequential I/O 4Discarded pure call 3Floating promise 2Stateful /g regex 1남의 저장소에서 머지된 PR 과 남의 코드에서 고칠 규칙을 뽑고, 그 규칙으로 다른 저장소를 고쳐 PR 을 낸다. 머지·거절이 다시 규칙을 다듬는다 — 규칙 9개 중 6개가 머지로 검증 · 저장소 26곳 · 거절 26 · 대기 22. Fix rules come from PRs strangers merged and from strangers' code; we apply them to other repos as PRs, and merges and rejections sharpen them again — 6 of 9 rules verified by a merge · 26 projects · closed 26 · open 22. 전체 기록 →Full record →
머지된 성능 PR 909건을 긁어 형태로 묶는다. 무엇이 실제로 고쳐지는지가 거기 있다 — 그리고 기계로 잡을 수 있는 건 5%뿐이라는 것도.
We mine 909 merged performance PRs and group them by shape — what actually gets fixed, and that only 5% is mechanizable.
규칙 후보를 오픈소스 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 로 낸다. 남의 저장소 메인테이너가 받아주거나 거절한다. 점수는 자기가 자기를 채점하지만 머지는 못 조작한다.
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.
머지된 37건은 0~20일 만에 들어갔다.
The 37 merged PRs went in within 0–20 days.
카드의 기여자 수는 기준 GitHub 커밋 작성자 집계다. 같은 사람의 여러 계정이나 봇이 각각 포함될 수 있다.
Contributor counts use GitHub commit authors as of . Multiple accounts for one person and bots may be counted separately.
머지됨 37 · 승인 · 머지 대기 1 · 리뷰 진행 1 · 아무도 안 봄 18 · 보류 2 · 닫힘 26 — 전체 기록은 IMPACT.md. 상태는 GitHub 에서 자동 갱신된다. 기준선은 각 저장소의 최근 닫힌 PR 60건 중 외부 기여자 머지의 중앙값이다 — 평균은 묵은 몇 건이 통째로 끌고 간다(next.js 는 평균 7.8일인데 중앙값 1.1일). 초록불은 그 안, 빨간불은 초과, 보라불은 +7일을 넘긴 보류다. 옆의 수락률은 그 저장소가 최근 닫힌 외부 PR 을 얼마나 머지했는지다 — 늦은 게 우리 것만 밀린 건지, 원래 대부분 거절인 곳인지를 가른다. 보류는 GitHub 에 없는 상태다 — 닫히지도 머지되지도 않은 채 그 저장소의 평소 속도를 크게 지난 것을 우리가 따로 센다.
37 merged · 1 approved · 1 in review · 18 nobody has looked · 2 stalled · 26 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 7.8 days but its median is 1.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.
빌드 산출물·테스트·벤더 파일은 알아서 제외한다. 빈 파일로 점수를 부풀리는 것도 막는다.
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
감점 비중은 인지 복잡도 37 · 함수 길이 23 · 중복 9 · 파일 크기 8 · 순차 I/O 5 · O(n²) 5(캡 기준, 합 87). 나머지 24종은 등급을 건드리지 않고 파일·줄만 짚는다.
Caps by axis: complexity 37, function length 23, duplication 9, file size 8, sequential I/O 5, O(n²) 5 — 87 total. The other twenty-four never touch the grade; they name the file and line.
채우기만 하고 아무도 읽지 않는 Set·Map·배열. 소비하던 코드가 리팩터로 사라진 흔적이라 지워도 동작이 같고, 매번 채우는 비용만 사라진다. knip 은 .add 도 "사용"으로 봐서 못 잡는다.A Set, Map, or array that is filled but never read — what a refactor left behind. Removing it changes nothing but the cost of filling it. knip misses it because .add counts as a use.
const seen = new Set(); for (const n of nodes) { seen.add(n.id); // 아무도 안 읽음 render(n); }
for (const n of nodes) { render(n); }
행마다 다른 배열을 .find·.some·.includes 로 처음부터 훑는 자리. 한 번 Map·Set 으로 색인해두면 조회가 O(1)이 된다 — 데이터가 커질수록 차이가 제곱으로 벌어진다.Each row rescans another array with .find/.some/.includes. Index it once into a Map/Set and each lookup is O(1) — the gap grows with the square of the data.
테스트·프론트 구역, 정적 외곽, n 이 잘린 자리는 세지 않고 3곳 미만은 면제. 10k줄당 유예 1.0, 캡 5 — 판정 난 초기 PR 6건 중 4건이 닫혀서, 오탐이 등급을 뒤집지 않게 낮게 잡았다.Test/frontend zones, static outers, and capped-n sites are excluded; under three sites is free. Free below 1.0 per 10k lines, capped at 5 — four of the first six decided PRs were closed, so a false positive must not flip a grade.
for (const row of rows) { const user = users.find((u) => u.id === row.userId); }
const byId = new Map(users.map((u) => [u.id, u])); for (const row of rows) { const user = byId.get(row.userId); }
루프 한 바퀴마다 DB·네트워크를 부르는 N+1, 그리고 서로의 결과를 쓰지 않는 await 를 줄 세우는 것. IN (...) 한 번이나 Promise.all 로 묶으면 왕복이 셈으로 줄어든다.A DB or network call per loop pass (N+1), and awaits that never use each other's result queued one by one. One IN (...) or a Promise.all cuts the round trips — provable by counting.
같은 결함이라 한 축. 37곳 재측정에서 기존 점수와 rho=−0.18 로 독립이고 11곳(30%)에서 발동한다. 3곳 미만은 안 센다(재시도·커서 페이지네이션은 순차가 맞다). 유예 3.0/1000파일, 캡 5.One defect, one axis. Across 37 re-measured repos it is independent of the score (rho=−0.18) and fires in 11 (30%). 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.
const user = await getUser(id); const posts = await getPosts(id);
const [user, posts] = await Promise.all([ getUser(id), getPosts(id), ]);
s.replace(…)·arr.concat(…) 를 부르고 결과를 대입하지 않는다. 문자열은 불변이라 그 줄은 아무것도 안 한다 — 지우려던 줄이 남고, 붙이려던 말이 빠진다.Calls s.replace(…) or arr.concat(…) and drops the result. Strings are immutable, so the line does nothing — what it meant to remove stays, what it meant to append is lost.
slug.replace(/\s+/g, '-'); if (!ok) new Error('bad slug');
slug = slug.replace(/\s+/g, '-'); if (!ok) throw new Error('bad slug');
async 함수를 await 없이 부르고 결과를 버린다. 실패해도 아무도 모르고, 호출한 쪽은 끝나기 전에 다음 단계로 넘어간다.An async call with no await and nothing holding the result. Failures vanish, and the caller moves on before it finishes.
async function save(data) { writeCache(data); log('saved'); }
async function save(data) { await writeCache(data); log('saved'); }
/g 정규식을 공유한 채 루프에서 .test() 하면 lastIndex 가 다음 호출로 새어, 같은 입력에 참·거짓이 번갈아 나온다. 성능이 아니라 조용히 틀린 답이다.A shared /g regex used with .test() in a loop leaks lastIndex into the next call, so the same input flips between true and false. Not slow — silently wrong.
const TAG = /^v\d+/g; tags.filter((t) => TAG.test(t));
const TAG = /^v\d+/; tags.filter((t) => TAG.test(t));
블록 본문 find·filter·some 콜백이 값을 돌려주지 않는다. 늘 undefined 라 find 는 아무것도 못 찾고, Promise.all 은 아무것도 안 기다린다.A block-bodied find/filter/some callback that never returns. It always yields undefined, so find finds nothing and Promise.all waits for nothing.
const user = users.find((u) => { u.id === id; });
const user = users.find((u) => { return u.id === id; });
this.x === this.x·a.end || a.end 처럼 같은 식을 양쪽에 둔다. 늘 참이거나 늘 같은 값이라 비교하려던 다른 쪽이 빠진 자리다.The same expression on both sides — this.x === this.x, a.end || a.end. Always true or always the same value: the other operand it meant to compare is missing.
isEquivalent(e) { return this.xref === this.xref; }
isEquivalent(e) { return this.xref === e.xref; }
forEach(async …) 는 콜백이 돌려주는 프라미스를 버린다. 호출한 쪽은 기다리지 않고 다음 줄로 가고, 안에서 난 실패는 아무도 못 본다.forEach(async …) drops the promise each callback returns. The caller moves on without waiting, and failures inside go unseen.
ids.forEach(async (id) => { await remove(id); }); log('done');
for (const id of ids) { await remove(id); } log('done');
한 번에 읽어야 하는 양. 중첩 함수·주석을 뺀 자기 코드 줄 기준, 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.
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.
deep-equal set opuniqWith(arr, isEqual) 류 — 원시값 배열이면 Set 한 번으로 끝날 일을 깊은 비교로 이중 순회한다.uniqWith(arr, isEqual) and friends — a nested deep-equal scan where one Set pass would do for primitives.
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.
write-only collection채우기만 하고 한 번도 읽지 않는 Map·Set. 소비하던 코드가 사라진 자리다 — knip 은 못 잡는다(export 가 아니고, .add 도 참조라 "쓰였다"로 보인다).A Map/Set that is only ever written, never read — the consumer is gone. knip misses it: not an export, and .add still counts as a reference.
dead code (knip)미사용 export·파일. 진단은 항상, 점수 반영은 --dead 옵트인(라이브러리 공개 API 오탐 때문).Unused exports and files. Always diagnosed; scored only with --dead (public APIs would false-positive).
크기 지표가 원리상 못 보는 결함을 따로 짚는다. 점수엔 넣지 않는다 — 이건 채점이 아니라 작업 목록이다.
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)
구조 지표만 잰다 — 설계·테스트·문서·보안·성능·커뮤니티, 그 소프트웨어가 준 가치는 재지 않는다. 오래 살아남아 기능을 많이 감당한 코드일수록 이 지표는 불리하다. 낮은 등급이 나쁜 프로젝트라는 뜻이 아니다.
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.
3천 줄 유틸과 20만 줄 백엔드를 같은 링에 세우면 큰 게 늘 진다. 그래서 코드줄로 체급을 나눈다(파일 수로 나누면 파일을 잘게 쪼갠 쪽이 체급만 올라간다). 체급이 크기를 지운다면 종류는 성격을 지운다 — 엔진·컴파일러 11개는 중앙 64점으로 툴체인(85)·라이브러리(85)·프레임워크(80)보다 구조적으로 낮고, 앱은 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 64 — below toolchains (85), libraries (85) and frameworks (80) — 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.
규칙이 다르면 점수를 나란히 놓을 수 없다. 아래는 무엇이 바뀌어서 점수가 움직였는지이지, 저장소가 좋아졌다·나빠졌다는 뜻이 아니다. 보드를 다시 발행한 판만 싣는다 — 발행 없이 지나간 판은 아무도 그 점수를 본 적이 없다. 현재 판은 v17다.
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 v17.
체급으로 나눠 같은 덩치끼리 겨루게 한다.
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.