hanlint 0.0.8 → 0.0.9
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.
- package/README.md +9 -4
- package/data/exemplars.json +2 -26
- package/data/holeKinds.json +1 -12
- package/data/patterns.json +0 -11
- package/data/platformApis.txt +3 -1
- package/data/ruleCategories.json +0 -4
- package/data/ruleDocs.json +4 -8
- package/data/ruleMechanisms.json +0 -4
- package/data/spelling.json +1 -1
- package/data/version.json +1 -1
- package/koglType1.LICENSE.md +25 -0
- package/package.json +6 -2
- package/src/cli/main.js +6 -7
- package/src/config/settings.js +16 -12
- package/src/fingerprint/build.js +10 -7
- package/src/rules/code/installImport.js +2 -1
- package/src/rules/document/keywordHeading.js +7 -2
- package/src/rules/document/tableOddCell.js +2 -1
- package/src/rules/registry.js +0 -8
- package/src/rules/sentence/danglingDeixis.js +15 -0
- package/src/rules/sentence/negationRedefine.js +2 -1
- package/src/rules/structure/headingUniform.js +3 -3
- package/src/rules/structure/introLong.js +3 -1
- package/data/coverageTypes.txt +0 -7
- package/src/rules/paragraph/topicBreak.js +0 -26
- package/src/rules/sentence/ellipsis.js +0 -32
- package/src/rules/structure/headingQuestion.js +0 -19
- package/src/rules/structure/sectionResult.js +0 -38
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# hanlint
|
|
2
2
|
|
|
3
|
-
한국어 글에서 반복되는 결함을 결정적으로 잡는 린터다. 번역투, 상투어,
|
|
4
|
-
|
|
3
|
+
한국어 글에서 반복되는 결함을 결정적으로 잡는 린터다. 번역투, 상투어, 이중 피동, 명사 나열, 조각난
|
|
4
|
+
문단과 코드 튜토리얼의 계약 위반을 집는다. 맞춤법 전체를 추측하지 않고 앞뒤 낱말로 확정되는 일부 표기만
|
|
5
|
+
본다. 런타임 의존성이 없고 Node 18 이상이면 된다.
|
|
5
6
|
|
|
6
7
|
```powershell
|
|
7
8
|
npx hanlint
|
|
@@ -40,6 +41,10 @@ for (const finding of lintFile("글.md")) console.log(finding.line, finding.rule
|
|
|
40
41
|
|
|
41
42
|
파이썬 패키지 (`pip install hanlint`) 와 같은 규칙, 같은 fixture, 같은 출력이다. 지문 지도 (`audit`, `map`),
|
|
42
43
|
문체 프로파일, 초안 비교 (`diff`), 평가자 겹침 (`coverage`) 은 파이썬 쪽에만 있다. 무엇을 잡고 무엇을 잡지 않는지는
|
|
43
|
-
[
|
|
44
|
+
[전체 사용 안내](https://github.com/eddmpython/hanlint#readme)와
|
|
45
|
+
[제품 경계](https://github.com/eddmpython/hanlint/blob/main/skills/specs/start/product.md)에서 확인할 수 있다.
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
## 라이선스
|
|
48
|
+
|
|
49
|
+
코드와 나머지 데이터는 [MIT](LICENSE)다. `data/easyWords.json`은 국립국어원 자료의 파생물이며
|
|
50
|
+
[공공누리 제1유형 고지](koglType1.LICENSE.md)가 적용된다.
|
package/data/exemplars.json
CHANGED
|
@@ -78,12 +78,6 @@
|
|
|
78
78
|
"after": "```text\n한 줄\n두 줄\n세 줄\n네 줄\n다섯 줄\n```\n\n두 번째 실행은 셋째 줄만 다르고 나머지는 위와 같습니다.\n",
|
|
79
79
|
"moved": "거의 같은 블록을 다시 싣지 않고 달라진 줄만 말했다"
|
|
80
80
|
},
|
|
81
|
-
{
|
|
82
|
-
"rule": "ellipsis",
|
|
83
|
-
"before": "속도는 조금 느려집니다...",
|
|
84
|
-
"after": "속도는 조금 느려집니다.",
|
|
85
|
-
"moved": "말끝을 흐리지 않고 같은 사실로 문장을 끝냈다"
|
|
86
|
-
},
|
|
87
81
|
{
|
|
88
82
|
"rule": "emojiBullet",
|
|
89
83
|
"before": "- ✅ 파일을 만듭니다\n",
|
|
@@ -138,12 +132,6 @@
|
|
|
138
132
|
"after": "오늘 안에 정해진 양식을 냅니다.",
|
|
139
133
|
"moved": "관청 말을 평소에 쓰는 말로 바꿨다"
|
|
140
134
|
},
|
|
141
|
-
{
|
|
142
|
-
"rule": "headingQuestion",
|
|
143
|
-
"before": "## 파일은 어디에 생길까요?\n\n첫 문단.\n\n## 왜 안 열릴까요?\n\n둘째 문단.\n\n## 크기는 어떻게 바꿀까요?\n\n셋째 문단.\n",
|
|
144
|
-
"after": "## 파일이 생기는 자리\n\n첫 문단.\n\n## 왜 안 열릴까요?\n\n둘째 문단.\n\n## 인쇄용 크기로 다시 저장\n\n셋째 문단.\n",
|
|
145
|
-
"moved": "질문 제목을 하나만 남기고 나머지는 행동과 결과로 바꿨다"
|
|
146
|
-
},
|
|
147
135
|
{
|
|
148
136
|
"rule": "headingSentence",
|
|
149
137
|
"before": "## 표가 제대로 들어왔는지 확인합니다\n\n열 다섯 개가 모두 보이면 된 것입니다.\n",
|
|
@@ -188,8 +176,8 @@
|
|
|
188
176
|
},
|
|
189
177
|
{
|
|
190
178
|
"rule": "introLong",
|
|
191
|
-
"before": "
|
|
192
|
-
"after": "
|
|
179
|
+
"before": "무엇을 먼저 볼까요? 여기서 시작합니다. 그래서 준비합니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n## 첫 절\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n## 둘째 절\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n",
|
|
180
|
+
"after": "무엇을 먼저 볼까요? 여기서 시작합니다. 그래서 준비합니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n## 첫 절\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n## 둘째 절\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n표를 엽니다. 값이 보입니다. 그래서 다음으로 넘어갑니다.\n\n",
|
|
193
181
|
"moved": "미룬 배경 설명을 걷고 도입을 네 문단 안으로 줄였다"
|
|
194
182
|
},
|
|
195
183
|
{
|
|
@@ -307,12 +295,6 @@
|
|
|
307
295
|
"after": "도입입니다.\n\n## 절\n\n이 코드는 줄 수를 출력합니다.\n\n```python\nprint(1)\n```\n\n## 몇 줄이 나올까?\n\n끝.\n",
|
|
308
296
|
"moved": "코드가 하는 일을 한 문장으로 먼저 말했다"
|
|
309
297
|
},
|
|
310
|
-
{
|
|
311
|
-
"rule": "sectionResult",
|
|
312
|
-
"before": "도입.\n\n## 말투 찾기\n\n첫 문단입니다. 이유가 붙습니다. 그래서 이어집니다.\n\n둘째 문단입니다. 이유가 붙습니다. 그래서 이어집니다.\n\n셋째 문단입니다. 이유가 붙습니다. 그래서 이어집니다.\n\n넷째 문단입니다. 이유가 붙습니다. 그래서 이어집니다.\n\n## 결과는 어디에 남나?\n\n끝.\n",
|
|
313
|
-
"after": "도입.\n\n## 말투 찾기\n\n첫 문단입니다. 이유가 붙습니다. 그래서 이어집니다.\n\n둘째 문단입니다. 이유가 붙습니다. 그래서 이어집니다.\n\n셋째 문단입니다. 이유가 붙습니다. 그래서 이어집니다.\n\npython make_qr.py 를 돌리면 폴더에 link.png 가 생깁니다.\n\n## 결과는 어디에 남나?\n\n끝.\n",
|
|
314
|
-
"moved": "절 끝에 독자가 자기 화면에서 확인할 결과를 하나 남겼다"
|
|
315
|
-
},
|
|
316
298
|
{
|
|
317
299
|
"rule": "spacing",
|
|
318
300
|
"before": "할수 있다고 생각한지 두번째입니다.",
|
|
@@ -331,12 +313,6 @@
|
|
|
331
313
|
"after": "| 이름 | 시간 |\n|---|---|\n| pandas | 3.72초 |\n| Polars | 0.18초 |\n| DuckDB | 0.13초 |\n| PyArrow | 0.02초 |\n| Dask | 2.12초 |\n",
|
|
332
314
|
"moved": "열의 잣대를 하나로 맞췄다"
|
|
333
315
|
},
|
|
334
|
-
{
|
|
335
|
-
"rule": "topicBreak",
|
|
336
|
-
"before": "## 절\n\n메모리는 절반으로 줄어듭니다. 자리를 덜 쓰면 그만큼 빨라집니다.\n\n이제 정렬을 봅니다. 순서를 바꾸면 결과가 달라집니다.\n",
|
|
337
|
-
"after": "## 절\n\n메모리는 절반으로 줄어듭니다. 자리를 덜 쓰면 그만큼 빨라집니다.\n\n메모리를 아낀 그 표를 이번에는 정렬해 봅니다. 순서를 바꾸면 결과가 달라집니다.\n",
|
|
338
|
-
"moved": "새 문단 첫 문장에서 앞 문단이 만든 것을 이름으로 다시 불렀다"
|
|
339
|
-
},
|
|
340
316
|
{
|
|
341
317
|
"rule": "translationese",
|
|
342
318
|
"before": "모든 분야에 있어서 기준이 필요합니다.",
|
package/data/holeKinds.json
CHANGED
|
@@ -12,16 +12,6 @@
|
|
|
12
12
|
"profile"
|
|
13
13
|
]
|
|
14
14
|
},
|
|
15
|
-
{
|
|
16
|
-
"id": "flow",
|
|
17
|
-
"name": "흐름",
|
|
18
|
-
"symbol": "F",
|
|
19
|
-
"ansi": 214,
|
|
20
|
-
"hex": "#E69F00",
|
|
21
|
-
"rules": [
|
|
22
|
-
"topicBreak"
|
|
23
|
-
]
|
|
24
|
-
},
|
|
25
15
|
{
|
|
26
16
|
"id": "deixis",
|
|
27
17
|
"name": "지시",
|
|
@@ -136,8 +126,7 @@
|
|
|
136
126
|
"doubleNegative",
|
|
137
127
|
"negationRedefine",
|
|
138
128
|
"dash",
|
|
139
|
-
"imperativePeriod"
|
|
140
|
-
"ellipsis"
|
|
129
|
+
"imperativePeriod"
|
|
141
130
|
]
|
|
142
131
|
}
|
|
143
132
|
]
|
package/data/patterns.json
CHANGED
|
@@ -88,17 +88,6 @@
|
|
|
88
88
|
],
|
|
89
89
|
"source": "글쓰기 스킬의 도입은 결핍부터. 이태준 문장강화의 첫머리"
|
|
90
90
|
},
|
|
91
|
-
{
|
|
92
|
-
"name": "수치 비교",
|
|
93
|
-
"form": "{A}는 {수치}, {B}는 {수치}입니다",
|
|
94
|
-
"when": "둘을 견줄 때. 형용사로 말하지 않고 잰 값을 나란히 놓는다",
|
|
95
|
-
"example": "같은 질문에 pandas 는 3.7초, Polars 는 0.18초가 걸립니다.",
|
|
96
|
-
"instead": "속도는 조금 느려집니다...",
|
|
97
|
-
"avoids": [
|
|
98
|
-
"ellipsis"
|
|
99
|
-
],
|
|
100
|
-
"source": "글쓰기 스킬의 수치와 인용은 직접 확인한 값만 쓴다"
|
|
101
|
-
},
|
|
102
91
|
{
|
|
103
92
|
"name": "미룬 것 회수",
|
|
104
93
|
"form": "앞에서 미룬 {이름}을 이제 봅니다",
|
package/data/platformApis.txt
CHANGED
|
@@ -6,7 +6,9 @@ os\.startfile\( 윈도우 subprocess.run(["open", 경로]) 나 xdg-open
|
|
|
6
6
|
\bmsvcrt\b 윈도우 termios 나 다른 입력 방법
|
|
7
7
|
\bwin32(?:api|con|gui|com)\b 윈도우 pywin32 는 윈도우 전용이다
|
|
8
8
|
\bosascript\b 맥 다른 운영체제에는 AppleScript 가 없다
|
|
9
|
-
/proc/
|
|
9
|
+
# /proc/ 는 뺐다. 코드에 박히는 API 호출이 아니라 셸 명령의 경로이고, 컨테이너 안에서는
|
|
10
|
+
# 리눅스가 기본 실행 환경이다. 실측: 기준 말뭉치의 지적 여덟 건이 전부 이 한 줄이었고 전부
|
|
11
|
+
# 쿠버네티스 안내서의 kubectl exec 와 컨테이너 안 셸 기록이라 독자가 멈추지 않는다 (2026-08-31).
|
|
10
12
|
\bfcntl\b 리눅스와 맥 윈도우에는 fcntl 이 없다
|
|
11
13
|
resource\.getrusage\( 리눅스와 맥 윈도우에는 resource 모듈이 없다
|
|
12
14
|
[A-Za-z]:\\\\ 윈도우 절대 경로 대신 상대 경로나 pathlib
|
package/data/ruleCategories.json
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"doublePassive": "sentence",
|
|
13
13
|
"draftHistory": "sentence",
|
|
14
14
|
"duplicateBlock": "code",
|
|
15
|
-
"ellipsis": "sentence",
|
|
16
15
|
"emojiBullet": "structure",
|
|
17
16
|
"endingRepeat": "sentence",
|
|
18
17
|
"enoughOnce": "document",
|
|
@@ -22,7 +21,6 @@
|
|
|
22
21
|
"fillerOpener": "sentence",
|
|
23
22
|
"firstResultDistance": "code",
|
|
24
23
|
"hardWord": "sentence",
|
|
25
|
-
"headingQuestion": "structure",
|
|
26
24
|
"headingSentence": "structure",
|
|
27
25
|
"headingSkip": "structure",
|
|
28
26
|
"headingUniform": "structure",
|
|
@@ -47,10 +45,8 @@
|
|
|
47
45
|
"promiseRecall": "document",
|
|
48
46
|
"redundantPair": "sentence",
|
|
49
47
|
"sectionNoProse": "structure",
|
|
50
|
-
"sectionResult": "structure",
|
|
51
48
|
"spacing": "orthography",
|
|
52
49
|
"spelling": "orthography",
|
|
53
50
|
"tableOddCell": "document",
|
|
54
|
-
"topicBreak": "paragraph",
|
|
55
51
|
"translationese": "sentence"
|
|
56
52
|
}
|
package/data/ruleDocs.json
CHANGED
|
@@ -5,14 +5,13 @@
|
|
|
5
5
|
"confusable": "학생으로써, 정답을 맞추다, 나와 틀리다, 감기가 낳다 처럼 뜻이 다른 말을 바꿔 쓴 자리.\n\n왜: 로서/로써, 맞히다/맞추다, 다르다/틀리다, 낫다/낳다 는 소리가 같거나 비슷해 뜻이 바뀐 줄 모르고 쓴다.\n 독자는 뜻으로 읽다가 걸린다.\n어디서: 국립국어원 한글 맞춤법 제57항 (구별하여 적는 말), 온라인가나다 298340, 표준국어대사전 뜻풀이.\n 앞뒤 낱말로 뜻이 확정되는 자리만 사전에 뒀다 (data/confusable.toml).\n고치기: 항목마다 바른 말을 fix 로 낸다. 활용이 바뀌는 것 (틀려→달라, 낳은→나은) 은 활용까지 바꾼다.\n안 잡는 것: 앞뒤 낱말로 확정되지 않는 자리. 이로써, 그것과 다르다 처럼 이미 맞는 것. 백틱과 따옴표 안의 인용.",
|
|
6
6
|
"connectorRepeat": "같은 접속부사로 시작하는 문장이 한 문단 안에 연달아 오는 자리.\n\n왜: 그리고, 그래서 를 연달아 붙이면 문장 사이의 관계가 전부 같은 것으로 읽히고 리듬이 죽는다.\n어디서: 글쓰기 스킬의 사실과 목소리 (같은 접속사를 연달아 쓰지 않는다). 이태준 문장강화, 유시민\n 글쓰기 특강. 접속부사 목록은 data/connectors.txt. 셈은 반복 기제 (rules/shared/repeat.py) 의 runsOf 다.\n고치기: 앞 문장에서 만든 파일이나 값의 이름을 다시 불러 잇는다. 접속사는 그렇게 이어지지 않을 때만.\n안 잡는 것: 문단이 바뀌면 다시 센다. 다른 접속사가 이어지는 것은 잡지 않는다.",
|
|
7
7
|
"countMismatch": "같은 단위로 서로 다른 수를 약속하는 글. 여섯 가지를 소개한다고 열고 다섯 가지라고 닫는 것.\n\n왜: 독자는 어느 쪽을 믿어야 할지 모른다. 약속과 이행이 어긋난 글은 나머지도 의심받는다.\n어디서: 실측. 블로그 004 가 도입에서 여섯 방식을 약속하고 결말에서 다섯 가지라고 적었고 사람 평가자가\n 네 라운드 내내 집었다. 이런 검사를 하는 선행 도구가 없다 (조사 결과). 단위 목록은 data/countUnits.txt.\n고치기: 수를 맞추거나, 서로 다른 것을 세는 문장이면 무엇을 세는지 이름을 붙인다.\n안 잡는 것: 단위가 다른 수 (여섯 가지 와 다섯 열). 같은 수의 반복. 본문 절 안의 수 (둘째 절의 두 가지 와\n 넷째 절의 네 가지 는 다른 목록이다. 실측: 002. 같은 절 안의 두 수도 백과와 수필에서 11/20 이 오탐이었다).\n 도입의 약속과 마지막 절의 결산만 견주고, 절이 없는 글은 전체가 config.countMismatchSpan 줄 안일 때만 견준다.\n `3단계` 처럼 숫자가 붙은 단계는 서수라 세지 않는다.",
|
|
8
|
-
"danglingDeixis": "지시어가 있는데 바로 앞 문장과 화제어가 하나도 겹치지 않는 자리.\n\n왜: 이것 이 가리킬 것이 앞 문장에 없다. 독자는 되돌아가도 못 찾는다. 지시어 가운데 가장 나쁜 꼴이다.\n어디서: 지문 계층이 처음 가능하게 한 규칙이다. 독자가 손에 든 화제어 (독자 상태 fingerprint/readerState.py 의\n recent) 와의 자카드 중첩이 0 이면 짚는다. 블로그 004 실측에서 사람 평가자가 네 라운드 내내 지시어를 집었다.\n고치기: 가리키는 대상의 이름을 쓴다. 앞 문장에 그 이름이 없다면 앞 문장에 먼저 세운다.\n안 잡는 것: 앞 문장과 화제어가 겹치는 지시어는 deixis 가 짚는다. 글의 첫 문장은 앞이 없으므로 여기서\n 잡는다.",
|
|
8
|
+
"danglingDeixis": "지시어가 있는데 바로 앞 문장과 화제어가 하나도 겹치지 않는 자리.\n\n왜: 이것 이 가리킬 것이 앞 문장에 없다. 독자는 되돌아가도 못 찾는다. 지시어 가운데 가장 나쁜 꼴이다.\n어디서: 지문 계층이 처음 가능하게 한 규칙이다. 독자가 손에 든 화제어 (독자 상태 fingerprint/readerState.py 의\n recent) 와의 자카드 중첩이 0 이면 짚는다. 블로그 004 실측에서 사람 평가자가 네 라운드 내내 지시어를 집었다.\n고치기: 가리키는 대상의 이름을 쓴다. 앞 문장에 그 이름이 없다면 앞 문장에 먼저 세운다.\n안 잡는 것: 앞 문장과 화제어가 겹치는 지시어는 deixis 가 짚는다. 글의 첫 문장은 앞이 없으므로 여기서\n 잡는다. 앞 문장이 물음이나 감탄이면 그 문장이 선행어라 낱말이 안 겹쳐도 잡지 않는다 (isPrompt).",
|
|
9
9
|
"dash": "em 대시 (U+2014) 와 en 대시 (U+2013). 코드 블록까지 글 전체를 본다.\n\n왜: AI 가 매끄럽게 고친다면서 새로 넣는 대표적인 부호다. 한국어 문장 부호가 아니라 독자가 멈춘다.\n GPT-4.1 이 인간 대비 3.28배 쓴다는 조사가 있다.\n어디서: 글쓰기 스킬의 사실과 목소리. Wikipedia Signs of AI writing 4.6. Freeburg 조사.\n고치기: 부연은 마침표로 끊거나 괄호로, 범위는 물결표로 쓴다. 2020~2024.\n안 잡는 것: 하이픈 (-) 과 물결표. 코드 안의 대시도 잡고, 설정 (ignoreFences) 이 지문에서 뺀 펜스 안도 잡는다.\n 렌더러가 지우는 펜스라도 파일에 있는 글자다. 코드가 대시를 정말 필요로 하면 그 파일에서 이 규칙을 끈다.",
|
|
10
10
|
"deixis": "이것, 해당 값, 위의 코드, 이러한 같은 지시어. 앞 문장에 가리킬 것이 있는 경우.\n\n왜: 독자가 스크롤을 되돌려 무엇을 가리키는지 찾아야 한다. 되돌리는 자리는 전부 이탈 지점이다.\n어디서: 글쓰기 스킬의 설명을 풀어 쓰기. 목록은 data/deixis.txt. 독자가 손에 든 화제어는 독자 상태\n (fingerprint/readerState.py) 의 recent 다.\n고치기: 가리키는 파일, 값, 코드의 이름을 쓴다. 이것으로 파일이 만들어집니다 는 save 로 link.png 가\n 만들어집니다 로.\n안 잡는 것: 앞 문장과 화제어가 하나도 겹치지 않는 지시어. 그것은 danglingDeixis 가 따로 짚는다.\n 앞서 말한 X 처럼 이름을 다시 부르는 회수 표지.",
|
|
11
11
|
"doubleNegative": "하지 않으면 안 된다, 하지 않을 수 없다 같은 이중 부정.\n\n왜: 부정을 두 번 겹쳐 긍정을 만들면 독자가 한 번 더 뒤집어 읽는다. 일본어 직역에서 온 꼴이다.\n어디서: urimal.org 김민지 2020 번역 투, 나무위키 이중 부정.\n고치기: 긍정으로 쓴다. 노력하지 않으면 안 된다 는 노력해야 한다, 잊지 않을 수 없다 는 잊는다.\n 하다 동사는 기계가 fix 로 낸다.\n안 잡는 것: 뜻을 일부러 약하게 하는 이중 부정 (나쁘지 않다). 그것은 다른 구문이다.",
|
|
12
12
|
"doublePassive": "되어지다, 보여지다, 잊혀지다 처럼 피동에 어지다 를 또 붙인 이중 피동.\n\n왜: 피동이 두 번 겹치면 누가 하는지 한 번 더 흐려진다. 독자는 주어를 찾아 되돌아간다.\n어디서: 국립국어원 어문 규범. im-not-ai A-8 (오경순 2010, 김은일 2015, 서보현 2018). 피동 어간 사전은\n data/passiveStems.txt.\n고치기: 하나만 남긴다. 되어진다 는 된다, 보여진다 는 보인다, 잊혀진 은 잊힌. 활용 후보가 하나이고\n 바뀌는 조각이 인용 밖에 한 번만 있을 때는 검토를 마친 확정 치환으로 고친다.\n안 잡는 것: 만들어진다 같은 단순 피동. 어간이 피동사가 아니면 지적하지 않는다. 따옴표와 인라인 코드\n 안은 글쓴이의 사용이 아니라 인용이므로 지적하지 않는다.",
|
|
13
13
|
"draftHistory": "글쓴이의 수정 이력과 자기 검증 기록. 처음에는 ~라고 썼습니다, ~것을 확인했습니다.\n\n왜: 독자는 초고를 본 적이 없다. 무엇을 어떻게 고쳤는지는 독자가 지금 하려는 일과 아무 관계가 없고,\n 글쓴이가 직접 확인했다는 보고도 독자가 자기 화면에서 할 일을 대신해 주지 못한다. 알아낸 사실만\n 결과 문장으로 남기고 알아낸 과정은 뺀다.\n어디서: cinch 의 전역 blog-writing 스킬, 무엇을 빼는가의 반드시 빼는 것 첫 항목. 실측 사례는 블로그 004 의\n `ibis-framework[duckdb] 만 설치한 환경에서 아래 코드가 그대로 도는 것을 확인했습니다` 로, 평가자가\n 3라운드에서 글쓴이의 자기 검증 기록으로 읽힌다고 집었다. 표지 목록은 data/draftHistory.txt.\n고치기: 알아낸 사실만 남긴다. `확인했습니다` 를 지우고 그 사실을 바로 쓴다. 독자가 할 일이면 `확인해\n 보세요` 로 시킨다. 과정을 남기고 싶으면 회고 글로 따로 뺀다.\n안 잡는 것: 수치의 출처를 밝히는 문장 (`일곱 번씩 재서 가운데 값을 적었습니다`). 사실 확인은 글쓰기\n 규칙이 요구하는 것이라 재는 방법은 남긴다. 인용 안의 같은 표현.",
|
|
14
14
|
"duplicateBlock": "앞선 코드나 출력 블록과 거의 같은 블록.\n\n왜: 서른 줄 가운데 한 줄만 다른 블록을 두 번 읽으면 독자는 다른 한 줄을 찾느라 표를 훑는다. 다른 줄만 보이거나\n 앞 블록을 가리키는 것이 낫다.\n어디서: 실측. 블로그 004 에서 두 엔진의 출력 서른 줄 가운데 다른 것은 한 줄이었고 사람 평가자가 집었다.\n 임계는 config.duplicateBlockRatio, 최소 줄 수는 넷.\n고치기: 다른 줄만 남기고 나머지는 앞 블록을 가리킨다. 정말 둘 다 필요하면 무엇이 다른지 산문에서 먼저 말한다.\n안 잡는 것: 넉 줄 미만 블록. 종류가 다른 블록 (코드와 출력). 임계 아래. notice 로만 낸다.",
|
|
15
|
-
"ellipsis": "말줄임표.\n\n왜: 말끝을 흐리면 독자가 결론을 받아 가지 못한다. 설명글의 독자는 답을 찾으러 왔다.\n어디서: 글쓰기 스킬의 사실과 목소리.\n고치기: 문장을 끝까지 쓴다. 모르면 모른다고 쓴다.\n안 잡는 것: 코드 블록 안의 점 셋. 인라인 코드와 따옴표 안의 점 셋. 인용 구간은 사용이 아니라 표기이고\n거기서 점 셋은 생략할 인자를 뜻한다 (실측: 이슈 2번의 ``make_qr(\"...\")``. 그전에는 여는 괄호와\n대괄호만 예외로 두어 그 사이에 큰따옴표가 끼면 예외가 풀렸다). 괄호 안 명령 예시에서 옵션 뒤의 생략\n인자를 뜻하는 기호 (`kubectl get -o yaml …`). 영숫자에 붙은 점 셋 (`v0.0.1...HEAD` 같은 범위 표기.\nCHANGELOG 의 compare URL 이 실측 사례다). 산문만 본다.",
|
|
16
15
|
"emojiBullet": "이모지로 시작하는 목록 항목.\n\n왜: 불릿마다 이모지를 달면 장식이 정보를 가리고 AI 가 쓴 홍보 글처럼 읽힌다.\n어디서: Wikipedia Signs of AI writing 4.7 (Emoji as formatting), Bloomberg 2026-01 (LinkedIn AI 게시물).\n고치기: 이모지를 지우고 항목의 첫 낱말이 내용을 말하게 한다.\n안 잡는 것: 본문 문장 안의 이모지. 목록 항목의 첫 자리만 본다.",
|
|
17
16
|
"endingRepeat": "같은 종결어미가 endingRun 개 이어지는데 그 사이에 인과도 질문도 독자를 부르는 말도 없는 자리.\n\n왜: 습니다 가 네 번 이어지고 그 사이에 이유를 잇는 말도 독자에게 거는 말도 없으면 리듬이 멈추고 사실이\n 나란히 놓이기만 한다. 서너 문장에 한 번은 앞뒤 인과를 잇는 문장이 있어야 읽힌다.\n어디서: AI 문체 신호 조사 (im-not-ai E-2 동일 종결어미 4문장 이상, gist 패턴35), 경희대 글쓰기 교육의\n 것이다 반복. cinch 의 전역 blog-writing 스킬, 설명을 풀어 쓰기의 짧은 문장만 늘어놓지 않는다. 임계는\n config.endingRun.\n고치기: 하나를 질문으로 바꾸거나, 두 문장을 인과로 잇거나, 독자가 할 행동을 동사로 끝낸다.\n안 잡는 것: 인과 표지 (그래서, 때문에) 나 독자 호출이 든 문장. 그 문장이 연속을 끊는다. 어미가 같아도\n 이유로 이어져 있으면 리듬이 산 것이다. 실측: 이 조건을 넣기 전 발행본 다섯 편에서 56건이 나왔고\n 그중 대부분이 합니다체 자체를 센 것이었다. 명령형과 의문형의 연속. 종결어미를 못 정한 문장 (없음).\n 글쓰기 규칙의 위반이 아니라 리듬 신호라 notice 로 낸다.",
|
|
18
17
|
"enoughOnce": "여기까지면 충분합니다 로 끊는 문장이 한 글에 두 번 이상 나오는 것.\n\n왜: 기본 사용법이 끝나는 지점을 한 번 끊어 주면 독자는 여기서 멈춰도 된다는 것을 안다. 그 문장이\n 절마다 나오면 계속 끝나는 척하다 이어지는 꼴이 되고, 어디가 진짜 끝인지 알 수 없어 신호가 죽는다.\n어디서: cinch 의 전역 blog-writing 스킬, 절은 앞 절이 못 한 일에서 시작한다. `여기까지면 만들어 쓰는 데\n 충분합니다. 아래부터는 ~할 때 봅니다.` 글 전체에 한 번만 쓴다. 절마다 넣으면 계속 끝나는 척하다\n 이어지는 꼴이 된다. 표지 목록은 data/enoughMarkers.txt.\n고치기: 기본 사용법이 진짜로 끝나는 자리 하나만 남기고 나머지는 지운다. 지운 자리는 앞 절이 만든 것을\n 이름으로 부르고 다음 행동을 붙이는 문장으로 잇는다.\n안 잡는 것: 한 번만 쓴 글. 인용 안의 같은 문장은 지문이 이미 인용으로 걸러 둔다.",
|
|
@@ -22,17 +21,16 @@
|
|
|
22
21
|
"fillerOpener": "다음으로, 이어서, 마지막에는 으로만 잇는 문장.\n\n왜: 앞에서 무엇을 만들었는지 부르지 않고 순서 표지만 붙이면 독자는 무엇을 이어받는지 모른 채 다음\n 행동을 읽는다.\n어디서: 글쓰기 스킬의 쉬운 말로 바로 쓰기.\n고치기: 앞 절에서 만든 파일이나 확인한 값을 이름으로 다시 부르고 다음 행동을 붙인다. link.png 는\n 33픽셀입니다. 명함에 넣기에는 너무 작습니다.\n안 잡는 것: 문장 중간의 이어서. 목록 항목.",
|
|
23
22
|
"firstResultDistance": "첫 코드나 표나 그림이 나오기 전의 산문 문단이 firstResultMaxParagraphs 를 넘는 글.\n\n왜: 따라 하러 온 독자는 실행할 것이 나올 때까지 배경을 읽는다. 문단 넷이 넘으면 이탈한다.\n어디서: 실측. 블로그 004 의 셋째 문단까지 숫자가 하나도 없어 사람 평가자가 집었다. 글쓰기 스킬의 도입은\n 결핍부터 (첫 코드 블록이 도입 안에 온다). 임계는 config.firstResultMaxParagraphs.\n고치기: 첫 실행할 것을 앞으로 당기거나 그 앞의 문단을 뒤로 보낸다.\n안 잡는 것: 코드도 표도 그림도 없는 글 (참고 문서). introLong 이 도입만 보는 것과 달리 절 경계를 넘어 센다.\n notice 로만 낸다.",
|
|
24
23
|
"hardWord": "금일, 익월, 상기, 소정의, 노하우, 인프라, 피드백 처럼 쉬운 말이 있는 어려운 한자어와 외래어.\n\n왜: 평소에 쓰지 않는 말은 독자가 머릿속에서 한 번 번역한다. 같은 뜻이면 더 자주 쓰는 말로 바로 쓴다.\n 글쓰기 스킬의 쉬운 말로 바로 쓰기 다.\n어디서: 국립국어원 다듬은 말 (korean.go.kr 개선 > 다듬은 말) 과 표준국어대사전의 동의어 정보. 사전은\n data/easyWords.toml 이고 항목마다 출처가 있다. 사용자는 설정의 dictionary.easyWords 로 더한다.\n고치기: 항목마다 쉬운 말을 fix 로 제안한다. 문맥에 따라 다른 말이 맞을 수 있어 hanlint fix 는 적용하지\n 않고 사람이 고른다.\n안 잡는 것: 사전에 없는 말. 고유명사와 제품 이름. 백틱과 따옴표 안의 인용. 전문 용어가 정확해서 꼭\n 필요한 자리 (그때는 이 규칙을 끄거나 인라인 제어로 그 문단만 끈다). 제안이라 notice 로만 낸다.",
|
|
25
|
-
"headingQuestion": "H2 셋 이상 가운데 headingQuestionRatio 넘게 물음표로 끝나는 목차.\n\n왜: 절 제목을 전부 질문으로 도배하면 순서가 있는 과정이 아니라 FAQ 로 보인다. 독자는 목차를 훑어\n 출발점에서 결과까지 가는 길을 찾는데, 질문만 늘어서면 어느 것을 먼저 읽어야 하는지 알 수 없다.\n어디서: cinch 의 전역 blog-writing 스킬, 사실과 목소리. 절 제목을 전부 질문으로 도배하면 순서가 있는\n 과정이 아니라 FAQ 로 보인다. headingUniform 의 반대쪽이다. 임계는 config.headingQuestionRatio.\n고치기: 질문 제목을 절반 아래로 줄이고 나머지는 행동이나 결과로 바꾼다. 오류 원인 찾기, pandas 가\n 멈추는 자리처럼 형태를 섞는다.\n안 잡는 것: H2 가 둘 이하인 글. 정말 FAQ 문서라면 이 규칙을 끈다. 본문 문장의 물음표 수는 세지 않는다\n (그것은 noQuestion 이 반대쪽에서 본다).",
|
|
26
24
|
"headingSentence": "본문 문장을 그대로 복사한 절 제목.\n\n왜: 절 제목은 간단명료하게 쓴다. 합니다, 하세요 로 끝나는 제목은 목차에서 대상이 안 보이고 본문과\n 같은 문장이 두 번 읽힌다.\n어디서: 운영자 규칙 (섹션 타이틀은 간단명료하게). 글쓰기 스킬의 절 제목 규칙.\n고치기: 대상이나 질문을 짧게 쓴다. 파일을 엽니다 는 파일 열기, 왜 파일이 열리지 않을까.\n안 잡는 것: 물음표로 끝나는 질문 제목. 명사로 끝나는 제목. config.headingSentenceMaxLevel 보다 깊은 제목.\n 절 제목 아래에 문장형 부제를 두는 형식 (강의 교안의 H3 부제는 계약상 행동과 결과를 말하는 문장이다) 은\n 그 값을 2 로 두어 H2 까지만 본다. 실측: eddmpython-course 여섯 편에서 부제 62개가 전부 걸렸다.",
|
|
27
25
|
"headingSkip": "제목 수준을 건너뛴 자리. H2 다음에 H4 가 오거나 글이 H3 으로 시작하는 것.\n\n왜: 목차가 계층을 잃는다. 화면 낭독기와 목차 생성기가 건너뛴 수준에서 길을 잃고, 독자는 어느 절\n 아래인지 모른다.\n어디서: markdownlint 계열의 헤딩 레벨 검사. Wikipedia Signs of AI writing 4.10 (헤더 남용).\n고치기: 한 수준씩 내려간다. frontmatter 의 title 이 H1 이므로 본문은 H2 부터 시작한다.\n안 잡는 것: 올라가는 것 (H4 다음 H2). 절이 끝나고 새 절이 시작하는 정상 흐름이다.",
|
|
28
|
-
"headingUniform": "H2 셋 이상 가운데 headingUniformRatio 넘게 같은 글자로 끝나는 목차.\n\n왜: 목차 전체가 기 로 끝나면 순서가 있는 과정이 아니라 같은 항목의 나열로 읽힌다.\n어디서: 실측. 블로그 004 의 H2 여덟 개 중 일곱 개가 기 로 끝났고 목차가 한 박자로만 읽혔다. 글쓰기\n 스킬의 절 제목 규칙 (예시의 어미를 규칙으로 삼지 않는다). 임계는 config.headingUniformRatio. 셈은 반복\n 기제 (rules/shared/repeat.py) 의 shareOf 다.\n고치기: 형태를 섞는다. 오류 원인 찾기, 왜 파일이 열리지 않을까, pandas 가 멈추는 자리. 제목을 바꾸기\n 전에 그 제목을 참조하는 자리가 있는지 본다. 실측: eddmpython 은 media.json 이 절 제목으로 이미지를\n 묶고 있어 제목만 고치자 게이트가 깨졌다.\n안 잡는 것: H2 가 둘 이하인 글. H3 이하.
|
|
26
|
+
"headingUniform": "H2 셋 이상 가운데 headingUniformRatio 넘게 같은 글자로 끝나는 목차.\n\n왜: 목차 전체가 기 로 끝나면 순서가 있는 과정이 아니라 같은 항목의 나열로 읽힌다.\n어디서: 실측. 블로그 004 의 H2 여덟 개 중 일곱 개가 기 로 끝났고 목차가 한 박자로만 읽혔다. 글쓰기\n 스킬의 절 제목 규칙 (예시의 어미를 규칙으로 삼지 않는다). 임계는 config.headingUniformRatio. 셈은 반복\n 기제 (rules/shared/repeat.py) 의 shareOf 다.\n고치기: 형태를 섞는다. 오류 원인 찾기, 왜 파일이 열리지 않을까, pandas 가 멈추는 자리. 제목을 바꾸기\n 전에 그 제목을 참조하는 자리가 있는지 본다. 실측: eddmpython 은 media.json 이 절 제목으로 이미지를\n 묶고 있어 제목만 고치자 게이트가 깨졌다.\n안 잡는 것: H2 가 둘 이하인 글. H3 이하. **한글 음절로 끝나지 않는 제목.** 어미는 한글이다. 버전과\n 날짜 (`[0.0.2] - 2026-08-27` 같은 CHANGELOG 절), 한자 독음 괄호로 닫은 수필 제목\n (`約婚[약혼]까지의 來歷[내력]` 은 `]` 로 끝난다), 라틴 약어로 닫은 제목 (`소스 IP`) 이 여기 든다.\n 실측: 셋 다 기준 말뭉치에서 오탐이었다 (2026-08-31).\n왜 notice 인가: 정탐과 오탐의 목차 표면이 같다. `구현하기 / 사용하기 / 같이 보기` (정탐) 와\n `생성하기 / 적용하기 / 검증하기 / 정리하기` (쿠버네티스 태스크 문서의 관례) 를 표층이 못 가른다.\n 그 목차가 순서 있는 과정인지 관례인지는 사람이 본다. 표본 20건 가운데 정탐이 하나였다.",
|
|
29
27
|
"imperativePeriod": "명령형과 청유형 뒤의 마침표.\n\n왜: 마침표는 평서형 종결에만 쓴다. 세요, 십시오, 합시다, 해라 는 마침표 없이 끝낸다. 한 글 안에서\n 기준이 갈리면 독자가 부호를 읽느라 멈춘다.\n어디서: 글쓰기 스킬의 사실과 목소리. eddmpython 의 발행 게이트가 같은 규칙을 정규식으로 막는다.\n고치기: 마침표를 지운다. 기계가 fix 로 낸다.\n안 잡는 것: 평서형 뒤의 마침표. 따옴표 안의 명령문은 문장 부호 규칙이 달라 보지 않는다.",
|
|
30
28
|
"inputFileSource": "코드가 읽는 파일과 쓰는 폴더가 글 어디에서도 만들어지지 않은 자리.\n\n왜: 위에서 아래로 따라 하는 독자는 그 줄에서 파일 없음 오류로 멈춘다. 글쓴이 컴퓨터에는 있어서 글쓴이는 모른다.\n어디서: 실측. 블로그 004 에서 read_excel(\"sales_small.xlsx\") 의 파일을 만드는 코드가 없었고 data 폴더가 어디에서도\n 만들어지지 않았다. 사람 평가자가 두 라운드에 걸쳐 집었다. 읽기와 쓰기 함수 목록은 fingerprint/codeMarkers.py 의\n 정규식이고, 앞선 코드가 만든 파일은 독자 상태 (fingerprint/readerState.py) 의 files 다.\n고치기: 파일을 만드는 코드 블록을 앞에 두거나, 산문에서 그 파일을 어떻게 준비하는지 이름을 불러 말한다.\n안 잡는 것: 앞선 코드가 쓴 파일. 앞선 산문이 이름을 부른 파일 (독자가 준비하는 것으로 본다). 별표와 URL 과\n 서식 자리표시자가 든 경로. 폴더는 error 가 아니라 notice 다.",
|
|
31
29
|
"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 로 낸다.",
|
|
32
30
|
"introImage": "도입에 그림이 introMaxImages 보다 많은 글.\n\n왜: 도입은 독자를 첫 결과로 데려가는 구간이다. 그림이 쌓이면 스크롤이 길어지고, 실행할 수 있는 것이\n 첫 화면 밖으로 밀려난다. 도입에서 밀린 독자는 본문을 안 읽는다.\n어디서: cinch 의 전역 blog-writing 스킬, 도입은 결핍부터. 도입은 문단 넷과 이미지 한 장을 넘지 않는다.\n introLong 이 문단만 세므로 그림은 이 규칙이 센다. 임계는 config.introMaxImages.\n고치기: 넘치는 그림을 그것이 설명하는 본문 절로 내린다. 대표 그림 한 장만 도입에 둔다.\n안 잡는 것: H2 가 없는 글 (전체가 도입이라 셀 수 없다). 본문 절의 그림 수는 세지 않는다.",
|
|
33
|
-
"introLong": "첫 H2 앞의 산문 문단이 introMaxParagraphs 를 넘는 도입.\n\n왜: 도입은 문단 넷과 이미지 한 장을 넘지 않는다. 첫 코드 블록이나 실행할 수 있는 것이 그 안에 와야\n 한다. 결핍 문단이 길어지면 그것 자체가 미룬 배경 설명이 된다.\n어디서: 글쓰기 스킬의 도입은 결핍부터. 임계는 config.introMaxParagraphs.\n고치기: 넘치는 문단을 첫 절로 내리거나 지운다. 정의 문단이면 지운다. 검색해 들어온 독자는 그것이\n 무엇인지 이미 안다.\n안 잡는 것: H2 가 없는
|
|
31
|
+
"introLong": "첫 H2 앞의 산문 문단이 introMaxParagraphs 를 넘는 도입.\n\n왜: 도입은 문단 넷과 이미지 한 장을 넘지 않는다. 첫 코드 블록이나 실행할 수 있는 것이 그 안에 와야\n 한다. 결핍 문단이 길어지면 그것 자체가 미룬 배경 설명이 된다.\n어디서: 글쓰기 스킬의 도입은 결핍부터. 임계는 config.introMaxParagraphs.\n고치기: 넘치는 문단을 첫 절로 내리거나 지운다. 정의 문단이면 지운다. 검색해 들어온 독자는 그것이\n 무엇인지 이미 안다.\n안 잡는 것: H2 가 없는 글 (전체가 도입이라 셀 수 없다). **본문 절이 하나뿐인 글과 도입이 문단의\n 절반을 넘는 글.** 둘 다 절 구조가 없는 글이지 도입이 긴 글이 아니다. 실측: 위키뉴스 단신은 본문이\n 문단 나열이고 끝에 `## 출처` 하나가 붙어 H2 가 하나 있다는 이유로 기사 전체가 도입으로 세어졌다.\n 제목 구조가 없는 수필은 도입 문단이 78개였다. 표본 20건의 오탐 11건이 전부 이 꼴이었다 (2026-08-31).",
|
|
34
32
|
"japaneseLoan": "구좌, 익일, 잔고, 고참, 망년회 같은 일본어투 낱말.\n\n왜: 순화어가 이미 자리 잡아 일본어투가 낡고 딱딱하게 읽힌다.\n어디서: 국립국어원 일본어 투 용어 순화 자료집 2005, 꼭 가려 써야 할 일본어 투 용어 50개 2019.\n 사전은 data/japaneseLoan.toml.\n고치기: 순화어로 바꾼다. 구좌 는 계좌, 익일 은 다음날.\n안 잡는 것: 앞뒤에 한글이 붙은 자리. 익일 은 잡지만 익일이라는 은 다른 낱말일 수 있어 두지 않고\n 경계를 본다. 굳어진 한자어 (견적 등) 는 사전에 넣지 않았다.",
|
|
35
|
-
"keywordHeading": "제목이 약속한 대표 검색어의 말이 절 제목 어디에도 없는 글.\n\n왜: 검색해 들어온 독자는 본문을 읽기 전에 목차를 훑는다. 제목에서 본 말이 목차에 한 번도 없으면 이\n 글이 자기 질문을 다루는지 확신하지 못한 채 뒤로 간다. 제목은 한 번 약속하고 목차는 그 약속이\n 어디에서 지켜지는지 보이는 자리다.\n어디서: 실측. eddmpython 003 을 평가자 다섯 역할이 네 라운드 읽었고 2라운드의 검색 독자가 집었다.\n `절 제목만 위에서 아래로 훑으면 목차 전체에 파이썬 이라는 말이 한 번도 안 나온다. 나는 브라우저\n 안에서 만드는 도구가 아니라 파이썬 코드를 찾아 들어온 사람이다.` 필드 이름은 config.keywordField.\n고치기: 그 말이 실제로 하는 일을 맡은 절의 제목에 넣는다. 003 이면 `파이썬으로 QR코드 파일 만들기`.\n 억지로 모든 제목에 넣지 않는다. 한 번이면 된다.\n안 잡는 것: keywordField 를 설정하지 않은 글. 검색어가
|
|
33
|
+
"keywordHeading": "제목이 약속한 대표 검색어의 말이 절 제목 어디에도 없는 글.\n\n왜: 검색해 들어온 독자는 본문을 읽기 전에 목차를 훑는다. 제목에서 본 말이 목차에 한 번도 없으면 이\n 글이 자기 질문을 다루는지 확신하지 못한 채 뒤로 간다. 제목은 한 번 약속하고 목차는 그 약속이\n 어디에서 지켜지는지 보이는 자리다.\n어디서: 실측. eddmpython 003 을 평가자 다섯 역할이 네 라운드 읽었고 2라운드의 검색 독자가 집었다.\n `절 제목만 위에서 아래로 훑으면 목차 전체에 파이썬 이라는 말이 한 번도 안 나온다. 나는 브라우저\n 안에서 만드는 도구가 아니라 파이썬 코드를 찾아 들어온 사람이다.` 필드 이름은 config.keywordField.\n고치기: 그 말이 실제로 하는 일을 맡은 절의 제목에 넣는다. 003 이면 `파이썬으로 QR코드 파일 만들기`.\n 억지로 모든 제목에 넣지 않는다. 한 번이면 된다.\n안 잡는 것: keywordField 를 설정하지 않은 글. 검색어가 제목에도 첫 문단에도 없는 글 (그것은\n keywordMissing 이 먼저 짚는다). H2 가 셋 미만인 글. 한 글자 낱말. 뜻이 같은 다른 말로 목차를 쓴\n 자리는 갈라내지 못하므로 notice 로만 낸다 (실측: 001 의 `시작` 은 목차에 없지만 `프로젝트 폴더\n 열기` 가 그 일을 한다).\n왜 제목만 보지 않나: 제목에만 있는지 보면 **제목엔 없고 첫 문단에만 있는 검색어**가 두 규칙 사이로\n 빠진다. keywordMissing 은 제목과 첫 문단을 함께 보므로 통과시키고, 여기서는 제목에 없다고\n 건너뛴다. keywordMissing 과 같은 자리 (제목 + 첫 문단) 를 본다 (2026-08-31).",
|
|
36
34
|
"keywordMissing": "대표 검색어가 제목에도 첫 문단에도 없는 글.\n\n왜: 검색해서 들어온 독자는 자기가 검색창에 친 이름을 첫 화면에서 찾는다. 그 이름이 안 보이면 본문을\n 읽기 전에 뒤로 간다.\n어디서: 글쓰기 스킬의 검색 독자 역할 (독자가 검색창에 친 이름과 이 글이 쓰는 이름이 다르면 첫 문단에서\n 처리한다). 필드 이름은 config.keywordField.\n고치기: 제목이나 첫 문단에 그 이름을 그대로 쓴다. 이름이 다르면 첫 문단에서 그 차이를 한 문장으로.\n안 잡는 것: keywordField 를 설정하지 않은 글. 그 필드가 비어 있는 글.",
|
|
37
35
|
"loneSubheading": "H2 아래 소제목이 하나뿐이고 그 제목이 절 제목의 말을 되풀이하는 자리.\n\n왜: 소제목은 절 안을 나누려고 둔다. 하나뿐이면 나눈 것이 없고, 그 하나가 절 제목과 같은 말이면\n 독자는 같은 제목을 두 번 읽는다. 목차만 길어지고 얻는 것이 없다.\n어디서: cinch 의 전역 blog-writing 스킬, 블로그 글의 뼈대 마지막 문장. 한 절 아래에 소제목이 하나뿐이고\n 같은 뜻을 되풀이하면 소제목을 지운다.\n고치기: 소제목을 지우고 본문을 절 제목 아래로 올린다. 정말 둘로 나뉘면 소제목을 하나 더 만든다.\n안 잡는 것: 소제목이 둘 이상인 절. H4 이하. 절 제목에 없는 말이 소제목에 하나라도 있으면 새 이야기를 여는\n 것이므로 하나여도 제 몫을 한다. 실측: 002 의 `AI에게 원문과 이 페이지 주소 보내기` 아래 `AI에게 보낼\n 요청 예시` 는 `AI` 하나만 겹치고 `요청` 과 `예시` 를 새로 꺼내므로 오탐이었다. 화제어는 근사라\n notice 로만 낸다.",
|
|
38
36
|
"longSentence": "어절 수가 longSentenceMax 를 넘는 문장.\n\n왜: 문장이 길면 독자가 주어와 서술어를 잇느라 되돌아간다. 짧은 문장과 긴 문장을 섞으라는 규칙의 상한이다.\n어디서: 실측. 블로그 다섯 편의 최장 문장이 23, 33, 23, 26, 45 어절이었고 30 을 넘는 둘은 읽어 보니 한 문장에\n 목록 넷을 넣은 것이었다. 임계는 config.longSentenceMax.\n고치기: 마침표로 끊는다. 나열이면 목록으로 꺼낸다.\n안 잡는 것: 임계 아래 문장. 코드와 표. 글쓰기 규칙의 위반이 아니라 신호라 notice 로 낸다.",
|
|
@@ -47,10 +45,8 @@
|
|
|
47
45
|
"promiseRecall": "뒤에서 다루겠습니다 라고 미뤄 놓고 앞에서 미룬 것을 부르는 표지가 글 어디에도 없는 것.\n\n왜: 뒤에서 다루겠습니다 를 써 놓고 끝까지 안 나오면 정보 배분이 아니라 누락이다. 회수할 때는 미뤘다는\n 사실을 밝히고 앞에서 쓴 이름을 다시 부른다.\n어디서: 글쓰기 스킬의 무엇을 빼는가 (미룬 것은 같은 글 안에서 회수한다). 이런 검사를 하는 선행 도구가\n 없다 (조사 결과). 표지 목록은 data/promiseMarkers.txt 와 recallMarkers.txt. 약속과 회수는 독자 상태\n (fingerprint/readerState.py) 가 문장 순서로 쌓고, 글을 다 읽은 독자 (final) 에게 묻는다.\n고치기: 뒤 절에서 앞에서 미룬 X 로 부르며 다룬다. 회수하지 못할 것은 애초에 미루지 말고 지운다.\n안 잡는 것: 회수 표지가 하나라도 있는 글. 어느 약속을 어느 회수가 갚았는지는 대조하지 않는다.\n 표지 근사라 notice 로만 낸다.",
|
|
48
46
|
"redundantPair": "역전 앞, 그때 당시, 매 시간마다 같은 겹말.\n\n왜: 뜻이 같은 두 말을 겹쳐 쓴 것이라 한 쪽이 군더더기다.\n어디서: 국립국어원 새국어생활 2005 한규희 겹말은 가능한 한 줄이자, 법조신문 넘쳐나는 겹말들.\n 사전은 data/redundantPair.toml.\n고치기: 하나만 남긴다. 역전 앞 은 역 앞, 그때 당시 는 그때.\n안 잡는 것: 뜻을 강조하려고 일부러 겹친 말. 사전에 없는 겹말.",
|
|
49
47
|
"sectionNoProse": "설명글 없이 코드, 표, 이미지만 있는 절.\n\n왜: 절은 제목, 시각 자료, 설명글, 그리고 설명을 보조하는 예시로 짠다. 코드와 표만 쌓인 절은 독자가\n 무엇을 보는지 모른다.\n어디서: 운영자 규칙 (섹션은 타이틀, 서브타이틀, 시각, 설명글, 예시). 글쓰기 스킬의 절의 짜임.\n고치기: 코드가 하는 일을 한 문장으로 먼저 말하고 그다음에 이름을 하나씩 푼다.\n안 잡는 것: 목록만 있는 절 (더 해 볼 것). 설명이 목록 안에 있다. 빈 절.",
|
|
50
|
-
"sectionResult": "산문만 있고 눈으로 볼 결과 (코드, 표, 그림, 끼워 넣은 영상이나 실행 칸, 파일 이름, 출력 문장) 가 없는 본문 절.\n\n왜: 따라 하는 독자는 한 절을 다 읽으면 자기 화면에서 확인할 것 하나를 기대한다. 확인할 것이 없는 절은\n 과정이 아니라 참고 자료다. 글쓰기 스킬의 한 절을 다 읽으면 결과 하나가 남는다.\n어디서: 글쓰기 스킬의 절은 앞 절이 못 한 일에서 시작한다. sectionShapeMinParagraphs 문단을 넘는 절만 본다\n (설치, 계정 만들기 같은 짧은 구간은 뺀다).\n고치기: 그 절에서 독자가 만들거나 확인할 것을 하나 넣는다. 파일 하나, 화면 한 줄이면 된다.\n안 잡는 것: 도입과 마지막 절 (결말은 확인으로 닫는다). 짧은 절. 코드나 표나 그림이 있는 절. 링크 하나뿐인\n 문단이나 주소만 있는 문단 (영상 카드, 실행 칸) 이 있는 절 (실측: eddmpython-course 01 의 한글 문서 절은 영상이\n 셋인데 결과 없는 절로 잡혔다). 파일 이름이나 출력을 말하는 문장이 있는 절. 참고 문서라 결과가 원래 없으면 이\n 규칙을 끈다. notice 로만 낸다.",
|
|
51
48
|
"spacing": "할수 있다, 것같다, 떠난지 3년, 한번도, 두번, 수 밖에, 뿐만아니라 처럼 자주 틀리는 띄어쓰기.\n\n왜: 의존 명사와 단위 명사는 띄고 조사는 붙인다는 규칙이 가장 자주 무너지는 자리다. 틀리면 독자가\n 낱말 경계를 다시 세운다.\n어디서: 국립국어원 한글 맞춤법 제5장 띄어쓰기 (제41항 조사, 제42항 의존 명사, 제43항 단위 명사).\n 항목마다 근거가 붙어 있다 (data/spacing.toml). 관형형 ㄹ 뒤의 의존 명사는 받침 부류 {ㄹ} 로 잡는다.\n고치기: 항목마다 띄거나 붙인 꼴을 fix 로 낸다. hanlint fix 가 적용한다.\n안 잡는 것: 뿐, 만큼, 대로 (체언 뒤 조사와 관형형 뒤 의존 명사를 표층으로 가르지 못한다). 못하다/못 하다,\n 안되다/안 되다 (뜻이 갈린다). 실수, 술수 처럼 ㄹ 받침 명사 뒤의 수. 백틱과 따옴표 안의 인용.",
|
|
52
49
|
"spelling": "됬, 돼고, 않 해, 왠일, 몇일, 금새, 오랫만, 어떻해, 할께, 바램 처럼 자주 틀리는 표기.\n\n왜: 틀린 표기는 독자가 먼저 알아채고 그 순간 글보다 글쓴이를 본다. 사람 작가가 가장 자주 지적받는\n 자리이고 전부 결정적으로 잡힌다.\n어디서: 국립국어원 한글 맞춤법 (제27항 붙임 2 며칠, 제35항 붙임 2 되어→돼, 제53항 -ㄹ게, 제56항 -든지),\n 온라인가나다 답변, 표준국어대사전 표제어. 항목마다 근거가 붙어 있다 (data/spelling.toml).\n고치기: 항목마다 고친 표기를 fix 로 낸다. hanlint fix 가 적용한다.\n안 잡는 것: 뜻에 따라 둘 다 맞는 자리 (데/대, 이따가/있다가, 못하다/못 하다, 한번/한 번, 안되다/안 되다).\n 백틱과 따옴표 안의 인용. 사전에 없는 표기.",
|
|
53
50
|
"tableOddCell": "한 열이 전부 같은 단위로 잰 값인데 한 칸만 다른 것을 담은 표.\n\n왜: 독자는 표의 한 열을 같은 잣대로 훑는다. 그 가운데 한 칸만 다른 것을 재고 있으면 훑던 눈이 그 자리에서\n 멈추고, 그 칸이 다른 뜻이라는 것을 본문에서 다시 찾아야 한다.\n어디서: 실측. 블로그 004 의 여섯 라이브러리 비교표에서 시간 열 아홉 칸 중 여덟 칸이 초인데 한 칸이\n `직접 부르지 않음` 이었고, 메모리 열도 여덟 칸이 MB 인데 한 칸이 `넘길 때 453MB 에서 700MB` 였다.\n 평가자가 표를 훑다가 열의 뜻이 바뀌는 자리에서 멈춘다고 집었다. 임계는 config.tableOddCellMinRows.\n고치기: 그 칸을 같은 잣대로 다시 재거나, 그 줄을 표에서 빼서 본문 문장으로 옮긴다. 표에 남겨야 하면 열을\n 나눠 다른 잣대라는 것을 열 제목으로 보인다.\n안 잡는 것: 줄이 tableOddCellMinRows 보다 적은 표. 첫 열 (이름 열이라 재는 값이 아니다). 빈 칸과 `-`.\n 딴 칸이 둘 이상인 열 (그 열은 원래 자유 서술이다). 근사라 notice 로만 낸다.",
|
|
54
|
-
"topicBreak": "같은 절 안에서 앞 문단과 화제어가 하나도 겹치지 않는 문단.\n\n왜: 앞 문단과의 관계를 새 문단 첫 문장에 드러내라는 규칙의 기계 판이다. 화제어가 전혀 안 겹치면 독자는\n 어디로 넘어왔는지 모른다.\n어디서: 글쓰기 스킬의 사실과 목소리. TextTiling (Hearst 1997) 의 인접 블록 어휘 중첩을 임베딩 없이\n 자카드로 잰다. 지문 계층이 가능하게 한 규칙이다. 최소 문장 수는 config.topicBreakMinSentences.\n고치기: 앞 문단에서 만든 파일이나 값의 이름을 새 문단 첫 문장에서 다시 부른다.\n안 잡는 것: 절이 바뀌는 자리 (절 경계는 화제가 바뀌는 것이 정상). 한 문장짜리 문단 (중첩이 원래 작다).\n 화제어 근사가 거칠어 notice 로만 낸다.",
|
|
55
51
|
"translationese": "~에 있어서, ~에 의해, ~을 통해, ~로부터, ~에도 불구하고 같은 번역투.\n\n왜: 영어와 일본어 구문을 그대로 옮긴 꼴이라 한국어로 읽을 때 한 박자 늦다. 독자가 머릿속에서\n 다시 번역한다.\n어디서: 국립국어원 새국어생활 1990 과 2012, urimal.org 김민지 2020, egowriting 번역투 고치기,\n kubernetes.io 한국어 현지화 가이드. 사전은 data/translationese.toml.\n고치기: 항목마다 고친 예가 있다. 에 있어서 는 에서, 로부터 는 에게, 음에도 불구하고 는 지만.\n 기계가 바꿀 수 있는 것은 fix 로 준다.\n안 잡는 것: 사전에 없는 번역투. 무생물 주어 타동사 구문처럼 구문 분석이 필요한 것은 잡지 않는다."
|
|
56
52
|
}
|
package/data/ruleMechanisms.json
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"doublePassive": "dictionary",
|
|
13
13
|
"draftHistory": "dictionary",
|
|
14
14
|
"duplicateBlock": "repeat",
|
|
15
|
-
"ellipsis": "dictionary",
|
|
16
15
|
"emojiBullet": "dictionary",
|
|
17
16
|
"endingRepeat": "repeat",
|
|
18
17
|
"enoughOnce": "repeat",
|
|
@@ -22,7 +21,6 @@
|
|
|
22
21
|
"fillerOpener": "dictionary",
|
|
23
22
|
"firstResultDistance": "threshold",
|
|
24
23
|
"hardWord": "dictionary",
|
|
25
|
-
"headingQuestion": "repeat",
|
|
26
24
|
"headingSentence": "threshold",
|
|
27
25
|
"headingSkip": "threshold",
|
|
28
26
|
"headingUniform": "repeat",
|
|
@@ -47,10 +45,8 @@
|
|
|
47
45
|
"promiseRecall": "reader",
|
|
48
46
|
"redundantPair": "dictionary",
|
|
49
47
|
"sectionNoProse": "threshold",
|
|
50
|
-
"sectionResult": "threshold",
|
|
51
48
|
"spacing": "dictionary",
|
|
52
49
|
"spelling": "dictionary",
|
|
53
50
|
"tableOddCell": "contrast",
|
|
54
|
-
"topicBreak": "contrast",
|
|
55
51
|
"translationese": "dictionary"
|
|
56
52
|
}
|
package/data/spelling.json
CHANGED
package/data/version.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version": "0.0.
|
|
1
|
+
{"version": "0.0.9"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# 국립국어원 파생 데이터 라이선스 (공공누리 제1유형)
|
|
2
|
+
|
|
3
|
+
아래 두 파일은 국립국어원이 공공누리 제1유형 (출처 표시) 으로 공개한 자료에서 왔다. 나머지 hanlint
|
|
4
|
+
코드와 데이터의 MIT 와는 별개다.
|
|
5
|
+
|
|
6
|
+
## `learningVocabulary.tsv`
|
|
7
|
+
|
|
8
|
+
- 원저작자: 국립국어원
|
|
9
|
+
- 자료: 한국어 학습용 어휘 목록 (5,965개)
|
|
10
|
+
- 원본: <https://www.korean.go.kr/front/etcData/etcDataView.do?etc_seq=70&mn_id=46&pageIndex=9>
|
|
11
|
+
- 받은 날: 2026-08-30
|
|
12
|
+
- 라이선스: [공공누리 제1유형 출처 표시](https://www.kogl.or.kr/info/licenseType1.do)
|
|
13
|
+
- 변경: cp949 를 UTF-8 로 옮기고 열 이름을 바꿨다. 낱말, 품사, 풀이, 등급의 내용은 바꾸지 않았다.
|
|
14
|
+
만드는 절차와 원본 필드는 `learningVocabularySource.toml` 이 소유한다.
|
|
15
|
+
|
|
16
|
+
## `easyWords.toml`
|
|
17
|
+
|
|
18
|
+
- 원저작자: 국립국어원
|
|
19
|
+
- 자료: 다듬은 말 (<https://www.korean.go.kr/front/imprv/refineList.do>) 과 표준국어대사전
|
|
20
|
+
(<https://stdict.korean.go.kr>) 의 동의어 정보
|
|
21
|
+
- 라이선스: [공공누리 제1유형 출처 표시](https://www.kogl.or.kr/info/licenseType1.do)
|
|
22
|
+
- 변경: 어려운 낱말과 쉬운 말의 짝만 뽑아 TOML 로 옮겼다.
|
|
23
|
+
|
|
24
|
+
공공누리 제1유형은 출처를 밝히면 상업적 이용과 변형을 포함해 자유롭게 쓸 수 있다. 이 파일이 그
|
|
25
|
+
출처 표시다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hanlint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "한국어 글 검사기. Korean prose linter for Markdown: translationese, noun piles, double passives, weak structure. Zero dependencies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"src",
|
|
16
16
|
"data",
|
|
17
17
|
"LICENSE",
|
|
18
|
+
"koglType1.LICENSE.md",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
20
21
|
"engines": {
|
|
@@ -23,6 +24,9 @@
|
|
|
23
24
|
"scripts": {
|
|
24
25
|
"test": "node --test \"test/*.test.js\""
|
|
25
26
|
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"pyproc": "0.0.23"
|
|
29
|
+
},
|
|
26
30
|
"keywords": [
|
|
27
31
|
"korean",
|
|
28
32
|
"korean-language",
|
|
@@ -37,7 +41,7 @@
|
|
|
37
41
|
"proofreading",
|
|
38
42
|
"text-linter"
|
|
39
43
|
],
|
|
40
|
-
"license": "
|
|
44
|
+
"license": "SEE LICENSE IN koglType1.LICENSE.md",
|
|
41
45
|
"repository": {
|
|
42
46
|
"type": "git",
|
|
43
47
|
"url": "git+https://github.com/eddmpython/hanlint.git",
|
package/src/cli/main.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* audit, guard, arena, blueprint, evidence, entailment 같은 확장 명령은 파이썬 패키지에 있다.
|
|
19
19
|
*/
|
|
20
20
|
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
21
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
21
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
22
22
|
|
|
23
23
|
import { fingerprint, lintText, ruleDoc, ruleNames, ruleSummary, version } from "../index.js";
|
|
24
24
|
import { Baseline, build as buildBaseline, DEFAULT_NAME as DEFAULT_BASELINE, load as loadBaseline, prune as pruneBaseline, render as renderBaseline } from "../baseline/store.js";
|
|
@@ -72,17 +72,15 @@ const THRESHOLD_FIELDS = [
|
|
|
72
72
|
"endingRun",
|
|
73
73
|
"factListMinSentences",
|
|
74
74
|
"factListMaxMeanLength",
|
|
75
|
-
"
|
|
75
|
+
"flowValleyMinSentences",
|
|
76
76
|
"longSentenceMax",
|
|
77
77
|
"duplicateBlockRatio",
|
|
78
78
|
"firstResultMaxParagraphs",
|
|
79
|
-
"sectionResultMinParagraphs",
|
|
80
79
|
"introMaxImages",
|
|
81
|
-
"headingQuestionRatio",
|
|
82
80
|
"moreLaterMaxChars",
|
|
83
81
|
"tableOddCellMinRows",
|
|
84
82
|
];
|
|
85
|
-
const FLOAT_FIELDS = new Set(["headingUniformRatio", "factListMaxMeanLength"
|
|
83
|
+
const FLOAT_FIELDS = new Set(["headingUniformRatio", "factListMaxMeanLength"]);
|
|
86
84
|
|
|
87
85
|
const USAGE = `사용법: hanlint 글.md [다른.md ...] [--format text|compact|json|github] [--severity all|error|notice] [--errors-only]
|
|
88
86
|
[--config 파일] [--preset blog|report|docs] [--disable 규칙] [--baseline [파일]] [--output 파일] [--quiet]
|
|
@@ -239,8 +237,9 @@ function configFrom(options, paths) {
|
|
|
239
237
|
function configLabel(config) {
|
|
240
238
|
if (config.source === null) return "기본값";
|
|
241
239
|
const rel = relative(process.cwd(), config.source);
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
// 경로 구분자는 언제나 /. 설정이 작업 폴더 밖일 때 두 판의 첫 줄이 갈렸다. 파이썬과 같다.
|
|
241
|
+
const label = !rel || rel.startsWith("..") || isAbsolute(rel) ? config.source : rel;
|
|
242
|
+
return label.split(sep).join("/");
|
|
244
243
|
}
|
|
245
244
|
|
|
246
245
|
/** 설정 출처와 지금 도는 프리셋. 기본 프리셋이면 이름을 빼서 줄이 길어지지 않게 한다.
|
package/src/config/settings.js
CHANGED
|
@@ -14,22 +14,30 @@ import { projectPatches } from "../data/patches.js";
|
|
|
14
14
|
*/
|
|
15
15
|
export const PRESETS = {
|
|
16
16
|
blog: [],
|
|
17
|
-
report: ["noQuestion", "
|
|
17
|
+
report: ["noQuestion", "firstResultDistance", "introImage", "moreLater", "numberOrphan"],
|
|
18
18
|
docs: [
|
|
19
19
|
"noQuestion",
|
|
20
|
-
"sectionResult",
|
|
21
20
|
"firstResultDistance",
|
|
22
21
|
"introImage",
|
|
23
22
|
"moreLater",
|
|
24
23
|
"draftHistory",
|
|
25
24
|
"blockUnread",
|
|
26
25
|
"numberOrphan",
|
|
26
|
+
"duplicateBlock",
|
|
27
|
+
"headingUniform",
|
|
27
28
|
],
|
|
28
29
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// 서사 글에서만 끄는 것. 설명글의 전제 (독자가 문장 사이 이유를 따라간다, 말끝을 맺는다) 가 장면과
|
|
31
|
+
// 대사에는 성립하지 않는다. 파이썬 settings.py 의 NARRATIVE 와 같다.
|
|
32
|
+
// 참고 문서와 자습서는 명령 전후 상태와 완결된 예제를 보이려 같은 블록을 다시 둔다. 파이썬의 REFERENCE 와 같다.
|
|
33
|
+
const REFERENCE = ["duplicateBlock", "headingUniform"];
|
|
34
|
+
// 백과 표제어는 첫 절 앞의 머리말이 길게 정의와 요약을 담는 것이 관례다. 파이썬의 ENCYCLOPEDIC 과 같다.
|
|
35
|
+
const ENCYCLOPEDIC = ["introLong"];
|
|
36
|
+
const NARRATIVE = ["factListParagraph"];
|
|
37
|
+
PRESETS.guide = [...PRESETS.blog, ...REFERENCE];
|
|
38
|
+
PRESETS.essay = [...PRESETS.report, ...NARRATIVE];
|
|
39
|
+
PRESETS.fiction = [...PRESETS.report, ...NARRATIVE];
|
|
40
|
+
PRESETS.encyclopedia = [...PRESETS.docs, ...ENCYCLOPEDIC];
|
|
33
41
|
|
|
34
42
|
/** 프리셋 → 견줄 프로파일의 종류. 정본은 파이썬 config/settings.py 의 PROFILE_OF 다. @type {Record<string, string>} */
|
|
35
43
|
export const PROFILE_OF = {
|
|
@@ -73,13 +81,11 @@ export const DEFAULT_PRESET = PRESET_NAMES[0];
|
|
|
73
81
|
* @property {number} endingRun
|
|
74
82
|
* @property {number} factListMinSentences
|
|
75
83
|
* @property {number} factListMaxMeanLength
|
|
76
|
-
* @property {number}
|
|
84
|
+
* @property {number} flowValleyMinSentences
|
|
77
85
|
* @property {number} longSentenceMax
|
|
78
86
|
* @property {number} duplicateBlockRatio
|
|
79
87
|
* @property {number} firstResultMaxParagraphs
|
|
80
|
-
* @property {number} sectionResultMinParagraphs
|
|
81
88
|
* @property {number} introMaxImages
|
|
82
|
-
* @property {number} headingQuestionRatio
|
|
83
89
|
* @property {number} moreLaterMaxChars
|
|
84
90
|
* @property {number} tableOddCellMinRows
|
|
85
91
|
* @property {number} registerMinShare
|
|
@@ -113,13 +119,11 @@ export function defaultConfig() {
|
|
|
113
119
|
endingRun: 4,
|
|
114
120
|
factListMinSentences: 3,
|
|
115
121
|
factListMaxMeanLength: 8.0,
|
|
116
|
-
|
|
122
|
+
flowValleyMinSentences: 2,
|
|
117
123
|
longSentenceMax: 30,
|
|
118
124
|
duplicateBlockRatio: 0.9,
|
|
119
125
|
firstResultMaxParagraphs: 4,
|
|
120
|
-
sectionResultMinParagraphs: 3,
|
|
121
126
|
introMaxImages: 1,
|
|
122
|
-
headingQuestionRatio: 0.5,
|
|
123
127
|
moreLaterMaxChars: 150,
|
|
124
128
|
tableOddCellMinRows: 4,
|
|
125
129
|
// 기준 말뭉치에서 일관된 에세이 하위 5%는 0.7576, 실제 혼합 기사는 0.625였다.
|
package/src/fingerprint/build.js
CHANGED
|
@@ -98,20 +98,20 @@ import { overlap, topicsOf, unionOf } from "./topics.js";
|
|
|
98
98
|
*/
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* 문장 안의 인용 구간.
|
|
101
|
+
* 문장 안의 인용 구간. 블록 좌표의 구간을 문장 좌표로 옮긴다. 따옴표 쌍도 블록에서 재서 넘겨받는다.
|
|
102
|
+
* 문장 안에서 재면 문장 분리가 따옴표 안에서 잘릴 때 쌍이 안 잡혀 인용 예외가 풀린다. 파이썬과 같다.
|
|
102
103
|
* @param {import("../analysis/splitSentences.js").Sentence} sentence
|
|
103
|
-
* @param {[number, number][]}
|
|
104
|
+
* @param {[number, number][]} blockSpans
|
|
104
105
|
* @returns {[number, number][]}
|
|
105
106
|
*/
|
|
106
|
-
function quotedIn(sentence,
|
|
107
|
+
function quotedIn(sentence, blockSpans) {
|
|
107
108
|
/** @type {[number, number][]} */
|
|
108
109
|
const spans = [];
|
|
109
|
-
for (const [start, end] of
|
|
110
|
+
for (const [start, end] of blockSpans) {
|
|
110
111
|
const lo = Math.max(start, sentence.start);
|
|
111
112
|
const hi = Math.min(end, sentence.end);
|
|
112
113
|
if (lo < hi) spans.push([lo - sentence.start, hi - sentence.start]);
|
|
113
114
|
}
|
|
114
|
-
spans.push(...markers.quoteSpans(sentence.text));
|
|
115
115
|
const unique = new Map(spans.map((span) => [`${span[0]},${span[1]}`, span]));
|
|
116
116
|
return [...unique.values()].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
117
117
|
}
|
|
@@ -219,12 +219,15 @@ function buildSection(build, section, sectionIndex) {
|
|
|
219
219
|
continue;
|
|
220
220
|
}
|
|
221
221
|
const text = plainText(block.text);
|
|
222
|
-
|
|
222
|
+
// 인라인 코드와 따옴표 쌍을 둘 다 블록 좌표에서 잰다. 문장 분리가 인용 안에서 잘려도 쌍이 남는다.
|
|
223
|
+
const blockSpans = [...codeSpans(block.text, text), ...markers.quoteSpans(text)].sort(
|
|
224
|
+
(a, b) => a[0] - b[0] || a[1] - b[1],
|
|
225
|
+
);
|
|
223
226
|
/** @type {SentencePrint[]} */
|
|
224
227
|
const blockSentences = [];
|
|
225
228
|
for (const sentence of splitSentences(text)) {
|
|
226
229
|
const line = block.startLine + countIn(text, "\n", sentence.start);
|
|
227
|
-
const quoted = quotedIn(sentence,
|
|
230
|
+
const quoted = quotedIn(sentence, blockSpans);
|
|
228
231
|
const made = makeSentencePrint(build, sentence.text, line, block, sectionIndex, quoted);
|
|
229
232
|
// 만든 자리에서 바로 쌓는다. 다음 문장의 index 가 이 길이에서 나오므로 미루면 셈이 어긋난다.
|
|
230
233
|
sentences.push(made);
|
|
@@ -5,7 +5,8 @@ import { DOCUMENT, NOTICE, finding } from "../finding.js";
|
|
|
5
5
|
export const name = "installImport";
|
|
6
6
|
export const mechanism = "contrast";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// 캡처는 백틱과 한글에서 멈춘다. 안 멈추면 설치 줄 뒤의 산문을 패키지로 등록한다. 파이썬과 같다.
|
|
9
|
+
const INSTALL = /(?:pip\s+install|uv\s+add|uv\s+pip\s+install|conda\s+install|poetry\s+add)\s+([^\n#|&;`가-힣]+)/g;
|
|
9
10
|
const IMPORT = /^\s*(?:import\s+([\w.]+(?:\s*,\s*[\w.]+)*)|from\s+([\w.]+)\s+import\b)/;
|
|
10
11
|
const REQUIREMENT = /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[([^\]]*)\])?/;
|
|
11
12
|
const PY_FILE = /\b(\w+)\.py\b/g;
|
|
@@ -19,9 +19,14 @@ export function run(doc, config) {
|
|
|
19
19
|
const headings = doc.headings.filter(([level]) => level === 2);
|
|
20
20
|
if (!keyword || headings.length < MIN_HEADINGS) return [];
|
|
21
21
|
const joined = headings.map(([, text]) => text).join(" / ");
|
|
22
|
-
|
|
22
|
+
// keywordMissing 과 같은 자리를 본다. 제목만 보면 첫 문단에만 있는 검색어가 두 규칙 사이로 빠진다. 파이썬과 같다.
|
|
23
|
+
const first = doc.paragraphs.length ? doc.paragraphs[0].text : "";
|
|
24
|
+
const promised = `${title}\n${first}`;
|
|
25
|
+
const missing = keyword
|
|
26
|
+
.split(/\s+/)
|
|
27
|
+
.filter((word) => word.length >= MIN_WORD && promised.includes(word) && !joined.includes(word));
|
|
23
28
|
if (!missing.length) return [];
|
|
24
29
|
return [
|
|
25
|
-
finding(name, headings[0][2], joined,
|
|
30
|
+
finding(name, headings[0][2], joined, `글이 내건 \`${missing[0]}\` 가 절 제목 어디에도 없다. 목차를 훑는 독자가 이 글이 그것을 다루는지 못 본다`, null, NOTICE, DOCUMENT, -1),
|
|
26
31
|
];
|
|
27
32
|
}
|
|
@@ -5,7 +5,8 @@ import { DOCUMENT, NOTICE, finding } from "../finding.js";
|
|
|
5
5
|
export const name = "tableOddCell";
|
|
6
6
|
export const mechanism = "contrast";
|
|
7
7
|
const SEPARATOR = /^:?-{2,}:?$/;
|
|
8
|
-
|
|
8
|
+
// 단위는 한 글자 이상이다. 0 을 허용하면 맨숫자 칸이 빈 단위를 받아 지적문이 깨진다. 파이썬과 같다.
|
|
9
|
+
const MEASURE = /^([+-]?\d[\d,]*(?:\.\d+)?)\s*([^\s\d]{1,6})$/;
|
|
9
10
|
const EMPTY = ["", "-", "--"];
|
|
10
11
|
|
|
11
12
|
/** (블록 안 줄 오프셋, 칸 목록). 구분 줄은 뺀다. @param {string} text @returns {[number, string[]][]} */
|
package/src/rules/registry.js
CHANGED
|
@@ -24,7 +24,6 @@ import * as spacing from "./orthography/spacing.js";
|
|
|
24
24
|
import * as spelling from "./orthography/spelling.js";
|
|
25
25
|
import * as factListParagraph from "./paragraph/factListParagraph.js";
|
|
26
26
|
import * as paraFragment from "./paragraph/paraFragment.js";
|
|
27
|
-
import * as topicBreak from "./paragraph/topicBreak.js";
|
|
28
27
|
import * as cliche from "./sentence/cliche.js";
|
|
29
28
|
import * as hardWord from "./sentence/hardWord.js";
|
|
30
29
|
import * as connectorRepeat from "./sentence/connectorRepeat.js";
|
|
@@ -35,7 +34,6 @@ import * as dash from "./sentence/dash.js";
|
|
|
35
34
|
import * as deixis from "./sentence/deixis.js";
|
|
36
35
|
import * as doubleNegative from "./sentence/doubleNegative.js";
|
|
37
36
|
import * as doublePassive from "./sentence/doublePassive.js";
|
|
38
|
-
import * as ellipsis from "./sentence/ellipsis.js";
|
|
39
37
|
import * as endingRepeat from "./sentence/endingRepeat.js";
|
|
40
38
|
import * as euiChain from "./sentence/euiChain.js";
|
|
41
39
|
import * as fillerOpener from "./sentence/fillerOpener.js";
|
|
@@ -50,7 +48,6 @@ import * as translationese from "./sentence/translationese.js";
|
|
|
50
48
|
import * as blockUnread from "./structure/blockUnread.js";
|
|
51
49
|
import * as bridgeRepeat from "./structure/bridgeRepeat.js";
|
|
52
50
|
import * as emojiBullet from "./structure/emojiBullet.js";
|
|
53
|
-
import * as headingQuestion from "./structure/headingQuestion.js";
|
|
54
51
|
import * as introImage from "./structure/introImage.js";
|
|
55
52
|
import * as loneSubheading from "./structure/loneSubheading.js";
|
|
56
53
|
import * as moreLater from "./structure/moreLater.js";
|
|
@@ -59,7 +56,6 @@ import * as headingSkip from "./structure/headingSkip.js";
|
|
|
59
56
|
import * as headingUniform from "./structure/headingUniform.js";
|
|
60
57
|
import * as introLong from "./structure/introLong.js";
|
|
61
58
|
import * as sectionNoProse from "./structure/sectionNoProse.js";
|
|
62
|
-
import * as sectionResult from "./structure/sectionResult.js";
|
|
63
59
|
|
|
64
60
|
/**
|
|
65
61
|
* @typedef {object} Rule
|
|
@@ -79,7 +75,6 @@ export const RULES = [
|
|
|
79
75
|
deixis,
|
|
80
76
|
doubleNegative,
|
|
81
77
|
doublePassive,
|
|
82
|
-
ellipsis,
|
|
83
78
|
endingRepeat,
|
|
84
79
|
euiChain,
|
|
85
80
|
fillerOpener,
|
|
@@ -94,11 +89,9 @@ export const RULES = [
|
|
|
94
89
|
translationese,
|
|
95
90
|
factListParagraph,
|
|
96
91
|
paraFragment,
|
|
97
|
-
topicBreak,
|
|
98
92
|
blockUnread,
|
|
99
93
|
bridgeRepeat,
|
|
100
94
|
emojiBullet,
|
|
101
|
-
headingQuestion,
|
|
102
95
|
introImage,
|
|
103
96
|
loneSubheading,
|
|
104
97
|
moreLater,
|
|
@@ -107,7 +100,6 @@ export const RULES = [
|
|
|
107
100
|
headingUniform,
|
|
108
101
|
introLong,
|
|
109
102
|
sectionNoProse,
|
|
110
|
-
sectionResult,
|
|
111
103
|
countMismatch,
|
|
112
104
|
enoughOnce,
|
|
113
105
|
fieldEcho,
|
|
@@ -8,6 +8,20 @@ import { hasLocalAntecedent } from "../shared/localAntecedent.js";
|
|
|
8
8
|
export const name = "danglingDeixis";
|
|
9
9
|
export const mechanism = "reader";
|
|
10
10
|
|
|
11
|
+
const QUESTION = "의문";
|
|
12
|
+
const EXCLAIM = ["!", "!"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 앞 문장이 물음이나 감탄인가. 그러면 그 문장 자체가 지시어의 선행어다.
|
|
16
|
+
* 뜻은 파이썬 rules/sentence/danglingDeixis.py 의 isPrompt 가 소유한다.
|
|
17
|
+
* @param {{mood: string, text: string} | null} previous
|
|
18
|
+
*/
|
|
19
|
+
function isPrompt(previous) {
|
|
20
|
+
if (!previous) return false;
|
|
21
|
+
const text = previous.text.replace(/\s+$/u, "");
|
|
22
|
+
return previous.mood === QUESTION || EXCLAIM.some((mark) => text.endsWith(mark));
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
12
26
|
export function run(doc) {
|
|
13
27
|
const findings = [];
|
|
@@ -16,6 +30,7 @@ export function run(doc) {
|
|
|
16
30
|
if (hasLocalAntecedent(sentence)) continue;
|
|
17
31
|
const reader = doc.reader.beforeSentence[sentence.index];
|
|
18
32
|
if (overlap(reader.recent, sentence.topics) > 0) continue;
|
|
33
|
+
if (isPrompt(reader.previous)) continue;
|
|
19
34
|
findings.push(
|
|
20
35
|
finding(name, sentence.line, sentence.text, `\`${sentence.deixis[0]}\` ${fitJosa(sentence.deixis[0], "이")} 가리킬 것이 앞 문장에 없다. 가리키는 파일, 값, 코드의 이름을 쓴다`, null, "error", SENTENCE, sentence.index, null, null, danglingDeixisCandidates(sentence, reader.previous)),
|
|
21
36
|
);
|
|
@@ -4,7 +4,8 @@ import { COPULA as COPULA_KIND, lastWord, parsePredicate } from "../../analysis/
|
|
|
4
4
|
|
|
5
5
|
export const name = "negationRedefine";
|
|
6
6
|
export const mechanism = "repeat";
|
|
7
|
-
|
|
7
|
+
// 관형형 `아닌` 은 뺐다. 문장을 끝낼 수 없어 앞 문장이 부정이라는 전제를 못 만족한다. 파이썬과 같다.
|
|
8
|
+
const NEGATION = /단순(?:한|히)\s?.{0,15}?(?:이|가)\s?아(?:닙니다|니다|니에요|니죠)/;
|
|
8
9
|
const COPULA = /(입니다|이다|이에요|예요)[.!]?$/;
|
|
9
10
|
|
|
10
11
|
/** @param {string} text */
|
|
@@ -11,12 +11,12 @@ export const mechanism = "repeat";
|
|
|
11
11
|
*/
|
|
12
12
|
export function run(doc, config) {
|
|
13
13
|
const headings = doc.headings.filter(([level]) => level === 2);
|
|
14
|
-
//
|
|
14
|
+
// 어미는 한글이다. 숫자, 부호, 라틴 문자로 끝나는 제목은 판정에서 뺀다. 파이썬과 같다.
|
|
15
15
|
const eligible = headings.filter(([, text]) => {
|
|
16
16
|
if (!text.trim()) return false;
|
|
17
17
|
const trimmed = text.trimEnd();
|
|
18
18
|
const char = trimmed[trimmed.length - 1];
|
|
19
|
-
return
|
|
19
|
+
return char >= "가" && char <= "힣";
|
|
20
20
|
});
|
|
21
21
|
if (eligible.length < 3) return [];
|
|
22
22
|
const [best, count, total] = shareOf(
|
|
@@ -27,7 +27,7 @@ export function run(doc, config) {
|
|
|
27
27
|
);
|
|
28
28
|
if (count / total >= config.headingUniformRatio) {
|
|
29
29
|
return [
|
|
30
|
-
finding(name, eligible[0][2], eligible.map(([, text]) => text).join(" / "), `H2 ${eligible.length}개 중 ${count}개가 \`${best}\` 로 끝난다. 목차가 한 어미로 끝나면 과정이 아니라 나열로 읽힌다.
|
|
30
|
+
finding(name, eligible[0][2], eligible.map(([, text]) => text).join(" / "), `H2 ${eligible.length}개 중 ${count}개가 \`${best}\` 로 끝난다. 목차가 한 어미로 끝나면 과정이 아니라 나열로 읽힌다. 그 문서의 관례라면 둔다`, null, "notice", DOCUMENT, -1),
|
|
31
31
|
];
|
|
32
32
|
}
|
|
33
33
|
return [];
|
|
@@ -9,8 +9,10 @@ export const mechanism = "threshold";
|
|
|
9
9
|
* @param {import("../../config/settings.js").Config} config
|
|
10
10
|
*/
|
|
11
11
|
export function run(doc, config) {
|
|
12
|
-
|
|
12
|
+
// 본문 절이 하나뿐이거나 도입이 문단의 절반을 넘으면 절이 없는 글이지 도입이 긴 글이 아니다. 파이썬과 같다.
|
|
13
|
+
if (doc.bodySections.length < 2) return [];
|
|
13
14
|
const paragraphs = doc.intro.paragraphs;
|
|
15
|
+
if (paragraphs.length * 2 > doc.paragraphs.length) return [];
|
|
14
16
|
if (paragraphs.length > config.introMaxParagraphs) {
|
|
15
17
|
const over = paragraphs[config.introMaxParagraphs];
|
|
16
18
|
return [
|
package/data/coverageTypes.txt
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# 사람 평가자의 지적을 유형으로 가르는 표지. `유형<TAB>정규식`. coverage 가 못 집은 지적을 모을 때 쓴다. 위에서부터 먼저 맞는 것이 이긴다.
|
|
2
|
-
파일과 폴더 파일|폴더|경로|만들어지지|생성|없어서|없다
|
|
3
|
-
설치와 실행 설치|import|모듈|패키지|실행|오류|에러|Error|멈춘다|돌아가지
|
|
4
|
-
순서와 구조 순서|먼저|앞에서|뒤에서|절|목차|위치|옮|건너
|
|
5
|
-
용어와 설명 설명|뜻|처음 나온|용어|무엇인지|모른|정의
|
|
6
|
-
숫자와 표 숫자|표|열|행|값|측정|잰
|
|
7
|
-
문장과 표현 문장|주어|서술어|표현|반복|같은 말|어색
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { NOTICE, PARAGRAPH, finding } from "../finding.js";
|
|
3
|
-
|
|
4
|
-
export const name = "topicBreak";
|
|
5
|
-
export const mechanism = "contrast";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
9
|
-
* @param {import("../../config/settings.js").Config} config
|
|
10
|
-
*/
|
|
11
|
-
export function run(doc, config) {
|
|
12
|
-
const findings = [];
|
|
13
|
-
for (const section of doc.sections) {
|
|
14
|
-
const paragraphs = section.paragraphs;
|
|
15
|
-
for (let i = 0; i + 1 < paragraphs.length; i++) {
|
|
16
|
-
const previous = paragraphs[i];
|
|
17
|
-
const paragraph = paragraphs[i + 1];
|
|
18
|
-
if (paragraph.overlapWithPrevious !== 0) continue;
|
|
19
|
-
if (Math.min(previous.sentenceCount, paragraph.sentenceCount) < config.topicBreakMinSentences) continue;
|
|
20
|
-
findings.push(
|
|
21
|
-
finding(name, paragraph.startLine, paragraph.sentences[0].text, "앞 문단과 화제어가 하나도 겹치지 않는다. 앞에서 만든 것의 이름을 첫 문장에서 다시 부른다", null, NOTICE, PARAGRAPH, paragraph.index),
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return findings;
|
|
26
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { insideAny } from "../../fingerprint/markers.js";
|
|
3
|
-
import { SENTENCE, finding } from "../finding.js";
|
|
4
|
-
|
|
5
|
-
export const name = "ellipsis";
|
|
6
|
-
export const mechanism = "dictionary";
|
|
7
|
-
// 영숫자에 붙은 점 셋 (`v0.0.1...HEAD`, compare URL) 은 범위 표기라 말줄임표가 아니다.
|
|
8
|
-
// 인라인 코드와 따옴표 안은 지문이 인용 구간으로 이미 표시해 두었으므로 여기서 다시 재지 않는다.
|
|
9
|
-
const ELLIPSIS = /(?<![A-Za-z0-9])(…|\.{3,})(?![A-Za-z0-9])/g;
|
|
10
|
-
const CLI_OPTION = /(?:^|\s)--?[A-Za-z0-9]/;
|
|
11
|
-
|
|
12
|
-
/** 괄호 안 명령 예시의 생략 인자. 예: (kubectl get -o yaml …). @param {string} text @param {number} start @param {number} end */
|
|
13
|
-
function insideCommandExample(text, start, end) {
|
|
14
|
-
const opened = text.lastIndexOf("(", start);
|
|
15
|
-
const closed = text.indexOf(")", end);
|
|
16
|
-
return opened >= 0 && closed >= 0 && !text.slice(opened, start).includes(")") && !text.slice(end, closed).includes("(") && CLI_OPTION.test(text.slice(opened + 1, closed));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
20
|
-
export function run(doc) {
|
|
21
|
-
const findings = [];
|
|
22
|
-
for (const sentence of doc.sentences) {
|
|
23
|
-
const found = [...sentence.text.matchAll(ELLIPSIS)].filter((m) => {
|
|
24
|
-
const start = /** @type {number} */ (m.index);
|
|
25
|
-
return !insideAny(start, start + m[0].length, sentence.quoted) && !insideCommandExample(sentence.text, start, start + m[0].length);
|
|
26
|
-
});
|
|
27
|
-
if (found.length) {
|
|
28
|
-
findings.push(finding(name, sentence.line, sentence.text, "말줄임표로 말끝을 흐리면 독자가 결론을 받아 가지 못한다. 문장을 끝까지 쓴다", null, "error", SENTENCE, sentence.index));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return findings;
|
|
32
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { DOCUMENT, finding } from "../finding.js";
|
|
3
|
-
|
|
4
|
-
export const name = "headingQuestion";
|
|
5
|
-
export const mechanism = "repeat";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
9
|
-
* @param {import("../../config/settings.js").Config} config
|
|
10
|
-
*/
|
|
11
|
-
export function run(doc, config) {
|
|
12
|
-
const headings = doc.headings.filter(([level]) => level === 2);
|
|
13
|
-
if (headings.length < 3) return [];
|
|
14
|
-
const asking = headings.filter(([, text]) => text.trimEnd().endsWith("?")).map(([, text]) => text);
|
|
15
|
-
if (asking.length / headings.length <= config.headingQuestionRatio) return [];
|
|
16
|
-
return [
|
|
17
|
-
finding(name, headings[0][2], asking.join(" / "), `H2 ${headings.length}개 중 ${asking.length}개가 물음표로 끝난다. 목차가 과정이 아니라 FAQ 로 읽힌다. 형태를 섞는다`, null, "error", DOCUMENT, -1),
|
|
18
|
-
];
|
|
19
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { CODE, EMBED, IMAGE, TABLE } from "../../document/model.js";
|
|
3
|
-
import { NOTICE, SECTION, finding } from "../finding.js";
|
|
4
|
-
|
|
5
|
-
export const name = "sectionResult";
|
|
6
|
-
export const mechanism = "threshold";
|
|
7
|
-
const RESULT_KINDS = [CODE, EMBED, IMAGE, TABLE];
|
|
8
|
-
const FILE_NAME = /[\w가-힣-]+\.(?:csv|xlsx|xls|parquet|json|png|jpg|svg|db|sqlite|txt|py|pdf|html|md)\b/;
|
|
9
|
-
const OUTPUT_WORDS = ["출력", "화면에", "찍힙니다", "나옵니다", "뜹니다", "보입니다", "만들어집니다", "생깁니다", "저장됩니다"];
|
|
10
|
-
|
|
11
|
-
/** @param {import("../../fingerprint/build.js").SectionPrint} section */
|
|
12
|
-
function leavesResult(section) {
|
|
13
|
-
if (section.blockKinds.some((kind) => RESULT_KINDS.includes(kind))) return true;
|
|
14
|
-
for (const paragraph of section.paragraphs) {
|
|
15
|
-
for (const sentence of paragraph.sentences) {
|
|
16
|
-
if (FILE_NAME.test(sentence.text) || OUTPUT_WORDS.some((word) => sentence.text.includes(word))) return true;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
24
|
-
* @param {import("../../config/settings.js").Config} config
|
|
25
|
-
*/
|
|
26
|
-
export function run(doc, config) {
|
|
27
|
-
const findings = [];
|
|
28
|
-
const body = doc.bodySections;
|
|
29
|
-
body.forEach((section, index) => {
|
|
30
|
-
if (index === body.length - 1) return;
|
|
31
|
-
if (section.paragraphs.length <= config.sectionResultMinParagraphs) return;
|
|
32
|
-
if (leavesResult(section)) return;
|
|
33
|
-
findings.push(
|
|
34
|
-
finding(name, section.startLine, section.title, "이 절에 독자가 확인할 결과 (코드, 표, 그림, 파일, 출력) 가 없다. 만들거나 확인할 것을 하나 넣는다", null, NOTICE, SECTION, section.index),
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
return findings;
|
|
38
|
-
}
|