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,5 +1,5 @@
1
1
  // @ts-check
2
- /** Reader Contract 검사와 이유가 붙은 국소 Patch 검증. */
2
+ /** Reader Contract 검사와, 이유를 붙여 한 곳만 바꾸는 Patch의 검증. */
3
3
  import { sha256 } from "../digest.js";
4
4
 
5
5
  import { Patch } from "../config/patch.js";
@@ -19,7 +19,7 @@ function digest(text) {
19
19
  return sha256(text);
20
20
  }
21
21
 
22
- /** 원문의 보호 표면을 모두 덮는 version 1 Contract 초안을 만든다. @param {string} text @param {string} reader @param {string} goal */
22
+ /** 원문의 지켜야 할 숫자·링크·코드를 모두 담는 version 1 Contract 초안을 만든다. @param {string} text @param {string} reader @param {string} goal */
23
23
  export function contractFromText(text, reader, goal) {
24
24
  const contract = new Contract(reader, goal, factLines(text));
25
25
  const result = surfaceDiff(contract.text, text);
@@ -27,15 +27,15 @@ export function contractFromText(text, reader, goal) {
27
27
  for (const kind of ["missingNumbers", "missingUrls", "missingCode", "missingLinks"]) {
28
28
  for (const value of result[/** @type {keyof typeof result} */ (kind)]) missing.push(`${kind}=${value}`);
29
29
  }
30
- if (missing.length) throw new Error(`reader 또는 goal이 원문에 없는 보호 원자를 넣었다: ${missing.join(", ")}`);
30
+ if (missing.length) throw new Error(`reader나 goal에 원문에 없는 숫자·링크·코드가 들었다: ${missing.join(", ")}`);
31
31
  if (surfaceViolationCount(result)) {
32
32
  const issues = Object.entries(result).flatMap(([kind, values]) => values.map((value) => `${kind}=${value}`));
33
- throw new Error(`계약 초안이 원문의 보호 표면을 모두 담지 못했다: ${issues.join(", ")}`);
33
+ throw new Error(`계약 초안이 원문의 지켜야 할 숫자·링크·코드를 모두 담지 못했다: ${issues.join(", ")}`);
34
34
  }
35
35
  return contract;
36
36
  }
37
37
 
38
- /** 원문의 자동 표면과 한 수준의 제목을 분리한 version 2 Contract 초안을 만든다. @param {string} text @param {string} reader @param {string} goal @param {number} [outlineLevel] @param {string[]} [facts] */
38
+ /** 원문에서 자동으로 찾은 숫자·링크·코드와 한 수준의 제목을 따로 담은 version 2 Contract 초안을 만든다. @param {string} text @param {string} reader @param {string} goal @param {number} [outlineLevel] @param {string[]} [facts] */
39
39
  export function contractFromTextV2(text, reader, goal, outlineLevel = 2, facts = []) {
40
40
  const doc = parseMarkdown(text);
41
41
  const headings = headingsOf(doc, outlineLevel).map((heading) => heading.text);
@@ -50,7 +50,7 @@ export function contractFromTextV2(text, reader, goal, outlineLevel = 2, facts =
50
50
  const result = surfaceDiff(contract.text, text);
51
51
  if (surfaceViolationCount(result)) {
52
52
  const issues = Object.entries(result).flatMap(([kind, values]) => values.map((value) => `${kind}=${value}`));
53
- throw new Error(`reader, goal 또는 facts가 원문에 없는 보호 원자를 넣었다: ${issues.join(", ")}`);
53
+ throw new Error(`reader, goal, facts에 원문에 없는 숫자·링크·코드가 들었다: ${issues.join(", ")}`);
54
54
  }
55
55
  return contract;
56
56
  }
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** 선택한 규칙의 Finding 줄과 원문 변경 창을 대조한다. */
2
+ /** 고른 규칙의 지적이 있는 줄과 원문에서 바뀐 범위를 대조한다. */
3
3
  export function editIssues(text, patch, findings, policy) {
4
4
  if (!Object.keys(policy).length) return [];
5
5
  const limits = policy[patch.reason];
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** Reader Contract가 잠근 제목 순서와 글 지문의 구조 요약. */
2
+ /** Reader Contract가 고정한 제목 순서와, 글 분석 결과에서 뽑은 구조 요약. */
3
3
 
4
4
  /** @typedef {{position: number, expected: string | null, actual: string | null}} OutlineMismatch */
5
5
  /** @typedef {{matches: boolean, level: number, expected: string[], actual: string[], mismatches: OutlineMismatch[]}} OutlineDiff */
@@ -1,14 +1,14 @@
1
1
  // @ts-check
2
- /** Reader Contract 검사와 Patch 검증의 결정적 영수증. */
2
+ /** Reader Contract 검사와 Patch 검증의 결과 기록. 같은 입력이면 늘 같은 결과다. */
3
3
 
4
4
  import { findingAsDict } from "../rules/finding.js";
5
5
  import { readerDebts } from "../rules/readerDebt.js";
6
6
  import { compareText, surfaceViolationCount } from "./surface.js";
7
7
 
8
8
  export const CHECK_MEANING = "violationCount는 선언한 보호 원자와 hanlint error의 수다. facts의 관계와 진실, 빠진 의미, 독자 효용과 자연스러움은 검증하지 않는다";
9
- export const CHECK_MEANING_V2 = "violationCount는 선언한 보호 원자, 제목 구조, 잠금 문구 누락과 hanlint error의 수다. facts의 관계와 진실, 빠진 의미, 독자 효용과 자연스러움은 검증하지 않는다";
9
+ export const CHECK_MEANING_V2 = "violationCount는 계약에 적은 숫자·링크·코드의 어긋남, 제목 구조의 어긋남, 빠진 고정 문구, hanlint 오류를 더한 수다. facts 사이의 관계와 사실 여부, 빠진 뜻, 독자에게 쓸모 있는지와 자연스러운지는 검사하지 않는다";
10
10
  export const PATCH_MEANING = "verified는 정확히 한 자리를 바꾸고 명시한 기존 위반을 줄이며 새 보호 원자 위반과 새 error를 만들지 않았다는 뜻뿐이다. 수정문의 의미와 진실, 자연스러움은 승인하지 않는다";
11
- export const PATCH_MEANING_V2 = "verified는 정확히 한 자리를 바꾸고 명시한 기존 위반을 줄이며 새 계약 위반과 새 error를 만들지 않고 선택한 수정 예산을 지켰다는 뜻뿐이다. 수정문의 의미와 진실, 자연스러움은 승인하지 않는다";
11
+ export const PATCH_MEANING_V2 = "verified는 정확히 한 곳을 바꾸고, 적어 둔 기존 위반을 줄이고, 새 계약 위반과 새 오류를 만들지 않고, 고른 수정 예산을 지켰다는 뜻뿐이다. 고친 문장의 뜻이 맞는지, 사실인지, 자연스러운지는 보증하지 않는다";
12
12
 
13
13
  /** @param {import("../rules/finding.js").Finding[]} findings */
14
14
  function errorRules(findings) {
@@ -32,7 +32,7 @@ export class CheckResult {
32
32
  this.document = document;
33
33
  this.missingFacts = missingFacts;
34
34
  if (![1, 2].includes(contractVersion)) {
35
- throw new Error(`check result contractVersion 은 1 또는 2다: ${contractVersion}`);
35
+ throw new Error(`check result의 contractVersion은 1이나 2여야 한다. 받은 값: ${contractVersion}`);
36
36
  }
37
37
  const hasStructure = outline !== null && document !== null;
38
38
  if (hasStructure !== (contractVersion === 2)) {
@@ -89,7 +89,7 @@ export class PatchResult {
89
89
  this.resultText = values.resultText;
90
90
  this.contractVersion = values.contractVersion;
91
91
  if (![1, 2].includes(values.contractVersion)) {
92
- throw new Error(`patch result contractVersion 은 1 또는 2다: ${values.contractVersion}`);
92
+ throw new Error(`patch result의 contractVersion은 1이나 2여야 한다. 받은 값: ${values.contractVersion}`);
93
93
  }
94
94
  }
95
95
 
@@ -137,7 +137,7 @@ export class PatchResult {
137
137
  }
138
138
  }
139
139
 
140
- /** 사람이 결론, 근거, 다음 행동 순서로 읽는 검사 영수증. @param {CheckResult} result */
140
+ /** 사람이 결론, 근거, 다음 할 일의 순서로 읽는 검사 결과. @param {CheckResult} result */
141
141
  export function renderCheck(result) {
142
142
  const lines = [result.violationCount === 0 ? "계약 위반 없음" : `계약 위반 ${result.violationCount}건`];
143
143
  const labels = [
@@ -162,19 +162,19 @@ export function renderCheck(result) {
162
162
  if (result.document) {
163
163
  lines.push(`- 글: 문장 ${result.document.sentenceCount}, 문단 ${result.document.paragraphCount}, 절 ${result.document.sectionCount}`);
164
164
  result.document.sections.forEach((section, index) => {
165
- lines.push(` ${index + 1}. ${section.heading} (${section.line}행, 문단 ${section.paragraphCount}, 코드 ${section.codeBlockCount})`);
165
+ lines.push(` ${index + 1}. ${section.heading} (${section.line}줄, 문단 ${section.paragraphCount}, 코드 ${section.codeBlockCount})`);
166
166
  });
167
167
  }
168
- lines.push(`- lint: error ${result.errorCount}, notice ${result.noticeCount}`);
168
+ lines.push(`- lint: 오류 ${result.errorCount}건, 경고 ${result.noticeCount}건`);
169
169
  if (result.errorCount) {
170
170
  lines.push(` ${Object.entries(errorRules(result.findings)).map(([rule, count]) => `${rule} ${count}`).join(", ")}`);
171
171
  }
172
172
  const debts = readerDebts(result.findings);
173
- lines.push(debts.length ? `- 독자 부채: ${debts.length}건` : "- 독자 부채: 없음");
174
- for (const debt of debts) lines.push(` ${debt.line}행 [${debt.rule}] ${debt.why}`);
175
- if (result.violationCount) lines.push("\n다음: 계약과 글 가운데 틀린 쪽을 바로잡고 같은 check를 다시 실행한다");
176
- else if (result.noticeCount) lines.push("\n다음: notice를 읽고 고칠지 유지할지 판단한 뒤 사람과 LLM 평가로 넘어간다");
177
- else lines.push("\n다음: 세어서 잡히는 계약 위반이 없다. 사람과 LLM 평가로 넘어간다");
173
+ lines.push(debts.length ? `- 독자가 막힐 곳: ${debts.length}건` : "- 독자가 막힐 곳: 없음");
174
+ for (const debt of debts) lines.push(` ${debt.line}줄 [${debt.rule}] ${debt.why}`);
175
+ if (result.violationCount) lines.push("\n다음: 계약과 글 가운데 틀린 쪽을 고치고 같은 check를 다시 돌린다");
176
+ else if (result.noticeCount) lines.push("\n다음: 경고를 읽고 고칠지 그대로 둘지 정한다");
177
+ else lines.push("\n다음: 규칙으로 셀 수 있는 계약 위반은 없다. 뜻과 자연스러움은 사람이 읽고 판단한다");
178
178
  lines.push("", result.contractVersion === 1 ? CHECK_MEANING : CHECK_MEANING_V2);
179
179
  return lines.join("\n");
180
180
  }
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** 계약 본문과 결과 글 사이의 보호 원자 차이. */
2
+ /** 계약 본문과 결과 글 사이에서 지켜야 할 숫자·링크·코드의 차이. */
3
3
 
4
4
  /**
5
5
  * @typedef {object} SurfaceDiff
@@ -107,7 +107,7 @@ export function surfaceViolationCount(diff) {
107
107
  return Object.values(diff).reduce((total, values) => total + values.length, 0);
108
108
  }
109
109
 
110
- /** 원문의 보호 원자를 뜻 없는 정렬 집합으로 분리한다. @param {string} text */
110
+ /** 원문의 지켜야 할 숫자·링크·코드를 뜻과 상관없이 정렬한 집합으로 나눈다. @param {string} text */
111
111
  export function protectedSurface(text) {
112
112
  const surfaceText = text.normalize("NFC");
113
113
  return {
@@ -118,7 +118,7 @@ export function protectedSurface(text) {
118
118
  };
119
119
  }
120
120
 
121
- /** 원문의 보호 표면을 모두 덮는 Contract 사실 후보 줄. @param {string} text */
121
+ /** 원문의 지켜야 할 숫자·링크·코드를 모두 담는 Contract 사실 후보 줄. @param {string} text */
122
122
  export function factLines(text) {
123
123
  const surfaceText = text.normalize("NFC");
124
124
  const seen = new Set();
@@ -144,7 +144,7 @@ export function factLines(text) {
144
144
  ),
145
145
  );
146
146
  if (!uncovered.size) {
147
- throw new Error("원문에 자동으로 보호할 숫자, URL, 인라인 코드, 링크 목적지가 없다. facts를 직접 작성한다");
147
+ throw new Error("원문에 자동으로 지킬 숫자, URL, 인라인 코드, 링크 목적지가 없다. facts를 직접 작성한다");
148
148
  }
149
149
 
150
150
  const selected = new Set();
package/src/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  // @ts-check
2
2
  /**
3
- * hanlint: 한국어 글에서 AI 와 사람이 반복해서 어기는 결함을 결정적으로 잡는 린터. 공개 표면은 이 파일 한 곳이다.
3
+ * hanlint: 한국어 글에서 규칙으로 잡을 수 있는 잘못을 찾는 검사기. 공개 API는 이 파일 한 곳에 있다.
4
4
  *
5
5
  * ```js
6
6
  * import { lintText, lintFile } from "hanlint";
7
7
  * for (const f of lintFile("글.md")) console.log(f.line, f.rule, f.why);
8
8
  * ```
9
9
  *
10
- * 합격과 불합격을 판정하지 않는다. 지적 목록이 비어 있다는 것은 세어서 잡히는 결함이 없다는 뜻이다.
10
+ * 합격과 불합격을 판정하지 않는다. 지적 목록이 비어 있다는 것은 규칙으로 잡을 수 있는 잘못이 없다는 뜻이다.
11
11
  * 같은 공개 API를 Node와 브라우저가 공유한다. 파일 경로를 읽는 API만 Node 전용이다.
12
12
  */
13
13
  import { readExternal } from "./data/read.js";
@@ -26,7 +26,7 @@ import { configFromMapping, defaultConfig } from "./config/settings.js";
26
26
  import { loadVersion } from "./data/load.js";
27
27
  import { parseMarkdown } from "./document/parseMarkdown.js";
28
28
  import { SOURCE_SUFFIXES, replaceLiteral, sourceLiterals } from "./document/sourceText.js";
29
- import { parseSheet, renderSheet, renderSheetJson } from "./report/sheet.js";
29
+ import { applyRows, parseSheet, renderSheet, renderSheetJson, sheetRows } from "./report/sheet.js";
30
30
  import { headingsOf } from "./document/model.js";
31
31
  import { applyFixes } from "./edit/applyFixes.js";
32
32
  import { buildFingerprint } from "./fingerprint/build.js";
@@ -54,6 +54,8 @@ export {
54
54
  parseSheet,
55
55
  renderSheet,
56
56
  renderSheetJson,
57
+ sheetRows,
58
+ applyRows,
57
59
  replaceLiteral,
58
60
  sourceLiterals,
59
61
  CONTRACT_VERSION,
@@ -82,7 +84,7 @@ export {
82
84
  export const version = loadVersion();
83
85
 
84
86
  /**
85
- * 글을 한 번 읽어 지문을 만든다.
87
+ * 글을 한 번 읽어 분석 결과를 만든다.
86
88
  * @param {string} text
87
89
  * @param {import("./config/settings.js").Config} [config]
88
90
  * @param {string | null} [path]
@@ -102,7 +104,7 @@ export function lintText(text, config = defaultConfig(), path = null) {
102
104
  }
103
105
 
104
106
  /**
105
- * 파일을 UTF-8 로 읽어 검사한다.
107
+ * 파일을 UTF-8로 읽어 검사한다.
106
108
  * @param {string} path
107
109
  * @param {import("./config/settings.js").Config} [config]
108
110
  */
@@ -110,7 +112,7 @@ export function lintFile(path, config = defaultConfig()) {
110
112
  return lintText(readExternal(path), config, path);
111
113
  }
112
114
 
113
- /** 지적, 문체 본보기, 승인 고침과 지문을 같은 분석에서 제공한다. @param {string} text @param {import("./config/settings.js").Config} config */
115
+ /** 지적, 문체 예시, 승인 고침과 분석 결과를 같은 분석에서 준다. @param {string} text @param {import("./config/settings.js").Config} config */
114
116
  export function inspectText(text, config = defaultConfig()) {
115
117
  const document = fingerprint(text, config);
116
118
  const findings = runAll(document, config);
@@ -130,7 +132,7 @@ export function learnText(before, after, config = defaultConfig()) {
130
132
  };
131
133
  }
132
134
 
133
- /** 자유 원고의 표면과 H2 순서를 비교한다. 빈 글과 반복 제목도 관찰하며 승인 계약을 만들지 않는다. @param {string} before @param {string} after @param {import("./config/settings.js").Config} config */
135
+ /** 자유 원고의 숫자·링크·코드와 H2 순서를 비교한다. 빈 글과 반복 제목도 관찰하며 승인 계약을 만들지 않는다. @param {string} before @param {string} after @param {import("./config/settings.js").Config} config */
134
136
  export function compareRevision(before, after, config = defaultConfig()) {
135
137
  const original = before.normalize("NFC");
136
138
  const outline = { level: 2, headings: headingsOf(parseMarkdown(original), 2).map((heading) => heading.text) };
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** Python learn/edits.py와 같은 원문 문장 대응 및 지적 소멸 후보다. */
2
+ /** Python learn/edits.py와 같은 방법으로 문장을 짝짓고, 지적이 사라진 문장 짝을 후보로 낸다. */
3
3
  import { sourceSentenceTexts } from "../fingerprint/sourceSentence.js";
4
4
  import { readerKind } from "../fingerprint/readerState.js";
5
5
  import { localCue, SENTENCE } from "../rules/finding.js";
@@ -21,7 +21,7 @@ export function learnExemplars(beforeDoc, afterDoc, beforeFindings, afterFinding
21
21
  if (!afterText || before.text.trim() === afterPlain) continue;
22
22
  seen.add(key);
23
23
  learned.push({ rule: finding.rule, before: (rawBefore.get(before.index) ?? before.text).trim(), after: afterText,
24
- moved: "실제 수정본의 문장으로 바꿈", why: finding.why, beforeLine: before.line,
24
+ moved: "실제로 고친 문장으로 바꿨다", why: finding.why, beforeLine: before.line,
25
25
  afterLines: [...new Set(after.map((item) => item.line))], sentence: before.text.trim(),
26
26
  cue: localCue(finding), reader: readerKind(before, beforeDoc.reader.beforeSentence[finding.at]),
27
27
  ...(preset ? { presets: [preset] } : {}),
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** Python learn/operations.py의 표면 치환 후보와 증거 묶음. */
2
+ /** Python learn/operations.py의 치환 후보와 증거 묶음. */
3
3
  import { operationFromApproval } from "../data/operations.js";
4
4
  import { sourceSentenceTexts } from "../fingerprint/sourceSentence.js";
5
5
  import { changedSentencePairs } from "./pairs.js";
package/src/regex.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
  /**
3
- * 파이썬 re 와 같은 뜻으로 쓰는 얇은 껍질. search 는 첫 자리, at 은 문자열 첫머리에서만, all 은 전부.
3
+ * 파이썬 re와 같은 뜻으로 쓰는 얇은 껍질. search는 첫 일치, at은 문자열 첫머리에서만, all은 전부.
4
4
  * 정규식 문법은 데이터 파일과 규칙에서 파이썬과 같은 부분집합만 쓴다 (lookbehind 포함, 이름 그룹 없음).
5
5
  */
6
6
 
@@ -41,7 +41,7 @@ export function compile(source, flags = "") {
41
41
  };
42
42
  }
43
43
 
44
- /** 정규식 특수문자를 이스케이프한다 (파이썬 re.escape 의 쓰임새). @param {string} text */
44
+ /** 정규식 특수문자를 이스케이프한다 (파이썬 re.escape의 쓰임새). @param {string} text */
45
45
  export function escape(text) {
46
46
  return text.replace(SPECIALS, (c) => BACKSLASH + c);
47
47
  }
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
- /** 한 줄에 지적 하나. AI 와 스크립트가 읽는 꼴이다. 파이썬 report/compactReport.py 와 같은 글자다. */
2
+ /** 한 줄에 지적 하나. AI와 스크립트가 읽는 형식이다. 파이썬 report/compactReport.py와 같은 글자다. */
3
3
 
4
- /** 줄바꿈과 이어진 공백을 하나로 눕힌다. 한 줄은 이 꼴의 계약이다. @param {string} text */
4
+ /** 줄바꿈과 이어진 공백을 하나로 눕힌다. 한 줄은 이 형식의 계약이다. @param {string} text */
5
5
  function flat(text) {
6
6
  return text.split(/\s+/).filter(Boolean).join(" ");
7
7
  }
@@ -9,6 +9,10 @@ function flat(text) {
9
9
  /** @param {string} path @param {import("../rules/finding.js").Finding[]} findings */
10
10
  export function renderCompact(path, findings) {
11
11
  return findings
12
- .map((f) => `${path}:${f.line} [${f.rule}] ${f.severity === "notice" ? "notice: " : ""}${flat(f.why)}` + (f.fix ? ` 고친 뒤: ${flat(f.fix)}` : ""))
12
+ .map((f) => {
13
+ const label = f.severity === "notice" ? "경고: " : "";
14
+ const fixLabel = f.severity === "notice" ? "제안" : "고친 뒤";
15
+ return `${path}:${f.line} [${f.rule}] ${label}${flat(f.why)}` + (f.fix ? ` ${fixLabel}: ${flat(f.fix)}` : "");
16
+ })
13
17
  .join("\n");
14
18
  }
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  /**
3
- * 지문 계층의 JSON 꼴. 파이썬 report/fingerprintJson.py 와 같은 키 순서다. 층마다 한 번씩만 적고 위층은
4
- * 아래층을 index 로 가리킨다. 실수는 소수 여섯째 자리로 맞춰 두 구현이 같은 글자를 낸다.
3
+ * 지문 계층의 JSON 꼴. 파이썬 report/fingerprintJson.py와 같은 키 순서다. 층마다 한 번씩만 적고 위층은
4
+ * 아래층을 index로 가리킨다. 실수는 소수 여섯째 자리로 맞춰 두 구현이 같은 글자를 낸다.
5
5
  */
6
6
 
7
7
  import { roundHalfEven } from "../text.js";
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** GitHub Actions 의 워크플로 명령 꼴. PR 에 줄 단위 주석이 붙는다. */
2
+ /** GitHub Actions의 워크플로 명령 꼴. PR에 줄 단위 주석이 붙는다. */
3
3
 
4
4
  /** @param {string} path @param {import("../rules/finding.js").Finding[]} findings */
5
5
  export function renderGithub(path, findings) {
@@ -1,8 +1,8 @@
1
1
  // @ts-check
2
2
  /**
3
- * 기계가 읽는 꼴. 평가 루프의 0층 입력이다. 파이썬 json.dumps(indent=2, ensure_ascii=False) 와 같은 글자다.
4
- * 지적마다 `exemplar` 를 붙인다. 승인 원문 완전 일치 `patch`는 맞는 지적에만 별도로 붙인다.
5
- * 실제 수정 차이는 파이썬의 exemplarLift와 patchMemory 탐침에서 따로 잰다.
3
+ * 기계가 읽는 형식. 평가 루프의 0층 입력이다. 파이썬 json.dumps(indent=2, ensure_ascii=False)와 같은 글자다.
4
+ * 지적마다 `exemplar`를 붙인다. 승인 원문 완전 일치 `patch`는 맞는 지적에만 별도로 붙인다.
5
+ * 실제 수정 차이는 파이썬의 exemplarLift와 patchMemory 실험 스크립트에서 따로 잰다.
6
6
  */
7
7
  import { exemplarFor } from "../data/exemplars.js";
8
8
  import { findingAsDict } from "../rules/finding.js";
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** 한 문체로 저장한 본보기와 문형을 검사한 글의 문체에 맞춘다. */
2
+ /** 한 문체로 저장한 예시와 문형을 검사한 글의 문체에 맞춘다. */
3
3
  import { HAPNIDA, REGISTERS, convertTemplate } from "../analysis/grammar/index.js";
4
4
 
5
5
  /** 섞임과 없음은 데이터 정본의 합니다체를 유지한다. @param {string | null | undefined} register */
@@ -14,7 +14,7 @@ export function exemplarInRegister(exemplar, register) {
14
14
  const after = convertTemplate(exemplar.after, target);
15
15
  if (before.skipped || after.skipped) {
16
16
  throw new Error(
17
- exemplar.rule + " 본보기의 서술어를 못 풀었다: " + (before.skipped + after.skipped),
17
+ exemplar.rule + " 예시의 서술어를 못 풀었다: " + (before.skipped + after.skipped),
18
18
  );
19
19
  }
20
20
  return { rule: exemplar.rule, before: before.text, after: after.text, moved: exemplar.moved, presets: exemplar.presets };
Binary file
@@ -1,12 +1,12 @@
1
1
  // @ts-check
2
2
  /**
3
- * 사람이 읽는 지적 목록. `파일:줄` 로 시작해 편집기가 바로 연다. 파이썬 report/textReport.py 와 같은 글자다.
4
- * 끝에 본보기를 붙인다. 전과 후를 제 줄에 둔다. 왜 한 줄이 아닌지는 파이썬 report/textReport.py 가 소유한다.
3
+ * 사람이 읽는 지적 목록. `파일:줄`로 시작해 편집기가 바로 연다. 파이썬 report/textReport.py와 같은 글자다.
4
+ * 끝에 예시를 붙인다. 전과 후를 제 줄에 둔다. 왜 한 줄이 아닌지는 파이썬 report/textReport.py가 소유한다.
5
5
  */
6
6
  import { exemplarFor, shortened, twoLines } from "../data/exemplars.js";
7
7
  import { exemplarInRegister } from "./registerMatch.js";
8
8
 
9
- /** 그 글에 나온 규칙의 본보기. 이름 순이고 규칙 하나에 세 줄이다. @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} register @param {string | null | undefined} preset @param {import("../data/exemplars.js").Exemplar[]} customExemplars */
9
+ /** 그 글에 나온 규칙의 예시. 이름 순이고 규칙 하나에 세 줄이다. @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} register @param {string | null | undefined} preset @param {import("../data/exemplars.js").Exemplar[]} customExemplars */
10
10
  function exemplarLines(findings, register, preset, customExemplars) {
11
11
  const lines = [];
12
12
  let cut = false;
@@ -19,15 +19,15 @@ function exemplarLines(findings, register, preset, customExemplars) {
19
19
  lines.push(` [${name}]`, ` 전 ${before}`, ` 후 ${after}`);
20
20
  }
21
21
  if (!lines.length) return [];
22
- const head = "본보기 (고치기 전, 고친 뒤)";
23
- return [cut ? `${head}. 잘린 것은 hanlint explain <규칙>` : head, ...lines];
22
+ const head = "예시 (고치기 전, 고친 뒤)";
23
+ return [cut ? `${head}. 잘린 것은 hanlint explain <규칙>으로 본다` : head, ...lines];
24
24
  }
25
25
 
26
- /** 본보기 아래에 붙이는 선택지. 지적 자리별이며 순위는 없다. @param {import("../rules/finding.js").Finding[]} findings */
26
+ /** 예시 아래에 붙이는 선택지. 지적 자리별이며 순위는 없다. @param {import("../rules/finding.js").Finding[]} findings */
27
27
  function candidateLines(findings) {
28
28
  const chosen = findings.filter((finding) => finding.candidates.length);
29
29
  if (!chosen.length) return [];
30
- const lines = ["후보 (기계가 고르지 않음)"];
30
+ const lines = ["후보 (hanlint가 하나를 고르지 않았다)"];
31
31
  for (const finding of chosen) {
32
32
  lines.push(` ${finding.line}줄 [${finding.rule}]`);
33
33
  for (const candidate of finding.candidates) lines.push(` - ${candidate.text} (${candidate.why})`);
@@ -35,21 +35,40 @@ function candidateLines(findings) {
35
35
  return lines;
36
36
  }
37
37
 
38
- /** @param {string} path @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} [register] @param {string | null | undefined} [preset] @param {import("../data/exemplars.js").Exemplar[]} [customExemplars] */
39
- export function renderText(path, findings, register = null, preset = null, customExemplars = []) {
40
- if (!findings.length) return `${path} 집은 자리 없음`;
38
+ /** 접은 경고. 규칙마다 개수와 줄 번호 한 줄. 뜻은 파이썬 textReport.py의 noticeLines가 소유한다. @param {import("../rules/finding.js").Finding[]} notices */
39
+ function noticeLines(notices) {
40
+ /** @type {Map<string, number[]>} */
41
+ const byRule = new Map();
42
+ for (const finding of notices) {
43
+ if (!byRule.has(finding.rule)) byRule.set(finding.rule, []);
44
+ /** @type {number[]} */ (byRule.get(finding.rule)).push(finding.line);
45
+ }
46
+ const lines = [`경고 ${notices.length}건 (접어 두었다. 다 보려면 --notices)`];
47
+ for (const rule of [...byRule.keys()].sort()) {
48
+ const numbers = [...new Set(/** @type {number[]} */ (byRule.get(rule)))].sort((a, b) => a - b);
49
+ lines.push(` ${rule} ${/** @type {number[]} */ (byRule.get(rule)).length} ${numbers.join(", ")}줄`);
50
+ }
51
+ return lines;
52
+ }
53
+
54
+ /** @param {string} path @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} [register] @param {string | null | undefined} [preset] @param {import("../data/exemplars.js").Exemplar[]} [customExemplars] @param {boolean} [unfoldNotices] */
55
+ export function renderText(path, findings, register = null, preset = null, customExemplars = [], unfoldNotices = false) {
56
+ if (!findings.length) return `${path} 오류 없음`;
41
57
  const errors = findings.filter((f) => f.severity === "error").length;
42
58
  const notices = findings.length - errors;
43
- const summary = `${path} 집은 자리 ${errors}` + (notices ? `, 확인할 자리 ${notices}` : "");
59
+ const summary = `${path} 오류 ${errors}건` + (notices ? `, 경고 ${notices}건` : "");
44
60
  const lines = [summary, ""];
45
- for (const f of findings) {
46
- const tag = `[${f.rule}]` + (f.severity === "notice" ? " 확인" : "");
61
+ const folded = unfoldNotices ? [] : findings.filter((f) => f.severity === "notice");
62
+ const shown = findings.filter((f) => unfoldNotices || f.severity === "error");
63
+ for (const f of shown) {
64
+ const tag = `[${f.rule}]` + (f.severity === "notice" ? " 경고" : "");
47
65
  lines.push(`${path}:${f.line} ${tag}`, ` ${f.quote}`, ` ${f.why}`);
48
- if (f.fix) lines.push(` 고친 뒤: ${f.fix}`);
66
+ if (f.fix) lines.push(` ${f.severity === "notice" ? "제안" : "고친 뒤"}: ${f.fix}`);
49
67
  lines.push("");
50
68
  }
51
- lines.push(...exemplarLines(findings, register, preset, customExemplars));
52
- const candidates = candidateLines(findings);
69
+ if (folded.length) lines.push(...noticeLines(folded), "");
70
+ lines.push(...exemplarLines(shown, register, preset, customExemplars));
71
+ const candidates = candidateLines(shown);
53
72
  if (candidates.length) lines.push("", ...candidates);
54
73
  return lines.join("\n").replace(/\n+$/, "");
55
74
  }
@@ -27,21 +27,21 @@ function percent(value) {
27
27
  function selectedProfile(config) {
28
28
  if (config.profile) return userProfile(config.profile);
29
29
  const kind = PROFILE_OF[config.preset];
30
- if (kind === null) throw new Error(`${config.preset} 은 종류 프로파일이 없어 사양을 낼 수 없다`);
30
+ if (kind === null) throw new Error(`${config.preset} 프리셋은 비교할 프로파일이 없어 스펙을 낼 수 없다`);
31
31
  const profile = profileOf(kind);
32
32
  if (profile === null) throw new Error(`${kind} 프로파일을 찾지 못했다`);
33
33
  return profile;
34
34
  }
35
35
 
36
36
  /**
37
- * 현재 규칙판을 쓰기 전 숫자 사양으로 편다. 품질 점수나 새 임계는 만들지 않는다.
37
+ * 현재 규칙 세트를 쓰기 전 숫자 사양으로 편다. 품질 점수나 새 임계는 만들지 않는다.
38
38
  * @param {import("./settings.js").Config} [config]
39
39
  * @param {string} [register]
40
40
  * @param {number | null} [targetChars]
41
41
  */
42
42
  export function writingSpec(config = defaultConfig(), register = HAPNIDA, targetChars = null) {
43
- if (!REGISTERS.includes(register)) throw new Error(`register 는 ${REGISTERS.join(", ")} 가운데 하나다: ${register}`);
44
- if (targetChars !== null && targetChars <= 0) throw new Error(`chars 는 1 이상이어야 한다: ${targetChars}`);
43
+ if (!REGISTERS.includes(register)) throw new Error(`register는 ${REGISTERS.join(", ")} 가운데 하나여야 한다. 받은 값: ${register}`);
44
+ if (targetChars !== null && targetChars <= 0) throw new Error(`chars는 1 이상이어야 한다. 받은 값: ${targetChars}`);
45
45
  const profile = selectedProfile(config);
46
46
  const sentence = profile.sentence;
47
47
  const paragraph = profile.paragraph.get("sentenceCount");
@@ -62,7 +62,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
62
62
  }
63
63
  rows.push({ id: "amount", label: "분량", guidance: amount, basis: [`profile.${profile.kind}.paragraph.sentenceCount`] });
64
64
 
65
- let length = `중앙 ${lengthP50}어절, 문장 90%가 ${percentile(lengthHistogram, 90)}어절 이하`;
65
+ let length = `중앙값 ${lengthP50}어절, 문장 90%가 ${percentile(lengthHistogram, 90)}어절 이하`;
66
66
  const lengthBasis = [`profile.${profile.kind}.sentence.length`];
67
67
  if (enabled(config, "longSentence")) {
68
68
  length += `. ${config.longSentenceMax}어절을 넘으면 longSentence가 지적`;
@@ -71,7 +71,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
71
71
  rows.push({ id: "sentenceLength", label: "문장 길이", guidance: length, basis: lengthBasis });
72
72
 
73
73
  if (profile.endingRuns !== null) {
74
- let endingText = `같은 끝맺음 연속 중앙 ${percentile(profile.endingRuns, 50)}개, 90%가 ${percentile(profile.endingRuns, 90)}개 이하`;
74
+ let endingText = `같은 끝맺음 연속 중앙값 ${percentile(profile.endingRuns, 50)}개, 90%가 ${percentile(profile.endingRuns, 90)}개 이하`;
75
75
  const endingBasis = [`profile.${profile.kind}.endingRuns`];
76
76
  if (enabled(config, "endingRepeat")) {
77
77
  endingText += `. 인과나 독자 호출 없이 ${config.endingRun}개부터 endingRepeat가 지적`;
@@ -85,13 +85,13 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
85
85
  rows.push({
86
86
  id: "commas",
87
87
  label: "쉼표",
88
- guidance: `문장 중앙 ${percentile(commas, 50)}개, 90%가 ${percentile(commas, 90)}개 이하, 99%가 ${percentile(commas, 99)}개 이하`,
88
+ guidance: `문장 중앙값 ${percentile(commas, 50)}개, 90%가 ${percentile(commas, 90)}개 이하, 99%가 ${percentile(commas, 99)}개 이하`,
89
89
  basis: [`profile.${profile.kind}.sentence.commas`],
90
90
  });
91
91
 
92
92
  const eui = sentence.get("euiCount");
93
93
  if (!eui) throw new Error("프로파일에 sentence.euiCount가 없다");
94
- let euiText = `문장 중앙 ${percentile(eui, 50)}개, 90%가 ${percentile(eui, 90)}개 이하`;
94
+ let euiText = `문장 중앙값 ${percentile(eui, 50)}개, 90%가 ${percentile(eui, 90)}개 이하`;
95
95
  const euiBasis = [`profile.${profile.kind}.sentence.euiCount`];
96
96
  if (enabled(config, "euiChain")) {
97
97
  euiText += ". 3개부터, 붙은 2개부터 euiChain이 지적";
@@ -114,7 +114,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
114
114
  rows.push({
115
115
  id: "newTopics",
116
116
  label: "새 화제",
117
- guidance: `문장 중앙 ${percentile(newTopics, 50)}개, 90%가 ${percentile(newTopics, 90)}개 이하`,
117
+ guidance: `문장 중앙값 ${percentile(newTopics, 50)}개, 90%가 ${percentile(newTopics, 90)}개 이하`,
118
118
  basis: [`profile.${profile.kind}.sentence.newTopics`],
119
119
  });
120
120
 
@@ -122,7 +122,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
122
122
  rows.push({
123
123
  id: "connector",
124
124
  label: "문두 접속",
125
- guidance: `문서 중앙은 문장의 ${percent(connector.p50)}%, 문서 90%가 ${percent(connector.p90)}% 이하`,
125
+ guidance: `문서 중앙값은 문장의 ${percent(connector.p50)}%, 문서 90%가 ${percent(connector.p90)}% 이하`,
126
126
  basis: [`profile.${profile.kind}.rates.connector`],
127
127
  });
128
128
 
@@ -132,8 +132,8 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
132
132
  const numberP90 = percentile(numbers, 90);
133
133
  const numberText =
134
134
  numberP50 === 0 && numberP90 === 0
135
- ? "문장 중앙과 90%에서 숫자 없음"
136
- : `문장 중앙 ${numberP50}개, 90%가 ${numberP90}개 이하`;
135
+ ? "문장 중앙값과 90%에서 숫자 없음"
136
+ : `문장 중앙값 ${numberP50}개, 90%가 ${numberP90}개 이하`;
137
137
  rows.push({ id: "numbers", label: "숫자", guidance: numberText, basis: [`profile.${profile.kind}.sentence.numbers`] });
138
138
 
139
139
  const question = profile.rates.question;
@@ -142,10 +142,10 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
142
142
  if (enabled(config, "noQuestion")) {
143
143
  questionText = config.enforceStyle.includes("noQuestion")
144
144
  ? "선택한 정책: 절이 2개 이상이면 글 전체에 물음표 최소 1개"
145
- : "물음표 부재는 notice다. 질문 없이 충분히 설명했다면 유지한다";
145
+ : "물음표가 없으면 경고다. 질문 없이 충분히 설명했다면 그대로 둔다";
146
146
  questionBasis = [`profile.${profile.kind}.rates.question`, "rule.noQuestion"];
147
147
  } else {
148
- questionText = `규칙 요구 없음. 문서 중앙은 문장의 ${percent(question.p50)}%, 문서 90%가 ${percent(question.p90)}% 이하`;
148
+ questionText = `규칙 요구 없음. 문서 중앙값은 문장의 ${percent(question.p50)}%, 문서 90%가 ${percent(question.p90)}% 이하`;
149
149
  questionBasis = [`profile.${profile.kind}.rates.question`];
150
150
  }
151
151
  rows.push({ id: "question", label: "물음표", guidance: questionText, basis: questionBasis });
@@ -161,14 +161,14 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
161
161
  };
162
162
  }
163
163
 
164
- /** 사람이 읽는 꼴. basis는 JSON에서 확인한다. @param {ReturnType<typeof writingSpec>} spec */
164
+ /** 사람이 읽는 형식. basis는 JSON에서 확인한다. @param {ReturnType<typeof writingSpec>} spec */
165
165
  export function renderWritingSpec(spec) {
166
166
  const target = spec.targetChars !== null ? `, ${spec.targetChars}자` : "";
167
167
  const lines = [
168
- `hanlint spec ${spec.preset} 종류, ${spec.register}체${target}. 같은 규칙판의 임계와 ${spec.profile} 프로파일을 쓰기 전 숫자로 편다`,
168
+ `hanlint spec ${spec.preset} 종류, ${spec.register}체${target}. 같은 규칙의 기준값과 ${spec.profile} 글의 평소 수치를 쓰기 전에 보여 준다`,
169
169
  ];
170
170
  const width = Math.max(...spec.rows.map((row) => row.label.length));
171
171
  for (const row of spec.rows) lines.push(` ${row.label.padEnd(width)} ${row.guidance}`);
172
- lines.push("", "이 사양은 품질 점수가 아니다. 쓴 뒤 같은 설정으로 hanlint <글.md>를 실행해 Finding을 확인한다");
172
+ lines.push("", "이 스펙은 품질 점수가 아니다. 다 쓴 뒤 같은 설정으로 hanlint <글.md>를 실행해 지적을 확인한다");
173
173
  return lines.join("\n");
174
174
  }
@@ -42,7 +42,7 @@ export function run(doc) {
42
42
  if (!isDataFile(path)) continue;
43
43
  const name2 = fileName(path);
44
44
  if (have.has(name2) || doc.reader.mentionedBefore(block.index, name2)) continue;
45
- findings.push(finding(name, line, code.trim(), `\`${name2}\` 을 읽는데 글 어디에서도 만들지 않았다. 독자는 여기서 파일 없음 오류로 멈춘다`, null, "error", DOCUMENT, block.index));
45
+ findings.push(finding(name, line, code.trim(), `\`${name2}\` 파일을 읽는데 글 어디에서도 만들지 않았다. 독자는 여기서 파일 없음 오류로 멈춘다`, null, "error", DOCUMENT, block.index));
46
46
  }
47
47
  for (const path of [...writeTargets(code), ...reads]) {
48
48
  if (NOT_A_PATH.some((mark) => path.includes(mark))) continue;
@@ -108,7 +108,7 @@ export function run(doc) {
108
108
  const acceptable = packageOf().get(top) ?? new Set([normalize(top)]);
109
109
  if (![...acceptable].some((pkg) => packages.has(pkg))) {
110
110
  const packageNames = [...acceptable].sort().join("`, `");
111
- findings.push(finding(name, line, code.trim(), `\`${top}\` 를 import 하는데 설치 줄에 허용되는 이름 \`${packageNames}\` 가 없다. 독자는 ModuleNotFoundError 에서 멈춘다`, null, "error", DOCUMENT, block.index));
111
+ findings.push(finding(name, line, code.trim(), `\`${top}\` 모듈을 import하는데 설치 줄에 그 모듈이 든 패키지(\`${packageNames}\`)가 없다. 독자는 ModuleNotFoundError에서 멈춘다`, null, "error", DOCUMENT, block.index));
112
112
  }
113
113
  }
114
114
  }
@@ -120,7 +120,7 @@ export function run(doc) {
120
120
  const name2 = normalize(parsed[1]);
121
121
  const extra = parsed[2] ? normalize(parsed[2]) : null;
122
122
  if (packages.has(name2) && (extra === null || (packages.get(name2)?.has(extra) ?? false))) continue;
123
- findings.push(finding(name, line, code.trim(), `이 줄은 \`${requirement}\` 가 있어야 돈다. ${why}. 설치 줄에 없다`, null, NOTICE, DOCUMENT, block.index));
123
+ findings.push(finding(name, line, code.trim(), `이 줄은 \`${requirement}\` 패키지가 있어야 실행되는데 설치 줄에 없다. ${why}`, null, NOTICE, DOCUMENT, block.index));
124
124
  }
125
125
  }
126
126
  }
@@ -27,7 +27,7 @@ export function run(doc) {
27
27
  for (const [pattern, source, platform, alternative] of platformApis()) {
28
28
  pattern.lastIndex = 0;
29
29
  if (pattern.test(code)) {
30
- findings.push(finding(name, line, code.trim(), `\`${source}\` 은 ${platform} 전용이다. 다른 운영체제 독자는 여기서 멈춘다. ${alternative}`, null, NOTICE, DOCUMENT, block.index));
30
+ findings.push(finding(name, line, code.trim(), `\`${source}\` 부분은 ${platform} 전용이다. 다른 운영체제의 독자는 여기서 멈춘다. ${alternative}`, null, NOTICE, DOCUMENT, block.index));
31
31
  break;
32
32
  }
33
33
  }
@@ -1,4 +1,5 @@
1
1
  // @ts-check
2
+ import { fitJosa } from "../../analysis/grammar/josa.js";
2
3
  import { DOCUMENT, finding } from "../finding.js";
3
4
 
4
5
  export const name = "countMismatch";
@@ -50,7 +51,7 @@ export function run(doc, config) {
50
51
  const conflict = promises.slice(i + 1).find((p) => p[1] !== numberA && comparable(doc, lineA, p[0], config.countMismatchSpan));
51
52
  if (!conflict) continue;
52
53
  const [lineB, , textB] = conflict;
53
- findings.push(finding(name, lineB, textB, `${lineA}번째 줄은 \`${textA}\` 인데 여기는 \`${textB}\` 다. 같은 단위로 다른 수를 약속하면 독자가 어느 쪽을 믿을지 모른다`, null, "error", DOCUMENT, -1));
54
+ findings.push(finding(name, lineB, textB, `${lineA}번째 줄은 \`${textA}\`${fitJosa(textA, "이라고")} 했는데 여기서는 \`${textB}\`${fitJosa(textB, "이라고")} 한다. 같은 단위로 다른 수를 약속하면 독자가 어느 쪽을 믿을지 모른다`, null, "error", DOCUMENT, -1));
54
55
  break;
55
56
  }
56
57
  }