From ebfd871f6c84b166ee08495a0fdee4b9e72f1596 Mon Sep 17 00:00:00 2001 From: Minho Yoo <13810291+irontaek@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:03:33 +0900 Subject: [PATCH] feat(landing): syntax-highlight rule examples at build time Claude-Session: https://claude.ai/code/session_01W9Mk18mBNfkPfgg3hYweKq --- index.html | 54 +++++++++++++++++++----------------- tools/update-impact-cards.py | 24 ++++++++++++++-- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/index.html b/index.html index 732429b..1a5f8b7 100644 --- a/index.html +++ b/index.html @@ -329,6 +329,8 @@ .axr .axx figcaption{margin:0;font-size:11px;font-weight:700;padding:5px 10px;border-bottom:1px solid var(--line);color:var(--bug);} .axr .axx figure.ok figcaption{color:var(--gS);} .axr .axx pre{margin:0;padding:9px 10px;border:0;border-radius:0;background:none;box-shadow:none;font-family:var(--font-mono);font-size:12px;line-height:1.55;color:var(--ink);overflow-x:auto;} +.axr .axx .tk{color:#cf222e;} .axr .axx .ts,.axr .axx .tr{color:#0a3069;} .axr .axx .tc{color:#6e7781;font-style:italic;} +.axr .axx .tf{color:#8250df;} .axr .axx .tn{color:#0550ae;} @media (max-width:720px){.axr .axx{grid-template-columns:1fr;}} .dxc{border:1px solid var(--line);border-radius:var(--radius-sm);padding:14px 16px;background:var(--paper);} @@ -784,32 +786,32 @@
감점 비중은 인지 복잡도 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));
한 번에 읽어야 하는 양. 중첩 함수·주석을 뺀 자기 코드 줄 기준, 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.
채우기만 하고 아무도 읽지 않는 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));
한 번에 읽어야 하는 양. 중첩 함수·주석을 뺀 자기 코드 줄 기준, 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.
{dko}{den}
{note}{code}{chips}