hanlint 0.0.18 → 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 (125) hide show
  1. package/README.md +17 -16
  2. package/data/calqueFrame.json +20 -20
  3. package/data/cliches.json +33 -33
  4. package/data/confusable.json +20 -20
  5. package/data/easyWords.json +12 -12
  6. package/data/exemplars.json +30 -30
  7. package/data/hiddenDeps.txt +11 -11
  8. package/data/japaneseLoan.json +27 -27
  9. package/data/koreanProse.txt +31 -31
  10. package/data/patterns.json +26 -26
  11. package/data/phraseRepeat.json +2 -2
  12. package/data/platformApis.txt +11 -11
  13. package/data/redundantPair.json +16 -16
  14. package/data/ruleDocs.json +56 -56
  15. package/data/screenNarration.json +10 -10
  16. package/data/screenSentence.json +12 -12
  17. package/data/screenTone.json +6 -6
  18. package/data/screenWord.json +70 -70
  19. package/data/spacing.json +11 -11
  20. package/data/spelling.json +29 -29
  21. package/data/translationese.json +23 -24
  22. package/data/usageCounts.report.json +2 -2
  23. package/data/version.json +1 -1
  24. package/package.json +1 -1
  25. package/src/analysis/grammar/ending.js +1 -1
  26. package/src/analysis/grammar/hangul.js +4 -4
  27. package/src/analysis/grammar/josa.js +1 -1
  28. package/src/analysis/grammar/register.js +1 -1
  29. package/src/analysis/grammar/voice.js +1 -1
  30. package/src/analysis/index.js +2 -2
  31. package/src/analysis/josa.js +1 -1
  32. package/src/analysis/splitSentences.js +1 -1
  33. package/src/analysis/tokenize.js +13 -13
  34. package/src/baseline/store.js +8 -8
  35. package/src/cli/help.js +11 -11
  36. package/src/cli/main.js +135 -124
  37. package/src/cli/walk.js +1 -1
  38. package/src/cli/welcome.js +8 -8
  39. package/src/config/loadConfig.js +3 -3
  40. package/src/config/patch.js +6 -6
  41. package/src/config/readerContract.js +26 -26
  42. package/src/config/settings.js +30 -28
  43. package/src/config/toml.js +9 -9
  44. package/src/data/exemplars.js +21 -21
  45. package/src/data/load.js +6 -6
  46. package/src/data/operations.js +14 -14
  47. package/src/data/patches.js +10 -10
  48. package/src/data/patterns.js +5 -5
  49. package/src/data/profiles.js +5 -5
  50. package/src/data/prose.js +5 -5
  51. package/src/document/model.js +4 -4
  52. package/src/document/parseMarkdown.js +7 -7
  53. package/src/document/plainText.js +3 -3
  54. package/src/document/sourceText.js +24 -24
  55. package/src/edit/applyFixes.js +0 -0
  56. package/src/fingerprint/build.js +6 -6
  57. package/src/fingerprint/codeBlocks.js +3 -3
  58. package/src/fingerprint/codeMarkers.js +4 -4
  59. package/src/fingerprint/dictionaries.js +7 -7
  60. package/src/fingerprint/markers.js +7 -7
  61. package/src/fingerprint/readerState.js +9 -9
  62. package/src/fingerprint/sourceSentence.js +1 -1
  63. package/src/fingerprint/topics.js +1 -1
  64. package/src/guard/contract.js +6 -6
  65. package/src/guard/editLocality.js +1 -1
  66. package/src/guard/outline.js +1 -1
  67. package/src/guard/receipt.js +13 -13
  68. package/src/guard/surface.js +4 -4
  69. package/src/index.js +6 -6
  70. package/src/learn/edits.js +2 -2
  71. package/src/learn/operations.js +1 -1
  72. package/src/regex.js +2 -2
  73. package/src/report/compactReport.js +3 -3
  74. package/src/report/fingerprintJson.js +2 -2
  75. package/src/report/githubReport.js +1 -1
  76. package/src/report/jsonReport.js +2 -2
  77. package/src/report/registerMatch.js +2 -2
  78. package/src/report/sheet.js +16 -16
  79. package/src/report/textReport.js +12 -12
  80. package/src/report/writingSpec.js +15 -15
  81. package/src/rules/code/inputFileSource.js +1 -1
  82. package/src/rules/code/installImport.js +2 -2
  83. package/src/rules/code/platformApi.js +1 -1
  84. package/src/rules/document/countMismatch.js +2 -1
  85. package/src/rules/document/enoughOnce.js +2 -1
  86. package/src/rules/document/fieldEcho.js +10 -6
  87. package/src/rules/document/keywordHeading.js +1 -1
  88. package/src/rules/document/keywordMissing.js +1 -1
  89. package/src/rules/document/noQuestion.js +2 -2
  90. package/src/rules/document/promiseRecall.js +1 -1
  91. package/src/rules/document/tableOddCell.js +1 -1
  92. package/src/rules/finding.js +4 -4
  93. package/src/rules/paragraph/factListParagraph.js +1 -1
  94. package/src/rules/paragraph/paraFragment.js +1 -1
  95. package/src/rules/readerDebt.js +1 -1
  96. package/src/rules/registry.js +26 -26
  97. package/src/rules/sentence/connectorRepeat.js +2 -1
  98. package/src/rules/sentence/danglingDeixis.js +1 -1
  99. package/src/rules/sentence/dash.js +1 -1
  100. package/src/rules/sentence/deixis.js +1 -1
  101. package/src/rules/sentence/doubleNegative.js +2 -1
  102. package/src/rules/sentence/doublePassive.js +2 -1
  103. package/src/rules/sentence/draftHistory.js +2 -1
  104. package/src/rules/sentence/endingRepeat.js +3 -2
  105. package/src/rules/sentence/euiChain.js +1 -1
  106. package/src/rules/sentence/fillerOpener.js +1 -1
  107. package/src/rules/sentence/imperativePeriod.js +1 -1
  108. package/src/rules/sentence/longSentence.js +1 -1
  109. package/src/rules/sentence/negationRedefine.js +1 -1
  110. package/src/rules/sentence/numberOrphan.js +1 -1
  111. package/src/rules/sentence/outsideProfile.js +8 -3
  112. package/src/rules/sentence/phraseRepeat.js +2 -1
  113. package/src/rules/shared/candidates.js +2 -2
  114. package/src/rules/shared/dictionaryRule.js +9 -2
  115. package/src/rules/shared/repeat.js +2 -2
  116. package/src/rules/structure/bridgeRepeat.js +3 -3
  117. package/src/rules/structure/emojiBullet.js +1 -1
  118. package/src/rules/structure/headingSkip.js +1 -1
  119. package/src/rules/structure/headingUniform.js +2 -1
  120. package/src/rules/structure/loneSubheading.js +1 -1
  121. package/src/rules/structure/moreLater.js +1 -1
  122. package/src/text.js +2 -2
  123. package/src/usage/counts.js +3 -3
  124. package/src/usage/index.js +3 -3
  125. package/src/usage/sentences.js +17 -17
package/src/cli/main.js CHANGED
@@ -1,21 +1,21 @@
1
1
  // @ts-check
2
2
  /**
3
- * 명령줄 진입점. 파이썬 CLI 와 같은 인자, 같은 출력, 같은 종료 코드다 (0 지적 없음, 1 error 있음, 2 파일이나 설정 문제).
3
+ * 명령줄 진입점. 파이썬 CLI와 같은 인자, 같은 출력, 같은 종료 코드다 (0 지적 없음, 1 error 있음, 2 파일이나 설정 문제).
4
4
  *
5
5
  * ```
6
6
  * hanlint 인자가 없으면 첫 화면. 무엇을 칠 수 있는지 보인다
7
- * hanlint 글.md [다른.md ...] 검사. 서브커맨드 없이 파일이나 폴더만 주면 lint 다. `-` 는 stdin
7
+ * hanlint 글.md [다른.md ...] 검사. 서브커맨드 없이 파일이나 폴더만 주면 lint다. `-`는 stdin
8
8
  * hanlint fix 글.md 기계가 고칠 수 있는 지적을 원문에 적용
9
9
  * hanlint print 글.md 분석표 계층 JSON
10
- * hanlint rules 규칙 목록을 부류로 묶어서
10
+ * hanlint rules 규칙 목록을 묶음로 묶어서
11
11
  * hanlint explain <규칙> 규칙의 기술서
12
- * hanlint patterns 문장 틀. --rule 로 그 규칙을 피하는 것만
13
- * hanlint primer --preset <종류> 쓰기 전에 읽는 한 장. 작법서 일곱 줄과 켜진 규칙의 고치는 법, 본보기 전후
14
- * hanlint hook 에이전트 훅. --prime 은 쓰기 전 작법서, 나머지는 쓴 마크다운의 비차단 검사
12
+ * hanlint patterns 문장 틀. --rule로 그 규칙을 피하는 것만
13
+ * hanlint primer --preset <종류> 쓰기 전에 읽는 한 장. 글쓰기 지침 일곱 줄과 켜진 규칙의 고치는 법, 예시 전후
14
+ * hanlint hook 에이전트 훅. --prime은 쓰기 전 글쓰기 지침, 나머지는 쓴 마크다운의 비차단 검사
15
15
  * hanlint baseline 글들/ 지금 있는 지적을 잠근다. 그다음부터 새것만 막힌다
16
16
  * hanlint baseline 글들/ 지금 있는 지적을 잠근다. 그다음부터 새것만 막힌다
17
17
  * hanlint doctor 설정과 꺼진 규칙
18
- * hanlint init 주석 달린 hanlint.toml. --output 과 --preset blog|report|docs
18
+ * hanlint init 주석 달린 hanlint.toml. --output과 --preset blog|report|docs
19
19
  * hanlint contract init 글.md 원문에서 최소 Reader Contract 초안
20
20
  * ```
21
21
  * audit, guard, arena, blueprint, evidence, entailment 같은 확장 명령은 파이썬 패키지에 있다.
@@ -88,11 +88,11 @@ const PYTHON_ONLY = [
88
88
  ];
89
89
  const FORMATS = ["text", "compact", "json", "github"];
90
90
  const SEVERITIES = ["all", "error", "notice"];
91
- /** 오탐과 미탐을 받는 자리. 뜻은 파이썬 cli/commands/explain.py 가 소유한다. */
91
+ /** 오탐과 미탐을 받는 자리. 뜻은 파이썬 cli/commands/explain.py가 소유한다. */
92
92
  const ISSUES = "github.com/eddmpython/hanlint/issues";
93
93
  const STDIN = "-";
94
94
  const STDIN_NAME = "<stdin>";
95
- /** 폴더를 주면 이 확장자만 찾는다. 파이썬 cli/commands/shared.py 의 MARKDOWN 과 같다. */
95
+ /** 폴더를 주면 이 확장자만 찾는다. 파이썬 cli/commands/shared.py의 MARKDOWN과 같다. */
96
96
  const THRESHOLD_FIELDS = [
97
97
  "fragmentRun",
98
98
  "introMaxParagraphs",
@@ -180,13 +180,13 @@ function parseArgs(args) {
180
180
  options[arg] = true;
181
181
  continue;
182
182
  }
183
- // 값을 줘도 되고 안 줘도 되는 옵션. 다음 인자가 다른 옵션이거나 없으면 true 로 둔다 (파이썬의 nargs="?").
183
+ // 값을 줘도 되고 안 줘도 되는 옵션. 다음 인자가 다른 옵션이거나 없으면 true로 둔다 (파이썬의 nargs="?").
184
184
  if (kind === "optional" && inlineValue === null && (args[i + 1] === undefined || args[i + 1].startsWith("--"))) {
185
185
  options[arg] = true;
186
186
  continue;
187
187
  }
188
188
  const value = inlineValue ?? args[++i];
189
- if (value === undefined) throw new UsageError(`${arg} 에 값이 없다`);
189
+ if (value === undefined) throw new UsageError(`${arg}에 값이 없다`);
190
190
  if (kind === "list") {
191
191
  const list = /** @type {string[]} */ (options[arg] ?? []);
192
192
  list.push(value);
@@ -199,10 +199,10 @@ function parseArgs(args) {
199
199
  }
200
200
 
201
201
  /**
202
- * 서브커맨드 없이 파일이나 옵션만 주면 lint 로 본다. 빈 인자는 여기서 다루지 않는다. `main` 이 첫 화면으로 보낸다.
202
+ * 서브커맨드 없이 파일이나 옵션만 주면 lint로 본다. 빈 인자는 여기서 다루지 않는다. `main`이 첫 화면으로 보낸다.
203
203
  * @param {string[]} argv
204
204
  */
205
- export /** 파일 이름이 아니라 서브커맨드를 치려던 것으로 보이나. 뜻은 파이썬 cli/main.py 가 소유한다. @param {string} word */
205
+ export /** 파일 이름이 아니라 서브커맨드를 치려던 것으로 보이나. 뜻은 파이썬 cli/main.py가 소유한다. @param {string} word */
206
206
  function looksLikeCommand(word) {
207
207
  if (!word || !/^[A-Za-z]+$/.test(word)) return false;
208
208
  return !existsSync(word);
@@ -213,15 +213,15 @@ function normalizeArgv(argv) {
213
213
  if (COMMANDS.includes(argv[0]) || PYTHON_ONLY.includes(argv[0]) || ["-h", "--help", "--version"].includes(argv[0])) return argv;
214
214
  if (looksLikeCommand(argv[0])) {
215
215
  const near = nearNames(argv[0], [...COMMANDS, ...PYTHON_ONLY]);
216
- const hint = near.length ? ` 가까운 이름: ${near.join(", ")}.` : "";
217
- throw new Error(`${argv[0]} 는 모르는 명령이다.${hint} 전체 목록은 hanlint --help`);
216
+ const hint = near.length ? ` 비슷한 이름: ${near.join(", ")}.` : "";
217
+ throw new Error(`없는 명령: ${argv[0]}.${hint} 전체 목록은 hanlint --help`);
218
218
  }
219
219
  return ["lint", ...argv];
220
220
  }
221
221
 
222
222
  /** @param {string} value @param {string[]} choices @param {string} option */
223
223
  function choose(value, choices, option) {
224
- if (!choices.includes(value)) throw new UsageError(`${option} 는 ${choices.join(", ")} 가운데 하나다: ${value}`);
224
+ if (!choices.includes(value)) throw new UsageError(`${option} 값으로 쓸 수 없다: ${value}. ${choices.join(", ")} 가운데 하나를 쓴다`);
225
225
  return value;
226
226
  }
227
227
 
@@ -232,10 +232,10 @@ function emit(text, output) {
232
232
  return;
233
233
  }
234
234
  writeFileSync(output, text.endsWith("\n") ? text : `${text}\n`, "utf-8");
235
- process.stderr.write(`${output} 에 썼다\n`);
235
+ process.stderr.write(`${output}에 썼다\n`);
236
236
  }
237
237
 
238
- /** 설정을 찾기 시작할 폴더. 첫 실제 파일의 폴더, 전부 stdin 이면 현재 폴더. @param {string[]} paths */
238
+ /** 설정을 찾기 시작할 폴더. 첫 실제 파일의 폴더, 전부 stdin이면 현재 폴더. @param {string[]} paths */
239
239
  function startFolder(paths) {
240
240
  for (const path of paths) if (path !== STDIN) return dirname(resolve(path));
241
241
  return process.cwd();
@@ -247,8 +247,8 @@ function checkDisabled(config) {
247
247
  const unknown = [...config.disable].filter((rule) => !names.includes(rule)).sort();
248
248
  if (!unknown.length) return;
249
249
  const near = nearNames(unknown[0], names);
250
- const hint = near.length ? ` 가까운 이름: ${near.join(", ")}.` : "";
251
- throw new Error(`모르는 규칙 이름: ${unknown.join(", ")}.${hint} 목록은 hanlint rules --names`);
250
+ const hint = near.length ? ` 비슷한 이름: ${near.join(", ")}.` : "";
251
+ throw new Error(`없는 규칙 이름: ${unknown.join(", ")}.${hint} 목록은 hanlint rules --names`);
252
252
  }
253
253
 
254
254
  /** @param {Record<string, string | string[] | boolean>} options @param {string[]} paths */
@@ -278,7 +278,7 @@ function header(config) {
278
278
 
279
279
  /** @param {Map<string, import("../rules/finding.js").Finding[]>} results */
280
280
  function summary(results) {
281
- // 글에 실제로 있는 수. 거른 뒤가 아니라 거르기 전을 센다. 뜻은 파이썬 shared.summary 가 소유한다.
281
+ // 글에 실제로 있는 수. 거른 뒤가 아니라 거르기 전을 센다. 뜻은 파이썬 shared.summary가 소유한다.
282
282
  let errors = 0;
283
283
  let notices = 0;
284
284
  for (const findings of results.values()) {
@@ -287,16 +287,16 @@ function summary(results) {
287
287
  else if (f.severity === "notice") notices += 1;
288
288
  }
289
289
  }
290
- return `파일 ${results.size}개, error ${errors}, notice ${notices}`;
290
+ return `파일 ${results.size}개, 오류 ${errors}건, 경고 ${notices}건`;
291
291
  }
292
292
 
293
- /** (이름, 본문). `-` 면 stdin 을 UTF-8 로 읽는다. @param {string} path @param {string} stdinName @returns {[string, string]} */
293
+ /** (이름, 본문). `-`면 stdin을 UTF-8로 읽는다. @param {string} path @param {string} stdinName @returns {[string, string]} */
294
294
  function readInput(path, stdinName) {
295
295
  if (path === STDIN) return [stdinName, readFileSync(0, "utf-8")];
296
296
  return [path, readOne(path)];
297
297
  }
298
298
 
299
- /** 글 하나를 읽는다. 폴더를 주면 말로 막는다. 뜻은 파이썬 shared.readFile 이 소유한다. @param {string} path */
299
+ /** 글 하나를 읽는다. 폴더를 주면 말로 막는다. 뜻은 파이썬 shared.readFile이 소유한다. @param {string} path */
300
300
  function readOne(path) {
301
301
  let isFolder = false;
302
302
  try {
@@ -304,11 +304,11 @@ function readOne(path) {
304
304
  } catch {
305
305
  isFolder = false;
306
306
  }
307
- if (isFolder) throw new Error(`${path} 는 폴더다. 이 명령은 글 하나를 본다. 폴더는 hanlint 와 hanlint fix 가 받는다`);
307
+ if (isFolder) throw new Error(`폴더를 받았다: ${path}. 이 명령은 글 하나를 본다. 폴더는 hanlint와 hanlint fix가 받는다`);
308
308
  return readFileSync(path, "utf-8");
309
309
  }
310
310
 
311
- /** 폴더를 주면 그 아래 마크다운을 편다. 파일과 `-` 는 그대로 둔다. @param {string[]} paths */
311
+ /** 폴더를 주면 그 아래 마크다운을 편다. 파일과 `-`는 그대로 둔다. @param {string[]} paths */
312
312
  function collectFiles(paths) {
313
313
  /** @type {string[]} */
314
314
  const found = [];
@@ -330,7 +330,7 @@ function collectFiles(paths) {
330
330
  const inside = markdownUnder(path).sort();
331
331
  if (!inside.length)
332
332
  throw new Error(
333
- `${path} 안에 마크다운 파일이 없다. 점으로 시작하는 폴더와 node_modules 는 건너뛴다. 그 안을 보려면 그 폴더를 직접 준다`,
333
+ `${path} 안에 마크다운 파일이 없다. 점으로 시작하는 폴더와 node_modules는 건너뛴다. 그 안을 보려면 그 폴더를 직접 준다`,
334
334
  );
335
335
  found.push(...inside);
336
336
  }
@@ -346,7 +346,7 @@ function commonest(findings) {
346
346
  return [...counted.keys()].sort((a, b) => (counted.get(b) ?? 0) - (counted.get(a) ?? 0) || a.localeCompare(b))[0];
347
347
  }
348
348
 
349
- /** `hanlint fix` 가 실제로 고칠 error 수. 고침이 달렸다는 것과 자리를 잡을 수 있다는 것은 다르다.
349
+ /** `hanlint fix`가 실제로 고칠 error 수. 고침이 달렸다는 것과 자리를 잡을 수 있다는 것은 다르다.
350
350
  * @param {Map<string, string>} texts @param {Map<string, import("../rules/finding.js").Finding[]>} results */
351
351
  function fixableCount(texts, results) {
352
352
  let total = 0;
@@ -358,7 +358,7 @@ function fixableCount(texts, results) {
358
358
  return total;
359
359
  }
360
360
 
361
- /** 검사 끝에 붙는 다음 행동 한 줄. 뜻은 파이썬 cli/commands/shared.py 가 소유한다.
361
+ /** 검사 끝에 붙는 다음 행동 한 줄. 뜻은 파이썬 cli/commands/shared.py가 소유한다.
362
362
  * @param {Map<string, import("../rules/finding.js").Finding[]>} results @param {number} [fixable] */
363
363
  function nextStep(results, fixable = 0) {
364
364
  const findings = [...results.values()].flat();
@@ -366,17 +366,17 @@ function nextStep(results, fixable = 0) {
366
366
  const notices = findings.length - errors.length;
367
367
  if (errors.length) {
368
368
  const rule = commonest(errors);
369
- if (fixable) return `다음: error ${errors.length}건 가운데 ${fixable}건은 hanlint fix 가 바로 고친다. 나머지는 손으로 고친다`;
370
- if (patternsAvoiding(rule).length) return `다음: error ${errors.length}건을 고친다. 다시 쓸 틀은 hanlint patterns --rule ${rule}`;
371
- return `다음: error ${errors.length}건을 고친다. 규칙이 왜 있는지는 hanlint explain ${rule}`;
369
+ if (fixable) return `다음: 오류 ${errors.length}건 가운데 ${fixable}건은 hanlint fix가 바로 고친다. 나머지는 손으로 고친다`;
370
+ if (patternsAvoiding(rule).length) return `다음: 오류 ${errors.length}건을 고친다. 다시 쓸 틀은 hanlint patterns --rule ${rule}`;
371
+ return `다음: 오류 ${errors.length}건을 고친다. 규칙이 왜 있는지는 hanlint explain ${rule}`;
372
372
  }
373
- if (notices) return `다음: error 0. 확인 권고 ${notices}건을 읽고 판단한 뒤 사람과 LLM 평가로 넘어간다`;
374
- return "다음: 세어서 잡히는 결함이 없다. 좋은 글이라는 뜻은 아니므로 사람과 LLM 평가로 넘어간다";
373
+ if (notices) return `다음: 오류는 없다. 경고 ${notices}건을 읽고 고칠지 정한다`;
374
+ return "다음: 규칙으로 찾은 잘못이 없다. 좋은 글이라는 뜻은 아니니 사람이 읽어 본다";
375
375
  }
376
376
 
377
377
  /** @param {Baseline} baseline */
378
378
  function lockedNote(baseline) {
379
- return `잠근 자리 ${baseline.count}건은 넘겼다 (${baseline.source}). 그 문장을 고치면 다시 나온다`;
379
+ return `baseline 파일에 든 지적 ${baseline.count}건은 보이지 않았다 (${baseline.source}). 그 문장을 고치면 다시 나온다`;
380
380
  }
381
381
 
382
382
  /** 옵션과 설정에서 잠금 파일을 연다. 둘 다 없으면 빈 잠금이다. @param {Record<string, string | string[] | boolean>} options @param {import("../config/settings.js").Config} config */
@@ -401,10 +401,10 @@ function runBaseline(args) {
401
401
  }
402
402
  const baseline = options["--prune"] ? pruneBaseline(loadBaseline(target), results) : buildBaseline(results, target);
403
403
  writeFileSync(target, `${renderBaseline(baseline)}\n`, "utf-8");
404
- const what = options["--prune"] ? "잠금을 줄였다" : "지적을 잠갔다";
404
+ const what = options["--prune"] ? "글에 더 없는 지적을 지웠다" : "지금 있는 지적이다";
405
405
  process.stdout.write(
406
- `${baseline.source} 에 ${baseline.count}건을 적었다. ${files.length}개 글의 ${what}.\n` +
407
- "다음: 검사할 때 --baseline 을 붙이거나 설정에 baseline 을 적는다. 잠긴 자리를 고치면 새 지적으로 다시 나온다\n",
406
+ `${baseline.source}에 ${baseline.count}건을 적었다. ${files.length}개 글에서 ${what}.\n` +
407
+ "다음: 검사할 때 --baseline을 붙이거나 설정에 baseline을 적는다. baseline 파일에 든 문장을 고치면 새 지적으로 다시 나온다\n",
408
408
  );
409
409
  return 0;
410
410
  }
@@ -446,7 +446,7 @@ function runContract(args) {
446
446
  }
447
447
  const output = options["--output"];
448
448
  if (typeof output === "string" && existsSync(output) && !options["--force"]) {
449
- throw new Error(`${output} 가 이미 있다. 덮어쓰려면 --force`);
449
+ throw new Error(`이미 있는 파일: ${output}. 덮어쓰려면 --force를 붙인다`);
450
450
  }
451
451
  const outline = options["--outline"];
452
452
  const facts = /** @type {string[]} */ (options["--fact"] ?? []);
@@ -567,18 +567,18 @@ function runFix(args) {
567
567
  const lines = [];
568
568
  for (const path of files) {
569
569
  const text = readFileSync(path, "utf-8");
570
- // notice 는 제안이라 손으로 정한다. 확정된 error 만 원문에 넣는다.
570
+ // notice는 제안이라 손으로 정한다. 확정된 error만 원문에 넣는다.
571
571
  const errors = runAll(fingerprint(text, config, path), config).filter((f) => f.severity === "error");
572
572
  const result = applyFixes(text, errors);
573
573
  for (const [line, fragment, replacement] of result.applied) lines.push(`${path}:${line} ${fragment} → ${replacement}`);
574
574
  for (const [line, fragment, reason] of result.skipped) lines.push(`${path}:${line} 건너뜀 ${fragment}: ${reason}`);
575
575
  if (result.text !== text && !options["--dry-run"]) writeFileSync(path, result.text, "utf-8");
576
- // 남은 수를 함께 말한다. `0곳 고침, 0곳 건너뜀` 만 보면 손볼 것이 없다는 뜻으로 읽힌다
576
+ // 남은 수를 함께 말한다. `0곳 고침, 0곳 건너뜀`만 보면 손볼 것이 없다는 뜻으로 읽힌다
577
577
  const left = errors.length - result.applied.length;
578
- const rest = left ? `. 손으로 고칠 error ${left}건이 남았다 (hanlint ${path})` : "";
578
+ const rest = left ? `. 손으로 고칠 오류 ${left}건이 남았다 (hanlint ${path})` : "";
579
579
  lines.push(
580
580
  `${path} ${result.applied.length}곳 고침, ${result.skipped.length}곳 건너뜀` +
581
- (options["--dry-run"] ? " (미리보기, 파일은 그대로)" : "") +
581
+ (options["--dry-run"] ? " (미리보기라 파일은 그대로다)" : "") +
582
582
  rest,
583
583
  );
584
584
  }
@@ -589,7 +589,7 @@ function runFix(args) {
589
589
  /** @param {string[]} args */
590
590
  function runPrint(args) {
591
591
  const { options, positionals } = parseArgs(args);
592
- if (positionals.length !== 1) throw new UsageError("마크다운 파일 하나가 필요하다. `-` 는 stdin");
592
+ if (positionals.length !== 1) throw new UsageError("마크다운 파일 하나가 필요하다. `-`는 stdin");
593
593
  const layer = choose(/** @type {string} */ (options["--layer"] ?? "all"), LAYERS, "--layer");
594
594
  const config = configFrom(options, positionals);
595
595
  const [name, text] = readInput(positionals[0], /** @type {string} */ (options["--path"] ?? STDIN_NAME));
@@ -632,15 +632,15 @@ function runRules(args) {
632
632
  let tail = `규칙 ${names.length}개`;
633
633
  if (off.size) {
634
634
  const byPreset = PRESETS[config.preset].length;
635
- tail += `, 그중 ${off.size}개가 꺼져 있다 (preset ${config.preset} 이 ${byPreset}개, disable 이 ${off.size - byPreset}개)`;
635
+ tail += ` 가운데 ${off.size}개가 꺼져 있다 (${config.preset} 프리셋 ${byPreset}개, disable ${off.size - byPreset}개)`;
636
636
  }
637
637
  lines.push(`${tail}. 하나를 자세히 보려면 hanlint explain <규칙>`);
638
- lines.push(`프리셋은 ${PRESET_NAMES.join(", ")} 이고 hanlint init --preset <이름> 이 설정에 적는다`);
638
+ lines.push(`프리셋: ${PRESET_NAMES.join(", ")}. hanlint init --preset <이름>이 설정 파일에 적는다`);
639
639
  emit(lines.join("\n"), output);
640
640
  return 0;
641
641
  }
642
642
 
643
- /** 여러 줄짜리 본보기를 첫 줄에만 표를 달고 나머지는 맞춰 들여쓴다. @param {string} text @param {string} label */
643
+ /** 여러 줄짜리 예시를 첫 줄에만 표를 달고 나머지는 맞춰 들여쓴다. @param {string} text @param {string} label */
644
644
  function indent(text, label) {
645
645
  const pad = " ".repeat(label.length);
646
646
  return text.replace(/\n+$/, "").split("\n").map((line, index) => (index === 0 ? label : pad) + line).join("\n");
@@ -651,7 +651,7 @@ const NEAR_LIMIT = 3;
651
651
  /** 앞 몇 글자가 같으면 가까운 이름으로 보는가. */
652
652
  const NEAR_PREFIX = 3;
653
653
 
654
- /** 오타에 가까운 이름. 파이썬 cli/commands/explain.py 의 nearNames 와 같다. @param {string} query @param {string[]} names */
654
+ /** 오타에 가까운 이름. 파이썬 cli/commands/explain.py의 nearNames와 같다. @param {string} query @param {string[]} names */
655
655
  function nearNames(query, names) {
656
656
  const lowered = query.toLowerCase();
657
657
  /** @type {[number, string][]} */
@@ -671,15 +671,15 @@ function runExplain(args) {
671
671
  const names = ruleNames();
672
672
  if (!positionals.length) {
673
673
  process.stdout.write("규칙 이름 하나가 필요하다. 예: hanlint explain doublePassive\n");
674
- process.stdout.write(`\n규칙 ${names.length}개를 부류로 묶어 보려면 hanlint rules, 이름만 보려면 hanlint rules --names\n`);
674
+ process.stdout.write(`\n규칙 ${names.length}개를 카테고리별로 보려면 hanlint rules, 이름만 보려면 hanlint rules --names\n`);
675
675
  return 2;
676
676
  }
677
- if (positionals.length !== 1) throw new UsageError("규칙 이름 하나가 필요하다. hanlint rules 로 목록을 본다");
677
+ if (positionals.length !== 1) throw new UsageError("규칙 이름 하나가 필요하다. hanlint rules로 목록을 본다");
678
678
  const wanted = positionals[0];
679
679
  if (!names.includes(wanted)) {
680
680
  const near = nearNames(wanted, names);
681
- const hint = near.length ? ` 이것을 찾았나: ${near.join(", ")}` : " hanlint rules 로 목록을 본다";
682
- throw new Error(`모르는 규칙: ${wanted}.${hint}`);
681
+ const hint = near.length ? ` 비슷한 이름: ${near.join(", ")}` : " 목록은 hanlint rules";
682
+ throw new Error(`없는 규칙: ${wanted}.${hint}`);
683
683
  }
684
684
  const category = ruleCategory(wanted);
685
685
  const config = configFrom(options, []);
@@ -698,18 +698,18 @@ function runExplain(args) {
698
698
  return 0;
699
699
  }
700
700
  const mechanism = ruleMechanism(wanted);
701
- process.stdout.write(`${wanted} (${CATEGORY_TITLES[category]})\n기제: ${mechanism}. ${MECHANISMS[mechanism]}\n\n${ruleDoc(wanted)}\n`);
701
+ process.stdout.write(`${wanted} (${CATEGORY_TITLES[category]})\n방식: ${MECHANISMS[mechanism]} (${mechanism})\n\n${ruleDoc(wanted)}\n`);
702
702
  const exemplar = exemplarOf ? exemplarInRegister(exemplarOf, register) : undefined;
703
703
  if (exemplar) {
704
- process.stdout.write("\n본보기\n");
704
+ process.stdout.write("\n예시\n");
705
705
  process.stdout.write(`${indent(exemplar.before, " 전 ")}\n`);
706
706
  process.stdout.write(`${indent(exemplar.after, " 후 ")}\n`);
707
707
  process.stdout.write(` 달라진 것: ${exemplar.moved}\n`);
708
708
  }
709
709
  const siblings = names.filter((name) => ruleCategory(name) === category && name !== wanted);
710
- process.stdout.write(`\n같은 부류: ${siblings.join(", ")}\n`);
711
- process.stdout.write(`끄려면 hanlint.toml 의 disable 에 ${wanted} 를 넣는다. 한 자리만 끄려면 <!-- hanlint-disable ${wanted} -->\n`);
712
- process.stdout.write(`정당한 문장인데 잡혔으면 끄고 끝내지 말고 알려 준다: ${ISSUES}\n`);
710
+ process.stdout.write(`\n같은 카테고리: ${siblings.join(", ")}\n`);
711
+ process.stdout.write(`끄려면 hanlint.toml의 disable 목록에 넣는다: ${wanted}. 한 곳만 끄려면 <!-- hanlint-disable ${wanted} -->\n`);
712
+ process.stdout.write(`맞는 문장인데 잡혔다면 끄기만 하지 말고 여기에 알린다: ${ISSUES}\n`);
713
713
  return 0;
714
714
  }
715
715
 
@@ -719,7 +719,7 @@ function runPatterns(args) {
719
719
  const rule = /** @type {string | undefined} */ (options["--rule"]);
720
720
  const register = choose(/** @type {string} */ (options["--register"] ?? HAPNIDA), REGISTERS, "--register");
721
721
  const chosen = (rule ? patternsAvoiding(rule) : patterns()).map((pattern) => patternInRegister(pattern, register));
722
- if (!chosen.length) throw new Error(` 을 피하는 문형이 없다. hanlint patterns 로 전부 본다`);
722
+ if (!chosen.length) throw new Error(`을 피하는 문형이 없다. hanlint patterns로 전부 본다`);
723
723
  const output = /** @type {string | undefined} */ (options["--output"]);
724
724
  if (options["--format"] === "json") {
725
725
  emit(JSON.stringify({ version: 1, patterns: chosen }, null, 2), output);
@@ -727,7 +727,7 @@ function runPatterns(args) {
727
727
  }
728
728
  const lines = [];
729
729
  for (const pattern of chosen) {
730
- lines.push(`${pattern.name} (${pattern.avoids.join(", ")} 를 피한다)`);
730
+ lines.push(`${pattern.name} (피하는 규칙: ${pattern.avoids.join(", ")})`);
731
731
  lines.push(` 틀 ${pattern.form}`);
732
732
  lines.push(` 언제 ${pattern.when}`);
733
733
  lines.push(indent(pattern.example, " 예시 "));
@@ -735,13 +735,13 @@ function runPatterns(args) {
735
735
  lines.push(` 출처 ${pattern.source}`);
736
736
  lines.push("");
737
737
  }
738
- lines.push(`문형 ${chosen.length}개. 예시는 전부 hanlint 를 error 0 으로 통과한다 (게이트가 확인한다)`);
738
+ lines.push(`문형 ${chosen.length}개. 예시 문장은 모두 hanlint 검사에서 오류가 없다`);
739
739
  if (!rule) lines.push("지적을 받은 자리를 다시 쓰려면 hanlint patterns --rule <규칙>");
740
740
  emit(lines.join("\n"), output);
741
741
  return 0;
742
742
  }
743
743
 
744
- /** 여러 줄을 한 줄로. 줄바꿈은 ¶ 다. 파이썬 cli/commands/primer.py 의 flat 과 같다. @param {string} text */
744
+ /** 여러 줄을 한 줄로. 줄바꿈은 ¶ 다. 파이썬 cli/commands/primer.py의 flat과 같다. @param {string} text */
745
745
  function flat(text) {
746
746
  return text
747
747
  .split("\n")
@@ -751,7 +751,7 @@ function flat(text) {
751
751
  }
752
752
 
753
753
  /**
754
- * 설정에서 켜진 규칙마다 항목 하나. 부류 순서, 그 안은 이름 순서. 파이썬 primerEntries 와 같다.
754
+ * 설정에서 켜진 규칙마다 항목 하나. 묶음 순서, 그 안은 이름 순서. 파이썬 primerEntries와 같다.
755
755
  * @param {import("../config/settings.js").Config} config @param {string} register
756
756
  */
757
757
  function primerEntries(config, register) {
@@ -761,7 +761,7 @@ function primerEntries(config, register) {
761
761
  for (const name of ruleNames()) {
762
762
  if (ruleCategory(name) !== category || off.has(name)) continue;
763
763
  const exemplar = exemplarFor(name, config.preset, config.exemplars);
764
- if (!exemplar) throw new Error(`본보기가 없는 규칙: ${name}`);
764
+ if (!exemplar) throw new Error(`예시가 없는 규칙: ${name}`);
765
765
  const shown = exemplarInRegister(exemplar, register);
766
766
  entries.push({ rule: name, category, fix: ruleFix(name), before: shown.before, after: shown.after });
767
767
  }
@@ -769,17 +769,17 @@ function primerEntries(config, register) {
769
769
  return entries;
770
770
  }
771
771
 
772
- /** 작법서를 절마다 한 줄로. 제목 두 칸 띄고 본문이다. 파이썬 proseLines 와 같다. */
772
+ /** 글쓰기 지침를 절마다 한 줄로. 제목 두 칸 띄고 본문이다. 파이썬 proseLines와 같다. */
773
773
  function proseLines() {
774
774
  return proseSections().map(([title, body]) => `${title} ${body.split(/\s+/).filter(Boolean).join(" ")}`);
775
775
  }
776
776
 
777
- /** 사람과 AI 가 읽는 한 장. 파이썬 renderPrimer 와 글자 단위로 같다. @param {ReturnType<typeof primerEntries>} entries @param {string} preset @param {string} register */
777
+ /** 사람과 AI가 읽는 한 장. 파이썬 renderPrimer와 글자 단위로 같다. @param {ReturnType<typeof primerEntries>} entries @param {string} preset @param {string} register */
778
778
  function renderPrimer(entries, preset, register) {
779
779
  const lines = [
780
780
  ...proseLines(),
781
781
  "",
782
- `hanlint primer ${preset} 종류, ${register}체, 규칙 ${entries.length}개. 줄마다 규칙 이름, 고치는 법, 전 (그 규칙에 잡히는 글), 후 (그 본보기에서 고친 글) 순서다. 본보기의 줄바꿈은 다`,
782
+ `hanlint primer ${preset} 종류, ${register}체, 규칙 ${entries.length}개. 줄마다 규칙 이름, 고치는 법, 전(그 규칙에 잡히는 글), 후(그 예시에서 고친 글) 순서다. 예시의 줄바꿈은 ¶로 보인다`,
783
783
  ];
784
784
  for (const [category, title] of Object.entries(CATEGORY_TITLES)) {
785
785
  const inside = entries.filter((entry) => entry.category === category);
@@ -787,7 +787,7 @@ function renderPrimer(entries, preset, register) {
787
787
  lines.push("", `${title} (${inside.length})`);
788
788
  for (const entry of inside) lines.push(`${entry.rule} ${entry.fix} 전: ${flat(entry.before)} 후: ${flat(entry.after)}`);
789
789
  }
790
- lines.push("", "본보기는 해당 문맥의 예시다. 용어와 사실을 보존하고, 치환을 다른 문맥에 적용하지 않는다. 규칙을 더 보려면 hanlint explain <규칙>");
790
+ lines.push("", "예시는 그 문장에만 맞는다. 용어와 사실은 그대로 두고, 바꾼 말을 다른 글에 그대로 옮겨 쓰지 않는다. 규칙을 더 보려면 hanlint explain <규칙>");
791
791
  return lines.join("\n");
792
792
  }
793
793
 
@@ -816,7 +816,7 @@ function runSpec(args) {
816
816
  if (options["--chars"] !== undefined) {
817
817
  const raw = /** @type {string} */ (options["--chars"]);
818
818
  targetChars = Number(raw);
819
- if (!Number.isInteger(targetChars) || targetChars <= 0) throw new UsageError(`--chars 는 1 이상의 정수다: ${raw}`);
819
+ if (!Number.isInteger(targetChars) || targetChars <= 0) throw new UsageError(`--chars는 1 이상의 정수다: ${raw}`);
820
820
  }
821
821
  const result = writingSpec(config, register, targetChars);
822
822
  const output = /** @type {string | undefined} */ (options["--output"]);
@@ -825,7 +825,7 @@ function runSpec(args) {
825
825
  return 0;
826
826
  }
827
827
 
828
- /** 작법서를 싣는 이벤트. Claude Code와 Codex 모두 이 둘의 additionalContext를 모델 문맥에 넣는다. */
828
+ /** 글쓰기 지침를 싣는 이벤트. Claude Code와 Codex 모두 이 둘의 additionalContext를 모델 문맥에 넣는다. */
829
829
  const PRIME_EVENTS = ["SessionStart", "UserPromptSubmit"];
830
830
 
831
831
  /** 에이전트가 다음 모델 요청에 넣는 비차단 문맥. @param {string} eventName @param {string} context */
@@ -833,10 +833,10 @@ function hookContext(eventName, context) {
833
833
  return JSON.stringify({ hookSpecificOutput: { hookEventName: eventName, additionalContext: context } });
834
834
  }
835
835
 
836
- /** Finding을 되먹이는 문맥의 본문. 파일마다 한 덩어리다. 파이썬 findingContext 와 같다. @param {string} eventName @param {[string, import("../rules/finding.js").Finding[]][]} found */
836
+ /** Finding을 되먹이는 문맥의 본문. 파일마다 한 덩어리다. 파이썬 findingContext와 같다. @param {string} eventName @param {[string, import("../rules/finding.js").Finding[]][]} found */
837
837
  function findingContext(eventName, found) {
838
838
  const where = eventName === "Stop" ? "마지막 답변" : "방금 쓴 마크다운";
839
- return `hanlint가 ${where}에서 센 Finding입니다.\n${found.map(([name, items]) => renderCompact(name, items)).join("\n")}`;
839
+ return `hanlint가 ${where}에서 찾은 지적입니다.\n${found.map(([name, items]) => renderCompact(name, items)).join("\n")}`;
840
840
  }
841
841
 
842
842
  /** Codex apply_patch 본문에서 쓴 파일을 가리키는 머리 줄 (Codex 훅 문서, 2026-09-21 확인). */
@@ -891,7 +891,7 @@ function writtenFindings(payload, options) {
891
891
 
892
892
  /** 명령 훅 JSON을 stdin에서 읽어 저장 파일이나 마지막 답변을 검사한다. 언제나 종료 코드 0이다. @param {string[]} args */
893
893
  function runHook(args) {
894
- if (args.includes("--prime") && args.includes("--reply")) throw new UsageError("--prime 과 --reply 는 함께 쓰지 않는다");
894
+ if (args.includes("--prime") && args.includes("--reply")) throw new UsageError("--prime과 --reply는 함께 쓰지 않는다");
895
895
  const { options, positionals } = parseArgs(args);
896
896
  const agent = choose(/** @type {string} */ (options["--agent"] ?? "claude"), ["claude", "codex"], "--agent");
897
897
  try {
@@ -929,13 +929,13 @@ function runHook(args) {
929
929
  return 0;
930
930
  }
931
931
 
932
- /** 잠근 지적이 몇 건인지. baseline 이 빚을 감추는 자리가 되지 않게 늘 보인다. @param {import("../config/settings.js").Config} config */
932
+ /** 잠근 지적이 몇 건인지. baseline이 빚을 감추는 자리가 되지 않게 늘 보인다. @param {import("../config/settings.js").Config} config */
933
933
  function baselineState(config) {
934
- if (!config.baseline) return "없다 (hanlint baseline 글들/ 로 지금 지적을 잠근다)";
934
+ if (!config.baseline) return "없음 (만들려면 hanlint baseline 글들/)";
935
935
  try {
936
- return `${loadBaseline(config.baseline).count}건이 ${config.baseline} 에 잠겨 있다. 그 문장을 고치면 다시 나온다`;
936
+ return `${config.baseline}에 지적 ${loadBaseline(config.baseline).count}건이 들어 있다. 그 문장을 고치면 다시 나온다`;
937
937
  } catch (error) {
938
- return `${config.baseline} 를 못 읽었다: ${/** @type {Error} */ (error).message}`;
938
+ return `읽지 못했다: ${config.baseline} (${/** @type {Error} */ (error).message})`;
939
939
  }
940
940
  }
941
941
 
@@ -952,22 +952,22 @@ function runDoctor(args) {
952
952
  `설정 ${configLabel(config)}`,
953
953
  `프리셋 ${config.preset} (${PRESET_NAMES.join(", ")} 가운데)`,
954
954
  `규칙 ${names.length - off.length}개 켜짐, ${off.length}개 꺼짐`,
955
- `잠금 ${baselineState(config)}`,
956
- `폴더 점으로 시작하는 폴더와 ${SKIPPED_FOLDERS.join(", ")} 는 건너뛴다. 직접 주면 검사한다`,
955
+ `baseline 파일 ${baselineState(config)}`,
956
+ `폴더 건너뛰는 것: 점으로 시작하는 폴더, ${SKIPPED_FOLDERS.join(", ")}. 직접 주면 검사한다`,
957
957
  ];
958
958
  if (off.length) lines.push(`꺼진 규칙 ${off.join(", ")}`);
959
- lines.push("", "다음: hanlint 글.md 로 검사한다. 설정이 기본값이면 hanlint init 으로 파일을 만든다");
959
+ lines.push("", "다음: hanlint 글.md로 검사한다. 설정 파일이 없으면 hanlint init으로 만든다");
960
960
  process.stdout.write(`${lines.join("\n")}\n`);
961
961
  return 0;
962
962
  }
963
963
 
964
- /** 주석 달린 hanlint.toml. 파이썬 `hanlint init` 과 같은 글자다. @param {string} [preset] */
964
+ /** 주석 달린 hanlint.toml. 파이썬 `hanlint init`과 같은 글자다. @param {string} [preset] */
965
965
  export function renderInit(preset = "blog") {
966
966
  const defaults = defaultConfig();
967
967
  const lines = [
968
- "# hanlint 설정. 규칙을 끄려면 disable 에 이름을 넣는다. 규칙의 기술서는 hanlint explain <규칙>.",
968
+ "# hanlint 설정. 규칙을 끄려면 disable에 이름을 넣는다. 규칙 설명은 hanlint explain <규칙>으로 본다.",
969
969
  "",
970
- "# 글의 종류. 그 종류에 안 맞는 규칙을 처음부터 끈다. disable 은 그 위에 더한다.",
970
+ "# 글의 종류. 그 종류에 안 맞는 규칙을 처음부터 끈다. disable은 그 위에 더한다.",
971
971
  ];
972
972
  for (const [name, off] of Object.entries(PRESETS)) lines.push(`# ${name}: ${off.length ? off.join(", ") : "전부 켠다"}`);
973
973
  lines.push(`preset = "${preset}"`, "", "# 규칙 목록과 한 줄 설명");
@@ -981,23 +981,23 @@ export function renderInit(preset = "blog") {
981
981
  "",
982
982
  "disable = []",
983
983
  "",
984
- "# 대표 검색어를 읽을 frontmatter 필드. 없으면 keywordMissing 은 돌지 않는다",
984
+ "# 대표 검색어를 읽을 frontmatter 필드. 없으면 keywordMissing은 돌지 않는다",
985
985
  '# keywordField = "primaryKeyword"',
986
986
  "",
987
- "# 도입과 마지막 절이 담아야 하는 frontmatter 필드. 비어 있으면 fieldEcho 는 돌지 않는다",
987
+ "# 도입과 마지막 절이 담아야 하는 frontmatter 필드. 비어 있으면 fieldEcho는 돌지 않는다",
988
988
  '# introFields = ["readerQuestion"]',
989
989
  '# endingFields = ["readerTakeaway"]',
990
990
  "",
991
- "# hanlint profile build 가 만든 파일. 있으면 종류의 프로파일 대신 그것과 견준다 (outsideProfile)",
991
+ "# hanlint profile build가 만든 파일. 있으면 글 종류의 기본 프로파일 대신 이 파일과 비교한다(outsideProfile)",
992
992
  '# profile = "profile.json"',
993
993
  "",
994
- "# hanlint baseline 이 만든 잠금 파일. 있으면 그 안의 지적은 넘기고 새로 생긴 것만 막는다",
994
+ "# hanlint baseline이 만든 baseline 파일. 있으면 그 안의 지적은 보이지 않고 새로 생긴 지적만 막는다",
995
995
  '# baseline = ".hanlint-baseline.json"',
996
996
  "",
997
- "# 코드도 산문도 아닌 펜스의 언어 표기. 장면 계약과 도표 원문은 코드 블록으로 세지 않고 분석표에서 뺀다",
997
+ "# 코드도 산문도 아닌 펜스의 언어 이름. 이 펜스는 코드 블록으로 세지 않고 검사에서도 뺀다",
998
998
  '# ignoreFences = ["course-scene", "mermaid"]',
999
999
  "",
1000
- "# 임계. 기본값의 기준은 hanlint 의 config/settings.py 다",
1000
+ "# 기준값. 아래 값이 기본값이다",
1001
1001
  );
1002
1002
  for (const name of THRESHOLD_FIELDS) {
1003
1003
  const value = /** @type {number} */ (defaults[name]);
@@ -1006,9 +1006,9 @@ export function renderInit(preset = "blog") {
1006
1006
  }
1007
1007
  lines.push(
1008
1008
  "",
1009
- "# 용례. nounPile 은 그 종류의 실제 글에서 usageMin 편 이상의 문서에 나온 명사 연쇄를 짚지 않고, 사전 규칙은",
1010
- "# 그 종류의 문서 usageShare 이상에 나오는 항목 (사업보고서의 `에 대한`) 을 짚지 않는다.",
1011
- "# 종류는 프리셋이 정한다 (report 만 있다). 빈 문자열이면 어떤 프리셋에서도 용례를 보지 않는다",
1009
+ "# 용례. 그 종류의 실제 글에 흔한 명사 연쇄(nounPile)와 사전 표현은 짚지 않는다. 명사 연쇄는 usageMin편 이상의",
1010
+ "# 문서에 나온 것, 사전 표현은 문서의 usageShare 비율 이상에 나온 것(사업보고서의 `에 대한`)이다.",
1011
+ "# 종류는 프리셋이 정한다 (report만 있다). 빈 문자열이면 어떤 프리셋에서도 용례를 보지 않는다",
1012
1012
  '# usageKind = ""',
1013
1013
  "# usageMin = 3",
1014
1014
  "# usageShare = 0.9",
@@ -1019,31 +1019,31 @@ export function renderInit(preset = "blog") {
1019
1019
  '# cliches = ["우리의 여정"]',
1020
1020
  '# translationese = [{ pattern = "에 대한 이해", fix = "를 아는 것" }]',
1021
1021
  "",
1022
- "# 표면 치환이 바꾸면 안 되는 한국어 고유명사와 프로젝트 용어. 수치와 라틴 식별자는 자동으로 보호한다",
1022
+ "# 자동 고침이 건드리지 말아야 할 한국어 고유명사와 프로젝트 용어. 숫자와 영문 식별자는 저절로 지킨다",
1023
1023
  '# protectedTerms = ["한린트", "김민지"]',
1024
- '# enforceStyle = ["noQuestion", "nounPile"] # 선택한 문체 신호만 error로 강제한다',
1024
+ '# enforceStyle = ["noQuestion", "nounPile"] # 고른 문체 규칙만 오류로 낸다',
1025
1025
  "",
1026
- "# 사람이 승인한 프로젝트 본보기. 같은 규칙과 프리셋의 내장 본보기를 덮어쓴다",
1026
+ "# 사람이 승인한 프로젝트 예시. 같은 규칙과 프리셋의 기본 예시 대신 쓴다",
1027
1027
  "# [[exemplars]]",
1028
1028
  '# rule = "translationese"',
1029
1029
  '# before = "설계에 대한 이해가 필요합니다."',
1030
1030
  '# after = "설계를 알아야 합니다."',
1031
- '# moved = "명사구를 서술어로 풀어 씀"',
1031
+ '# moved = "명사구를 서술어로 풀어 썼다"',
1032
1032
  '# presets = ["blog"]',
1033
1033
  "",
1034
- "# 사람이 승인한 국소 고침. 원문을 포함한 모든 조건이 맞을 때만 그대로 재생한다",
1034
+ "# 사람이 승인한 그 자리만의 고침. 원문을 포함해 조건이 모두 맞을 때만 그대로 적용한다",
1035
1035
  "# [[patches]]",
1036
1036
  '# rule = "translationese"',
1037
1037
  '# before = "설계에 대한 이해가 필요합니다."',
1038
1038
  '# after = "설계를 알아야 합니다."',
1039
- '# moved = "명사구를 서술어로 풀어 씀"',
1039
+ '# moved = "명사구를 서술어로 풀어 썼다"',
1040
1040
  '# sourceText = "설계에 대한 이해가 필요합니다." # before의 마크다운까지 보존한 선택용 원문',
1041
1041
  '# sentence = "설계에 대한 이해가 필요합니다." # before에서 마크다운 표식을 걷은 선택용 원문',
1042
1042
  '# cue = "에 대한"',
1043
1043
  '# reader = "new"',
1044
1044
  '# presets = ["blog"]',
1045
1045
  "",
1046
- "# 사람이 적용 범위까지 승인한 표면 치환. 단어 경계 한 자리와 보호 원자가 맞을 때만 결과를 낸다",
1046
+ "# 사람이 적용 범위까지 승인한 치환. 단어 경계가 맞고 숫자·링크·코드가 그대로일 때만 바꾼다",
1047
1047
  "# [[operations]]",
1048
1048
  '# before = "여러가지"',
1049
1049
  '# after = "여러 가지"',
@@ -1058,15 +1058,15 @@ function runInit(args) {
1058
1058
  const { options } = parseArgs(args);
1059
1059
  const path = /** @type {string} */ (options["--output"] ?? "hanlint.toml");
1060
1060
  const preset = choose(/** @type {string} */ (options["--preset"] ?? "blog"), PRESET_NAMES, "--preset");
1061
- if (existsSync(path) && !options["--force"]) throw new Error(`${path} 가 이미 있다. 덮어쓰려면 --force`);
1061
+ if (existsSync(path) && !options["--force"]) throw new Error(`이미 있는 파일: ${path}. 덮어쓰려면 --force를 붙인다`);
1062
1062
  writeFileSync(path, renderInit(preset), "utf-8");
1063
1063
  const off = PRESETS[preset];
1064
- const tail = off.length ? `preset ${preset} 이 ${off.length}개를 끈다` : `preset ${preset} 은 규칙을 전부 켠다`;
1065
- process.stdout.write(`${path} 를 만들었다. ${tail}. 더 끄려면 disable 에 이름을 넣는다\n`);
1064
+ const tail = off.length ? `${preset} 프리셋은 규칙 ${off.length}개를 끈다` : `${preset} 프리셋은 규칙을 전부 켠다`;
1065
+ process.stdout.write(`만들었다: ${path}. ${tail}. 더 끄려면 disable에 이름을 넣는다\n`);
1066
1066
  return 0;
1067
1067
  }
1068
1068
 
1069
- /** 폴더 아래 소스 파일. 뜻은 파이썬 cli/commands/sheet.py 의 sourceUnder 가 소유한다. @param {string} folder @returns {string[]} */
1069
+ /** 폴더 아래 소스 파일. 뜻은 파이썬 cli/commands/sheet.py의 sourceUnder가 소유한다. @param {string} folder @returns {string[]} */
1070
1070
  function sourceUnder(folder) {
1071
1071
  /** @type {string[]} */
1072
1072
  const found = [];
@@ -1076,24 +1076,24 @@ function sourceUnder(folder) {
1076
1076
  if (!isSkipped(name)) found.push(...sourceUnder(path));
1077
1077
  } else if (SOURCE_SUFFIXES.includes(extname(name).toLowerCase())) found.push(path);
1078
1078
  }
1079
- // 구분자를 / 로 맞춘 뒤 정렬한다. 뜻은 파이썬 sourceUnder 가 소유한다.
1079
+ // 구분자를 / 로 맞춘 뒤 정렬한다. 뜻은 파이썬 sourceUnder가 소유한다.
1080
1080
  const posix = (item) => item.split(sep).join("/");
1081
1081
  return found.sort((a, b) => (posix(a) < posix(b) ? -1 : posix(a) > posix(b) ? 1 : 0));
1082
1082
  }
1083
1083
 
1084
- /** 작업 폴더 기준 상대 경로, 구분자는 `/`. 파이썬 relativeLabel 과 같은 글자를 낸다. @param {string} path */
1084
+ /** 작업 폴더 기준 상대 경로, 구분자는 `/`. 파이썬 relativeLabel과 같은 글자를 낸다. @param {string} path */
1085
1085
  function relativeLabel(path) {
1086
1086
  const rel = relative(process.cwd(), resolve(path));
1087
1087
  const label = !rel || rel.startsWith("..") || isAbsolute(rel) ? path : rel;
1088
1088
  return label.split(sep).join("/").replaceAll("\\", "/");
1089
1089
  }
1090
1090
 
1091
- /** 표의 자리 글자. 뜻은 파이썬 SheetRow.place 가 소유한다. @param {import("../report/sheet.js").SheetRow} row */
1091
+ /** 표의 자리 글자. 뜻은 파이썬 SheetRow.place가 소유한다. @param {import("../report/sheet.js").SheetRow} row */
1092
1092
  function placeOf(row) {
1093
1093
  return row.column > 0 ? `${row.file}:${row.line}:${row.column}` : `${row.file}:${row.line}`;
1094
1094
  }
1095
1095
 
1096
- /** 표의 고침을 파일에 쓴다. 뜻은 파이썬 applySheet 가 소유한다. @param {string} sheetPath @param {boolean} dryRun @returns {[string[], string[]]} */
1096
+ /** 표의 고침을 파일에 쓴다. 뜻은 파이썬 applySheet가 소유한다. @param {string} sheetPath @param {boolean} dryRun @returns {[string[], string[]]} */
1097
1097
  function applySheet(sheetPath, dryRun) {
1098
1098
  const parsed = parseSheet(readFileSync(sheetPath, "utf-8"));
1099
1099
  /** @type {string[]} */
@@ -1111,7 +1111,7 @@ function applySheet(sheetPath, dryRun) {
1111
1111
  failed.push(...rows.map((row) => `${placeOf(row)}: 파일이 없다`));
1112
1112
  continue;
1113
1113
  }
1114
- // 되돌려 쓰는 뜻은 report/sheet.js 의 applyRows 가 소유한다 (브라우저의 저장소 쓰기와 같은 함수).
1114
+ // 되돌려 쓰는 뜻은 report/sheet.js의 applyRows가 소유한다 (브라우저의 저장소 쓰기와 같은 함수).
1115
1115
  const [rewritten, done, problems] = applyRows(readFileSync(file, "utf-8"), rows);
1116
1116
  applied.push(...done);
1117
1117
  failed.push(...problems);
@@ -1120,13 +1120,18 @@ function applySheet(sheetPath, dryRun) {
1120
1120
  return [applied, failed];
1121
1121
  }
1122
1122
 
1123
- /** 색인이 없을 때의 안내. 뜻은 파이썬 cli/commands/usage.py 의 missingIndex 가 소유한다. @param {string} kind @param {string} root */
1123
+ /** 세 자리마다 쉼표를 찍은 수. 파이썬의 `f"{n:,}"`와 같은 글자다. @param {number} value */
1124
+ function comma(value) {
1125
+ return value.toLocaleString("en-US");
1126
+ }
1127
+
1128
+ /** 색인이 없을 때의 안내. 뜻은 파이썬 cli/commands/usage.py의 missingIndex가 소유한다. @param {string} kind @param {string} root */
1124
1129
  function missingIndex(kind, root) {
1125
1130
  const kinds = listUsageIndexes(root).map((meta) => String(meta.kind)).join(", ") || "없음";
1126
- return `${kind} 색인이 없다 (있는 것: ${kinds}). hanlint usage build ${kind} <글 폴더> 로 만든다 (txt, md, jsonl. 하위 폴더 포함). 사업보고서는 scripts/fetch/dartReports.py, 위키백과는 scripts/fetch/koWikipedia.py 가 받는다`;
1131
+ return `${kind} 색인이 없다 (있는 것: ${kinds}). hanlint usage build ${kind} <글 폴더>로 만든다 (txt, md, jsonl. 하위 폴더 포함)`;
1127
1132
  }
1128
1133
 
1129
- /** 함께 쓰는 말 낱말 수 상한. 뜻은 파이썬 QUERY_WORDS 가 소유한다. */
1134
+ /** 함께 쓰는 말 단어 수 상한. 뜻은 파이썬 QUERY_WORDS가 소유한다. */
1130
1135
  const USAGE_QUERY_WORDS = 3;
1131
1136
 
1132
1137
  /** @param {[string, number][]} pairs */
@@ -1135,8 +1140,8 @@ function pairText(pairs) {
1135
1140
  }
1136
1141
 
1137
1142
  /**
1138
- * `hanlint usage "낱말 …" --kind report --limit 5`, `hanlint usage build <종류> <글 폴더>`, `hanlint usage kinds`. 뜻은 파이썬
1139
- * cli/commands/usage.py 가 소유한다.
1143
+ * `hanlint usage "단어 …" --kind report --limit 5`, `hanlint usage build <종류> <글 폴더>`, `hanlint usage kinds`. 뜻은 파이썬
1144
+ * cli/commands/usage.py가 소유한다.
1140
1145
  * @param {string[]} args
1141
1146
  */
1142
1147
  function runUsage(args) {
@@ -1144,7 +1149,7 @@ function runUsage(args) {
1144
1149
  const output = /** @type {string | undefined} */ (options["--output"]);
1145
1150
  const root = /** @type {string} */ (options["--root"] ?? defaultUsageRoot());
1146
1151
  const format = choose(/** @type {string} */ (options["--format"] ?? "text"), ["text", "json"], "--format");
1147
- if (!positionals.length) throw new UsageError('hanlint usage "낱말 …" 또는 hanlint usage build <종류> <글 폴더>');
1152
+ if (!positionals.length) throw new UsageError('hanlint usage "단어 …" 또는 hanlint usage build <종류> <글 폴더>');
1148
1153
  if (positionals[0] === "kinds") {
1149
1154
  const found = listUsageIndexes(root);
1150
1155
  if (format === "json") {
@@ -1152,11 +1157,11 @@ function runUsage(args) {
1152
1157
  return 0;
1153
1158
  }
1154
1159
  if (!found.length) {
1155
- emit(`${root} 에 색인이 없다. hanlint usage build <종류> <글 폴더> 로 만든다`, output);
1160
+ emit(`${root}에 색인이 없다. hanlint usage build <종류> <글 폴더>로 만든다`, output);
1156
1161
  return 0;
1157
1162
  }
1158
1163
  const lines = [`색인 ${found.length}개 (${root})`];
1159
- for (const meta of found) lines.push(` ${meta.kind}: 문서 ${meta.documents}편, 문장 ${meta.sentences}개, 토큰 ${meta.terms}종`);
1164
+ for (const meta of found) lines.push(` ${meta.kind}: 문서 ${comma(meta.documents)}편, 문장 ${comma(meta.sentences)}개, 단어 ${comma(meta.terms)}종`);
1160
1165
  emit(lines.join("\n"), output);
1161
1166
  return 0;
1162
1167
  }
@@ -1167,16 +1172,16 @@ function runUsage(args) {
1167
1172
  }
1168
1173
  const [, kind, folder] = positionals;
1169
1174
  if (!existsSync(folder) || !statSync(folder).isDirectory()) {
1170
- emit(`${folder} 는 폴더가 아니다`, output);
1175
+ emit(`폴더가 아니다: ${folder}`, output);
1171
1176
  return 2;
1172
1177
  }
1173
1178
  const result = buildUsageIndex(kind, readUsageDocuments(folder), root);
1174
- emit(`${join(root, kind)}: 문서 ${result.documents}편, 문장 ${result.sentences}개, 토큰 ${result.terms}종. hanlint usage "낱말" --kind ${kind} 로 묻는다`, output);
1179
+ emit(`${join(root, kind)}: 문서 ${comma(result.documents)}편, 문장 ${comma(result.sentences)}개, 단어 ${comma(result.terms)}종. hanlint usage "단어" --kind ${kind}로 찾는다`, output);
1175
1180
  return 0;
1176
1181
  }
1177
1182
  const kind = /** @type {string} */ (options["--kind"] ?? USAGE_KINDS[0]);
1178
1183
  const limit = Math.max(Number(options["--limit"] ?? 5), 0);
1179
- if (!Number.isInteger(limit)) throw new UsageError("--limit 은 정수다");
1184
+ if (!Number.isInteger(limit)) throw new UsageError("--limit은 정수다");
1180
1185
  const query = positionals.join(" ");
1181
1186
  const index = loadUsageIndex(kind, root);
1182
1187
  if (!index) {
@@ -1201,7 +1206,7 @@ function runUsage(args) {
1201
1206
  if (pairs.length) lines.push(` ${label}: ${pairText(/** @type {[string, number][]} */ (pairs))}`);
1202
1207
  }
1203
1208
  }
1204
- if (!hits.length && limit > 0) lines.push("쓰인 문장이 없다. 낱말을 줄이거나 다른 낱말로 묻는다");
1209
+ if (!hits.length && limit > 0) lines.push("쓰인 문장이 없다. 단어를 줄이거나 다른 단어로 찾는다");
1205
1210
  hits.forEach((hit, index) => {
1206
1211
  lines.push(`${index + 1}. ${hit.text}`);
1207
1212
  lines.push(` 문서 ${hit.documents}편, 출처 ${hit.source}`);
@@ -1210,7 +1215,7 @@ function runUsage(args) {
1210
1215
  return 0;
1211
1216
  }
1212
1217
 
1213
- /** `hanlint sheet 폴더/ --preset screen` 과 `hanlint sheet apply 시트.md`. 뜻은 파이썬 cli/commands/sheet.py 가 소유한다. @param {string[]} args */
1218
+ /** `hanlint sheet 폴더/ --preset screen`과 `hanlint sheet apply 시트.md`. 뜻은 파이썬 cli/commands/sheet.py가 소유한다. @param {string[]} args */
1214
1219
  function runSheet(args) {
1215
1220
  const { options, positionals } = parseArgs(args);
1216
1221
  const output = /** @type {string | undefined} */ (options["--output"]);
@@ -1231,13 +1236,13 @@ function runSheet(args) {
1231
1236
  /** @type {string[]} */
1232
1237
  const files = [];
1233
1238
  for (const target of positionals) {
1234
- if (!existsSync(target)) throw new Error(`${target} 이 없다`);
1239
+ if (!existsSync(target)) throw new Error(`없는 파일: ${target}`);
1235
1240
  if (statSync(target).isDirectory()) files.push(...sourceUnder(target));
1236
1241
  else files.push(target);
1237
1242
  }
1238
1243
  const config = configFrom(options, files);
1239
1244
  const format = choose(/** @type {string} */ (options["--format"] ?? "markdown"), ["markdown", "json"], "--format");
1240
- // 디스크의 파일을 읽어 report.sheetRows 에 넘긴다. 행의 뜻은 그쪽이 소유한다.
1245
+ // 디스크의 파일을 읽어 report.sheetRows에 넘긴다. 행의 뜻은 그쪽이 소유한다.
1241
1246
  const rows = sheetRows(files.map((file) => ({ label: relativeLabel(file), source: readFileSync(file, "utf-8") })), config, Boolean(options["--all"]));
1242
1247
  emit(format === "json" ? renderSheetJson(rows, config.preset, files.length) : renderSheet(rows, config.preset, files.length), output);
1243
1248
  return 0;
@@ -1258,8 +1263,14 @@ function dispatch(argv) {
1258
1263
  process.stdout.write(`hanlint ${version}\n`);
1259
1264
  return 0;
1260
1265
  }
1266
+ if (rest.includes("--help") || rest.includes("-h")) {
1267
+ // npm에는 명령마다의 도움말이 없다. 도움말을 찾은 사람에게 '모르는 옵션' 대신 전체 도움말을 보인다
1268
+ process.stdout.write(`${USAGE}
1269
+ `);
1270
+ return 0;
1271
+ }
1261
1272
  if (PYTHON_ONLY.includes(command)) {
1262
- throw new Error(`${command} 는 파이썬 패키지에 있다 (pip install hanlint). npm 은 ${COMMANDS.join(", ")} 을 제공한다`);
1273
+ throw new Error(`${command} 명령은 파이썬 hanlint에만 있다(pip install hanlint). npm hanlint의 명령: ${COMMANDS.join(", ")}`);
1263
1274
  }
1264
1275
  if (command === "lint") return runLint(rest);
1265
1276
  if (command === "contract") return runContract(rest);
@@ -1291,7 +1302,7 @@ export function main(argv) {
1291
1302
  }
1292
1303
  const failure = /** @type {NodeJS.ErrnoException} */ (error);
1293
1304
  if (failure.code === "ENOENT") {
1294
- process.stderr.write(`${failure.path} 를 찾지 못했다. 경로를 확인하거나 hanlint --help\n`);
1305
+ process.stderr.write(`찾지 못한 파일: ${failure.path}. 경로를 확인하거나 hanlint --help를 본다\n`);
1295
1306
  return 2;
1296
1307
  }
1297
1308
  process.stderr.write(`${failure.message}\n`);