hanlint 0.0.7 → 0.0.9

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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -9
  3. package/data/adjectiveStems.txt +118 -0
  4. package/data/baselineAnchors.txt +3 -0
  5. package/data/blindEvaluation.schema.json +26 -0
  6. package/data/bridgeOpeners.txt +10 -0
  7. package/data/candidateActionNouns.txt +39 -0
  8. package/data/candidateSplitEndings.txt +13 -0
  9. package/data/cliches.json +1 -1
  10. package/data/confusable.json +2 -2
  11. package/data/deixis.txt +10 -3
  12. package/data/draftHistory.txt +10 -0
  13. package/data/easyWords.json +0 -42
  14. package/data/enoughMarkers.txt +6 -0
  15. package/data/entailmentPredictions.schema.json +52 -0
  16. package/data/euiNouns.txt +3 -4
  17. package/data/evidenceEntailmentBenchmark.schema.json +126 -0
  18. package/data/exemplars.json +323 -0
  19. package/data/holeKinds.json +2 -14
  20. package/data/iVerbStems.txt +26 -0
  21. package/data/inNouns.txt +147 -0
  22. package/data/irregularStems.txt +64 -0
  23. package/data/nonNouns.txt +153 -0
  24. package/data/panelAssignmentReview.schema.json +117 -0
  25. package/data/panelJudgePredictions.schema.json +78 -0
  26. package/data/panelReviewBatch.schema.json +79 -0
  27. package/data/panelTrialSet.schema.json +44 -0
  28. package/data/patterns.json +103 -0
  29. package/data/platformApis.txt +3 -1
  30. package/data/profiles.json +1 -0
  31. package/data/readerCalls.txt +12 -7
  32. package/data/redundantPair.json +1 -1
  33. package/data/rieulStems.txt +30 -0
  34. package/data/ruleCategories.json +52 -0
  35. package/data/ruleDocs.json +27 -19
  36. package/data/ruleMechanisms.json +52 -0
  37. package/data/spacing.json +0 -12
  38. package/data/spelling.json +7 -25
  39. package/data/translationese.json +10 -0
  40. package/data/verbTails.txt +17 -0
  41. package/data/version.json +1 -1
  42. package/data/writingBrief.schema.json +64 -0
  43. package/data/writingBriefV2.schema.json +116 -0
  44. package/data/writingTrial.schema.json +36 -0
  45. package/koglType1.LICENSE.md +25 -0
  46. package/package.json +18 -6
  47. package/src/analysis/grammar/ending.js +336 -0
  48. package/src/analysis/grammar/hangul.js +94 -0
  49. package/src/analysis/grammar/index.js +5 -0
  50. package/src/analysis/grammar/josa.js +73 -0
  51. package/src/analysis/grammar/register.js +115 -0
  52. package/src/analysis/grammar/voice.js +43 -0
  53. package/src/analysis/index.js +4 -31
  54. package/src/analysis/josa.js +2 -0
  55. package/src/analysis/{surface/splitSentences.js → splitSentences.js} +1 -1
  56. package/src/analysis/{surface/tokenize.js → tokenize.js} +70 -13
  57. package/src/baseline/store.js +114 -0
  58. package/src/cli/main.js +508 -61
  59. package/src/cli/walk.js +30 -0
  60. package/src/cli/welcome.js +52 -0
  61. package/src/config/settings.js +111 -12
  62. package/src/data/exemplars.js +177 -0
  63. package/src/data/load.js +10 -0
  64. package/src/data/operations.js +278 -0
  65. package/src/data/patches.js +125 -0
  66. package/src/data/patterns.js +48 -0
  67. package/src/data/profiles.js +102 -0
  68. package/src/document/model.js +2 -0
  69. package/src/document/parseMarkdown.js +84 -11
  70. package/src/edit/applyFixes.js +0 -0
  71. package/src/fingerprint/build.js +60 -39
  72. package/src/{rules/shared → fingerprint}/codeBlocks.js +11 -11
  73. package/src/fingerprint/codeMarkers.js +73 -0
  74. package/src/fingerprint/markers.js +9 -0
  75. package/src/fingerprint/readerState.js +126 -0
  76. package/src/fingerprint/sourceSentence.js +37 -0
  77. package/src/fingerprint/topics.js +1 -1
  78. package/src/index.js +2 -16
  79. package/src/report/compactReport.js +6 -1
  80. package/src/report/fingerprintJson.js +8 -3
  81. package/src/report/jsonReport.js +50 -3
  82. package/src/report/operationMatch.js +31 -0
  83. package/src/report/patchMatch.js +44 -0
  84. package/src/report/registerMatch.js +40 -0
  85. package/src/report/textReport.js +41 -5
  86. package/src/rules/code/duplicateBlock.js +4 -3
  87. package/src/rules/code/firstResultDistance.js +1 -0
  88. package/src/rules/code/inputFileSource.js +23 -68
  89. package/src/rules/code/installImport.js +6 -5
  90. package/src/rules/code/platformApi.js +7 -6
  91. package/src/rules/document/countMismatch.js +22 -6
  92. package/src/rules/document/enoughOnce.js +25 -0
  93. package/src/rules/document/fieldEcho.js +38 -0
  94. package/src/rules/document/keywordHeading.js +32 -0
  95. package/src/rules/document/keywordMissing.js +1 -0
  96. package/src/rules/document/noQuestion.js +5 -1
  97. package/src/rules/document/promiseRecall.js +4 -2
  98. package/src/rules/document/tableOddCell.js +61 -0
  99. package/src/rules/finding.js +24 -2
  100. package/src/rules/orthography/confusable.js +1 -0
  101. package/src/rules/orthography/spacing.js +1 -0
  102. package/src/rules/orthography/spelling.js +1 -0
  103. package/src/rules/paragraph/factListParagraph.js +1 -0
  104. package/src/rules/paragraph/paraFragment.js +21 -11
  105. package/src/rules/registry.js +70 -9
  106. package/src/rules/sentence/cliche.js +1 -0
  107. package/src/rules/sentence/connectorRepeat.js +8 -7
  108. package/src/rules/sentence/danglingDeixis.js +23 -3
  109. package/src/rules/sentence/dash.js +2 -1
  110. package/src/rules/sentence/deixis.js +7 -3
  111. package/src/rules/sentence/doubleNegative.js +4 -1
  112. package/src/rules/sentence/doublePassive.js +38 -1
  113. package/src/rules/sentence/draftHistory.js +24 -0
  114. package/src/rules/sentence/endingRepeat.js +6 -2
  115. package/src/rules/sentence/euiChain.js +3 -2
  116. package/src/rules/sentence/fillerOpener.js +1 -0
  117. package/src/rules/sentence/hardWord.js +1 -0
  118. package/src/rules/sentence/imperativePeriod.js +2 -1
  119. package/src/rules/sentence/japaneseLoan.js +1 -0
  120. package/src/rules/sentence/longSentence.js +3 -1
  121. package/src/rules/sentence/negationRedefine.js +12 -2
  122. package/src/rules/sentence/nounPile.js +1 -0
  123. package/src/rules/sentence/numberOrphan.js +45 -0
  124. package/src/rules/sentence/outsideProfile.js +68 -0
  125. package/src/rules/sentence/redundantPair.js +1 -0
  126. package/src/rules/sentence/translationese.js +1 -0
  127. package/src/rules/shared/candidates.js +129 -0
  128. package/src/rules/shared/dictionaryRule.js +12 -2
  129. package/src/rules/shared/localAntecedent.js +12 -0
  130. package/src/rules/shared/repeat.js +48 -0
  131. package/src/rules/structure/blockUnread.js +33 -0
  132. package/src/rules/structure/bridgeRepeat.js +40 -0
  133. package/src/rules/structure/emojiBullet.js +1 -0
  134. package/src/rules/structure/headingSentence.js +9 -4
  135. package/src/rules/structure/headingSkip.js +1 -0
  136. package/src/rules/structure/headingUniform.js +12 -20
  137. package/src/rules/structure/introImage.js +19 -0
  138. package/src/rules/structure/introLong.js +4 -1
  139. package/src/rules/structure/loneSubheading.js +42 -0
  140. package/src/rules/structure/moreLater.js +48 -0
  141. package/src/rules/structure/sectionNoProse.js +1 -0
  142. package/src/text.js +19 -0
  143. package/data/coverageTypes.txt +0 -7
  144. package/src/rules/document/readerAbsent.js +0 -11
  145. package/src/rules/paragraph/topicBreak.js +0 -25
  146. package/src/rules/sentence/ellipsis.js +0 -18
  147. package/src/rules/shared/runs.js +0 -22
  148. package/src/rules/structure/sectionResult.js +0 -37
@@ -0,0 +1,126 @@
1
+ // @ts-check
2
+ /**
3
+ * 독자 상태. 글을 위에서 아래로 읽는 독자가 어느 자리에서 무엇을 손에 들고 무엇을 이미 보았는가.
4
+ * 파이썬 fingerprint/readerState.py 와 같다. reader 기제의 규칙은 어느 자리가 요구하는 것을 그 자리의 상태에
5
+ * 대 본다. 상태는 블록과 문장 순서로 쌓이고 한 번 쌓은 것은 바뀌지 않는다. 집합은 고치지 않는다.
6
+ */
7
+ import { CODE, PROSE } from "../document/model.js";
8
+ import { createdIn } from "./codeMarkers.js";
9
+ import { numeralsIn } from "./markers.js";
10
+
11
+ /**
12
+ * 한 자리 (문장 하나 또는 블록 하나) 를 읽기 직전의 독자.
13
+ * @typedef {object} ReaderState
14
+ * @property {import("./build.js").SentencePrint | null} previous 바로 앞에 읽은 산문 문장
15
+ * @property {Set<string>} recent 손에 든 화제어. 바로 앞 문장의 것이고 앞 문장이 없으면 빈 집합
16
+ * @property {Set<string>} known 이 자리 앞까지 산문에 나온 화제어 전부
17
+ * @property {Set<string>} numerals 지금까지 산문과 블록에 나온 수 (천 단위 쉼표를 뗀 꼴)
18
+ * @property {Set<string>} files 앞선 코드 블록이 만든 파일 이름과 폴더
19
+ * @property {number} sentencesRead 지금까지 읽은 산문 문장 수
20
+ * @property {[number, string][]} promises 지금까지 뒤로 미룬 표지 (줄, 원문)
21
+ * @property {[number, string][]} recalls 지금까지 앞을 회수한 표지 (줄, 원문)
22
+ */
23
+
24
+ /**
25
+ * 자리마다의 독자 상태. 문장은 index 로, 블록은 index 로 찾는다.
26
+ * @typedef {object} ReaderTrail
27
+ * @property {ReaderState[]} beforeSentence
28
+ * @property {ReaderState[]} beforeBlock
29
+ * @property {ReaderState} final 글을 다 읽은 독자
30
+ * @property {(blockIndex: number, name: string) => boolean} mentionedBefore 블록보다 앞선 산문이 그 이름을 불렀는가
31
+ */
32
+
33
+ /** @type {Set<string>} */
34
+ const NOTHING = new Set();
35
+
36
+ /**
37
+ * 현재 문장의 화제가 직전 독자에게 새 것인지 이미 손에 든 것인지 네 꼴로 줄인다.
38
+ * @param {import("./build.js").SentencePrint} sentence
39
+ * @param {ReaderState} state
40
+ */
41
+ export function readerKind(sentence, state) {
42
+ if (!sentence.topics.size) return "none";
43
+ if ([...sentence.topics].some((topic) => state.recent.has(topic))) return "recent";
44
+ if ([...sentence.topics].some((topic) => state.known.has(topic))) return "known";
45
+ return "new";
46
+ }
47
+
48
+ /** @param {Set<string>} base @param {Set<string>} added */
49
+ function union(base, added) {
50
+ if (!added.size) return base;
51
+ const out = new Set(base);
52
+ for (const value of added) out.add(value);
53
+ return out;
54
+ }
55
+
56
+ /** @param {ReaderState} state @param {import("./build.js").SentencePrint} sentence @returns {ReaderState} */
57
+ function afterSentence(state, sentence) {
58
+ return {
59
+ previous: sentence,
60
+ recent: sentence.topics,
61
+ known: union(state.known, sentence.topics),
62
+ numerals: union(state.numerals, numeralsIn(sentence.text)),
63
+ files: state.files,
64
+ sentencesRead: state.sentencesRead + 1,
65
+ promises: [...state.promises, ...sentence.promises.map((text) => /** @type {[number, string]} */ ([sentence.line, text]))],
66
+ recalls: [...state.recalls, ...sentence.recalls.map((text) => /** @type {[number, string]} */ ([sentence.line, text]))],
67
+ };
68
+ }
69
+
70
+ /**
71
+ * 산문 아닌 블록을 지난 독자. 수는 어느 블록에서든 보고 파일은 코드 블록이 만든다.
72
+ * @param {ReaderState} state
73
+ * @param {import("../document/model.js").Block} block
74
+ * @param {import("./codeBlocks.js").CodeBlock | undefined} code
75
+ * @returns {ReaderState}
76
+ */
77
+ function afterBlock(state, block, code) {
78
+ let files = state.files;
79
+ if (code) {
80
+ const [made, dirs] = createdIn(code.lines.map(([, line]) => line));
81
+ files = union(union(files, made), dirs);
82
+ }
83
+ return { ...state, numerals: union(state.numerals, numeralsIn(block.text)), files };
84
+ }
85
+
86
+ /**
87
+ * 블록 순서로 한 번 지나며 자리마다의 상태를 적는다. 산문 블록은 문장 하나씩, 나머지는 블록째 읽는다.
88
+ * @param {import("../document/model.js").Block[]} blocks
89
+ * @param {import("./codeBlocks.js").CodeBlock[]} codeBlocks
90
+ * @param {import("./build.js").SentencePrint[]} sentences
91
+ * @returns {ReaderTrail}
92
+ */
93
+ export function buildReaderTrail(blocks, codeBlocks, sentences) {
94
+ const codeByIndex = new Map(codeBlocks.map((code) => [code.index, code]));
95
+ /** @type {ReaderState[]} */
96
+ const beforeSentence = [];
97
+ /** @type {ReaderState[]} */
98
+ const beforeBlock = [];
99
+ /** @type {ReaderState} */
100
+ let state = { previous: null, recent: NOTHING, known: NOTHING, numerals: NOTHING, files: NOTHING, sentencesRead: 0, promises: [], recalls: [] };
101
+ let position = 0;
102
+ for (const block of blocks) {
103
+ beforeBlock.push(state);
104
+ if (block.kind === PROSE) {
105
+ while (position < sentences.length && sentences[position].blockIndex === block.index) {
106
+ beforeSentence.push(state);
107
+ state = afterSentence(state, sentences[position]);
108
+ position += 1;
109
+ }
110
+ } else {
111
+ state = afterBlock(state, block, block.kind === CODE ? codeByIndex.get(block.index) : undefined);
112
+ }
113
+ }
114
+ if (position !== sentences.length) {
115
+ throw new Error(`문장 ${sentences.length - position}개가 어느 블록에도 안 든다. 문장과 블록의 순서가 어긋났다`);
116
+ }
117
+ return {
118
+ beforeSentence,
119
+ beforeBlock,
120
+ final: state,
121
+ mentionedBefore(blockIndex, name) {
122
+ const read = beforeBlock[blockIndex].sentencesRead;
123
+ return sentences.slice(0, read).some((sentence) => sentence.text.includes(name));
124
+ },
125
+ };
126
+ }
@@ -0,0 +1,37 @@
1
+ // @ts-check
2
+ /** 평문 지문의 문장을 마크다운 표식을 보존한 원문 문장에 다시 맞춘다. */
3
+ import { splitSentences } from "../analysis/splitSentences.js";
4
+ import { plainText } from "../document/plainText.js";
5
+
6
+ /**
7
+ * @param {import("./build.js").DocumentPrint} document
8
+ * @returns {Map<number, string>}
9
+ */
10
+ export function sourceSentenceTexts(document) {
11
+ /** @type {Map<number, import("./build.js").SentencePrint[]>} */
12
+ const byBlock = new Map();
13
+ for (const sentence of document.sentences) {
14
+ const found = byBlock.get(sentence.blockIndex) ?? [];
15
+ found.push(sentence);
16
+ byBlock.set(sentence.blockIndex, found);
17
+ }
18
+ const blocks = new Map(document.blocks.map((block) => [block.index, block]));
19
+ const result = new Map();
20
+ for (const [blockIndex, plainSentences] of byBlock) {
21
+ const block = blocks.get(blockIndex);
22
+ if (!block) continue;
23
+ const rawSentences = splitSentences(block.text);
24
+ if (rawSentences.length !== plainSentences.length) continue;
25
+ if (plainSentences.some((plain, index) => plainText(rawSentences[index].text) !== plain.text)) continue;
26
+ plainSentences.forEach((plain, index) => result.set(plain.index, rawSentences[index].text));
27
+ }
28
+ return result;
29
+ }
30
+
31
+ /**
32
+ * @param {import("./build.js").DocumentPrint} document
33
+ * @param {import("./build.js").SentencePrint} sentence
34
+ */
35
+ export function sourceSentenceText(document, sentence) {
36
+ return sourceSentenceTexts(document).get(sentence.index);
37
+ }
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
  /** 화제어 집합. 조사를 뗀 명사 어절의 근사. 파이썬 fingerprint/topics.py 와 같다. */
3
- import { stripJosa, tailOf, words } from "../analysis/surface/tokenize.js";
3
+ import { stripJosa, tailOf, words } from "../analysis/tokenize.js";
4
4
  import { loadLines } from "../data/load.js";
5
5
 
6
6
  const WORD = /^[가-힣A-Za-z][가-힣A-Za-z0-9]*$/;
package/src/index.js CHANGED
@@ -8,11 +8,10 @@
8
8
  * ```
9
9
  *
10
10
  * 합격과 불합격을 판정하지 않는다. 지적 목록이 비어 있다는 것은 세어서 잡히는 결함이 없다는 뜻이다.
11
- * 지문 지도 (audit, map), 프로파일, kiwi 정밀 모드는 파이썬 패키지에 있다.
11
+ * 지문 지도 (audit, map) 와 프로파일은 파이썬 패키지에 있다.
12
12
  */
13
13
  import { readFileSync } from "node:fs";
14
14
 
15
- import { makeAnalyzer } from "./analysis/index.js";
16
15
  import { loadConfig } from "./config/loadConfig.js";
17
16
  import { configFromMapping, defaultConfig } from "./config/settings.js";
18
17
  import { loadVersion } from "./data/load.js";
@@ -25,19 +24,6 @@ import { ruleDoc, ruleNames, ruleSummary, runAll } from "./rules/registry.js";
25
24
  export { applyFixes, configFromMapping, defaultConfig, fingerprintDict, loadConfig, ruleDoc, ruleNames, ruleSummary };
26
25
  export const version = loadVersion();
27
26
 
28
- /** @type {Map<string, import("./analysis/index.js").Analyzer>} */
29
- const analyzers = new Map();
30
-
31
- /** @param {import("./config/settings.js").Config} config */
32
- export function analyzerFor(config) {
33
- let analyzer = analyzers.get(config.analyzer);
34
- if (!analyzer) {
35
- analyzer = makeAnalyzer(config.analyzer);
36
- analyzers.set(config.analyzer, analyzer);
37
- }
38
- return analyzer;
39
- }
40
-
41
27
  /**
42
28
  * 글을 한 번 읽어 지문을 만든다.
43
29
  * @param {string} text
@@ -45,7 +31,7 @@ export function analyzerFor(config) {
45
31
  * @param {string | null} [path]
46
32
  */
47
33
  export function fingerprint(text, config = defaultConfig(), path = null) {
48
- return buildFingerprint(parseMarkdown(text, path), analyzerFor(config), config);
34
+ return buildFingerprint(parseMarkdown(text, path), config);
49
35
  }
50
36
 
51
37
  /**
@@ -1,9 +1,14 @@
1
1
  // @ts-check
2
2
  /** 한 줄에 지적 하나. AI 와 스크립트가 읽는 꼴이다. 파이썬 report/compactReport.py 와 같은 글자다. */
3
3
 
4
+ /** 줄바꿈과 이어진 공백을 하나로 눕힌다. 한 줄은 이 꼴의 계약이다. @param {string} text */
5
+ function flat(text) {
6
+ return text.split(/\s+/).filter(Boolean).join(" ");
7
+ }
8
+
4
9
  /** @param {string} path @param {import("../rules/finding.js").Finding[]} findings */
5
10
  export function renderCompact(path, findings) {
6
11
  return findings
7
- .map((f) => `${path}:${f.line} [${f.rule}] ${f.why}` + (f.fix ? ` 고친 뒤: ${f.fix}` : ""))
12
+ .map((f) => `${path}:${f.line} [${f.rule}] ${flat(f.why)}` + (f.fix ? ` 고친 뒤: ${flat(f.fix)}` : ""))
8
13
  .join("\n");
9
14
  }
@@ -4,12 +4,14 @@
4
4
  * 아래층을 index 로 가리킨다. 실수는 소수 여섯째 자리로 맞춰 두 구현이 같은 글자를 낸다.
5
5
  */
6
6
 
7
+ import { roundHalfEven } from "../text.js";
8
+
7
9
  export const LAYERS = ["all", "sentences", "paragraphs", "sections", "document"];
8
10
 
9
11
  /** @param {number | null} value */
10
12
  function num(value) {
11
13
  if (value === null) return null;
12
- return Math.round(value * 1e6) / 1e6;
14
+ return roundHalfEven(value, 6);
13
15
  }
14
16
 
15
17
  /** @param {Set<string>} set */
@@ -29,6 +31,7 @@ function sentenceDict(s) {
29
31
  length: s.length,
30
32
  ending: s.ending,
31
33
  mood: s.mood,
34
+ register: s.register,
32
35
  commas: s.commas,
33
36
  connectorStart: s.connectorStart,
34
37
  causal: s.causal,
@@ -102,9 +105,11 @@ function documentDict(doc) {
102
105
  sectionCount: doc.sections.length,
103
106
  questionCount: doc.questionCount,
104
107
  readerCallCount: doc.readerCallCount,
108
+ register: doc.register,
109
+ registerShare: roundHalfEven(doc.registerShare, 3),
105
110
  countPromises: doc.countPromises.map(([n, unit, line, text]) => [n, unit, line, text]),
106
- promises: doc.promises.map(([line, text]) => [line, text]),
107
- recalls: doc.recalls.map(([line, text]) => [line, text]),
111
+ promises: doc.reader.final.promises.map(([line, text]) => [line, text]),
112
+ recalls: doc.reader.final.recalls.map(([line, text]) => [line, text]),
108
113
  disabled: doc.disabled.map(([name, start, end]) => [name, start, end]),
109
114
  };
110
115
  }
@@ -1,13 +1,60 @@
1
1
  // @ts-check
2
- /** 기계가 읽는 꼴. 평가 루프의 0층 입력이다. 파이썬 json.dumps(indent=2, ensure_ascii=False) 와 같은 글자다. */
2
+ /**
3
+ * 기계가 읽는 꼴. 평가 루프의 0층 입력이다. 파이썬 json.dumps(indent=2, ensure_ascii=False) 와 같은 글자다.
4
+ * 지적마다 `exemplar` 를 붙인다. 승인 원문 완전 일치 `patch`는 맞는 지적에만 별도로 붙인다.
5
+ * 실제 수정 차이는 파이썬의 exemplarLift와 patchMemory 탐침에서 따로 잰다.
6
+ */
7
+ import { exemplarFor } from "../data/exemplars.js";
3
8
  import { findingAsDict } from "../rules/finding.js";
9
+ import { exemplarInRegister } from "./registerMatch.js";
10
+ import { operationGuidance } from "./operationMatch.js";
11
+ import { patchData } from "./patchMatch.js";
12
+
13
+ /** @param {import("../rules/finding.js").Finding} finding @param {string | null | undefined} register @param {string | null | undefined} preset @param {import("../data/exemplars.js").Exemplar[]} customExemplars */
14
+ function findingWithExemplar(finding, document, register, preset, customExemplars, patches) {
15
+ const data = findingAsDict(finding);
16
+ const exemplar = exemplarFor(finding.rule, preset, customExemplars);
17
+ if (exemplar) {
18
+ const adapted = exemplarInRegister(exemplar, register);
19
+ data.exemplar = { before: adapted.before, after: adapted.after, moved: adapted.moved };
20
+ }
21
+ const selected = patchData(document, finding, preset, patches);
22
+ if (selected) data.patch = selected;
23
+ return data;
24
+ }
4
25
 
5
26
  /**
6
27
  * @param {Map<string, import("../rules/finding.js").Finding[]>} results
7
28
  * @param {string | null} [configLabel]
29
+ * @param {Map<string, string> | null} [registers]
30
+ * @param {string | null} [preset]
31
+ * @param {import("../data/exemplars.js").Exemplar[]} [customExemplars]
32
+ * @param {Map<string, import("../fingerprint/build.js").DocumentPrint> | null} [documents]
33
+ * @param {import("../data/patches.js").Patch[]} [patches]
34
+ * @param {import("../data/operations.js").SurfaceOperation[]} [operations]
35
+ * @param {string[]} [protectedTerms]
8
36
  */
9
- export function renderJson(results, configLabel = null) {
10
- const files = [...results].map(([path, findings]) => ({ path, findings: findings.map(findingAsDict) }));
37
+ export function renderJson(
38
+ results,
39
+ configLabel = null,
40
+ registers = null,
41
+ preset = null,
42
+ customExemplars = [],
43
+ documents = null,
44
+ patches = [],
45
+ operations = [],
46
+ protectedTerms = [],
47
+ ) {
48
+ const files = [...results].map(([path, findings]) => {
49
+ const document = documents?.get(path);
50
+ const entry = {
51
+ path,
52
+ findings: findings.map((finding) => findingWithExemplar(finding, document, registers?.get(path), preset, customExemplars, patches)),
53
+ };
54
+ const selectedOperations = operationGuidance(document, findings, preset, operations, patches, protectedTerms);
55
+ if (selectedOperations.length) entry.operations = selectedOperations;
56
+ return entry;
57
+ });
11
58
  /** @type {Record<string, unknown>} */
12
59
  const data = { version: 1 };
13
60
  if (configLabel !== null) data.config = configLabel;
@@ -0,0 +1,31 @@
1
+ // @ts-check
2
+ /** 현재 문장에 승인 표면 치환이 안전하게 하나만 적용되는지 계산한다. */
3
+ import { operationData, operationFor } from "../data/operations.js";
4
+ import { sourceSentenceTexts } from "../fingerprint/sourceSentence.js";
5
+ import { selectedPatch } from "./patchMatch.js";
6
+
7
+ /**
8
+ * @param {import("../fingerprint/build.js").DocumentPrint | null | undefined} document
9
+ * @param {import("../rules/finding.js").Finding[]} findings
10
+ * @param {string | null | undefined} preset
11
+ * @param {import("../data/operations.js").SurfaceOperation[]} operations
12
+ * @param {import("../data/patches.js").Patch[]} [patches]
13
+ * @param {string[]} [protectedTerms]
14
+ */
15
+ export function operationGuidance(document, findings, preset, operations, patches = [], protectedTerms = []) {
16
+ if (!document || !preset || !operations.length) return [];
17
+ const reserved = new Set(findings.filter((finding) => finding.at >= 0 && finding.fix !== null).map((finding) => finding.at));
18
+ for (const finding of findings) {
19
+ if (finding.at >= 0 && selectedPatch(document, finding, preset, patches)) reserved.add(finding.at);
20
+ }
21
+ const sourceTexts = sourceSentenceTexts(document);
22
+ const guidance = [];
23
+ for (const sentence of document.sentences) {
24
+ if (reserved.has(sentence.index)) continue;
25
+ const sourceText = sourceTexts.get(sentence.index);
26
+ if (sourceText === undefined) continue;
27
+ const applied = operationFor(sourceText, preset, operations, protectedTerms);
28
+ if (applied) guidance.push({ line: sentence.line, operation: operationData(applied, preset) });
29
+ }
30
+ return guidance;
31
+ }
@@ -0,0 +1,44 @@
1
+ // @ts-check
2
+ /** 현재 지적과 지문을 승인 패치의 선택 조건에 맞댄다. */
3
+ import { flatSentence, patchFor } from "../data/patches.js";
4
+ import { readerKind } from "../fingerprint/readerState.js";
5
+ import { sourceSentenceText } from "../fingerprint/sourceSentence.js";
6
+ import { SENTENCE, localCue } from "../rules/finding.js";
7
+
8
+ /**
9
+ * @param {import("../fingerprint/build.js").DocumentPrint | null | undefined} document
10
+ * @param {import("../rules/finding.js").Finding} finding
11
+ * @param {string | null | undefined} preset
12
+ * @param {import("../data/patches.js").Patch[]} patches
13
+ */
14
+ export function selectedPatch(document, finding, preset, patches) {
15
+ if (!patches.length || !document || finding.scope !== SENTENCE || finding.at < 0 || finding.at >= document.sentences.length) return undefined;
16
+ const sentence = document.sentences[finding.at];
17
+ const sourceText = sourceSentenceText(document, sentence);
18
+ if (!sourceText) return undefined;
19
+ const state = document.reader.beforeSentence[finding.at];
20
+ return patchFor(finding.rule, preset, sourceText, sentence.text, localCue(finding), readerKind(sentence, state), patches);
21
+ }
22
+
23
+ /**
24
+ * @param {import("../fingerprint/build.js").DocumentPrint | null | undefined} document
25
+ * @param {import("../rules/finding.js").Finding} finding
26
+ * @param {string | null | undefined} preset
27
+ * @param {import("../data/patches.js").Patch[]} patches
28
+ */
29
+ export function patchData(document, finding, preset, patches) {
30
+ const patch = selectedPatch(document, finding, preset, patches);
31
+ if (!patch || !preset) return undefined;
32
+ return {
33
+ before: patch.before,
34
+ after: patch.after,
35
+ moved: patch.moved,
36
+ match: {
37
+ sourceText: flatSentence(patch.sourceText),
38
+ sentence: flatSentence(patch.sentence),
39
+ preset,
40
+ cue: patch.cue,
41
+ reader: patch.reader,
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,40 @@
1
+ // @ts-check
2
+ /** 한 문체로 저장한 본보기와 문형을 검사한 글의 문체에 맞춘다. */
3
+ import { HAPNIDA, REGISTERS, convertTemplate } from "../analysis/grammar/index.js";
4
+
5
+ /** 섞임과 없음은 데이터 정본의 합니다체를 유지한다. @param {string | null | undefined} register */
6
+ export function targetRegister(register) {
7
+ return register && REGISTERS.includes(register) ? register : HAPNIDA;
8
+ }
9
+
10
+ /** @param {import("../data/exemplars.js").Exemplar} exemplar @param {string | null | undefined} register */
11
+ export function exemplarInRegister(exemplar, register) {
12
+ const target = targetRegister(register);
13
+ const before = convertTemplate(exemplar.before, target);
14
+ const after = convertTemplate(exemplar.after, target);
15
+ if (before.skipped || after.skipped) {
16
+ throw new Error(
17
+ exemplar.rule + " 본보기의 서술어를 못 풀었다: " + (before.skipped + after.skipped),
18
+ );
19
+ }
20
+ return { rule: exemplar.rule, before: before.text, after: after.text, moved: exemplar.moved, presets: exemplar.presets };
21
+ }
22
+
23
+ /** @param {import("../data/patterns.js").Pattern} pattern @param {string | null | undefined} register */
24
+ export function patternInRegister(pattern, register) {
25
+ const target = targetRegister(register);
26
+ const form = convertTemplate(pattern.form, target);
27
+ const example = convertTemplate(pattern.example, target);
28
+ const instead = convertTemplate(pattern.instead, target);
29
+ const skipped = form.skipped + example.skipped + instead.skipped;
30
+ if (skipped) throw new Error(pattern.name + " 문형의 서술어를 못 풀었다: " + skipped);
31
+ return {
32
+ name: pattern.name,
33
+ form: form.text,
34
+ when: pattern.when,
35
+ example: example.text,
36
+ instead: instead.text,
37
+ avoids: pattern.avoids,
38
+ source: pattern.source,
39
+ };
40
+ }
@@ -1,8 +1,42 @@
1
1
  // @ts-check
2
- /** 사람이 읽는 지적 목록. `파일:줄` 로 시작해 편집기가 바로 연다. 파이썬 report/textReport.py 와 같은 글자다. */
2
+ /**
3
+ * 사람이 읽는 지적 목록. `파일:줄` 로 시작해 편집기가 바로 연다. 파이썬 report/textReport.py 와 같은 글자다.
4
+ * 끝에 본보기를 붙인다. 전과 후를 제 줄에 둔다. 왜 한 줄이 아닌지는 파이썬 report/textReport.py 가 소유한다.
5
+ */
6
+ import { exemplarFor, shortened, twoLines } from "../data/exemplars.js";
7
+ import { exemplarInRegister } from "./registerMatch.js";
3
8
 
4
- /** @param {string} path @param {import("../rules/finding.js").Finding[]} findings */
5
- export function renderText(path, findings) {
9
+ /** 그 글에 나온 규칙의 본보기. 이름 순이고 규칙 하나에 세 줄이다. @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} register @param {string | null | undefined} preset @param {import("../data/exemplars.js").Exemplar[]} customExemplars */
10
+ function exemplarLines(findings, register, preset, customExemplars) {
11
+ const lines = [];
12
+ let cut = false;
13
+ for (const name of [...new Set(findings.map((f) => f.rule))].sort()) {
14
+ let exemplar = exemplarFor(name, preset, customExemplars);
15
+ if (!exemplar) continue;
16
+ exemplar = exemplarInRegister(exemplar, register);
17
+ const [before, after] = twoLines(exemplar);
18
+ cut = cut || shortened(exemplar);
19
+ lines.push(` [${name}]`, ` 전 ${before}`, ` 후 ${after}`);
20
+ }
21
+ if (!lines.length) return [];
22
+ const head = "본보기 (고치기 전, 고친 뒤)";
23
+ return [cut ? `${head}. 잘린 것은 hanlint explain <규칙>` : head, ...lines];
24
+ }
25
+
26
+ /** 본보기 아래에 붙이는 선택지. 지적 자리별이며 순위는 없다. @param {import("../rules/finding.js").Finding[]} findings */
27
+ function candidateLines(findings) {
28
+ const chosen = findings.filter((finding) => finding.candidates.length);
29
+ if (!chosen.length) return [];
30
+ const lines = ["후보 (기계가 고르지 않음)"];
31
+ for (const finding of chosen) {
32
+ lines.push(` ${finding.line}줄 [${finding.rule}]`);
33
+ for (const candidate of finding.candidates) lines.push(` - ${candidate.text} (${candidate.why})`);
34
+ }
35
+ return lines;
36
+ }
37
+
38
+ /** @param {string} path @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} [register] @param {string | null | undefined} [preset] @param {import("../data/exemplars.js").Exemplar[]} [customExemplars] */
39
+ export function renderText(path, findings, register = null, preset = null, customExemplars = []) {
6
40
  if (!findings.length) return `${path} 집은 자리 없음`;
7
41
  const errors = findings.filter((f) => f.severity === "error").length;
8
42
  const notices = findings.length - errors;
@@ -14,6 +48,8 @@ export function renderText(path, findings) {
14
48
  if (f.fix) lines.push(` 고친 뒤: ${f.fix}`);
15
49
  lines.push("");
16
50
  }
17
- lines.push(`규칙이 왜 있는지 보려면: hanlint explain ${findings[0].rule}`);
18
- return lines.join("\n");
51
+ lines.push(...exemplarLines(findings, register, preset, customExemplars));
52
+ const candidates = candidateLines(findings);
53
+ if (candidates.length) lines.push("", ...candidates);
54
+ return lines.join("\n").replace(/\n+$/, "");
19
55
  }
@@ -1,8 +1,9 @@
1
1
  // @ts-check
2
+ import { roundHalfEven } from "../../text.js";
2
3
  import { DOCUMENT, NOTICE, finding } from "../finding.js";
3
- import { codeBlocksOf } from "../shared/codeBlocks.js";
4
4
 
5
5
  export const name = "duplicateBlock";
6
+ export const mechanism = "repeat";
6
7
  const MIN_LINES = 4;
7
8
 
8
9
  /** 줄 다중집합의 겹침 비. 2 * 공통 / (a + b). @param {string[]} a @param {string[]} b */
@@ -28,7 +29,7 @@ function similarity(a, b) {
28
29
  */
29
30
  export function run(doc, config) {
30
31
  const findings = [];
31
- const blocks = codeBlocksOf(doc).filter((b) => b.lines.filter(([, line]) => line.trim()).length >= MIN_LINES);
32
+ const blocks = doc.codeBlocks.filter((b) => b.lines.filter(([, line]) => line.trim()).length >= MIN_LINES);
32
33
  for (let later = 0; later < blocks.length; later++) {
33
34
  const current = blocks[later];
34
35
  const currentLines = current.lines.filter(([, line]) => line.trim()).map(([, line]) => line.trim());
@@ -38,7 +39,7 @@ export function run(doc, config) {
38
39
  const ratio = similarity(previous.lines.filter(([, line]) => line.trim()).map(([, line]) => line.trim()), currentLines);
39
40
  if (ratio >= config.duplicateBlockRatio) {
40
41
  findings.push(
41
- finding(name, current.startLine, currentLines[0], `${previous.startLine}번째 줄의 블록과 ${Math.round(ratio * 100)}% 같다. 다른 줄만 남기거나 앞 블록을 가리킨다`, null, NOTICE, DOCUMENT, current.index),
42
+ finding(name, current.startLine, currentLines[0], `${previous.startLine}번째 줄의 블록과 ${roundHalfEven(ratio * 100, 0)}% 같다. 다른 줄만 남기거나 앞 블록을 가리킨다`, null, NOTICE, DOCUMENT, current.index),
42
43
  );
43
44
  break;
44
45
  }
@@ -3,6 +3,7 @@ import { CODE, IMAGE, TABLE } from "../../document/model.js";
3
3
  import { NOTICE, PARAGRAPH, finding } from "../finding.js";
4
4
 
5
5
  export const name = "firstResultDistance";
6
+ export const mechanism = "threshold";
6
7
  const RESULT_KINDS = [CODE, IMAGE, TABLE];
7
8
 
8
9
  /**