hanlint 0.0.8 → 0.0.10

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 (58) hide show
  1. package/README.md +45 -5
  2. package/data/checkResult.schema.json +75 -0
  3. package/data/checkResultV2.schema.json +98 -0
  4. package/data/exemplars.json +2 -26
  5. package/data/finding.schema.json +38 -0
  6. package/data/holeKinds.json +1 -12
  7. package/data/patch.schema.json +14 -0
  8. package/data/patchResult.schema.json +86 -0
  9. package/data/patchResultV2.schema.json +71 -0
  10. package/data/patterns.json +0 -11
  11. package/data/platformApis.txt +3 -1
  12. package/data/profiles.json +1 -1
  13. package/data/pythonPackages.txt +3 -2
  14. package/data/readerContract.schema.json +20 -0
  15. package/data/readerContractConformanceV1.json +168 -0
  16. package/data/readerContractV2.schema.json +64 -0
  17. package/data/ruleCategories.json +0 -4
  18. package/data/ruleDocs.json +7 -11
  19. package/data/ruleMechanisms.json +0 -4
  20. package/data/spelling.json +1 -1
  21. package/data/version.json +1 -1
  22. package/data/writingBrief.schema.json +1 -1
  23. package/data/writingBriefV2.schema.json +1 -1
  24. package/koglType1.LICENSE.md +25 -0
  25. package/package.json +6 -2
  26. package/src/cli/help.js +34 -0
  27. package/src/cli/main.js +247 -35
  28. package/src/config/patch.js +48 -0
  29. package/src/config/readerContract.js +245 -0
  30. package/src/config/settings.js +47 -14
  31. package/src/data/profiles.js +12 -0
  32. package/src/fingerprint/build.js +10 -7
  33. package/src/fingerprint/codeMarkers.js +1 -1
  34. package/src/guard/contract.js +174 -0
  35. package/src/guard/editLocality.js +27 -0
  36. package/src/guard/outline.js +46 -0
  37. package/src/guard/receipt.js +180 -0
  38. package/src/guard/surface.js +184 -0
  39. package/src/index.js +39 -1
  40. package/src/report/compactReport.js +1 -1
  41. package/src/report/writingSpec.js +174 -0
  42. package/src/rules/code/inputFileSource.js +5 -3
  43. package/src/rules/code/installImport.js +8 -6
  44. package/src/rules/document/keywordHeading.js +7 -2
  45. package/src/rules/document/noQuestion.js +4 -4
  46. package/src/rules/document/tableOddCell.js +2 -1
  47. package/src/rules/readerDebt.js +9 -0
  48. package/src/rules/registry.js +27 -8
  49. package/src/rules/sentence/danglingDeixis.js +15 -0
  50. package/src/rules/sentence/negationRedefine.js +2 -1
  51. package/src/rules/sentence/nounPile.js +1 -1
  52. package/src/rules/structure/headingUniform.js +3 -3
  53. package/src/rules/structure/introLong.js +3 -1
  54. package/data/coverageTypes.txt +0 -7
  55. package/src/rules/paragraph/topicBreak.js +0 -26
  56. package/src/rules/sentence/ellipsis.js +0 -32
  57. package/src/rules/structure/headingQuestion.js +0 -19
  58. package/src/rules/structure/sectionResult.js +0 -38
@@ -5,7 +5,8 @@ import { DOCUMENT, NOTICE, finding } from "../finding.js";
5
5
  export const name = "installImport";
6
6
  export const mechanism = "contrast";
7
7
 
8
- const INSTALL = /(?:pip\s+install|uv\s+add|uv\s+pip\s+install|conda\s+install|poetry\s+add)\s+([^\n#|&;]+)/g;
8
+ // 캡처는 백틱과 한글에서 멈춘다. 안 멈추면 설치 줄 뒤의 산문을 패키지로 등록한다. 파이썬과 같다.
9
+ const INSTALL = /(?:pip\s+install|uv\s+add|uv\s+pip\s+install|conda\s+install|poetry\s+add)\s+([^\n#|&;`가-힣]+)/g;
9
10
  const IMPORT = /^\s*(?:import\s+([\w.]+(?:\s*,\s*[\w.]+)*)|from\s+([\w.]+)\s+import\b)/;
10
11
  const REQUIREMENT = /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[([^\]]*)\])?/;
11
12
  const PY_FILE = /\b(\w+)\.py\b/g;
@@ -17,14 +18,14 @@ function stdlib() {
17
18
  return stdlibCache;
18
19
  }
19
20
 
20
- /** @type {Map<string, string> | null} */
21
+ /** @type {Map<string, Set<string>> | null} */
21
22
  let packageCache = null;
22
23
  function packageOf() {
23
24
  if (!packageCache) {
24
25
  packageCache = new Map();
25
26
  for (const line of loadLines("pythonPackages.txt")) {
26
27
  const [module, pkg] = line.split("\t");
27
- packageCache.set(module, pkg);
28
+ packageCache.set(module, new Set(pkg.split("|").map(normalize)));
28
29
  }
29
30
  }
30
31
  return packageCache;
@@ -104,9 +105,10 @@ export function run(doc) {
104
105
  for (const module of modules) {
105
106
  const top = module.split(".")[0];
106
107
  if (!top || stdlib().has(top) || local.has(top)) continue;
107
- const pkg = normalize(packageOf().get(top) ?? top);
108
- if (!packages.has(pkg)) {
109
- findings.push(finding(name, line, code.trim(), `\`${top}\` 를 import 하는데 설치 줄에 \`${pkg}\` 가 없다. 독자는 ModuleNotFoundError 에서 멈춘다`, null, "error", DOCUMENT, block.index));
108
+ const acceptable = packageOf().get(top) ?? new Set([normalize(top)]);
109
+ if (![...acceptable].some((pkg) => packages.has(pkg))) {
110
+ const packageNames = [...acceptable].sort().join("`, `");
111
+ findings.push(finding(name, line, code.trim(), `\`${top}\` 를 import 하는데 설치 줄에 허용되는 이름 \`${packageNames}\` 가 없다. 독자는 ModuleNotFoundError 에서 멈춘다`, null, "error", DOCUMENT, block.index));
110
112
  }
111
113
  }
112
114
  }
@@ -19,9 +19,14 @@ export function run(doc, config) {
19
19
  const headings = doc.headings.filter(([level]) => level === 2);
20
20
  if (!keyword || headings.length < MIN_HEADINGS) return [];
21
21
  const joined = headings.map(([, text]) => text).join(" / ");
22
- const missing = keyword.split(/\s+/).filter((word) => word.length >= MIN_WORD && title.includes(word) && !joined.includes(word));
22
+ // keywordMissing 과 같은 자리를 본다. 제목만 보면 첫 문단에만 있는 검색어가 두 규칙 사이로 빠진다. 파이썬과 같다.
23
+ const first = doc.paragraphs.length ? doc.paragraphs[0].text : "";
24
+ const promised = `${title}\n${first}`;
25
+ const missing = keyword
26
+ .split(/\s+/)
27
+ .filter((word) => word.length >= MIN_WORD && promised.includes(word) && !joined.includes(word));
23
28
  if (!missing.length) return [];
24
29
  return [
25
- finding(name, headings[0][2], joined, `제목이 약속한 \`${missing[0]}\` 가 절 제목 어디에도 없다. 목차를 훑는 독자가 이 글이 그것을 다루는지 못 본다`, null, NOTICE, DOCUMENT, -1),
30
+ finding(name, headings[0][2], joined, `글이 내건 \`${missing[0]}\` 가 절 제목 어디에도 없다. 목차를 훑는 독자가 이 글이 그것을 다루는지 못 본다`, null, NOTICE, DOCUMENT, -1),
26
31
  ];
27
32
  }
@@ -5,11 +5,11 @@ export const name = "noQuestion";
5
5
  export const mechanism = "threshold";
6
6
 
7
7
  /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
8
- export function run(doc) {
8
+ export function run(doc, config) {
9
9
  if (doc.bodySections.length < 2 || doc.questionCount > 0) return [];
10
10
  const first = doc.sentences.length ? doc.sentences[0] : null;
11
11
  const why = doc.readerCallCount === 0
12
- ? "물음표가 한 번도 없고 독자를 부르는 말도 없다. 독자가 품을 의문을 한 번은 대신 묻고, 독자가 할 행동을 동사로 끝내는 문장을 넣는다"
13
- : "물음표가 한 번도 없다. 독자가 품을 의문을 한 번은 대신 묻고 다음 문장에서 답한다";
14
- return [finding(name, first ? first.line : 1, first ? first.text : "", why, null, "error", DOCUMENT, -1)];
12
+ ? "물음표가 한 번도 없고 독자를 부르는 말도 없다. 질문이나 독자 호출이 필요한 글인지 판단하고 충분하면 유지한다"
13
+ : "물음표가 한 번도 없다. 질문이 필요한 글인지 판단하고 설명만으로 충분하면 유지한다";
14
+ return [finding(name, first ? first.line : 1, first ? first.text : "", why, null, config.enforceStyle.includes(name) ? "error" : "notice", DOCUMENT, -1)];
15
15
  }
@@ -5,7 +5,8 @@ import { DOCUMENT, NOTICE, finding } from "../finding.js";
5
5
  export const name = "tableOddCell";
6
6
  export const mechanism = "contrast";
7
7
  const SEPARATOR = /^:?-{2,}:?$/;
8
- const MEASURE = /^([+-]?\d[\d,]*(?:\.\d+)?)\s*([^\s\d]{0,6})$/;
8
+ // 단위는 한 글자 이상이다. 0 을 허용하면 맨숫자 칸이 빈 단위를 받아 지적문이 깨진다. 파이썬과 같다.
9
+ const MEASURE = /^([+-]?\d[\d,]*(?:\.\d+)?)\s*([^\s\d]{1,6})$/;
9
10
  const EMPTY = ["", "-", "--"];
10
11
 
11
12
  /** (블록 안 줄 오프셋, 칸 목록). 구분 줄은 뺀다. @param {string} text @returns {[number, string[]][]} */
@@ -0,0 +1,9 @@
1
+ // @ts-check
2
+ /** 독자 부채는 새 판정이 아니라 reader 기제 Finding을 사람이 읽는 이름으로 묶은 것이다. */
3
+
4
+ import { ruleMechanism } from "./registry.js";
5
+
6
+ /** @param {import("./finding.js").Finding[]} findings */
7
+ export function readerDebts(findings) {
8
+ return findings.filter((finding) => ruleMechanism(finding.rule) === "reader");
9
+ }
@@ -24,7 +24,6 @@ import * as spacing from "./orthography/spacing.js";
24
24
  import * as spelling from "./orthography/spelling.js";
25
25
  import * as factListParagraph from "./paragraph/factListParagraph.js";
26
26
  import * as paraFragment from "./paragraph/paraFragment.js";
27
- import * as topicBreak from "./paragraph/topicBreak.js";
28
27
  import * as cliche from "./sentence/cliche.js";
29
28
  import * as hardWord from "./sentence/hardWord.js";
30
29
  import * as connectorRepeat from "./sentence/connectorRepeat.js";
@@ -35,7 +34,6 @@ import * as dash from "./sentence/dash.js";
35
34
  import * as deixis from "./sentence/deixis.js";
36
35
  import * as doubleNegative from "./sentence/doubleNegative.js";
37
36
  import * as doublePassive from "./sentence/doublePassive.js";
38
- import * as ellipsis from "./sentence/ellipsis.js";
39
37
  import * as endingRepeat from "./sentence/endingRepeat.js";
40
38
  import * as euiChain from "./sentence/euiChain.js";
41
39
  import * as fillerOpener from "./sentence/fillerOpener.js";
@@ -50,7 +48,6 @@ import * as translationese from "./sentence/translationese.js";
50
48
  import * as blockUnread from "./structure/blockUnread.js";
51
49
  import * as bridgeRepeat from "./structure/bridgeRepeat.js";
52
50
  import * as emojiBullet from "./structure/emojiBullet.js";
53
- import * as headingQuestion from "./structure/headingQuestion.js";
54
51
  import * as introImage from "./structure/introImage.js";
55
52
  import * as loneSubheading from "./structure/loneSubheading.js";
56
53
  import * as moreLater from "./structure/moreLater.js";
@@ -59,7 +56,6 @@ import * as headingSkip from "./structure/headingSkip.js";
59
56
  import * as headingUniform from "./structure/headingUniform.js";
60
57
  import * as introLong from "./structure/introLong.js";
61
58
  import * as sectionNoProse from "./structure/sectionNoProse.js";
62
- import * as sectionResult from "./structure/sectionResult.js";
63
59
 
64
60
  /**
65
61
  * @typedef {object} Rule
@@ -79,7 +75,6 @@ export const RULES = [
79
75
  deixis,
80
76
  doubleNegative,
81
77
  doublePassive,
82
- ellipsis,
83
78
  endingRepeat,
84
79
  euiChain,
85
80
  fillerOpener,
@@ -94,11 +89,9 @@ export const RULES = [
94
89
  translationese,
95
90
  factListParagraph,
96
91
  paraFragment,
97
- topicBreak,
98
92
  blockUnread,
99
93
  bridgeRepeat,
100
94
  emojiBullet,
101
- headingQuestion,
102
95
  introImage,
103
96
  loneSubheading,
104
97
  moreLater,
@@ -107,7 +100,6 @@ export const RULES = [
107
100
  headingUniform,
108
101
  introLong,
109
102
  sectionNoProse,
110
- sectionResult,
111
103
  countMismatch,
112
104
  enoughOnce,
113
105
  fieldEcho,
@@ -162,6 +154,33 @@ export function ruleSummary(name) {
162
154
  return ruleDoc(name).split("\n")[0];
163
155
  }
164
156
 
157
+ /** 기술서의 네 절 표. 파이썬 rules/registry.py 의 REQUIRED_SECTIONS 와 같다. */
158
+ export const REQUIRED_SECTIONS = ["왜:", "어디서:", "고치기:", "안 잡는 것:"];
159
+
160
+ /** 기술서에서 절 하나의 본문을 한 줄로. 파이썬 docSection 과 같다. @param {string} doc @param {string} label */
161
+ export function docSection(doc, label) {
162
+ const collected = [];
163
+ let inside = false;
164
+ for (const raw of doc.split("\n")) {
165
+ const line = raw.trim();
166
+ if (!inside) {
167
+ if (line.startsWith(label)) {
168
+ inside = true;
169
+ collected.push(line.slice(label.length).trim());
170
+ }
171
+ continue;
172
+ }
173
+ if (!line || REQUIRED_SECTIONS.some((other) => line.startsWith(other))) break;
174
+ collected.push(line);
175
+ }
176
+ return collected.filter(Boolean).join(" ");
177
+ }
178
+
179
+ /** 규칙의 고치기 절 한 줄. primer 가 쓰기 전에 읽히는 그 문장이다. @param {string} name */
180
+ export function ruleFix(name) {
181
+ return docSection(ruleDoc(name), "고치기:");
182
+ }
183
+
165
184
  /**
166
185
  * 부류의 사람 이름. 파이썬 rules/registry.py 의 CATEGORY_TITLES 와 같은 값과 같은 순서다.
167
186
  * @type {Record<string, string>}
@@ -8,6 +8,20 @@ import { hasLocalAntecedent } from "../shared/localAntecedent.js";
8
8
  export const name = "danglingDeixis";
9
9
  export const mechanism = "reader";
10
10
 
11
+ const QUESTION = "의문";
12
+ const EXCLAIM = ["!", "!"];
13
+
14
+ /**
15
+ * 앞 문장이 물음이나 감탄인가. 그러면 그 문장 자체가 지시어의 선행어다.
16
+ * 뜻은 파이썬 rules/sentence/danglingDeixis.py 의 isPrompt 가 소유한다.
17
+ * @param {{mood: string, text: string} | null} previous
18
+ */
19
+ function isPrompt(previous) {
20
+ if (!previous) return false;
21
+ const text = previous.text.replace(/\s+$/u, "");
22
+ return previous.mood === QUESTION || EXCLAIM.some((mark) => text.endsWith(mark));
23
+ }
24
+
11
25
  /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
12
26
  export function run(doc) {
13
27
  const findings = [];
@@ -16,6 +30,7 @@ export function run(doc) {
16
30
  if (hasLocalAntecedent(sentence)) continue;
17
31
  const reader = doc.reader.beforeSentence[sentence.index];
18
32
  if (overlap(reader.recent, sentence.topics) > 0) continue;
33
+ if (isPrompt(reader.previous)) continue;
19
34
  findings.push(
20
35
  finding(name, sentence.line, sentence.text, `\`${sentence.deixis[0]}\` ${fitJosa(sentence.deixis[0], "이")} 가리킬 것이 앞 문장에 없다. 가리키는 파일, 값, 코드의 이름을 쓴다`, null, "error", SENTENCE, sentence.index, null, null, danglingDeixisCandidates(sentence, reader.previous)),
21
36
  );
@@ -4,7 +4,8 @@ import { COPULA as COPULA_KIND, lastWord, parsePredicate } from "../../analysis/
4
4
 
5
5
  export const name = "negationRedefine";
6
6
  export const mechanism = "repeat";
7
- const NEGATION = /단순(?:한|히)\s?.{0,15}?(?:이|가)\s?아(?:닙니다|니다|니에요|니죠|닌)/;
7
+ // 관형형 `아닌` 은 뺐다. 문장을 끝낼 수 없어 앞 문장이 부정이라는 전제를 못 만족한다. 파이썬과 같다.
8
+ const NEGATION = /단순(?:한|히)\s?.{0,15}?(?:이|가)\s?아(?:닙니다|니다|니에요|니죠)/;
8
9
  const COPULA = /(입니다|이다|이에요|예요)[.!]?$/;
9
10
 
10
11
  /** @param {string} text */
@@ -13,7 +13,7 @@ export function run(doc, config) {
13
13
  for (const sentence of doc.sentences) {
14
14
  if (sentence.nounRun >= config.nounPileMin) {
15
15
  findings.push(
16
- finding(name, sentence.line, sentence.text, `명사 ${sentence.nounRun}개가 조사 없이 이어진다. 관계가 표시되지 않아 독자가 조사를 끼워 넣는다. 동사로 되돌린다`, null, "error", SENTENCE, sentence.index),
16
+ finding(name, sentence.line, sentence.text, `명사 ${sentence.nounRun}개가 조사 없이 이어진다. 관계가 표시되지 않아 독자가 조사를 끼워 넣는다. 동사로 되돌린다`, null, config.enforceStyle.includes(name) ? "error" : "notice", SENTENCE, sentence.index),
17
17
  );
18
18
  }
19
19
  }
@@ -11,12 +11,12 @@ export const mechanism = "repeat";
11
11
  */
12
12
  export function run(doc, config) {
13
13
  const headings = doc.headings.filter(([level]) => level === 2);
14
- // 버전이나 날짜처럼 숫자로 끝나는 제목은 어미가 아니라 판정에서 뺀다.
14
+ // 어미는 한글이다. 숫자, 부호, 라틴 문자로 끝나는 제목은 판정에서 뺀다. 파이썬과 같다.
15
15
  const eligible = headings.filter(([, text]) => {
16
16
  if (!text.trim()) return false;
17
17
  const trimmed = text.trimEnd();
18
18
  const char = trimmed[trimmed.length - 1];
19
- return !(char >= "0" && char <= "9");
19
+ return char >= "가" && char <= "힣";
20
20
  });
21
21
  if (eligible.length < 3) return [];
22
22
  const [best, count, total] = shareOf(
@@ -27,7 +27,7 @@ export function run(doc, config) {
27
27
  );
28
28
  if (count / total >= config.headingUniformRatio) {
29
29
  return [
30
- finding(name, eligible[0][2], eligible.map(([, text]) => text).join(" / "), `H2 ${eligible.length}개 중 ${count}개가 \`${best}\` 로 끝난다. 목차가 한 어미로 끝나면 과정이 아니라 나열로 읽힌다. 형태를 섞는다`, null, "error", DOCUMENT, -1),
30
+ finding(name, eligible[0][2], eligible.map(([, text]) => text).join(" / "), `H2 ${eligible.length}개 중 ${count}개가 \`${best}\` 로 끝난다. 목차가 한 어미로 끝나면 과정이 아니라 나열로 읽힌다. 그 문서의 관례라면 둔다`, null, "notice", DOCUMENT, -1),
31
31
  ];
32
32
  }
33
33
  return [];
@@ -9,8 +9,10 @@ export const mechanism = "threshold";
9
9
  * @param {import("../../config/settings.js").Config} config
10
10
  */
11
11
  export function run(doc, config) {
12
- if (!doc.bodySections.length) return [];
12
+ // 본문 절이 하나뿐이거나 도입이 문단의 절반을 넘으면 절이 없는 글이지 도입이 긴 글이 아니다. 파이썬과 같다.
13
+ if (doc.bodySections.length < 2) return [];
13
14
  const paragraphs = doc.intro.paragraphs;
15
+ if (paragraphs.length * 2 > doc.paragraphs.length) return [];
14
16
  if (paragraphs.length > config.introMaxParagraphs) {
15
17
  const over = paragraphs[config.introMaxParagraphs];
16
18
  return [
@@ -1,7 +0,0 @@
1
- # 사람 평가자의 지적을 유형으로 가르는 표지. `유형<TAB>정규식`. coverage 가 못 집은 지적을 모을 때 쓴다. 위에서부터 먼저 맞는 것이 이긴다.
2
- 파일과 폴더 파일|폴더|경로|만들어지지|생성|없어서|없다
3
- 설치와 실행 설치|import|모듈|패키지|실행|오류|에러|Error|멈춘다|돌아가지
4
- 순서와 구조 순서|먼저|앞에서|뒤에서|절|목차|위치|옮|건너
5
- 용어와 설명 설명|뜻|처음 나온|용어|무엇인지|모른|정의
6
- 숫자와 표 숫자|표|열|행|값|측정|잰
7
- 문장과 표현 문장|주어|서술어|표현|반복|같은 말|어색
@@ -1,26 +0,0 @@
1
- // @ts-check
2
- import { NOTICE, PARAGRAPH, finding } from "../finding.js";
3
-
4
- export const name = "topicBreak";
5
- export const mechanism = "contrast";
6
-
7
- /**
8
- * @param {import("../../fingerprint/build.js").DocumentPrint} doc
9
- * @param {import("../../config/settings.js").Config} config
10
- */
11
- export function run(doc, config) {
12
- const findings = [];
13
- for (const section of doc.sections) {
14
- const paragraphs = section.paragraphs;
15
- for (let i = 0; i + 1 < paragraphs.length; i++) {
16
- const previous = paragraphs[i];
17
- const paragraph = paragraphs[i + 1];
18
- if (paragraph.overlapWithPrevious !== 0) continue;
19
- if (Math.min(previous.sentenceCount, paragraph.sentenceCount) < config.topicBreakMinSentences) continue;
20
- findings.push(
21
- finding(name, paragraph.startLine, paragraph.sentences[0].text, "앞 문단과 화제어가 하나도 겹치지 않는다. 앞에서 만든 것의 이름을 첫 문장에서 다시 부른다", null, NOTICE, PARAGRAPH, paragraph.index),
22
- );
23
- }
24
- }
25
- return findings;
26
- }
@@ -1,32 +0,0 @@
1
- // @ts-check
2
- import { insideAny } from "../../fingerprint/markers.js";
3
- import { SENTENCE, finding } from "../finding.js";
4
-
5
- export const name = "ellipsis";
6
- export const mechanism = "dictionary";
7
- // 영숫자에 붙은 점 셋 (`v0.0.1...HEAD`, compare URL) 은 범위 표기라 말줄임표가 아니다.
8
- // 인라인 코드와 따옴표 안은 지문이 인용 구간으로 이미 표시해 두었으므로 여기서 다시 재지 않는다.
9
- const ELLIPSIS = /(?<![A-Za-z0-9])(…|\.{3,})(?![A-Za-z0-9])/g;
10
- const CLI_OPTION = /(?:^|\s)--?[A-Za-z0-9]/;
11
-
12
- /** 괄호 안 명령 예시의 생략 인자. 예: (kubectl get -o yaml …). @param {string} text @param {number} start @param {number} end */
13
- function insideCommandExample(text, start, end) {
14
- const opened = text.lastIndexOf("(", start);
15
- const closed = text.indexOf(")", end);
16
- return opened >= 0 && closed >= 0 && !text.slice(opened, start).includes(")") && !text.slice(end, closed).includes("(") && CLI_OPTION.test(text.slice(opened + 1, closed));
17
- }
18
-
19
- /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
20
- export function run(doc) {
21
- const findings = [];
22
- for (const sentence of doc.sentences) {
23
- const found = [...sentence.text.matchAll(ELLIPSIS)].filter((m) => {
24
- const start = /** @type {number} */ (m.index);
25
- return !insideAny(start, start + m[0].length, sentence.quoted) && !insideCommandExample(sentence.text, start, start + m[0].length);
26
- });
27
- if (found.length) {
28
- findings.push(finding(name, sentence.line, sentence.text, "말줄임표로 말끝을 흐리면 독자가 결론을 받아 가지 못한다. 문장을 끝까지 쓴다", null, "error", SENTENCE, sentence.index));
29
- }
30
- }
31
- return findings;
32
- }
@@ -1,19 +0,0 @@
1
- // @ts-check
2
- import { DOCUMENT, finding } from "../finding.js";
3
-
4
- export const name = "headingQuestion";
5
- export const mechanism = "repeat";
6
-
7
- /**
8
- * @param {import("../../fingerprint/build.js").DocumentPrint} doc
9
- * @param {import("../../config/settings.js").Config} config
10
- */
11
- export function run(doc, config) {
12
- const headings = doc.headings.filter(([level]) => level === 2);
13
- if (headings.length < 3) return [];
14
- const asking = headings.filter(([, text]) => text.trimEnd().endsWith("?")).map(([, text]) => text);
15
- if (asking.length / headings.length <= config.headingQuestionRatio) return [];
16
- return [
17
- finding(name, headings[0][2], asking.join(" / "), `H2 ${headings.length}개 중 ${asking.length}개가 물음표로 끝난다. 목차가 과정이 아니라 FAQ 로 읽힌다. 형태를 섞는다`, null, "error", DOCUMENT, -1),
18
- ];
19
- }
@@ -1,38 +0,0 @@
1
- // @ts-check
2
- import { CODE, EMBED, IMAGE, TABLE } from "../../document/model.js";
3
- import { NOTICE, SECTION, finding } from "../finding.js";
4
-
5
- export const name = "sectionResult";
6
- export const mechanism = "threshold";
7
- const RESULT_KINDS = [CODE, EMBED, IMAGE, TABLE];
8
- const FILE_NAME = /[\w가-힣-]+\.(?:csv|xlsx|xls|parquet|json|png|jpg|svg|db|sqlite|txt|py|pdf|html|md)\b/;
9
- const OUTPUT_WORDS = ["출력", "화면에", "찍힙니다", "나옵니다", "뜹니다", "보입니다", "만들어집니다", "생깁니다", "저장됩니다"];
10
-
11
- /** @param {import("../../fingerprint/build.js").SectionPrint} section */
12
- function leavesResult(section) {
13
- if (section.blockKinds.some((kind) => RESULT_KINDS.includes(kind))) return true;
14
- for (const paragraph of section.paragraphs) {
15
- for (const sentence of paragraph.sentences) {
16
- if (FILE_NAME.test(sentence.text) || OUTPUT_WORDS.some((word) => sentence.text.includes(word))) return true;
17
- }
18
- }
19
- return false;
20
- }
21
-
22
- /**
23
- * @param {import("../../fingerprint/build.js").DocumentPrint} doc
24
- * @param {import("../../config/settings.js").Config} config
25
- */
26
- export function run(doc, config) {
27
- const findings = [];
28
- const body = doc.bodySections;
29
- body.forEach((section, index) => {
30
- if (index === body.length - 1) return;
31
- if (section.paragraphs.length <= config.sectionResultMinParagraphs) return;
32
- if (leavesResult(section)) return;
33
- findings.push(
34
- finding(name, section.startLine, section.title, "이 절에 독자가 확인할 결과 (코드, 표, 그림, 파일, 출력) 가 없다. 만들거나 확인할 것을 하나 넣는다", null, NOTICE, SECTION, section.index),
35
- );
36
- });
37
- return findings;
38
- }