hanlint 0.0.1 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  "doubleNegative": "하지 않으면 안 된다, 하지 않을 수 없다 같은 이중 부정.\n\n왜: 부정을 두 번 겹쳐 긍정을 만들면 독자가 한 번 더 뒤집어 읽는다. 일본어 직역에서 온 꼴이다.\n어디서: urimal.org 김민지 2020 번역 투, 나무위키 이중 부정.\n고치기: 긍정으로 쓴다. 노력하지 않으면 안 된다 는 노력해야 한다, 잊지 않을 수 없다 는 잊는다.\n 하다 동사는 기계가 fix 로 낸다.\n안 잡는 것: 뜻을 일부러 약하게 하는 이중 부정 (나쁘지 않다). 그것은 다른 구문이다.",
10
10
  "doublePassive": "되어지다, 보여지다, 잊혀지다 처럼 피동에 어지다 를 또 붙인 이중 피동.\n\n왜: 피동이 두 번 겹치면 누가 하는지 한 번 더 흐려진다. 독자는 주어를 찾아 되돌아간다.\n어디서: 국립국어원 어문 규범. im-not-ai A-8 (오경순 2010, 김은일 2015, 서보현 2018). 피동 어간 사전은\n data/passiveStems.txt. Kiwi 태그열 실측은 PRD 부록 A.\n고치기: 하나만 남긴다. 되어진다 는 된다, 보여진다 는 보인다, 잊혀진 은 잊힌.\n안 잡는 것: 만들어진다 같은 단순 피동. 어간이 피동사가 아니면 지적하지 않는다.",
11
11
  "duplicateBlock": "앞선 코드나 출력 블록과 거의 같은 블록.\n\n왜: 서른 줄 가운데 한 줄만 다른 블록을 두 번 읽으면 독자는 다른 한 줄을 찾느라 표를 훑는다. 다른 줄만 보이거나\n 앞 블록을 가리키는 것이 낫다.\n어디서: 실측. 블로그 004 에서 두 엔진의 출력 서른 줄 가운데 다른 것은 한 줄이었고 사람 평가자가 집었다.\n 임계는 config.duplicateBlockRatio, 최소 줄 수는 넷.\n고치기: 다른 줄만 남기고 나머지는 앞 블록을 가리킨다. 정말 둘 다 필요하면 무엇이 다른지 산문에서 먼저 말한다.\n안 잡는 것: 넉 줄 미만 블록. 종류가 다른 블록 (코드와 출력). 임계 아래. notice 로만 낸다.",
12
- "ellipsis": "말줄임표.\n\n왜: 말끝을 흐리면 독자가 결론을 받아 가지 못한다. 설명글의 독자는 답을 찾으러 왔다.\n어디서: 글쓰기 스킬의 사실과 목소리.\n고치기: 문장을 끝까지 쓴다. 모르면 모른다고 쓴다.\n안 잡는 것: 코드 블록 안의 점 셋. 괄호 안의 점 셋 (인라인 코드의 생략 표기). 산문만 본다.",
12
+ "ellipsis": "말줄임표.\n\n왜: 말끝을 흐리면 독자가 결론을 받아 가지 못한다. 설명글의 독자는 답을 찾으러 왔다.\n어디서: 글쓰기 스킬의 사실과 목소리.\n고치기: 문장을 끝까지 쓴다. 모르면 모른다고 쓴다.\n안 잡는 것: 코드 블록 안의 점 셋. 괄호 안의 점 셋 (인라인 코드의 생략 표기). 영숫자에 붙은 점 셋\n(`v0.0.1...HEAD` 같은 범위 표기. CHANGELOG 의 compare URL 이 실측 사례다). 산문만 본다.",
13
13
  "emojiBullet": "이모지로 시작하는 목록 항목.\n\n왜: 불릿마다 이모지를 달면 장식이 정보를 가리고 AI 가 쓴 홍보 글처럼 읽힌다.\n어디서: Wikipedia Signs of AI writing 4.7 (Emoji as formatting), Bloomberg 2026-01 (LinkedIn AI 게시물).\n고치기: 이모지를 지우고 항목의 첫 낱말이 내용을 말하게 한다.\n안 잡는 것: 본문 문장 안의 이모지. 목록 항목의 첫 자리만 본다.",
14
14
  "endingRepeat": "같은 종결어미로 끝나는 문장이 한 절 안에 endingRun 개 이상 이어지는 자리.\n\n왜: 습니다 가 네 번 이어지면 리듬이 죽고 AI 가 쓴 글처럼 읽힌다. 짧은 문장과 긴 문장, 평서와 질문을\n 섞으라는 규칙의 기계 판이다.\n어디서: AI 문체 신호 조사 (im-not-ai E-2 동일 종결어미 4문장 이상, gist 패턴35), 경희대 글쓰기 교육의\n 것이다 반복. 임계는 config.endingRun.\n고치기: 하나를 질문으로 바꾸거나, 두 문장을 인과로 잇거나, 독자가 할 행동을 동사로 끝낸다.\n안 잡는 것: 명령형과 의문형의 연속. 종결어미를 못 정한 문장 (없음) 은 연속을 끊는다. 글쓰기 규칙의\n 위반이 아니라 리듬 신호라 notice 로 낸다. 참고 문서는 다 로 끝나는 것이 정상이다.",
15
15
  "euiChain": "한 문장에 관형격 조사 의 가 셋 이상이거나, 의 로 끝나는 어절 둘이 붙어 있다 (회사의 팀의 결정).\n\n왜: 명사를 의 로 쌓으면 무엇이 무엇의 목적어인지 표시되지 않아 독자가 조사를 머릿속에서 끼워 넣는다.\n어디서: 글쓰기 스킬의 설명을 풀어 쓰기 (의 가 이어지면 문장으로 편다). 쿠버네티스 한국어 현지화 가이드\n (일본어 の 직역).\n고치기: 동사로 편다. 회사의 팀의 결정 은 회사에서 팀이 정한 것.\n안 잡는 것: 낱말 안의 의 (의미, 의사) 와 의 로 끝나는 낱말 (정의, 회의. 목록은 data/euiNouns.txt). 떨어져 있는\n 의 둘 (글쓰기 규칙의 정본은 사용자 저장소의 스킬이다) 은 자연스러운 한국어라 잡지 않는다. 실측: 문서에서\n 둘 기준이 오탐 4건을 냈다. surface 분석기는 의 뒤에 공백이 올 때만 세고 kiwi 는 JKG 태그를 센다.",
@@ -19,7 +19,7 @@
19
19
  "hardWord": "금일, 익월, 상기, 소정의, 노하우, 인프라, 피드백 처럼 쉬운 말이 있는 어려운 한자어와 외래어.\n\n왜: 평소에 쓰지 않는 말은 독자가 머릿속에서 한 번 번역한다. 같은 뜻이면 더 자주 쓰는 말로 바로 쓴다.\n 글쓰기 스킬의 쉬운 말로 바로 쓰기 다.\n어디서: 국립국어원 다듬은 말 (korean.go.kr 개선 > 다듬은 말) 과 표준국어대사전의 동의어 정보. 사전은\n data/easyWords.toml 이고 항목마다 출처가 있다. 사용자는 설정의 dictionary.easyWords 로 더한다.\n고치기: 항목마다 쉬운 말을 fix 로 제안한다. 문맥에 따라 다른 말이 맞을 수 있어 hanlint fix 는 적용하지\n 않고 사람이 고른다.\n안 잡는 것: 사전에 없는 말. 고유명사와 제품 이름. 백틱과 따옴표 안의 인용. 전문 용어가 정확해서 꼭\n 필요한 자리 (그때는 이 규칙을 끄거나 인라인 제어로 그 문단만 끈다). 제안이라 notice 로만 낸다.",
20
20
  "headingSentence": "본문 문장을 그대로 복사한 절 제목.\n\n왜: 절 제목은 간단명료하게 쓴다. 합니다, 하세요 로 끝나는 제목은 목차에서 대상이 안 보이고 본문과\n 같은 문장이 두 번 읽힌다.\n어디서: 운영자 규칙 (섹션 타이틀은 간단명료하게). 글쓰기 스킬의 절 제목 규칙.\n고치기: 대상이나 질문을 짧게 쓴다. 파일을 엽니다 는 파일 열기, 왜 파일이 열리지 않을까.\n안 잡는 것: 물음표로 끝나는 질문 제목. 명사로 끝나는 제목.",
21
21
  "headingSkip": "제목 수준을 건너뛴 자리. H2 다음에 H4 가 오거나 글이 H3 으로 시작하는 것.\n\n왜: 목차가 계층을 잃는다. 화면 낭독기와 목차 생성기가 건너뛴 수준에서 길을 잃고, 독자는 어느 절\n 아래인지 모른다.\n어디서: markdownlint 계열의 헤딩 레벨 검사. Wikipedia Signs of AI writing 4.10 (헤더 남용).\n고치기: 한 수준씩 내려간다. frontmatter 의 title 이 H1 이므로 본문은 H2 부터 시작한다.\n안 잡는 것: 올라가는 것 (H4 다음 H2). 절이 끝나고 새 절이 시작하는 정상 흐름이다.",
22
- "headingUniform": "H2 셋 이상 가운데 headingUniformRatio 넘게 같은 글자로 끝나는 목차.\n\n왜: 목차 전체가 기 로 끝나면 순서가 있는 과정이 아니라 같은 항목의 나열로 읽힌다.\n어디서: 실측. 블로그 004 의 H2 여덟 개 중 일곱 개가 기 로 끝났고 목차가 한 박자로만 읽혔다. 글쓰기\n 스킬의 절 제목 규칙 (예시의 어미를 규칙으로 삼지 않는다). 임계는 config.headingUniformRatio.\n고치기: 형태를 섞는다. 오류 원인 찾기, 왜 파일이 열리지 않을까, pandas 가 멈추는 자리.\n안 잡는 것: H2 가 둘 이하인 글. H3 이하.",
22
+ "headingUniform": "H2 셋 이상 가운데 headingUniformRatio 넘게 같은 글자로 끝나는 목차.\n\n왜: 목차 전체가 기 로 끝나면 순서가 있는 과정이 아니라 같은 항목의 나열로 읽힌다.\n어디서: 실측. 블로그 004 의 H2 여덟 개 중 일곱 개가 기 로 끝났고 목차가 한 박자로만 읽혔다. 글쓰기\n 스킬의 절 제목 규칙 (예시의 어미를 규칙으로 삼지 않는다). 임계는 config.headingUniformRatio.\n고치기: 형태를 섞는다. 오류 원인 찾기, 왜 파일이 열리지 않을까, pandas 가 멈추는 자리.\n안 잡는 것: H2 가 둘 이하인 글. H3 이하. 숫자로 끝나는 제목 (`[0.0.2] - 2026-08-27` 같은 CHANGELOG\n 절이 실측 사례다. 버전과 날짜는 어미가 아니다).",
23
23
  "imperativePeriod": "명령형과 청유형 뒤의 마침표.\n\n왜: 마침표는 평서형 종결에만 쓴다. 세요, 십시오, 합시다, 해라 는 마침표 없이 끝낸다. 한 글 안에서\n 기준이 갈리면 독자가 부호를 읽느라 멈춘다.\n어디서: 글쓰기 스킬의 사실과 목소리. eddmpython 의 발행 게이트가 같은 규칙을 정규식으로 막는다.\n고치기: 마침표를 지운다. 기계가 fix 로 낸다.\n안 잡는 것: 평서형 뒤의 마침표. 따옴표 안의 명령문은 문장 부호 규칙이 달라 보지 않는다.",
24
24
  "inputFileSource": "코드가 읽는 파일과 쓰는 폴더가 글 어디에서도 만들어지지 않은 자리.\n\n왜: 위에서 아래로 따라 하는 독자는 그 줄에서 파일 없음 오류로 멈춘다. 글쓴이 컴퓨터에는 있어서 글쓴이는 모른다.\n어디서: 실측. 블로그 004 에서 read_excel(\"sales_small.xlsx\") 의 파일을 만드는 코드가 없었고 data 폴더가 어디에서도\n 만들어지지 않았다. 사람 평가자가 두 라운드에 걸쳐 집었다. 읽기와 쓰기 함수 목록은 이 파일 위의 정규식이다.\n고치기: 파일을 만드는 코드 블록을 앞에 두거나, 산문에서 그 파일을 어떻게 준비하는지 이름을 불러 말한다.\n안 잡는 것: 앞선 코드가 쓴 파일. 앞선 산문이 이름을 부른 파일 (독자가 준비하는 것으로 본다). 별표와 URL 과\n 서식 자리표시자가 든 경로. 폴더는 error 가 아니라 notice 다.",
25
25
  "installImport": "설치 줄에 없는 패키지를 import 하는 코드. 그리고 코드 한 줄이 조용히 요구하는 숨은 의존성.\n\n왜: 독자는 글이 시킨 설치 줄만 실행한다. 목록에 없는 패키지는 ModuleNotFoundError 로 끝나고 extra 가 빠진\n 백엔드는 실행 중에 멈춘다.\n어디서: 실측. 블로그 004 가 ibis-framework[duckdb] 만 설치하고 sqlite 엔진에 붙었고 사람 평가자가 집었다.\n 모듈과 패키지 이름의 대응은 data/pythonPackages.txt, 표준 라이브러리는 data/pythonStdlib.txt, 숨은\n 의존성은 data/hiddenDeps.txt.\n고치기: 설치 줄에 그 패키지 (필요하면 extra 까지) 를 더한다.\n안 잡는 것: 설치 줄이 하나도 없는 글 (판단할 수 없다). 표준 라이브러리. 글이 만드는 .py 파일. 상대 import.\n 숨은 의존성은 notice 로 낸다.",
@@ -76,8 +76,8 @@
76
76
  "source": "egowriting 번역투 고치기"
77
77
  },
78
78
  {
79
- "pattern": "(?!서로의|따로의|별로의)[가-힣](?:로의|에의) ",
80
- "why": "전치사구를 명사화한 번역투다. `풍요로의 길` 은 `풍요에 이르는 길` 이다",
79
+ "pattern": "[가-힣](?:으로의|에의) ",
80
+ "why": "전치사구를 명사화한 번역투다. `성공으로의 길` 은 `성공에 이르는 길` 이다",
81
81
  "source": "국립국어원 새국어생활 1990"
82
82
  },
83
83
  {
package/data/version.json CHANGED
@@ -1 +1 @@
1
- {"version": "0.0.1"}
1
+ {"version": "0.0.7"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hanlint",
3
- "version": "0.0.1",
3
+ "version": "0.0.7",
4
4
  "description": "한국어 글에서 AI 와 사람이 반복해서 어기는 결함을 결정적으로 잡는 린터. 의존성 0",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +3,8 @@ import { SENTENCE, finding } from "../finding.js";
3
3
 
4
4
  export const name = "ellipsis";
5
5
  // 괄호 안의 점 셋 (`OVER (...)`) 은 인라인 코드의 생략 표기라 뺀다.
6
- const ELLIPSIS = /(?<![(\[])(…|\.{3,})(?![)\]])/;
6
+ // 영숫자에 붙은 점 셋 (`v0.0.1...HEAD`, compare URL) 은 범위 표기라 말줄임표가 아니다.
7
+ const ELLIPSIS = /(?<![(\[A-Za-z0-9])(…|\.{3,})(?![)\]A-Za-z0-9])/;
7
8
 
8
9
  /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
9
10
  export function run(doc) {
@@ -9,12 +9,18 @@ export const name = "headingUniform";
9
9
  */
10
10
  export function run(doc, config) {
11
11
  const headings = doc.headings.filter(([level]) => level === 2);
12
- if (headings.length < 3) return [];
12
+ // 버전이나 날짜처럼 숫자로 끝나는 제목은 어미가 아니라 판정에서 뺀다.
13
+ const eligible = headings.filter(([, text]) => {
14
+ if (!text.trim()) return false;
15
+ const trimmed = text.trimEnd();
16
+ const char = trimmed[trimmed.length - 1];
17
+ return !(char >= "0" && char <= "9");
18
+ });
19
+ if (eligible.length < 3) return [];
13
20
  // 파이썬 Counter.most_common 은 같은 수면 먼저 들어온 것이 이긴다. 삽입 순서를 지킨다.
14
21
  /** @type {Map<string, number>} */
15
22
  const lastChars = new Map();
16
- for (const [, text] of headings) {
17
- if (!text.trim()) continue;
23
+ for (const [, text] of eligible) {
18
24
  const trimmed = text.trimEnd();
19
25
  const char = trimmed[trimmed.length - 1];
20
26
  lastChars.set(char, (lastChars.get(char) ?? 0) + 1);
@@ -27,9 +33,9 @@ export function run(doc, config) {
27
33
  count = n;
28
34
  }
29
35
  }
30
- if (count / headings.length >= config.headingUniformRatio) {
36
+ if (count / eligible.length >= config.headingUniformRatio) {
31
37
  return [
32
- finding(name, headings[0][2], headings.map(([, text]) => text).join(" / "), `H2 ${headings.length}개 중 ${count}개가 \`${best}\` 로 끝난다. 목차가 한 어미로 끝나면 과정이 아니라 나열로 읽힌다. 형태를 섞는다`, null, "error", DOCUMENT, -1),
38
+ finding(name, eligible[0][2], eligible.map(([, text]) => text).join(" / "), `H2 ${eligible.length}개 중 ${count}개가 \`${best}\` 로 끝난다. 목차가 한 어미로 끝나면 과정이 아니라 나열로 읽힌다. 형태를 섞는다`, null, "error", DOCUMENT, -1),
33
39
  ];
34
40
  }
35
41
  return [];