hanlint 0.0.17 → 0.0.19

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.
Files changed (136) hide show
  1. package/README.md +23 -20
  2. package/data/calqueFrame.json +60 -0
  3. package/data/cliches.json +39 -29
  4. package/data/collocationStops.txt +28 -0
  5. package/data/confusable.json +20 -20
  6. package/data/easyWords.json +12 -246
  7. package/data/exemplars.json +50 -29
  8. package/data/hiddenDeps.txt +11 -11
  9. package/data/japaneseLoan.json +27 -27
  10. package/data/koreanProse.txt +64 -0
  11. package/data/patterns.json +26 -26
  12. package/data/phraseRepeat.json +11 -0
  13. package/data/platformApis.txt +11 -11
  14. package/data/redundantPair.json +16 -16
  15. package/data/ruleCategories.json +2 -0
  16. package/data/ruleDocs.json +56 -54
  17. package/data/ruleMechanisms.json +2 -0
  18. package/data/screenNarration.json +10 -10
  19. package/data/screenSentence.json +60 -12
  20. package/data/screenTone.json +6 -6
  21. package/data/screenWord.json +70 -110
  22. package/data/spacing.json +12 -12
  23. package/data/spelling.json +29 -29
  24. package/data/translationese.json +23 -24
  25. package/data/usageCounts.report.json +1676 -0
  26. package/data/version.json +1 -1
  27. package/package.json +1 -1
  28. package/src/analysis/grammar/ending.js +1 -1
  29. package/src/analysis/grammar/hangul.js +4 -4
  30. package/src/analysis/grammar/josa.js +1 -1
  31. package/src/analysis/grammar/register.js +1 -1
  32. package/src/analysis/grammar/voice.js +1 -1
  33. package/src/analysis/index.js +3 -3
  34. package/src/analysis/josa.js +1 -1
  35. package/src/analysis/splitSentences.js +1 -1
  36. package/src/analysis/tokenize.js +41 -26
  37. package/src/baseline/store.js +8 -8
  38. package/src/cli/help.js +11 -10
  39. package/src/cli/main.js +332 -182
  40. package/src/cli/walk.js +2 -2
  41. package/src/cli/welcome.js +8 -8
  42. package/src/config/loadConfig.js +3 -3
  43. package/src/config/patch.js +6 -6
  44. package/src/config/readerContract.js +26 -26
  45. package/src/config/settings.js +44 -25
  46. package/src/config/toml.js +9 -9
  47. package/src/data/exemplars.js +21 -21
  48. package/src/data/load.js +6 -6
  49. package/src/data/operations.js +14 -14
  50. package/src/data/patches.js +10 -10
  51. package/src/data/patterns.js +5 -5
  52. package/src/data/profiles.js +6 -6
  53. package/src/data/prose.js +27 -0
  54. package/src/document/model.js +4 -4
  55. package/src/document/parseMarkdown.js +7 -7
  56. package/src/document/plainText.js +3 -3
  57. package/src/document/sourceText.js +117 -19
  58. package/src/edit/applyFixes.js +0 -0
  59. package/src/fingerprint/build.js +18 -9
  60. package/src/fingerprint/codeBlocks.js +3 -3
  61. package/src/fingerprint/codeMarkers.js +4 -4
  62. package/src/fingerprint/dictionaries.js +34 -7
  63. package/src/fingerprint/markers.js +27 -5
  64. package/src/fingerprint/readerState.js +9 -9
  65. package/src/fingerprint/sourceSentence.js +1 -1
  66. package/src/fingerprint/topics.js +1 -1
  67. package/src/guard/contract.js +6 -6
  68. package/src/guard/editLocality.js +1 -1
  69. package/src/guard/outline.js +1 -1
  70. package/src/guard/receipt.js +13 -13
  71. package/src/guard/surface.js +4 -4
  72. package/src/index.js +9 -7
  73. package/src/learn/edits.js +2 -2
  74. package/src/learn/operations.js +1 -1
  75. package/src/regex.js +2 -2
  76. package/src/report/compactReport.js +7 -3
  77. package/src/report/fingerprintJson.js +2 -2
  78. package/src/report/githubReport.js +1 -1
  79. package/src/report/jsonReport.js +3 -3
  80. package/src/report/registerMatch.js +2 -2
  81. package/src/report/sheet.js +0 -0
  82. package/src/report/textReport.js +35 -16
  83. package/src/report/writingSpec.js +17 -17
  84. package/src/rules/code/inputFileSource.js +1 -1
  85. package/src/rules/code/installImport.js +2 -2
  86. package/src/rules/code/platformApi.js +1 -1
  87. package/src/rules/document/countMismatch.js +2 -1
  88. package/src/rules/document/enoughOnce.js +2 -1
  89. package/src/rules/document/fieldEcho.js +10 -6
  90. package/src/rules/document/keywordHeading.js +1 -1
  91. package/src/rules/document/keywordMissing.js +1 -1
  92. package/src/rules/document/noQuestion.js +2 -2
  93. package/src/rules/document/promiseRecall.js +1 -1
  94. package/src/rules/document/tableOddCell.js +1 -1
  95. package/src/rules/finding.js +4 -4
  96. package/src/rules/paragraph/factListParagraph.js +1 -1
  97. package/src/rules/paragraph/paraFragment.js +1 -1
  98. package/src/rules/readerDebt.js +1 -1
  99. package/src/rules/registry.js +30 -26
  100. package/src/rules/screen/screenWord.js +2 -1
  101. package/src/rules/sentence/calqueFrame.js +11 -0
  102. package/src/rules/sentence/cliche.js +7 -3
  103. package/src/rules/sentence/connectorRepeat.js +2 -1
  104. package/src/rules/sentence/danglingDeixis.js +1 -1
  105. package/src/rules/sentence/dash.js +1 -1
  106. package/src/rules/sentence/deixis.js +1 -1
  107. package/src/rules/sentence/doubleNegative.js +2 -1
  108. package/src/rules/sentence/doublePassive.js +2 -1
  109. package/src/rules/sentence/draftHistory.js +2 -1
  110. package/src/rules/sentence/endingRepeat.js +3 -2
  111. package/src/rules/sentence/euiChain.js +1 -1
  112. package/src/rules/sentence/fillerOpener.js +1 -1
  113. package/src/rules/sentence/hardWord.js +6 -3
  114. package/src/rules/sentence/imperativePeriod.js +1 -1
  115. package/src/rules/sentence/japaneseLoan.js +7 -3
  116. package/src/rules/sentence/longSentence.js +1 -1
  117. package/src/rules/sentence/negationRedefine.js +1 -1
  118. package/src/rules/sentence/nounPile.js +10 -4
  119. package/src/rules/sentence/numberOrphan.js +1 -1
  120. package/src/rules/sentence/outsideProfile.js +8 -3
  121. package/src/rules/sentence/phraseRepeat.js +54 -0
  122. package/src/rules/sentence/redundantPair.js +7 -3
  123. package/src/rules/sentence/translationese.js +7 -3
  124. package/src/rules/shared/candidates.js +2 -2
  125. package/src/rules/shared/dictionaryRule.js +46 -8
  126. package/src/rules/shared/repeat.js +2 -2
  127. package/src/rules/structure/bridgeRepeat.js +3 -3
  128. package/src/rules/structure/emojiBullet.js +1 -1
  129. package/src/rules/structure/headingSkip.js +1 -1
  130. package/src/rules/structure/headingUniform.js +2 -1
  131. package/src/rules/structure/loneSubheading.js +1 -1
  132. package/src/rules/structure/moreLater.js +1 -1
  133. package/src/text.js +2 -2
  134. package/src/usage/counts.js +54 -0
  135. package/src/usage/index.js +8 -0
  136. package/src/usage/sentences.js +681 -0
@@ -1,4 +1,5 @@
1
1
  // @ts-check
2
+ import { fitJosa } from "../../analysis/grammar/josa.js";
2
3
  import { matchedTexts } from "../../fingerprint/markers.js";
3
4
  import { SENTENCE, finding } from "../finding.js";
4
5
 
@@ -20,6 +21,6 @@ export function run(doc, config) {
20
21
  if (breaks.length < 2) return [];
21
22
  const first = breaks[0][0];
22
23
  return breaks.slice(1).map(([sentence, text]) =>
23
- finding(name, sentence.line, sentence.text, `\`${text}\` 로 끊는 문장이 ${first.line}행에 이미 있다. 글 전체에 한 번만 쓴다. 계속 끝나는 척하다 이어지는 꼴이 된다`, null, "error", SENTENCE, sentence.index),
24
+ finding(name, sentence.line, sentence.text, `\`${text}\`${fitJosa(text, "로")} 끊는 문장이 ${first.line}번째 줄에 이미 있다. 글 전체에 한 번만 쓴다. 여러 번 쓰면 끝난다고 해 놓고 글이 계속 이어진다`, null, "error", SENTENCE, sentence.index),
24
25
  );
25
26
  }
@@ -5,22 +5,26 @@ import { DOCUMENT, NOTICE, finding } from "../finding.js";
5
5
  export const name = "fieldEcho";
6
6
  export const mechanism = "contrast";
7
7
 
8
+ /** 글의 끝으로 보는 절의 수. 결말 뒤에 `더 해 볼 것` 목록이 붙는 경우가 흔하다. 파이썬과 같다. */
9
+ const ENDING_SECTIONS = 2;
10
+
8
11
  /**
9
12
  * @param {import("../../fingerprint/build.js").DocumentPrint} doc
10
- * @param {import("../../fingerprint/build.js").SectionPrint} section
13
+ * @param {import("../../fingerprint/build.js").SectionPrint[]} sections
11
14
  * @param {string[]} fields
12
15
  * @param {string} where
13
16
  */
14
- function check(doc, section, fields, where) {
17
+ function check(doc, sections, fields, where) {
15
18
  const findings = [];
19
+ const section = sections[0];
16
20
  for (const name_ of fields) {
17
21
  const value = doc.frontmatter[name_];
18
22
  if (!value) continue;
19
23
  const promised = topicsOf(value);
20
24
  if (!promised.size) continue;
21
- if ([...promised].some((word) => section.topics.has(word))) continue;
25
+ if ([...promised].some((word) => sections.some((spoken) => spoken.topics.has(word)))) continue;
22
26
  findings.push(
23
- finding(name, section.startLine, value, `frontmatter 의 \`${name_}\` 가 약속한 말이 ${where}에 하나도 없다. 약속한 것을 ${where}에서 그 말로 답한다`, null, NOTICE, DOCUMENT, section.index),
27
+ finding(name, section.startLine, value, `frontmatter의 \`${name_}\` 필드가 약속한 말이 ${where}에 하나도 없다. 약속한 것을 ${where}에서 그 말로 답한다`, null, NOTICE, DOCUMENT, section.index),
24
28
  );
25
29
  }
26
30
  return findings;
@@ -32,7 +36,7 @@ function check(doc, section, fields, where) {
32
36
  */
33
37
  export function run(doc, config) {
34
38
  if (!config.introFields.length && !config.endingFields.length) return [];
35
- const findings = check(doc, doc.intro, config.introFields, "도입");
36
- if (doc.sections.length > 1) findings.push(...check(doc, doc.sections[doc.sections.length - 1], config.endingFields, "마지막 절"));
39
+ const findings = check(doc, [doc.intro], config.introFields, "도입");
40
+ if (doc.sections.length > 1) findings.push(...check(doc, doc.sections.slice(-ENDING_SECTIONS), config.endingFields, "글의 끝"));
37
41
  return findings;
38
42
  }
@@ -27,6 +27,6 @@ export function run(doc, config) {
27
27
  .filter((word) => word.length >= MIN_WORD && promised.includes(word) && !joined.includes(word));
28
28
  if (!missing.length) return [];
29
29
  return [
30
- finding(name, headings[0][2], joined, `글이 내건 \`${missing[0]}\` 가 절 제목 어디에도 없다. 목차를 훑는 독자가 이 글이 그것을 다루는지 못 본다`, null, NOTICE, DOCUMENT, -1),
30
+ finding(name, headings[0][2], joined, `글이 내건 말이 절 제목 어디에도 없다: \`${missing[0]}\`. 목차를 훑는 독자는 이 글이 그것을 다루는지 알 수 없다`, null, NOTICE, DOCUMENT, -1),
31
31
  ];
32
32
  }
@@ -17,6 +17,6 @@ export function run(doc, config) {
17
17
  const head = `${title}\n${first ? first.text : ""}`;
18
18
  if (head.includes(keyword)) return [];
19
19
  return [
20
- finding(name, first ? first.startLine : 1, title || (first ? first.text : ""), `대표 검색어 \`${keyword}\` 가 제목에도 첫 문단에도 없다. 검색해 들어온 독자가 자기가 친 이름을 못 본다`, null, "error", DOCUMENT, -1),
20
+ finding(name, first ? first.startLine : 1, title || (first ? first.text : ""), `대표 검색어가 제목에도 첫 문단에도 없다: \`${keyword}\`. 검색해 들어온 독자는 자기가 친 이름을 보지 못한다`, null, "error", DOCUMENT, -1),
21
21
  ];
22
22
  }
@@ -9,7 +9,7 @@ export function run(doc, config) {
9
9
  if (doc.bodySections.length < 2 || doc.questionCount > 0) return [];
10
10
  const first = doc.sentences.length ? doc.sentences[0] : null;
11
11
  const why = doc.readerCallCount === 0
12
- ? "물음표가 한 번도 없고 독자를 부르는 말도 없다. 질문이나 독자 호출이 필요한 글인지 판단하고 충분하면 유지한다"
13
- : "물음표가 한 번도 없다. 질문이 필요한 글인지 판단하고 설명만으로 충분하면 유지한다";
12
+ ? "물음표가 한 번도 없고 독자에게 말을 거는 문장도 없다. 질문이나 독자에게 말을 거는 문장이 필요한 글인지 판단하고, 설명만으로 충분하면 그대로 둔다"
13
+ : "물음표가 한 번도 없다. 질문이 필요한 글인지 판단하고, 설명만으로 충분하면 그대로 둔다";
14
14
  return [finding(name, first ? first.line : 1, first ? first.text : "", why, null, config.enforceStyle.includes(name) ? "error" : "notice", DOCUMENT, -1)];
15
15
  }
@@ -9,6 +9,6 @@ export function run(doc) {
9
9
  const reader = doc.reader.final;
10
10
  if (!reader.promises.length || reader.recalls.length) return [];
11
11
  return reader.promises.map(([line, text]) =>
12
- finding(name, line, text, "뒤로 미룬 것을 앞에서 미룬 것으로 다시 부르는 자리가 글 어디에도 없다. 회수하거나 미루지 않는다", null, NOTICE, DOCUMENT, -1),
12
+ finding(name, line, text, "뒤로 미룬 이야기를 `앞에서 미룬 X`처럼 다시 언급하는 문장이 글 어디에도 없다. 뒤에서 다루거나 처음부터 미루지 않는다", null, NOTICE, DOCUMENT, -1),
13
13
  );
14
14
  }
@@ -53,7 +53,7 @@ export function run(doc, config) {
53
53
  const unit = [...kinds][0];
54
54
  const [offset, cell] = odd[0];
55
55
  findings.push(
56
- finding(name, block.startLine + offset, cell, `이 열의 나머지 ${values.length - 1}칸은 \`${unit}\` 으로 잰 값 하나인데 이 칸만 다르다. 같은 잣대로 다시 재거나 그 줄을 본문 문장으로 옮긴다`, null, NOTICE, DOCUMENT, block.index),
56
+ finding(name, block.startLine + offset, cell, `이 열의 다른 ${values.length - 1}칸은 모두 \`${unit}\` 단위인데 이 칸만 다르다. 같은 단위로 다시 재거나 그 줄을 본문 문장으로 옮긴다`, null, NOTICE, DOCUMENT, block.index),
57
57
  );
58
58
  }
59
59
  }
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** 지적 하나의 모양. 파이썬 rules/finding.py 와 같은 필드와 같은 JSON 순서다. */
2
+ /** 지적 하나의 형식. 파이썬 rules/finding.py와 필드도 JSON 순서도 같다. */
3
3
 
4
4
  export const ERROR = "error";
5
5
  export const NOTICE = "notice";
@@ -18,10 +18,10 @@ const MIN_LOCAL_CUE = 2;
18
18
  * @property {string} quote
19
19
  * @property {string} why
20
20
  * @property {string | null} fix 기계가 낼 수 있을 때만
21
- * @property {string} severity error 는 규칙 위반, notice 는 확인이 필요한 것
21
+ * @property {string} severity error는 규칙 위반, notice는 확인이 필요한 것
22
22
  * @property {string} scope sentence, paragraph, section, document
23
- * @property {number} at scope 가 가리키는 지문의 index
24
- * @property {string | null} fragment 원문에서 바꿀 조각. hanlint fix 가 replacement 로 바꾼다
23
+ * @property {number} at scope가 가리키는 분석 결과의 index
24
+ * @property {string | null} fragment 원문에서 바꿀 조각. hanlint fix가 replacement로 바꾼다
25
25
  * @property {string | null} replacement
26
26
  * @property {Candidate[]} candidates 뜻을 정하지 않고 형태로 좁힐 수 있을 때만. 순위와 점수는 없다
27
27
  */
@@ -20,7 +20,7 @@ export function run(doc, config) {
20
20
  if (paragraph.sentenceCount < config.factListMinSentences || isLinked(paragraph)) continue;
21
21
  if (paragraph.meanLength <= config.factListMaxMeanLength) {
22
22
  findings.push(
23
- finding(name, paragraph.startLine, paragraph.sentences[0].text, `문장 ${paragraph.sentenceCount}개에 인과 표지가 하나도 없다. 사실만 나열한 문단일 수 있다. 그래서, 때문에 로 잇는다`, null, NOTICE, PARAGRAPH, paragraph.index),
23
+ finding(name, paragraph.startLine, paragraph.sentences[0].text, `문장 ${paragraph.sentenceCount}개에 인과 접속사가 하나도 없다. 사실만 나열한 문단일 수 있다. \`그래서\`, \`때문에\`로 잇는다`, null, NOTICE, PARAGRAPH, paragraph.index),
24
24
  );
25
25
  }
26
26
  }
@@ -31,7 +31,7 @@ export function run(doc, config) {
31
31
  for (const [start] of runsOf(keys, config.fragmentRun)) {
32
32
  const first = paragraphs[start];
33
33
  findings.push(
34
- finding(name, first.startLine, first.sentences.length ? first.sentences[0].text : "", `${config.fragmentRun}개 문단이 연달아 한두 문장씩이다. 화제가 같으면 한 문단으로 묶는다. 줄바꿈은 화제가 바뀌는 자리에만`, null, "error", PARAGRAPH, first.index),
34
+ finding(name, first.startLine, first.sentences.length ? first.sentences[0].text : "", `${config.fragmentRun}개 문단이 연달아 한두 문장씩이다. 화제가 같으면 한 문단으로 묶는다. 줄바꿈은 화제가 바뀌는 곳에만 둔다`, null, "error", PARAGRAPH, first.index),
35
35
  );
36
36
  }
37
37
  }
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** 독자 부채는 새 판정이 아니라 reader 기제 Finding을 사람이 읽는 이름으로 묶은 것이다. */
2
+ /** 독자가 막힐 곳. 새로 판정하지 않고, reader 방법을 쓰는 규칙의 지적을 사람이 읽을 이름으로 묶는다. */
3
3
 
4
4
  import { ruleMechanism } from "./registry.js";
5
5
 
@@ -1,8 +1,8 @@
1
1
  // @ts-check
2
2
  /**
3
- * 규칙 등록부. 규칙 하나는 파일 하나이고 `name` 과 `run(doc, config)` 을 내보낸다. 파이썬은 폴더를 걸어 찾고
4
- * 여기는 목록으로 든다. test/rules.test.js 가 목록과 폴더와 ruleDocs.json 이 같은지 본다.
5
- * 기술서 (왜, 어디서, 고치기, 안 잡는 것) 는 파이썬 docstring 의 투영 data/ruleDocs.json 에서 읽는다.
3
+ * 규칙 목록. 규칙 하나는 파일 하나이고 `name`과 `run(doc, config)`을 내보낸다. 파이썬은 폴더를 걸어 찾고
4
+ * 여기는 목록으로 든다. test/rules.test.js가 목록과 폴더와 ruleDocs.json이 같은지 본다.
5
+ * 규칙 설명(왜, 어디서, 고치기, 안 잡는 것)은 파이썬 docstring을 옮긴 data/ruleDocs.json에서 읽는다.
6
6
  */
7
7
  import { enabled } from "../config/settings.js";
8
8
  import { loadRuleCategories, loadRuleDocs } from "../data/load.js";
@@ -24,9 +24,11 @@ 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 calqueFrame from "./sentence/calqueFrame.js";
27
28
  import * as cliche from "./sentence/cliche.js";
28
29
  import * as hardWord from "./sentence/hardWord.js";
29
30
  import * as connectorRepeat from "./sentence/connectorRepeat.js";
31
+ import * as phraseRepeat from "./sentence/phraseRepeat.js";
30
32
  import * as danglingDeixis from "./sentence/danglingDeixis.js";
31
33
  import * as draftHistory from "./sentence/draftHistory.js";
32
34
  import * as numberOrphan from "./sentence/numberOrphan.js";
@@ -64,14 +66,16 @@ import * as screenWord from "./screen/screenWord.js";
64
66
  /**
65
67
  * @typedef {object} Rule
66
68
  * @property {string} name
67
- * @property {string} mechanism 세는 방법. MECHANISMS 의 키 하나
69
+ * @property {string} mechanism 세는 방법. MECHANISMS의 키 하나
68
70
  * @property {(doc: import("../fingerprint/build.js").DocumentPrint, config: import("../config/settings.js").Config) => import("./finding.js").Finding[]} run
69
71
  */
70
72
 
71
73
  /** @type {Rule[]} */
72
74
  export const RULES = [
75
+ calqueFrame,
73
76
  cliche,
74
77
  connectorRepeat,
78
+ phraseRepeat,
75
79
  danglingDeixis,
76
80
  dash,
77
81
  draftHistory,
@@ -127,19 +131,19 @@ export const RULES = [
127
131
  ];
128
132
 
129
133
  /**
130
- * 규칙이 세는 방법의 닫힌 집합. 파이썬 rules/registry.py 의 MECHANISMS 와 같은 값과 같은 순서다.
134
+ * 규칙이 방식의 닫힌 목록. 파이썬 rules/registry.py의 MECHANISMS와 같은 값과 같은 순서다.
131
135
  * @type {Record<string, string>}
132
136
  */
133
137
  export const MECHANISMS = {
134
- dictionary: "사전. 낱말 목록과 정규식이 맞는 자리",
135
- repeat: "반복. 같은 모양이 창 안에서 N 번",
136
- threshold: "셈. 지문의 값이 임계를 넘거나 모양이 계약과 다른 자리",
137
- contrast: "대조. 두 자리를 맞대 어긋난 곳",
138
- reader: "독자 상태. 문장 순서대로 독자가 손에 든 것과 본 것에 그 자리가 요구하는 것을 맞댄다",
138
+ dictionary: "사전. 단어 목록과 정규식이 맞는 곳",
139
+ repeat: "반복. 같은 모양이 가까이에서 여러 번 나오는 곳",
140
+ threshold: "수치. 글에서 잰 값이 기준값을 넘거나 정해 둔 형식과 다른 곳",
141
+ contrast: "비교. 두 곳을 비교해 어긋난 곳",
142
+ reader: "독자. 앞에서 읽은 내용만으로 그 문장을 이해할 수 있는지 문장 순서대로 본다",
139
143
  };
140
144
  for (const rule of RULES) {
141
145
  if (!(rule.mechanism in MECHANISMS)) {
142
- throw new Error(`규칙 ${rule.name} 의 기제 ${rule.mechanism} 은 닫힌 집합 밖이다 (${Object.keys(MECHANISMS).join(", ")}). 새 기제는 규칙 하나 때문에 들어오지 않는다. 멈춰서 묻는다`);
146
+ throw new Error(`규칙 ${rule.name}의 방식이 닫힌 집합 밖이다: ${rule.mechanism} (있는 것: ${Object.keys(MECHANISMS).join(", ")}). 새 방법은 규칙 하나 때문에 더하지 않는다. 멈추고 묻는다`);
143
147
  }
144
148
  }
145
149
 
@@ -153,7 +157,7 @@ export function ruleNames() {
153
157
  /** @param {string} name */
154
158
  export function ruleDoc(name) {
155
159
  const docs = loadRuleDocs();
156
- if (!(name in docs) || !BY_NAME.has(name)) throw new Error(`모르는 규칙: ${name}. hanlint rules 로 목록을 본다`);
160
+ if (!(name in docs) || !BY_NAME.has(name)) throw new Error(`모르는 규칙: ${name}. hanlint rules로 목록을 본다`);
157
161
  return docs[name];
158
162
  }
159
163
 
@@ -162,10 +166,10 @@ export function ruleSummary(name) {
162
166
  return ruleDoc(name).split("\n")[0];
163
167
  }
164
168
 
165
- /** 기술서의 네 절 표. 파이썬 rules/registry.py 의 REQUIRED_SECTIONS 와 같다. */
169
+ /** 기술서의 네 절 표. 파이썬 rules/registry.py의 REQUIRED_SECTIONS와 같다. */
166
170
  export const REQUIRED_SECTIONS = ["왜:", "어디서:", "고치기:", "안 잡는 것:"];
167
171
 
168
- /** 기술서에서 절 하나의 본문을 한 줄로. 파이썬 docSection 과 같다. @param {string} doc @param {string} label */
172
+ /** 기술서에서 절 하나의 본문을 한 줄로. 파이썬 docSection과 같다. @param {string} doc @param {string} label */
169
173
  export function docSection(doc, label) {
170
174
  const collected = [];
171
175
  let inside = false;
@@ -184,41 +188,41 @@ export function docSection(doc, label) {
184
188
  return collected.filter(Boolean).join(" ");
185
189
  }
186
190
 
187
- /** 규칙의 고치기 절 한 줄. primer 가 쓰기 전에 읽히는 그 문장이다. @param {string} name */
191
+ /** 규칙의 고치기 절 한 줄. primer가 쓰기 전에 읽히는 그 문장이다. @param {string} name */
188
192
  export function ruleFix(name) {
189
193
  return docSection(ruleDoc(name), "고치기:");
190
194
  }
191
195
 
192
196
  /**
193
- * 부류의 사람 이름. 파이썬 rules/registry.py 의 CATEGORY_TITLES 와 같은 값과 같은 순서다.
197
+ * 묶음의 사람 이름. 파이썬 rules/registry.py의 CATEGORY_TITLES와 같은 값과 같은 순서다.
194
198
  * @type {Record<string, string>}
195
199
  */
196
200
  export const CATEGORY_TITLES = {
197
- sentence: "문장 안에서 세는 것",
198
- paragraph: "문단 사이에서 세는 것",
199
- structure: "글의 짜임에서 세는 것",
200
- document: "두 자리를 대조해 세는 것",
201
+ sentence: "문장",
202
+ paragraph: "문단",
203
+ structure: "글의 짜임",
204
+ document: "문서",
201
205
  orthography: "표기와 띄어쓰기",
202
- code: "코드 블록 사이를 대조하는 것",
203
- screen: "화면의 글에서 세는 것",
206
+ code: "코드 블록",
207
+ screen: "화면의 글",
204
208
  };
205
209
 
206
210
  /** 규칙이 세는 방법. @param {string} name */
207
211
  export function ruleMechanism(name) {
208
212
  const rule = BY_NAME.get(name);
209
- if (!rule) throw new Error(`모르는 규칙: ${name}. hanlint rules 로 목록을 본다`);
213
+ if (!rule) throw new Error(`모르는 규칙: ${name}. hanlint rules로 목록을 본다`);
210
214
  return rule.mechanism;
211
215
  }
212
216
 
213
- /** 규칙이 사는 부류. 정본은 파이썬 쪽 폴더이고 여기는 그 투영을 읽는다. @param {string} name */
217
+ /** 규칙이 사는 묶음. 정본은 파이썬 쪽 폴더이고 여기는 그 투영을 읽는다. @param {string} name */
214
218
  export function ruleCategory(name) {
215
219
  const categories = loadRuleCategories();
216
- if (!(name in categories)) throw new Error(`모르는 규칙: ${name}. hanlint rules 로 목록을 본다`);
220
+ if (!(name in categories)) throw new Error(`모르는 규칙: ${name}. hanlint rules로 목록을 본다`);
217
221
  return categories[name];
218
222
  }
219
223
 
220
224
  /**
221
- * 인라인 제어가 그 줄에서 그 규칙을 껐는가. `*` 는 전부다.
225
+ * 인라인 제어가 그 줄에서 그 규칙을 껐는가. `*`는 전부다.
222
226
  * @param {import("./finding.js").Finding} f
223
227
  * @param {[string, number, number][]} disabled
224
228
  */
@@ -1,10 +1,11 @@
1
1
  // @ts-check
2
2
  import { overridingFindings } from "../shared/dictionaryRule.js";
3
+ import { NOTICE } from "../finding.js";
3
4
 
4
5
  export const name = "screenWord";
5
6
  export const mechanism = "dictionary";
6
7
 
7
8
  /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
8
9
  export function run(doc) {
9
- return overridingFindings(doc, "screenWord", name);
10
+ return overridingFindings(doc, "screenWord", name, undefined, NOTICE);
10
11
  }
@@ -0,0 +1,11 @@
1
+ // @ts-check
2
+ import { overridingFindings } from "../shared/dictionaryRule.js";
3
+ import { NOTICE } from "../finding.js";
4
+
5
+ export const name = "calqueFrame";
6
+ export const mechanism = "dictionary";
7
+
8
+ /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
9
+ export function run(doc) {
10
+ return overridingFindings(doc, "calqueFrame", name, undefined, NOTICE);
11
+ }
@@ -1,10 +1,14 @@
1
1
  // @ts-check
2
+ import { ERROR } from "../finding.js";
2
3
  import { dictionaryFindings } from "../shared/dictionaryRule.js";
3
4
 
4
5
  export const name = "cliche";
5
6
  export const mechanism = "dictionary";
6
7
 
7
- /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
8
- export function run(doc) {
9
- return dictionaryFindings(doc, "cliches", name);
8
+ /**
9
+ * @param {import("../../fingerprint/build.js").DocumentPrint} doc
10
+ * @param {import("../../config/settings.js").Config} config
11
+ */
12
+ export function run(doc, config) {
13
+ return dictionaryFindings(doc, "cliches", name, ERROR, config);
10
14
  }
@@ -1,4 +1,5 @@
1
1
  // @ts-check
2
+ import { fitJosa } from "../../analysis/grammar/josa.js";
2
3
  import { SENTENCE, finding } from "../finding.js";
3
4
  import { runsOf } from "../shared/repeat.js";
4
5
 
@@ -15,7 +16,7 @@ export function run(doc) {
15
16
  for (const [start, length, connector] of runsOf(keys, 2)) {
16
17
  for (const sentence of sentences.slice(start + 1, start + length)) {
17
18
  findings.push(
18
- finding(name, sentence.line, sentence.text, `\`${connector}\` 로 시작하는 문장이 연달아 온다. 앞에서 만든 것의 이름을 불러 잇는다`, null, "error", SENTENCE, sentence.index),
19
+ finding(name, sentence.line, sentence.text, `\`${connector}\`${fitJosa(connector, "로")} 시작하는 문장이 연달아 온다. 앞에서 만든 것의 이름을 불러 잇는다`, null, "error", SENTENCE, sentence.index),
19
20
  );
20
21
  }
21
22
  }
@@ -32,7 +32,7 @@ export function run(doc) {
32
32
  if (overlap(reader.recent, sentence.topics) > 0) continue;
33
33
  if (isPrompt(reader.previous)) continue;
34
34
  findings.push(
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)),
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)),
36
36
  );
37
37
  }
38
38
  return findings;
@@ -13,7 +13,7 @@ export function run(doc) {
13
13
  block.text.split("\n").forEach((lineText, offset) => {
14
14
  if (DASHES.test(lineText)) {
15
15
  findings.push(
16
- finding(name, block.startLine + offset, lineText.trim(), "긴 줄표다. 부연은 마침표로 끊거나 괄호로, 범위는 물결표 ~ 로 쓴다", null, "error", DOCUMENT, block.index),
16
+ finding(name, block.startLine + offset, lineText.trim(), "긴 줄표다. 덧붙이는 말은 마침표로 끊거나 괄호에 넣고, 범위는 물결표(~)로 쓴다", null, "error", DOCUMENT, block.index),
17
17
  );
18
18
  }
19
19
  });
@@ -16,7 +16,7 @@ export function run(doc) {
16
16
  const reader = doc.reader.beforeSentence[sentence.index];
17
17
  if (overlap(reader.recent, sentence.topics) === 0) continue;
18
18
  findings.push(
19
- finding(name, sentence.line, sentence.text, `\`${sentence.deixis[0]}\` ${fitJosa(sentence.deixis[0], "은")} 독자가 스크롤을 되돌려야 하는 지시어다. 가리키는 파일, 값, 코드의 이름을 쓴다`, null, "error", SENTENCE, sentence.index),
19
+ finding(name, sentence.line, sentence.text, `\`${sentence.deixis[0]}\`${fitJosa(sentence.deixis[0], "은")} 독자가 스크롤을 되돌려야 하는 지시어다. 가리키는 파일, 값, 코드의 이름을 쓴다`, null, "error", SENTENCE, sentence.index),
20
20
  );
21
21
  }
22
22
  return findings;
@@ -1,4 +1,5 @@
1
1
  // @ts-check
2
+ import { fitJosa } from "../../analysis/grammar/josa.js";
2
3
  import { SENTENCE, finding } from "../finding.js";
3
4
 
4
5
  export const name = "doubleNegative";
@@ -24,7 +25,7 @@ export function run(doc) {
24
25
  const start = /** @type {number} */ (match.index);
25
26
  const fix = replacement ? sentence.text.slice(0, start) + replacement + sentence.text.slice(start + match[0].length) : null;
26
27
  findings.push(
27
- finding(name, sentence.line, sentence.text, `\`${match[1]}\` 는 이중 부정이다. 긍정으로 쓴다`, fix, "error", SENTENCE, sentence.index, replacement ? match[1] : null, replacement ?? null),
28
+ finding(name, sentence.line, sentence.text, `\`${match[1]}\`${fitJosa(match[1], "는")} 이중 부정이다. 긍정으로 쓴다`, fix, "error", SENTENCE, sentence.index, replacement ? match[1] : null, replacement ?? null),
28
29
  );
29
30
  }
30
31
  return findings;
@@ -1,4 +1,5 @@
1
1
  // @ts-check
2
+ import { fitJosa } from "../../analysis/grammar/josa.js";
2
3
  import { SENTENCE, finding } from "../finding.js";
3
4
  import { doublePassiveCandidates } from "../shared/candidates.js";
4
5
 
@@ -38,7 +39,7 @@ export function run(doc) {
38
39
  name,
39
40
  sentence.line,
40
41
  sentence.text,
41
- `\`${sentence.passives[0]}\` 는 피동에 어지다 를 또 붙인 이중 피동이다. 하나만 남긴다`,
42
+ `\`${sentence.passives[0]}\`${fitJosa(sentence.passives[0], "는")} 피동에 \`-어지다\`를 또 붙인 이중 피동이다. 하나만 남긴다`,
42
43
  approved ? candidates[0].text : null,
43
44
  "error",
44
45
  SENTENCE,
@@ -1,4 +1,5 @@
1
1
  // @ts-check
2
+ import { fitJosa } from "../../analysis/grammar/josa.js";
2
3
  import { insideAny, matchedSpans } from "../../fingerprint/markers.js";
3
4
  import { SENTENCE, finding } from "../finding.js";
4
5
 
@@ -16,7 +17,7 @@ export function run(doc, config) {
16
17
  for (const [start, end, text] of matchedSpans(sentence.text, "draftHistory.txt")) {
17
18
  if (insideAny(start, end, sentence.quoted)) continue;
18
19
  findings.push(
19
- finding(name, sentence.line, sentence.text, `\`${text}\` 는 글쓴이의 과정이다. 독자는 초고를 본 적이 없다. 알아낸 사실만 결과 문장으로 남긴다`, null, "error", SENTENCE, sentence.index),
20
+ finding(name, sentence.line, sentence.text, `\`${text}\`${fitJosa(text, "는")} 글쓴이의 과정이다. 독자는 초고를 본 적이 없다. 알아낸 사실만 결과 문장으로 남긴다`, null, "error", SENTENCE, sentence.index),
20
21
  );
21
22
  }
22
23
  }
@@ -1,4 +1,5 @@
1
1
  // @ts-check
2
+ import { fitJosa } from "../../analysis/grammar/josa.js";
2
3
  import { NOTICE, SENTENCE, finding } from "../finding.js";
3
4
  import { runsOf } from "../shared/repeat.js";
4
5
 
@@ -16,12 +17,12 @@ export function run(doc, config) {
16
17
  const sentences = section.paragraphs.flatMap((p) => p.sentences);
17
18
  const endings = sentences.map((s) => (COUNTED_ENDINGS.has(s.ending) ? s.ending : `__${s.index}`));
18
19
  for (const [start, length, ending] of runsOf(endings, config.endingRun)) {
19
- // 구간 어딘가에 이유를 잇는 말이나 독자를 부르는 말이 있으면 리듬이 산 것이다. 구간을 쪼개면
20
+ // 구간 어딘가에 인과 접속사나 독자에게 말을 거는 문장이 있으면 리듬이 산 것이다. 구간을 쪼개면
20
21
  // 조각마다 지적이 되어 오히려 늘어난다. 구간 전체를 하나로 보고 살린다.
21
22
  if (sentences.slice(start, start + length).some((s) => s.causal || s.readerCall)) continue;
22
23
  const first = sentences[start];
23
24
  findings.push(
24
- finding(name, first.line, first.text, `\`${ending}\` 로 끝나는 문장 ${length}개에 인과도 질문도 독자를 부르는 말도 없다. 질문, 인과, 행동 동사로 리듬을 바꾼다`, null, NOTICE, SENTENCE, first.index),
25
+ finding(name, first.line, first.text, `\`${ending}\`${fitJosa(ending, "로")} 끝나는 문장 ${length}개에 인과 접속사도 질문도 독자를 부르는 말도 없다. 질문, 인과 접속사, 행동 동사로 리듬을 바꾼다`, null, NOTICE, SENTENCE, first.index),
25
26
  );
26
27
  }
27
28
  }
@@ -11,7 +11,7 @@ export function run(doc) {
11
11
  for (const sentence of doc.sentences) {
12
12
  if (sentence.euiCount >= 3 || (sentence.euiCount >= 2 && euiAdjacent(sentence.text))) {
13
13
  findings.push(
14
- finding(name, sentence.line, sentence.text, `한 문장에 \`의\` 가 ${sentence.euiCount}번이다. 명사를 쌓은 자리라 관계가 표시되지 않는다. 동사로 편다`, null, "error", SENTENCE, sentence.index),
14
+ finding(name, sentence.line, sentence.text, `한 문장에 \`의\`가 ${sentence.euiCount}번이다. 명사를 쌓은 문장이라 관계가 표시되지 않는다. 동사로 편다`, null, "error", SENTENCE, sentence.index),
15
15
  );
16
16
  }
17
17
  }
@@ -12,7 +12,7 @@ export function run(doc) {
12
12
  const match = FILLER.exec(sentence.text);
13
13
  if (!match) continue;
14
14
  findings.push(
15
- finding(name, sentence.line, sentence.text, `\`${match[1]}\` 만 붙여 잇는 문장이다. 앞에서 만든 파일이나 값을 이름으로 다시 부르고 다음 행동을 붙인다`, null, "error", SENTENCE, sentence.index),
15
+ finding(name, sentence.line, sentence.text, `\`${match[1]}\`만 붙여 잇는 문장이다. 앞에서 만든 파일이나 값을 이름으로 다시 언급하고 다음 행동을 붙인다`, null, "error", SENTENCE, sentence.index),
16
16
  );
17
17
  }
18
18
  return findings;
@@ -5,7 +5,10 @@ import { dictionaryFindings } from "../shared/dictionaryRule.js";
5
5
  export const name = "hardWord";
6
6
  export const mechanism = "dictionary";
7
7
 
8
- /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
9
- export function run(doc) {
10
- return dictionaryFindings(doc, "easyWords", name, NOTICE);
8
+ /**
9
+ * @param {import("../../fingerprint/build.js").DocumentPrint} doc
10
+ * @param {import("../../config/settings.js").Config} config
11
+ */
12
+ export function run(doc, config) {
13
+ return dictionaryFindings(doc, "easyWords", name, NOTICE, config);
11
14
  }
@@ -14,7 +14,7 @@ export function run(doc) {
14
14
  const start = /** @type {number} */ (match.index);
15
15
  const fix = sentence.text.slice(0, start) + match[1] + sentence.text.slice(start + match[0].length);
16
16
  findings.push(
17
- finding(name, sentence.line, sentence.text, `\`${match[1]}\` 처럼 명령형과 청유형 뒤에는 마침표를 붙이지 않는다`, fix, "error", SENTENCE, sentence.index, match[0], match[1]),
17
+ finding(name, sentence.line, sentence.text, `명령·청유 문장 끝(\`${match[1]}\`)에 마침표를 붙이지 않는 표기 방침이다`, fix, "error", SENTENCE, sentence.index, match[0], match[1]),
18
18
  );
19
19
  }
20
20
  return findings;
@@ -1,10 +1,14 @@
1
1
  // @ts-check
2
+ import { ERROR } from "../finding.js";
2
3
  import { dictionaryFindings } from "../shared/dictionaryRule.js";
3
4
 
4
5
  export const name = "japaneseLoan";
5
6
  export const mechanism = "dictionary";
6
7
 
7
- /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
8
- export function run(doc) {
9
- return dictionaryFindings(doc, "japaneseLoan", name);
8
+ /**
9
+ * @param {import("../../fingerprint/build.js").DocumentPrint} doc
10
+ * @param {import("../../config/settings.js").Config} config
11
+ */
12
+ export function run(doc, config) {
13
+ return dictionaryFindings(doc, "japaneseLoan", name, ERROR, config);
10
14
  }
@@ -14,7 +14,7 @@ export function run(doc, config) {
14
14
  for (const sentence of doc.sentences) {
15
15
  if (sentence.length > config.longSentenceMax) {
16
16
  findings.push(
17
- finding(name, sentence.line, sentence.text, `어절 ${sentence.length}개다. 상한은 ${config.longSentenceMax}. 마침표로 끊거나 나열이면 목록으로 꺼낸다`, null, NOTICE, SENTENCE, sentence.index, null, null, longSentenceCandidates(sentence.text)),
17
+ finding(name, sentence.line, sentence.text, `어절이 ${sentence.length}개로 상한 ${config.longSentenceMax}개를 넘는다. 마침표로 끊거나 나열이면 목록으로 꺼낸다`, null, NOTICE, SENTENCE, sentence.index, null, null, longSentenceCandidates(sentence.text)),
18
18
  );
19
19
  }
20
20
  }
@@ -24,7 +24,7 @@ export function run(doc) {
24
24
  const current = sentences[i];
25
25
  const following = sentences[i + 1];
26
26
  if (NEGATION.test(current.text) && isDefinition(following.text)) {
27
- findings.push(finding(name, current.line, current.text, "단순한 X 가 아닙니다 뒤에 Y 입니다 로 재정의하는 공식이다. 앞 문장을 지우고 Y 를 바로 쓴다", null, "error", SENTENCE, current.index));
27
+ findings.push(finding(name, current.line, current.text, "`단순한 X가 아닙니다` 뒤에 `Y입니다`로 다시 정의하는 공식이다. 앞 문장을 지우고 Y를 바로 쓴다", null, "error", SENTENCE, current.index));
28
28
  }
29
29
  }
30
30
  }
@@ -1,4 +1,6 @@
1
1
  // @ts-check
2
+ import { nounRuns } from "../../analysis/index.js";
3
+ import { attested, usageKindOf } from "../../usage/counts.js";
2
4
  import { SENTENCE, finding } from "../finding.js";
3
5
 
4
6
  export const name = "nounPile";
@@ -10,12 +12,16 @@ export const mechanism = "threshold";
10
12
  */
11
13
  export function run(doc, config) {
12
14
  const findings = [];
15
+ const kind = usageKindOf(config);
13
16
  for (const sentence of doc.sentences) {
14
- if (sentence.nounRun >= config.nounPileMin) {
15
- findings.push(
16
- finding(name, sentence.line, sentence.text, `명사 ${sentence.nounRun}개가 조사 없이 이어진다. 관계가 표시되지 않아 독자가 조사를 끼워 넣는다. 동사로 되돌린다`, null, config.enforceStyle.includes(name) ? "error" : "notice", SENTENCE, sentence.index),
17
- );
17
+ if (sentence.nounRun < config.nounPileMin) continue;
18
+ if (kind) {
19
+ const piles = nounRuns(sentence.text).filter(([, length]) => length >= config.nounPileMin).map(([chain]) => chain);
20
+ if (piles.length && piles.every((chain) => attested(chain, kind, config.usageMin))) continue;
18
21
  }
22
+ findings.push(
23
+ finding(name, sentence.line, sentence.text, `명사 ${sentence.nounRun}개가 조사 없이 이어진다. 관계가 표시되지 않아 독자가 조사를 끼워 넣는다. 동사로 되돌린다`, null, config.enforceStyle.includes(name) ? "error" : "notice", SENTENCE, sentence.index),
24
+ );
19
25
  }
20
26
  return findings;
21
27
  }
@@ -37,7 +37,7 @@ export function run(doc, config) {
37
37
  if (reader.numerals.has(base) || numeralsIn(sentence.text.slice(0, start)).has(base)) continue;
38
38
  if (isAnchored(sentence.text, start)) continue;
39
39
  findings.push(
40
- finding(name, sentence.line, sentence.text, `\`${base}\` ${fitJosa(base, "가")} 여기서 처음 나오는 값이다. 무엇에서 올라간 것인지 알 수 없다. 그 값이 어디서 나왔는지 밝힌다`, null, NOTICE, SENTENCE, sentence.index),
40
+ finding(name, sentence.line, sentence.text, `\`${base}\`${fitJosa(base, "가")} 여기서 처음 나오는 값이다. 무엇에서 올라간 것인지 알 수 없다. 그 값이 어디서 나왔는지 밝힌다`, null, NOTICE, SENTENCE, sentence.index),
41
41
  );
42
42
  }
43
43
  }