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/walk.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  /**
3
3
  * 폴더에서 검사할 마크다운을 찾는 규칙. 첫 화면과 검사가 **같은 함수**를 쓴다.
4
- * 무엇을 건너뛰고 왜 그러는지는 파이썬 `cli/commands/shared.py` 의 SKIPPED_FOLDERS 가 소유한다.
4
+ * 무엇을 건너뛰고 왜 그러는지는 파이썬 `cli/commands/shared.py`의 SKIPPED_FOLDERS가 소유한다.
5
5
  */
6
6
  import { readdirSync, statSync } from "node:fs";
7
7
  import { join } from "node:path";
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  /**
3
- * `hanlint` 를 인자 없이 쳤을 때의 첫 화면. 파이썬 cli/welcome.py 와 같은 글자다.
4
- * 같은 폴더에서 두 구현을 돌리면 결과가 같다. 파일을 찾는 것은 검사와 같은 함수 (walk.markdownUnder) 다.
3
+ * `hanlint`를 인자 없이 쳤을 때의 첫 화면. 파이썬 cli/welcome.py와 같은 글자다.
4
+ * 같은 폴더에서 두 구현을 돌리면 결과가 같다. 파일을 찾는 것은 검사와 같은 함수 (walk.markdownUnder)다.
5
5
  */
6
6
  import { relative, sep } from "node:path";
7
7
 
@@ -27,24 +27,24 @@ export function welcome(version, folder) {
27
27
  const nearby = nearbyMarkdown(where);
28
28
  const example = nearby.length ? nearby[0] : "글.md";
29
29
  const shown = [
30
- [`hanlint ${example}`, "검사한다. 자리와 이유와 고칠 말이 나온다"],
31
- [`hanlint fix ${example}`, "기계가 확실히 고칠 수 있는 자리를 원문에 적용한다"],
30
+ [`hanlint ${example}`, "검사한다. 걸린 곳과 까닭과 고칠 말이 나온다"],
31
+ [`hanlint fix ${example}`, "기계가 확실히 고칠 수 있는 곳을 원문에 고친다"],
32
32
  [`hanlint audit ${example}`, "글의 모양을 지도와 분포로 본다"],
33
33
  ];
34
34
  const width = Math.max(...shown.map(([command]) => command.length));
35
35
  const lines = [
36
- `hanlint ${version} 한국어 글에서 세면 확정되는 결함을 집는다. 좋은 글인지는 판정하지 않는다`,
36
+ `hanlint ${version} 한국어 글에서 규칙으로 잡을 수 있는 잘못을 찾는다. 좋은 글인지는 판정하지 않는다`,
37
37
  "",
38
38
  ...shown.map(([command, why]) => ` ${command.padEnd(width)} ${why}`),
39
39
  ];
40
40
  if (nearby.length) {
41
- lines.push("", `이 폴더의 마크다운: ${nearby.join(", ")}. 폴더를 통째로 줘도 된다 (hanlint .)`);
41
+ lines.push("", `이 폴더의 마크다운: ${nearby.join(", ")}. 폴더를 통째로 줘도 된다(hanlint .)`);
42
42
  } else {
43
- lines.push("", "이 폴더에는 검사할 마크다운이 없다. 파일 하나나 폴더 하나를 인자로 준다");
43
+ lines.push("", "이 폴더에는 검사할 마크다운이 없다. 파일이나 폴더 하나를 인자로 준다");
44
44
  }
45
45
  lines.push(
46
46
  "",
47
- "처음이면 hanlint init 으로 설정을 만든다. 글의 종류가 블로그가 아니면 --preset report 나 --preset docs",
47
+ "처음이면 hanlint init으로 설정 파일을 만든다. 블로그 글이 아니면 --preset report나 --preset docs를 붙인다",
48
48
  "규칙 목록은 hanlint rules, 규칙 하나가 왜 있는지는 hanlint explain <규칙>, 지금 상태는 hanlint doctor",
49
49
  "전체 사용법은 hanlint --help",
50
50
  );
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** 설정 파일을 찾아 읽는다. `hanlint.toml` 이거나 `pyproject.toml` 의 `[tool.hanlint]` 다. 파이썬과 같은 탐색이다. */
2
+ /** 설정 파일을 찾아 읽는다. `hanlint.toml` 이거나 `pyproject.toml`의 `[tool.hanlint]`다. 파이썬과 같은 탐색이다. */
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
5
 
@@ -21,7 +21,7 @@ export function readConfigFile(path) {
21
21
  return config;
22
22
  }
23
23
 
24
- /** start 에서 위로 올라가며 hanlint.toml 이나 [tool.hanlint] 를 가진 pyproject.toml 을 찾는다. @param {string} start */
24
+ /** start에서 위로 올라가며 hanlint.toml이나 [tool.hanlint]를 가진 pyproject.toml을 찾는다. @param {string} start */
25
25
  export function findConfigFile(start) {
26
26
  let folder = resolve(start);
27
27
  while (true) {
@@ -40,7 +40,7 @@ export function findConfigFile(start) {
40
40
  }
41
41
 
42
42
  /**
43
- * `path` 를 주면 그 파일만 읽는다. 안 주면 `start` (기본 현재 폴더) 에서 찾고, 없으면 기본값이다.
43
+ * `path`를 주면 그 파일만 읽는다. 안 주면 `start` (기본 현재 폴더)에서 찾고, 없으면 기본값이다.
44
44
  * @param {string | null} [path]
45
45
  * @param {string | null} [start]
46
46
  */
@@ -5,9 +5,9 @@ import { sha256 } from "../digest.js";
5
5
  /** @param {unknown} value @param {string} where */
6
6
  function checkedString(value, where) {
7
7
  if (typeof value !== "string" || !value || value !== value.trim()) {
8
- throw new Error(`${where} 는 양끝 공백 없는 문자열이다`);
8
+ throw new Error(`${where}: 양끝 공백 없는 문자열이어야 한다`);
9
9
  }
10
- if (value !== value.normalize("NFC")) throw new Error(`${where} 는 NFC 문자열이어야 한다`);
10
+ if (value !== value.normalize("NFC")) throw new Error(`${where}: NFC 문자열이어야 한다`);
11
11
  return value;
12
12
  }
13
13
 
@@ -17,19 +17,19 @@ export class Patch {
17
17
  this.reason = checkedString(reason, "reason");
18
18
  this.before = checkedString(before, "before");
19
19
  this.after = checkedString(after, "after");
20
- if (this.before === this.after) throw new Error("patch before 와 after 는 달라야 한다");
20
+ if (this.before === this.after) throw new Error("patch before와 after는 달라야 한다");
21
21
  Object.freeze(this);
22
22
  }
23
23
 
24
24
  /** @param {unknown} raw */
25
25
  static fromMapping(raw) {
26
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("patch 는 JSON 객체다");
26
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("patch는 JSON 객체여야 한다");
27
27
  const data = /** @type {Record<string, unknown>} */ (raw);
28
28
  const expected = ["reason", "before", "after"];
29
29
  const unknown = Object.keys(data).filter((key) => !expected.includes(key)).sort();
30
30
  const missing = expected.filter((key) => !(key in data)).sort();
31
- if (unknown.length) throw new Error(`patch 의 모르는 키: ${unknown.join(", ")}`);
32
- if (missing.length) throw new Error(`patch 의 빠진 키: ${missing.join(", ")}`);
31
+ if (unknown.length) throw new Error(`patch의 모르는 키: ${unknown.join(", ")}`);
32
+ if (missing.length) throw new Error(`patch의 빠진 키: ${missing.join(", ")}`);
33
33
  return new Patch(
34
34
  /** @type {string} */ (data.reason),
35
35
  /** @type {string} */ (data.before),
@@ -9,19 +9,19 @@ export const CONTRACT_VERSIONS = Object.freeze([CONTRACT_VERSION, LATEST_CONTRAC
9
9
  /** @param {unknown} value @param {string} where */
10
10
  function checkedString(value, where) {
11
11
  if (typeof value !== "string" || !value || value !== value.trim()) {
12
- throw new Error(`${where} 는 양끝 공백 없는 문자열이다`);
12
+ throw new Error(`${where}: 양끝 공백 없는 문자열이어야 한다`);
13
13
  }
14
- if (value !== value.normalize("NFC")) throw new Error(`${where} 는 NFC 문자열이어야 한다`);
14
+ if (value !== value.normalize("NFC")) throw new Error(`${where}: NFC 문자열이어야 한다`);
15
15
  return value;
16
16
  }
17
17
 
18
18
  /** @param {unknown} value @param {string} where @param {boolean} [allowEmpty] */
19
19
  function checkedStrings(value, where, allowEmpty = false) {
20
20
  if (!Array.isArray(value) || (!allowEmpty && !value.length)) {
21
- throw new Error(`${where} 는 ${allowEmpty ? "문자열 배열" : "비지 않은 문자열 배열"}이다`);
21
+ throw new Error(`${where}: ${allowEmpty ? "문자열 배열" : "비지 않은 문자열 배열"}이어야 한다`);
22
22
  }
23
23
  const found = value.map((item, index) => checkedString(item, `${where} ${index + 1}번째`));
24
- if (new Set(found).size !== found.length) throw new Error(`${where} 에 같은 값이 두 번 있다`);
24
+ if (new Set(found).size !== found.length) throw new Error(`${where}에 같은 값이 두 번 있다`);
25
25
  return found;
26
26
  }
27
27
 
@@ -45,7 +45,7 @@ export class Contract {
45
45
  /** @param {string} reader @param {string} goal @param {string[]} facts @param {number} [version] */
46
46
  constructor(reader, goal, facts, version = CONTRACT_VERSION) {
47
47
  if (!Number.isInteger(version) || version !== CONTRACT_VERSION) {
48
- throw new Error(`reader contract version 은 ${CONTRACT_VERSION}이다: ${version}`);
48
+ throw new Error(`reader contract version이 맞지 않다: ${version} (맞는 값: ${CONTRACT_VERSION})`);
49
49
  }
50
50
  this.reader = checkedString(reader, "reader");
51
51
  this.goal = checkedString(goal, "goal");
@@ -56,13 +56,13 @@ export class Contract {
56
56
 
57
57
  /** @param {unknown} raw */
58
58
  static fromMapping(raw) {
59
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("reader contract 는 JSON 객체다");
59
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("reader contract는 JSON 객체여야 한다");
60
60
  const data = /** @type {Record<string, unknown>} */ (raw);
61
61
  const expected = ["version", "reader", "goal", "facts"];
62
62
  const unknown = Object.keys(data).filter((key) => !expected.includes(key)).sort();
63
63
  const missing = expected.filter((key) => !(key in data)).sort();
64
- if (unknown.length) throw new Error(`reader contract 의 모르는 키: ${unknown.join(", ")}`);
65
- if (missing.length) throw new Error(`reader contract 의 빠진 키: ${missing.join(", ")}`);
64
+ if (unknown.length) throw new Error(`reader contract의 모르는 키: ${unknown.join(", ")}`);
65
+ if (missing.length) throw new Error(`reader contract의 빠진 키: ${missing.join(", ")}`);
66
66
  return new Contract(
67
67
  /** @type {string} */ (data.reader),
68
68
  /** @type {string} */ (data.goal),
@@ -96,13 +96,13 @@ export class ProtectedSurface {
96
96
 
97
97
  /** @param {unknown} raw */
98
98
  static fromMapping(raw) {
99
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("surface 는 JSON 객체다");
99
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("surface는 JSON 객체여야 한다");
100
100
  const data = /** @type {Record<string, unknown>} */ (raw);
101
101
  const expected = ["numbers", "urls", "code", "links"];
102
102
  const unknown = Object.keys(data).filter((key) => !expected.includes(key)).sort();
103
103
  const missing = expected.filter((key) => !(key in data)).sort();
104
- if (unknown.length) throw new Error(`surface 의 모르는 키: ${unknown.join(", ")}`);
105
- if (missing.length) throw new Error(`surface 의 빠진 키: ${missing.join(", ")}`);
104
+ if (unknown.length) throw new Error(`surface의 모르는 키: ${unknown.join(", ")}`);
105
+ if (missing.length) throw new Error(`surface의 빠진 키: ${missing.join(", ")}`);
106
106
  return new ProtectedSurface(
107
107
  /** @type {string[]} */ (data.numbers),
108
108
  /** @type {string[]} */ (data.urls),
@@ -128,7 +128,7 @@ export class ProtectedSurface {
128
128
  export class Outline {
129
129
  /** @param {number} level @param {string[]} headings */
130
130
  constructor(level, headings) {
131
- if (!Number.isInteger(level) || level < 1 || level > 6) throw new Error(`outline.level 은 1~6 정수다: ${level}`);
131
+ if (!Number.isInteger(level) || level < 1 || level > 6) throw new Error(`outline.level은 1~6 정수여야 한다. 받은 값: ${level}`);
132
132
  this.level = level;
133
133
  this.headings = Object.freeze(checkedStrings(headings, "outline.headings"));
134
134
  Object.freeze(this);
@@ -136,13 +136,13 @@ export class Outline {
136
136
 
137
137
  /** @param {unknown} raw */
138
138
  static fromMapping(raw) {
139
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("outline 은 JSON 객체다");
139
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("outline은 JSON 객체여야 한다");
140
140
  const data = /** @type {Record<string, unknown>} */ (raw);
141
141
  const expected = ["level", "headings"];
142
142
  const unknown = Object.keys(data).filter((key) => !expected.includes(key)).sort();
143
143
  const missing = expected.filter((key) => !(key in data)).sort();
144
- if (unknown.length) throw new Error(`outline 의 모르는 키: ${unknown.join(", ")}`);
145
- if (missing.length) throw new Error(`outline 의 빠진 키: ${missing.join(", ")}`);
144
+ if (unknown.length) throw new Error(`outline의 모르는 키: ${unknown.join(", ")}`);
145
+ if (missing.length) throw new Error(`outline의 빠진 키: ${missing.join(", ")}`);
146
146
  return new Outline(/** @type {number} */ (data.level), /** @type {string[]} */ (data.headings));
147
147
  }
148
148
 
@@ -155,7 +155,7 @@ export class ContractV2 {
155
155
  /** @param {string} reader @param {string} goal @param {string[]} facts @param {ProtectedSurface | Record<string, unknown>} surface @param {Outline | Record<string, unknown>} outline @param {number} [version] */
156
156
  constructor(reader, goal, facts, surface, outline, version = LATEST_CONTRACT_VERSION, lockedFacts = [], editPolicy = {}) {
157
157
  if (!Number.isInteger(version) || version !== LATEST_CONTRACT_VERSION) {
158
- throw new Error(`reader contract version 은 ${LATEST_CONTRACT_VERSION}다: ${version}`);
158
+ throw new Error(`reader contract version이 맞지 않다: ${version} (맞는 값: ${LATEST_CONTRACT_VERSION})`);
159
159
  }
160
160
  this.reader = checkedString(reader, "reader");
161
161
  this.goal = checkedString(goal, "goal");
@@ -163,14 +163,14 @@ export class ContractV2 {
163
163
  this.surface = surface instanceof ProtectedSurface ? surface : ProtectedSurface.fromMapping(surface);
164
164
  this.outline = outline instanceof Outline ? outline : Outline.fromMapping(outline);
165
165
  this.lockedFacts = Object.freeze(checkedStrings(lockedFacts, "lockedFacts", true));
166
- if (this.lockedFacts.some((fact) => !this.facts.includes(fact))) throw new Error("lockedFacts must select approved facts");
167
- if (!editPolicy || typeof editPolicy !== "object" || Array.isArray(editPolicy)) throw new Error("editPolicy must be an object");
166
+ if (this.lockedFacts.some((fact) => !this.facts.includes(fact))) throw new Error("lockedFacts는 승인한 facts 가운데서 골라야 한다");
167
+ if (!editPolicy || typeof editPolicy !== "object" || Array.isArray(editPolicy)) throw new Error("editPolicy는 객체여야 한다");
168
168
  this.editPolicy = Object.freeze(Object.fromEntries(Object.entries(editPolicy).sort(([a], [b]) => compareText(a, b)).map(([rule, limits]) => {
169
- if (!rule || rule !== rule.trim()) throw new Error("editPolicy requires rule names");
169
+ if (!rule || rule !== rule.trim()) throw new Error("editPolicy에는 규칙 이름이 있어야 한다");
170
170
  if (!limits || typeof limits !== "object" || Array.isArray(limits) || Object.keys(limits).sort().join(",") !== "maxChars,maxLines") {
171
- throw new Error("editPolicy requires maxChars and maxLines");
171
+ throw new Error("editPolicy의 규칙마다 maxChars와 maxLines가 있어야 한다");
172
172
  }
173
- if (Object.values(limits).some((n) => !Number.isInteger(n) || n < 1)) throw new Error("editPolicy limits must be positive integers");
173
+ if (Object.values(limits).some((n) => !Number.isInteger(n) || n < 1)) throw new Error("editPolicy의 한도는 양의 정수여야 한다");
174
174
  return [rule, Object.freeze({maxChars: limits.maxChars, maxLines: limits.maxLines})];
175
175
  })));
176
176
  this.version = version;
@@ -179,13 +179,13 @@ export class ContractV2 {
179
179
 
180
180
  /** @param {unknown} raw */
181
181
  static fromMapping(raw) {
182
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("reader contract 는 JSON 객체다");
182
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("reader contract는 JSON 객체여야 한다");
183
183
  const data = /** @type {Record<string, unknown>} */ (raw);
184
184
  const expected = ["version", "reader", "goal", "facts", "surface", "outline"];
185
185
  const unknown = Object.keys(data).filter((key) => !expected.includes(key) && !["lockedFacts", "editPolicy"].includes(key)).sort();
186
186
  const missing = expected.filter((key) => !(key in data)).sort();
187
- if (unknown.length) throw new Error(`reader contract 의 모르는 키: ${unknown.join(", ")}`);
188
- if (missing.length) throw new Error(`reader contract 의 빠진 키: ${missing.join(", ")}`);
187
+ if (unknown.length) throw new Error(`reader contract의 모르는 키: ${unknown.join(", ")}`);
188
+ if (missing.length) throw new Error(`reader contract의 빠진 키: ${missing.join(", ")}`);
189
189
  return new ContractV2(
190
190
  /** @type {string} */ (data.reader),
191
191
  /** @type {string} */ (data.goal),
@@ -237,9 +237,9 @@ export class ContractV2 {
237
237
 
238
238
  /** @param {unknown} raw */
239
239
  export function parseContract(raw) {
240
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("reader contract 는 JSON 객체다");
240
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("reader contract는 JSON 객체여야 한다");
241
241
  const version = /** @type {Record<string, unknown>} */ (raw).version;
242
242
  if (version === CONTRACT_VERSION) return Contract.fromMapping(raw);
243
243
  if (version === LATEST_CONTRACT_VERSION) return ContractV2.fromMapping(raw);
244
- throw new Error(`reader contract version 은 ${CONTRACT_VERSIONS.join(",")} 가운데 하나다: ${version}`);
244
+ throw new Error(`reader contract version이 맞지 않다: ${version} (맞는 값: ${CONTRACT_VERSIONS.join(",")} 가운데 하나)`);
245
245
  }
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  /**
3
- * 설정과 임계 기본값. 정본은 파이썬 config/settings.py 이고 여기는 같은 값을 든다. 필드 이름이 곧 설정 파일의 키다.
4
- * 기본값을 바꾸면 양쪽을 같은 작업에서 바꾼다. tests/parity 가 두 구현의 결과를 견준다.
3
+ * 설정과 임계 기본값. 정본은 파이썬 config/settings.py이고 여기는 같은 값을 든다. 필드 이름이 곧 설정 파일의 키다.
4
+ * 기본값을 바꾸면 양쪽을 같은 작업에서 바꾼다. tests/parity가 두 구현의 결과를 견준다.
5
5
  */
6
6
 
7
7
  import { projectExemplars } from "../data/exemplars.js";
@@ -9,15 +9,16 @@ import { projectOperations } from "../data/operations.js";
9
9
  import { projectPatches } from "../data/patches.js";
10
10
 
11
11
  /**
12
- * 글의 종류마다 처음부터 끄고 시작할 규칙. 정본은 파이썬 config/settings.py 의 PRESETS 다.
12
+ * 글의 종류마다 처음부터 끄고 시작할 규칙. 정본은 파이썬 config/settings.py의 PRESETS다.
13
13
  * @type {Record<string, string[]>}
14
14
  */
15
- // 화면의 글에서만 켜는 것. 산문의 모든 종류가 처음부터 끈다. 파이썬 settings.py 의 SCREEN 과 같다.
15
+ // 화면의 글에서만 켜는 것. 산문의 모든 종류가 처음부터 끈다. 파이썬 settings.py의 SCREEN과 같다.
16
16
  const SCREEN = ["screenNarration", "screenSentence", "screenTone", "screenWord"];
17
17
  export const PRESETS = {
18
18
  blog: [...SCREEN],
19
- report: ["noQuestion", "firstResultDistance", "introImage", "moreLater", "numberOrphan", ...SCREEN],
19
+ report: ["noQuestion", "firstResultDistance", "introImage", "moreLater", "numberOrphan", "imperativePeriod", ...SCREEN],
20
20
  docs: [
21
+ "imperativePeriod",
21
22
  "noQuestion",
22
23
  "firstResultDistance",
23
24
  "introImage",
@@ -30,18 +31,18 @@ export const PRESETS = {
30
31
  ...SCREEN,
31
32
  ],
32
33
  };
33
- // 서사 글에서만 끄는 것. 설명글의 전제 (독자가 문장 사이 이유를 따라간다, 말끝을 맺는다) 가 장면과
34
- // 대사에는 성립하지 않는다. 파이썬 settings.py 의 NARRATIVE 와 같다.
35
- // 참고 문서와 자습서는 명령 전후 상태와 완결된 예제를 보이려 같은 블록을 다시 둔다. 파이썬의 REFERENCE 와 같다.
34
+ // 서사 글에서만 끄는 것. 설명글의 전제 (독자가 문장 사이 이유를 따라간다, 말끝을 맺는다)가 장면과
35
+ // 대사에는 성립하지 않는다. 파이썬 settings.py의 NARRATIVE와 같다.
36
+ // 참고 문서와 자습서는 명령 전후 상태와 완결된 예제를 보이려 같은 블록을 다시 둔다. 파이썬의 REFERENCE와 같다.
36
37
  const REFERENCE = ["duplicateBlock", "headingUniform"];
37
- // 백과 표제어는 첫 절 앞의 머리말이 길게 정의와 요약을 담는 것이 관례다. 파이썬의 ENCYCLOPEDIC 과 같다.
38
+ // 백과 표제어는 첫 절 앞의 머리말이 길게 정의와 요약을 담는 것이 관례다. 파이썬의 ENCYCLOPEDIC과 같다.
38
39
  const ENCYCLOPEDIC = ["introLong"];
39
40
  const NARRATIVE = ["factListParagraph"];
40
41
  PRESETS.guide = [...PRESETS.blog, ...REFERENCE];
41
42
  PRESETS.essay = [...PRESETS.report, ...NARRATIVE];
42
43
  PRESETS.fiction = [...PRESETS.report, ...NARRATIVE];
43
44
  PRESETS.encyclopedia = [...PRESETS.docs, ...ENCYCLOPEDIC];
44
- // 대화 답변에서 끄는 것. 글의 짜임을 재는 규칙은 글 한 편을 전제한다. 파이썬 settings.py 의 CONVERSATION 과 같다.
45
+ // 대화 답변에서 끄는 것. 글의 짜임을 재는 규칙은 글 한 편을 전제한다. 파이썬 settings.py의 CONVERSATION과 같다.
45
46
  const CONVERSATION = [
46
47
  "countMismatch",
47
48
  "duplicateBlock",
@@ -50,6 +51,7 @@ const CONVERSATION = [
50
51
  "headingSentence",
51
52
  "headingSkip",
52
53
  "headingUniform",
54
+ "imperativePeriod",
53
55
  "inputFileSource",
54
56
  "installImport",
55
57
  "introLong",
@@ -62,7 +64,7 @@ const CONVERSATION = [
62
64
  "sectionNoProse",
63
65
  ];
64
66
  PRESETS.chat = [...CONVERSATION, ...SCREEN];
65
- // 화면의 글에서 끄는 것. 문장과 문단과 글의 짜임을 재는 규칙은 화면의 글 한 마디에 전제가 없다. 파이썬 settings.py 의 SCREEN_OFF 와 같다.
67
+ // 화면의 글에서 끄는 것. 문장과 문단과 글의 짜임을 재는 규칙은 화면의 글 한 마디에 전제가 없다. 파이썬 settings.py의 SCREEN_OFF와 같다.
66
68
  const SCREEN_OFF = [
67
69
  "cliche",
68
70
  "connectorRepeat",
@@ -108,10 +110,10 @@ const SCREEN_OFF = [
108
110
  "platformApi",
109
111
  ];
110
112
  PRESETS.screen = SCREEN_OFF;
111
- /** enforceStyle 에 넣을 수 있는 규칙. 파이썬 settings.py 의 ENFORCEABLE 과 같다. */
112
- export const ENFORCEABLE = ["noQuestion", "nounPile", "screenNarration", "screenTone", "screenWord"];
113
+ /** enforceStyle에 넣을 수 있는 규칙. 파이썬 settings.py의 ENFORCEABLE과 같다. */
114
+ export const ENFORCEABLE = ["imperativePeriod", "noQuestion", "nounPile", "screenNarration", "screenTone", "screenWord"];
113
115
 
114
- /** 프리셋 → 견줄 프로파일의 종류. 정본은 파이썬 config/settings.py 의 PROFILE_OF 다. @type {Record<string, string>} */
116
+ /** 프리셋 → 견줄 프로파일의 종류. 정본은 파이썬 config/settings.py의 PROFILE_OF다. @type {Record<string, string>} */
115
117
  export const PROFILE_OF = {
116
118
  blog: "blog",
117
119
  report: "report",
@@ -124,7 +126,7 @@ export const PROFILE_OF = {
124
126
  screen: null,
125
127
  };
126
128
 
127
- /** 프리셋 → 용례 빈도표의 종류. 정본은 파이썬 config/settings.py 의 USAGE_OF 다. @type {Record<string, string>} */
129
+ /** 프리셋 → 용례 빈도표의 종류. 정본은 파이썬 config/settings.py의 USAGE_OF다. @type {Record<string, string>} */
128
130
  export const USAGE_OF = { report: "report" };
129
131
  /** 실린 빈도표의 종류. */
130
132
  export const USAGE_KINDS = [...new Set(Object.values(USAGE_OF))].sort();
@@ -135,30 +137,30 @@ export const DEFAULT_PRESET = PRESET_NAMES[0];
135
137
 
136
138
  /**
137
139
  * @typedef {object} Config
138
- * @property {string} preset 글의 종류. PRESETS 가 정한 규칙을 처음부터 끈다
140
+ * @property {string} preset 글의 종류. PRESETS가 정한 규칙을 처음부터 끈다
139
141
  * @property {string[]} enforceStyle
140
142
  * @property {Set<string>} disable 끌 규칙 이름
141
143
  * @property {string | null} keywordField
142
144
  * @property {string[]} introFields
143
145
  * @property {string[]} endingFields
144
146
  * @property {string | null} profile 사용자 프로파일 파일 경로
145
- * @property {number} profilePercentile outsideProfile 이 짚는 백분위
147
+ * @property {number} profilePercentile outsideProfile이 짚는 백분위
146
148
  * @property {number} countMismatchSpan 절이 없는 글이 이 줄 수 안일 때만 글 전체를 견준다
147
149
  * @property {string | null} baseline
148
150
  * @property {Record<string, unknown[]>} dictionary
149
- * @property {import("../data/exemplars.js").Exemplar[]} exemplars 사람이 승인한 프로젝트 본보기
151
+ * @property {import("../data/exemplars.js").Exemplar[]} exemplars 사람이 승인한 프로젝트 예시
150
152
  * @property {import("../data/patches.js").Patch[]} patches 사람이 승인한 국소 고침
151
153
  * @property {import("../data/operations.js").SurfaceOperation[]} operations 사람이 승인한 표면 치환
152
154
  * @property {string[]} protectedTerms 표면 치환이 바꾸면 안 되는 한국어 고유명사와 프로젝트 용어
153
155
  * @property {string[]} ignoreFences 지문에서 뺄 펜스의 언어 표기. 장면 계약과 도표 원문은 코드 블록이 아니다
154
- * @property {string | null} source 설정을 읽은 파일. 기본값이면 null. loadConfig 가 채운다
156
+ * @property {string | null} source 설정을 읽은 파일. 기본값이면 null. loadConfig가 채운다
155
157
  * @property {number} fragmentRun
156
158
  * @property {number} introMaxParagraphs
157
159
  * @property {number} headingUniformRatio
158
160
  * @property {number} headingSentenceMaxLevel
159
161
  * @property {number} bridgeRepeatMin
160
162
  * @property {number} nounPileMin
161
- * @property {string | null} usageKind 용례 빈도표의 종류. null 이면 프리셋이 정하고 "" 면 보지 않는다
163
+ * @property {string | null} usageKind 용례 빈도표의 종류. null이면 프리셋이 정하고 ""면 보지 않는다
162
164
  * @property {number} usageMin 명사 연쇄가 몇 편의 문서에 나와야 용례로 보는가
163
165
  * @property {number} usageShare 사전 항목이 그 종류의 문서 몇 할에 나오면 관용으로 보는가
164
166
  * @property {number} endingRun
@@ -223,7 +225,7 @@ export function enabled(config, ruleName) {
223
225
  return !config.disable.has(ruleName) && (!PRESETS[config.preset].includes(ruleName) || config.enforceStyle.includes(ruleName));
224
226
  }
225
227
 
226
- /** 지금 꺼져 있는 규칙 이름. 프리셋이 끈 것과 disable 이 끈 것을 합친다. @param {Config} config */
228
+ /** 지금 꺼져 있는 규칙 이름. 프리셋이 끈 것과 disable이 끈 것을 합친다. @param {Config} config */
227
229
  export function offRules(config) {
228
230
  return [...new Set([...PRESETS[config.preset].filter((name) => !config.enforceStyle.includes(name)), ...config.disable])].sort();
229
231
  }
@@ -235,21 +237,21 @@ export function configFromMapping(data) {
235
237
  if (key === "disable") {
236
238
  config.disable = new Set(/** @type {string[]} */ (value));
237
239
  } else if (key === "analyzer") {
238
- // 0.0.7 까지의 키. hanlint init 이 surface 를 써 넣었으므로 그 값은 조용히 넘기고 다른 값은 빠졌다고 알린다.
239
- if (value !== "surface") throw new Error(`analyzer 설정은 빠졌다. 분석기는 표층 하나라 키를 지운다: ${JSON.stringify(value)}`);
240
+ // 0.0.7 까지의 키. hanlint init이 surface를 써 넣었으므로 그 값은 조용히 넘기고 다른 값은 빠졌다고 알린다.
241
+ if (value !== "surface") throw new Error(`analyzer 설정은 이제 쓰지 않는다. 설정 파일에서 이 키를 지운다. 받은 값: ${JSON.stringify(value)}`);
240
242
  } else if (key === "preset") {
241
243
  if (!(/** @type {string} */ (value) in PRESETS)) {
242
- throw new Error(`preset 은 ${PRESET_NAMES.join(", ")} 가운데 하나다: ${JSON.stringify(value)}`);
244
+ throw new Error(`preset은 ${PRESET_NAMES.join(", ")} 가운데 하나여야 한다. 받은 값: ${JSON.stringify(value)}`);
243
245
  }
244
246
  config.preset = /** @type {string} */ (value);
245
247
  } else if (key === "enforceStyle") {
246
248
  if (!Array.isArray(value) || value.some((name) => !ENFORCEABLE.includes(name))) {
247
- throw new Error(`enforceStyle 은 ${ENFORCEABLE.join(", ")}의 배열이다`);
249
+ throw new Error(`enforceStyle은 다음 규칙 이름의 배열이어야 한다: ${ENFORCEABLE.join(", ")}`);
248
250
  }
249
251
  config.enforceStyle = [...value];
250
252
  } else if (key === "usageKind") {
251
253
  if (value !== null && value !== "" && !USAGE_KINDS.includes(/** @type {string} */ (value))) {
252
- throw new Error(`usageKind 는 ${USAGE_KINDS.join(", ")} 가운데 하나이거나 빈 문자열이다: ${JSON.stringify(value)}`);
254
+ throw new Error(`usageKind는 ${USAGE_KINDS.join(", ")} 가운데 하나이거나 빈 문자열이어야 한다. 받은 값: ${JSON.stringify(value)}`);
253
255
  }
254
256
  config.usageKind = /** @type {string | null} */ (value);
255
257
  } else if (key === "dictionary") {
@@ -262,14 +264,14 @@ export function configFromMapping(data) {
262
264
  config.operations = projectOperations(value, PRESET_NAMES);
263
265
  } else if (key === "ignoreFences" || key === "introFields" || key === "endingFields" || key === "protectedTerms") {
264
266
  if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.trim())) {
265
- throw new Error(`${key} 는 문자열 배열이다: ${JSON.stringify(value)}`);
267
+ throw new Error(`${key}: 문자열 배열이어야 한다. 받은 값: ${JSON.stringify(value)}`);
266
268
  }
267
269
  const names = value.map((item) => item.trim());
268
270
  config[key] = key === "ignoreFences" ? names.map((name) => name.toLowerCase()) : names;
269
271
  } else if (key !== "source" && key in config) {
270
272
  config[key] = value;
271
273
  } else {
272
- throw new Error(`모르는 설정 키: ${key}. hanlint init 이 만드는 파일의 키만 쓴다`);
274
+ throw new Error(`모르는 설정 키: ${key}. hanlint init이 만드는 파일의 키만 쓴다`);
273
275
  }
274
276
  }
275
277
  return config;
@@ -1,8 +1,8 @@
1
1
  // @ts-check
2
2
  /**
3
- * hanlint 설정에 쓰이는 TOML 부분집합을 읽는다. 의존성 0 을 지키려고 직접 쓴다.
3
+ * hanlint 설정에 쓰이는 TOML 부분집합을 읽는다. 의존성 0을 지키려고 직접 쓴다.
4
4
  *
5
- * 되는 것: 주석, `key = 값`, 점 키, `[table]`, `[[array]]`, 문자열 ("..." 와 '...'), 정수, 실수, 불리언,
5
+ * 되는 것: 주석, `key = 값`, 점 키, `[table]`, `[[array]]`, 문자열 ("..."와 '...'), 정수, 실수, 불리언,
6
6
  * 여러 줄 배열, 인라인 테이블. 안 되는 것: 여러 줄 문자열, 날짜. 만나면 어디서 막혔는지 말하고 던진다.
7
7
  */
8
8
 
@@ -28,7 +28,7 @@ class Parser {
28
28
  /** @param {string} message */
29
29
  error(message) {
30
30
  const line = this.text.slice(0, this.pos).split("\n").length;
31
- return new Error(`TOML ${line}번째 줄: ${message}. hanlint 가 읽는 TOML 부분집합이 아니다`);
31
+ return new Error(`TOML ${line}번째 줄: ${message}. hanlint가 읽는 TOML 부분집합이 아니다`);
32
32
  }
33
33
 
34
34
  document() {
@@ -102,7 +102,7 @@ class Parser {
102
102
  const next = node[part];
103
103
  if (Array.isArray(next)) node = /** @type {Record<string, unknown>} */ (next[next.length - 1]);
104
104
  else if (typeof next === "object" && next !== null) node = /** @type {Record<string, unknown>} */ (next);
105
- else throw this.error(`${part} 는 테이블이 아니다`);
105
+ else throw this.error(`테이블이 아니다: ${part}`);
106
106
  }
107
107
  return node;
108
108
  }
@@ -110,7 +110,7 @@ class Parser {
110
110
  table() {
111
111
  this.pos++;
112
112
  const parts = this.keyPath();
113
- if (this.text[this.pos] !== "]") throw this.error("] 가 없다");
113
+ if (this.text[this.pos] !== "]") throw this.error("]가 없다");
114
114
  this.pos++;
115
115
  this.current = this.descend(this.root, parts);
116
116
  }
@@ -118,13 +118,13 @@ class Parser {
118
118
  arrayTable() {
119
119
  this.pos += 2;
120
120
  const parts = this.keyPath();
121
- if (!this.text.startsWith("]]", this.pos)) throw this.error("]] 가 없다");
121
+ if (!this.text.startsWith("]]", this.pos)) throw this.error("]]가 없다");
122
122
  this.pos += 2;
123
123
  const parent = this.descend(this.root, parts.slice(0, -1));
124
124
  const name = parts[parts.length - 1];
125
125
  if (!(name in parent)) parent[name] = [];
126
126
  const list = parent[name];
127
- if (!Array.isArray(list)) throw this.error(`${name} 은 배열이 아니다`);
127
+ if (!Array.isArray(list)) throw this.error(`배열이 아니다: ${name}`);
128
128
  /** @type {Record<string, unknown>} */
129
129
  const entry = {};
130
130
  list.push(entry);
@@ -209,7 +209,7 @@ class Parser {
209
209
  items.push(this.value());
210
210
  this.skipBlank();
211
211
  if (this.text[this.pos] === ",") this.pos++;
212
- else if (this.text[this.pos] !== "]") throw this.error("배열에 , 나 ] 가 없다");
212
+ else if (this.text[this.pos] !== "]") throw this.error("배열에 쉼표나 ]가 없다");
213
213
  }
214
214
  }
215
215
 
@@ -233,7 +233,7 @@ class Parser {
233
233
  this.pos++;
234
234
  return table;
235
235
  }
236
- throw this.error("인라인 테이블에 , 나 } 가 없다");
236
+ throw this.error("인라인 테이블에 쉼표나 }가 없다");
237
237
  }
238
238
  }
239
239
  }