easyread-mcp 0.1.0
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.
- package/LICENSE +29 -0
- package/README.md +41 -0
- package/assets/dictionary.json +118 -0
- package/assets/resources.json +1149 -0
- package/dist/data/dictionary.js +61 -0
- package/dist/data/dictionary.js.map +1 -0
- package/dist/data/guidelines.js +63 -0
- package/dist/data/guidelines.js.map +1 -0
- package/dist/data/resources.js +76 -0
- package/dist/data/resources.js.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.js +9 -0
- package/dist/messages.js.map +1 -0
- package/dist/prompts/review.js +30 -0
- package/dist/prompts/review.js.map +1 -0
- package/dist/prompts/simplify.js +40 -0
- package/dist/prompts/simplify.js.map +1 -0
- package/dist/rules/acc/acc-01.js +28 -0
- package/dist/rules/acc/acc-01.js.map +1 -0
- package/dist/rules/acc/acc-02.js +28 -0
- package/dist/rules/acc/acc-02.js.map +1 -0
- package/dist/rules/acc/acc-03.js +28 -0
- package/dist/rules/acc/acc-03.js.map +1 -0
- package/dist/rules/acc/acc-04.js +28 -0
- package/dist/rules/acc/acc-04.js.map +1 -0
- package/dist/rules/acc/index.js +8 -0
- package/dist/rules/acc/index.js.map +1 -0
- package/dist/rules/config.js +20 -0
- package/dist/rules/config.js.map +1 -0
- package/dist/rules/engine.js +49 -0
- package/dist/rules/engine.js.map +1 -0
- package/dist/rules/index.js +11 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/num/index.js +8 -0
- package/dist/rules/num/index.js.map +1 -0
- package/dist/rules/num/num-01.js +27 -0
- package/dist/rules/num/num-01.js.map +1 -0
- package/dist/rules/num/num-02.js +21 -0
- package/dist/rules/num/num-02.js.map +1 -0
- package/dist/rules/num/num-03.js +24 -0
- package/dist/rules/num/num-03.js.map +1 -0
- package/dist/rules/num/num-04.js +21 -0
- package/dist/rules/num/num-04.js.map +1 -0
- package/dist/rules/registry.js +30 -0
- package/dist/rules/registry.js.map +1 -0
- package/dist/rules/report.js +69 -0
- package/dist/rules/report.js.map +1 -0
- package/dist/rules/sen/index.js +10 -0
- package/dist/rules/sen/index.js.map +1 -0
- package/dist/rules/sen/sen-01.js +33 -0
- package/dist/rules/sen/sen-01.js.map +1 -0
- package/dist/rules/sen/sen-02.js +40 -0
- package/dist/rules/sen/sen-02.js.map +1 -0
- package/dist/rules/sen/sen-03.js +33 -0
- package/dist/rules/sen/sen-03.js.map +1 -0
- package/dist/rules/sen/sen-04.js +31 -0
- package/dist/rules/sen/sen-04.js.map +1 -0
- package/dist/rules/sen/sen-05.js +27 -0
- package/dist/rules/sen/sen-05.js.map +1 -0
- package/dist/rules/sen/sen-07.js +58 -0
- package/dist/rules/sen/sen-07.js.map +1 -0
- package/dist/rules/str/index.js +9 -0
- package/dist/rules/str/index.js.map +1 -0
- package/dist/rules/str/str-02.js +22 -0
- package/dist/rules/str/str-02.js.map +1 -0
- package/dist/rules/str/str-03.js +24 -0
- package/dist/rules/str/str-03.js.map +1 -0
- package/dist/rules/typ/index.js +7 -0
- package/dist/rules/typ/index.js.map +1 -0
- package/dist/rules/typ/typ-01.js +21 -0
- package/dist/rules/typ/typ-01.js.map +1 -0
- package/dist/rules/typ/typ-02.js +23 -0
- package/dist/rules/typ/typ-02.js.map +1 -0
- package/dist/rules/typ/typ-03.js +28 -0
- package/dist/rules/typ/typ-03.js.map +1 -0
- package/dist/rules/types.js +2 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/voc/index.js +10 -0
- package/dist/rules/voc/index.js.map +1 -0
- package/dist/rules/voc/voc-01.js +27 -0
- package/dist/rules/voc/voc-01.js.map +1 -0
- package/dist/rules/voc/voc-02.js +38 -0
- package/dist/rules/voc/voc-02.js.map +1 -0
- package/dist/rules/voc/voc-03.js +30 -0
- package/dist/rules/voc/voc-03.js.map +1 -0
- package/dist/rules/voc/voc-04.js +26 -0
- package/dist/rules/voc/voc-04.js.map +1 -0
- package/dist/rules/voc/voc-05.js +34 -0
- package/dist/rules/voc/voc-05.js.map +1 -0
- package/dist/rules/voc/voc-06.js +36 -0
- package/dist/rules/voc/voc-06.js.map +1 -0
- package/dist/server.js +49 -0
- package/dist/server.js.map +1 -0
- package/dist/text/extractors.js +40 -0
- package/dist/text/extractors.js.map +1 -0
- package/dist/text/index.js +38 -0
- package/dist/text/index.js.map +1 -0
- package/dist/text/paragraphs.js +28 -0
- package/dist/text/paragraphs.js.map +1 -0
- package/dist/text/sentences.js +117 -0
- package/dist/text/sentences.js.map +1 -0
- package/dist/text/spans.js +9 -0
- package/dist/text/spans.js.map +1 -0
- package/dist/text/types.js +6 -0
- package/dist/text/types.js.map +1 -0
- package/dist/text/words.js +20 -0
- package/dist/text/words.js.map +1 -0
- package/dist/tools/analyze.js +73 -0
- package/dist/tools/analyze.js.map +1 -0
- package/dist/tools/guidelines.js +55 -0
- package/dist/tools/guidelines.js.map +1 -0
- package/dist/tools/lookup.js +79 -0
- package/dist/tools/lookup.js.map +1 -0
- package/dist/tools/validate.js +79 -0
- package/dist/tools/validate.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// TYP-02 괄호 남용: 한 문장에 여는 괄호 '('가 2개 이상이면 문장당 info. 괄호 속 정보는 문장으로 풀기 제안.
|
|
2
|
+
// 근거: Inclusion Europe §2(괄호보다 문장) · guidelines §5.
|
|
3
|
+
const MIN_PARENS = 2;
|
|
4
|
+
export const typ02 = {
|
|
5
|
+
id: "TYP-02",
|
|
6
|
+
group: "TYP",
|
|
7
|
+
defaultSeverity: "info",
|
|
8
|
+
check(ctx) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const s of ctx.sentences) {
|
|
11
|
+
const count = (s.text.match(/\(/g) ?? []).length;
|
|
12
|
+
if (count >= MIN_PARENS) {
|
|
13
|
+
findings.push({
|
|
14
|
+
ruleId: "TYP-02",
|
|
15
|
+
message: "한 문장에 괄호가 여러 개입니다. 괄호 속 내용을 문장으로 풀어 쓰세요.",
|
|
16
|
+
span: s.span,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return findings;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=typ-02.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typ-02.js","sourceRoot":"","sources":["../../../src/rules/typ/typ-02.ts"],"names":[],"mappings":"AAEA,wEAAwE;AACxE,oDAAoD;AACpD,MAAM,UAAU,GAAG,CAAC,CAAC;AAErB,MAAM,CAAC,MAAM,KAAK,GAAS;IACzB,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,MAAM;IACvB,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAkB,EAAE,CAAC;QACnC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;YACjD,IAAI,KAAK,IAAI,UAAU,EAAE,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,QAAQ;oBAChB,OAAO,EAAE,0CAA0C;oBACnD,IAAI,EAAE,CAAC,CAAC,IAAI;iBACb,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// TYP-03 쌍점/쌍반점: ':'·';' 출현마다 info. 문장으로 풀어쓰기 제안.
|
|
2
|
+
// 단, 숫자 사이의 ':'(시각 '3:00')는 오탐 방지를 위해 제외한다.
|
|
3
|
+
// 근거: 쉬운 언어 일반 원칙(문장부호 절제).
|
|
4
|
+
export const typ03 = {
|
|
5
|
+
id: "TYP-03",
|
|
6
|
+
group: "TYP",
|
|
7
|
+
defaultSeverity: "info",
|
|
8
|
+
check(ctx) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
const raw = ctx.raw;
|
|
11
|
+
for (let i = 0; i < raw.length; i++) {
|
|
12
|
+
const c = raw[i];
|
|
13
|
+
if (c === ";") {
|
|
14
|
+
findings.push({ ruleId: "TYP-03", message: "쌍반점(;)은 문장으로 풀어 쓰세요.", span: { start: i, end: i + 1 } });
|
|
15
|
+
}
|
|
16
|
+
else if (c === ":") {
|
|
17
|
+
const prev = raw[i - 1];
|
|
18
|
+
const next = raw[i + 1];
|
|
19
|
+
const betweenDigits = prev !== undefined && next !== undefined && /\d/.test(prev) && /\d/.test(next);
|
|
20
|
+
if (!betweenDigits) {
|
|
21
|
+
findings.push({ ruleId: "TYP-03", message: "쌍점(:)은 문장으로 풀어 쓰세요.", span: { start: i, end: i + 1 } });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return findings;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=typ-03.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typ-03.js","sourceRoot":"","sources":["../../../src/rules/typ/typ-03.ts"],"names":[],"mappings":"AAEA,kDAAkD;AAClD,4CAA4C;AAC5C,4BAA4B;AAC5B,MAAM,CAAC,MAAM,KAAK,GAAS;IACzB,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,MAAM;IACvB,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAkB,EAAE,CAAC;QACnC,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;QACpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACpC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;gBACd,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;YACvG,CAAC;iBAAM,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;gBACrB,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBACxB,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBACxB,MAAM,aAAa,GAAG,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACrG,IAAI,CAAC,aAAa,EAAE,CAAC;oBACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;gBACtG,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/rules/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { voc01 } from "./voc-01.js";
|
|
2
|
+
import { voc02 } from "./voc-02.js";
|
|
3
|
+
import { voc03 } from "./voc-03.js";
|
|
4
|
+
import { voc04 } from "./voc-04.js";
|
|
5
|
+
import { voc05 } from "./voc-05.js";
|
|
6
|
+
import { voc06 } from "./voc-06.js";
|
|
7
|
+
export { voc01, voc02, voc03, voc04, voc05, voc06 };
|
|
8
|
+
/** VOC 규칙군. registry가 정적으로 조립한다. */
|
|
9
|
+
export const vocRules = [voc01, voc02, voc03, voc04, voc05, voc06];
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/voc/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAEpD,oCAAoC;AACpC,MAAM,CAAC,MAAM,QAAQ,GAAoB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// VOC-01 어려운 한자어: 어절 정확 일치로 difficult 사전 매칭 → 대체어 제안.
|
|
2
|
+
// 근거: Inclusion Europe #8(일상어 사용) · ISO 24495-1(독자가 이해) · 국내 지침 어휘.
|
|
3
|
+
// v0.1은 어절 정확 일치만. 조사 결합 형태('상기의')는 T-08 사전 helper로 확장.
|
|
4
|
+
export const voc01 = {
|
|
5
|
+
id: "VOC-01",
|
|
6
|
+
group: "VOC",
|
|
7
|
+
defaultSeverity: "warning",
|
|
8
|
+
check(ctx) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const s of ctx.sentences) {
|
|
11
|
+
for (const w of s.words) {
|
|
12
|
+
const entry = ctx.dictionary.lookup(w.text);
|
|
13
|
+
if (entry?.category === "difficult") {
|
|
14
|
+
const alts = entry.alternatives.join(", ");
|
|
15
|
+
findings.push({
|
|
16
|
+
ruleId: "VOC-01",
|
|
17
|
+
message: `'${w.text}'은(는) 어려운 말입니다. '${alts}'(으)로 바꾸면 더 쉽습니다.`,
|
|
18
|
+
span: w.span,
|
|
19
|
+
suggestion: alts,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return findings;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=voc-01.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voc-01.js","sourceRoot":"","sources":["../../../src/rules/voc/voc-01.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,oEAAoE;AACpE,wDAAwD;AACxD,MAAM,CAAC,MAAM,KAAK,GAAS;IACzB,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,SAAS;IAC1B,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAkB,EAAE,CAAC;QACnC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YAC9B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBACxB,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBAC5C,IAAI,KAAK,EAAE,QAAQ,KAAK,WAAW,EAAE,CAAC;oBACpC,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC3C,QAAQ,CAAC,IAAI,CAAC;wBACZ,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,oBAAoB,IAAI,mBAAmB;wBAC9D,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,UAAU,EAAE,IAAI;qBACjB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// VOC-02 불필요한 외래어/외국어: (a) loanword 사전 정확 일치 또는 (b) 로마자 2자+ 연속.
|
|
2
|
+
// 근거: Inclusion Europe #8(일상어) · 국내 지침 어휘(한국어 대체어 우선).
|
|
3
|
+
// 어절당 최대 1건, (a) 우선. 단일 로마자('A형')는 제외.
|
|
4
|
+
const LATIN_2PLUS = /[A-Za-z]{2,}/;
|
|
5
|
+
export const voc02 = {
|
|
6
|
+
id: "VOC-02",
|
|
7
|
+
group: "VOC",
|
|
8
|
+
defaultSeverity: "warning",
|
|
9
|
+
check(ctx) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
for (const s of ctx.sentences) {
|
|
12
|
+
for (const w of s.words) {
|
|
13
|
+
// (a) 사전 loanword 정확 일치 — 우선
|
|
14
|
+
const entry = ctx.dictionary.lookup(w.text);
|
|
15
|
+
if (entry?.category === "loanword") {
|
|
16
|
+
const alts = entry.alternatives.join(", ");
|
|
17
|
+
findings.push({
|
|
18
|
+
ruleId: "VOC-02",
|
|
19
|
+
message: `'${w.text}'은(는) 외래어입니다. '${alts}'(으)로 바꾸면 더 쉽습니다.`,
|
|
20
|
+
span: w.span,
|
|
21
|
+
suggestion: alts,
|
|
22
|
+
});
|
|
23
|
+
continue; // 한 어절 1건
|
|
24
|
+
}
|
|
25
|
+
// (b) 로마자 2자 이상 연속 (사전 미등록 외국어)
|
|
26
|
+
if (LATIN_2PLUS.test(w.text)) {
|
|
27
|
+
findings.push({
|
|
28
|
+
ruleId: "VOC-02",
|
|
29
|
+
message: `'${w.text}'은(는) 외국어입니다. 한국어로 바꾸거나 뜻을 함께 적으세요.`,
|
|
30
|
+
span: w.span,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return findings;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=voc-02.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voc-02.js","sourceRoot":"","sources":["../../../src/rules/voc/voc-02.ts"],"names":[],"mappings":"AAEA,gEAAgE;AAChE,uDAAuD;AACvD,uCAAuC;AACvC,MAAM,WAAW,GAAG,cAAc,CAAC;AAEnC,MAAM,CAAC,MAAM,KAAK,GAAS;IACzB,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,SAAS;IAC1B,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAkB,EAAE,CAAC;QACnC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YAC9B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBACxB,6BAA6B;gBAC7B,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBAC5C,IAAI,KAAK,EAAE,QAAQ,KAAK,UAAU,EAAE,CAAC;oBACnC,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC3C,QAAQ,CAAC,IAAI,CAAC;wBACZ,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,kBAAkB,IAAI,mBAAmB;wBAC5D,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,UAAU,EAAE,IAAI;qBACjB,CAAC,CAAC;oBACH,SAAS,CAAC,UAAU;gBACtB,CAAC;gBACD,gCAAgC;gBAChC,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC7B,QAAQ,CAAC,IAAI,CAAC;wBACZ,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,qCAAqC;wBACxD,IAAI,EAAE,CAAC,CAAC,IAAI;qBACb,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// VOC-03 전문용어 뜻풀이 없음: terminology 어절이 **같은 문장에** 풀이 없이 쓰이면 어절마다 warning.
|
|
2
|
+
// 근거: Inclusion Europe #7(어려운 낱말은 설명) · guidelines §2(바꿀 수 없는 용어는 뜻풀이).
|
|
3
|
+
// 풀이 판정(보조): 같은 문장 raw에 아래 마커가 하나라도 있으면 '설명됨'으로 보고 넘어간다.
|
|
4
|
+
// v0.1은 어절 정확 일치만(조사 결합 '임의가입의'는 미탐 — backlog).
|
|
5
|
+
const GLOSS_MARKERS = ["이란", "라는 뜻", "뜻입니다", "뜻이", "설명", "("];
|
|
6
|
+
export const voc03 = {
|
|
7
|
+
id: "VOC-03",
|
|
8
|
+
group: "VOC",
|
|
9
|
+
defaultSeverity: "warning",
|
|
10
|
+
check(ctx) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
for (const s of ctx.sentences) {
|
|
13
|
+
if (GLOSS_MARKERS.some((m) => s.text.includes(m)))
|
|
14
|
+
continue; // 같은 문장에 풀이가 있으면 통과
|
|
15
|
+
for (const w of s.words) {
|
|
16
|
+
const entry = ctx.dictionary.lookup(w.text);
|
|
17
|
+
if (entry?.category === "terminology") {
|
|
18
|
+
findings.push({
|
|
19
|
+
ruleId: "VOC-03",
|
|
20
|
+
message: `'${w.text}'은(는) 전문용어입니다. 처음 나올 때 뜻을 함께 풀어 주세요.`,
|
|
21
|
+
span: w.span,
|
|
22
|
+
suggestion: entry.explanation,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return findings;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=voc-03.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voc-03.js","sourceRoot":"","sources":["../../../src/rules/voc/voc-03.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,wEAAwE;AACxE,yDAAyD;AACzD,gDAAgD;AAChD,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AAE9D,MAAM,CAAC,MAAM,KAAK,GAAS;IACzB,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,SAAS;IAC1B,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAkB,EAAE,CAAC;QACnC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YAC9B,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAAE,SAAS,CAAC,oBAAoB;YACjF,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBACxB,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBAC5C,IAAI,KAAK,EAAE,QAAQ,KAAK,aAAa,EAAE,CAAC;oBACtC,QAAQ,CAAC,IAAI,CAAC;wBACZ,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,sCAAsC;wBACzD,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,UAAU,EAAE,KAAK,CAAC,WAAW;qBAC9B,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// VOC-04 추상어/관용구/비유: 사전 idiom 어절을 정확 일치로 찾아 info로 안내한다(VOC-01 패턴).
|
|
2
|
+
// 근거: Inclusion Europe #10(은유·비유 금지) · guidelines §2(추상어·비유·관용구를 피한다).
|
|
3
|
+
// 매칭 어절마다 1건. v0.1은 어절 정확 일치만(조사 결합 '활성화를'·다어절 관용구는 backlog).
|
|
4
|
+
export const voc04 = {
|
|
5
|
+
id: "VOC-04",
|
|
6
|
+
group: "VOC",
|
|
7
|
+
defaultSeverity: "info",
|
|
8
|
+
check(ctx) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const s of ctx.sentences) {
|
|
11
|
+
for (const w of s.words) {
|
|
12
|
+
const entry = ctx.dictionary.lookup(w.text);
|
|
13
|
+
if (entry?.category === "idiom") {
|
|
14
|
+
findings.push({
|
|
15
|
+
ruleId: "VOC-04",
|
|
16
|
+
message: `'${w.text}'은(는) 추상적이거나 비유적인 표현입니다. 구체적인 행동으로 풀어 쓰면 더 쉽습니다.`,
|
|
17
|
+
span: w.span,
|
|
18
|
+
suggestion: entry.alternatives.join(", "),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return findings;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=voc-04.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voc-04.js","sourceRoot":"","sources":["../../../src/rules/voc/voc-04.ts"],"names":[],"mappings":"AAEA,mEAAmE;AACnE,uEAAuE;AACvE,8DAA8D;AAC9D,MAAM,CAAC,MAAM,KAAK,GAAS;IACzB,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,MAAM;IACvB,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAkB,EAAE,CAAC;QACnC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YAC9B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBACxB,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBAC5C,IAAI,KAAK,EAAE,QAAQ,KAAK,OAAO,EAAE,CAAC;oBAChC,QAAQ,CAAC,IAAI,CAAC;wBACZ,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,kDAAkD;wBACrE,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,UAAU,EAAE,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;qBAC1C,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// VOC-05 풀이 없는 줄임말/약어: abbreviation 어절의 '첫 등장'에 괄호 풀이가 없으면 warning.
|
|
2
|
+
// 근거: Inclusion Europe #12(약어 지양·풀어 설명) · guidelines §2(줄임말은 첫 등장에서 풀어 쓴다).
|
|
3
|
+
// 문서 전체(문장 순서)에서 약어별 첫 등장만 판정. 풀이 판정: 매칭 어절에 '(' 포함 또는
|
|
4
|
+
// 바로 다음 어절이 '('로 시작. v0.1은 어절 정확 일치만(조사 결합 'WHO가'는 미탐 — backlog).
|
|
5
|
+
export const voc05 = {
|
|
6
|
+
id: "VOC-05",
|
|
7
|
+
group: "VOC",
|
|
8
|
+
defaultSeverity: "warning",
|
|
9
|
+
check(ctx) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const words = ctx.sentences.flatMap((s) => s.words);
|
|
13
|
+
for (const [i, w] of words.entries()) {
|
|
14
|
+
const entry = ctx.dictionary.lookup(w.text);
|
|
15
|
+
if (entry?.category !== "abbreviation")
|
|
16
|
+
continue;
|
|
17
|
+
if (seen.has(w.text))
|
|
18
|
+
continue; // 각 약어의 첫 등장만
|
|
19
|
+
seen.add(w.text);
|
|
20
|
+
const next = words[i + 1];
|
|
21
|
+
const glossed = w.text.includes("(") || (next !== undefined && next.text.startsWith("("));
|
|
22
|
+
if (glossed)
|
|
23
|
+
continue;
|
|
24
|
+
findings.push({
|
|
25
|
+
ruleId: "VOC-05",
|
|
26
|
+
message: `'${w.text}'은(는) 줄임말입니다. 처음 나올 때 '${entry.alternatives.join(", ")}'처럼 뜻을 함께 적으세요.`,
|
|
27
|
+
span: w.span,
|
|
28
|
+
suggestion: entry.alternatives.join(", "),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return findings;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=voc-05.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voc-05.js","sourceRoot":"","sources":["../../../src/rules/voc/voc-05.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,4EAA4E;AAC5E,uDAAuD;AACvD,kEAAkE;AAClE,MAAM,CAAC,MAAM,KAAK,GAAS;IACzB,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,SAAS;IAC1B,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAkB,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACpD,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;YACrC,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAC5C,IAAI,KAAK,EAAE,QAAQ,KAAK,cAAc;gBAAE,SAAS;YACjD,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;gBAAE,SAAS,CAAC,cAAc;YAC9C,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACjB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1B,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1F,IAAI,OAAO;gBAAE,SAAS;YACtB,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,0BAA0B,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB;gBAC3F,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,UAAU,EAAE,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;aAC1C,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// VOC-06 지시어 과다: 지시어 어절 총수가 문장 수를 넘으면(밀도 > 문장당 1개) 문서 단위 info 1건.
|
|
2
|
+
// 근거: Inclusion Europe #12(대명사는 대상이 분명해야) · guidelines §2(지시어를 줄인다).
|
|
3
|
+
// 사전 불필요. 단일 글자 '이/그/저'는 어절 정확 일치라 '이순신'·'그림'을 잡지 않는다.
|
|
4
|
+
// 임계값(밀도)은 상수로 조정 가능. v0.1은 정확 일치만(조사 결합 '그것을'은 미탐 — backlog).
|
|
5
|
+
const DEMONSTRATIVES = new Set([
|
|
6
|
+
"이", "그", "저", "이것", "그것", "저것", "이거", "그거", "해당",
|
|
7
|
+
"이런", "그런", "저런", "이러한", "그러한", "저러한",
|
|
8
|
+
"여기", "거기", "저기", "이곳", "그곳",
|
|
9
|
+
]);
|
|
10
|
+
const MAX_PER_SENTENCE = 1; // 문장당 허용 밀도(초과 시 과다)
|
|
11
|
+
export const voc06 = {
|
|
12
|
+
id: "VOC-06",
|
|
13
|
+
group: "VOC",
|
|
14
|
+
defaultSeverity: "info",
|
|
15
|
+
check(ctx) {
|
|
16
|
+
const hits = [];
|
|
17
|
+
for (const s of ctx.sentences) {
|
|
18
|
+
for (const w of s.words) {
|
|
19
|
+
if (DEMONSTRATIVES.has(w.text))
|
|
20
|
+
hits.push(w);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const sentences = ctx.sentences.length;
|
|
24
|
+
const first = hits[0];
|
|
25
|
+
if (first === undefined || hits.length <= sentences * MAX_PER_SENTENCE)
|
|
26
|
+
return [];
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
ruleId: "VOC-06",
|
|
30
|
+
message: `지시어(이, 그, 해당 등)가 ${hits.length}번 나옵니다(문장 ${sentences}개). 가리키는 대상을 다시 적어 주세요.`,
|
|
31
|
+
span: first.span,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=voc-06.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voc-06.js","sourceRoot":"","sources":["../../../src/rules/voc/voc-06.ts"],"names":[],"mappings":"AAGA,kEAAkE;AAClE,qEAAqE;AACrE,uDAAuD;AACvD,+DAA+D;AAC/D,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC7B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IACjD,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK;IACrC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;CAC7B,CAAC,CAAC;AACH,MAAM,gBAAgB,GAAG,CAAC,CAAC,CAAC,qBAAqB;AAEjD,MAAM,CAAC,MAAM,KAAK,GAAS;IACzB,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,MAAM;IACvB,KAAK,CAAC,GAAG;QACP,MAAM,IAAI,GAAW,EAAE,CAAC;QACxB,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YAC9B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBACxB,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;oBAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC;QACD,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,IAAI,SAAS,GAAG,gBAAgB;YAAE,OAAO,EAAE,CAAC;QAClF,OAAO;YACL;gBACE,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,oBAAoB,IAAI,CAAC,MAAM,aAAa,SAAS,yBAAyB;gBACvF,IAAI,EAAE,KAAK,CAAC,IAAI;aACjB;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { GUIDELINES_MARKDOWN, buildChecklistMarkdown } from "./data/guidelines.js";
|
|
3
|
+
import { rules } from "./rules/index.js";
|
|
4
|
+
import { registerReviewPrompt } from "./prompts/review.js";
|
|
5
|
+
import { registerSimplifyPrompt } from "./prompts/simplify.js";
|
|
6
|
+
import { registerAnalyzeTool } from "./tools/analyze.js";
|
|
7
|
+
import { registerGuidelinesTool } from "./tools/guidelines.js";
|
|
8
|
+
import { registerLookupTool } from "./tools/lookup.js";
|
|
9
|
+
import { registerValidateTool } from "./tools/validate.js";
|
|
10
|
+
/**
|
|
11
|
+
* EASYREAD MCP 서버를 조립한다. 도구·프롬프트·리소스 등록만 담당하고 로직은 갖지 않는다(03 §1).
|
|
12
|
+
*/
|
|
13
|
+
export function createServer(dictionary, catalog) {
|
|
14
|
+
const server = new McpServer({ name: "easyread", version: "0.1.0" });
|
|
15
|
+
registerValidateTool(server, dictionary);
|
|
16
|
+
registerAnalyzeTool(server, dictionary);
|
|
17
|
+
registerLookupTool(server, dictionary);
|
|
18
|
+
registerGuidelinesTool(server);
|
|
19
|
+
// 프롬프트(FR-05·06·08, T-09)
|
|
20
|
+
registerSimplifyPrompt(server);
|
|
21
|
+
registerReviewPrompt(server);
|
|
22
|
+
// easyread://guidelines · guidelines/checklist (정적 Markdown, FR-09·T-09)
|
|
23
|
+
server.registerResource("guidelines", "easyread://guidelines", { description: "쉬운 정보 작성 지침 전문", mimeType: "text/markdown" }, () => ({
|
|
24
|
+
contents: [{ uri: "easyread://guidelines", mimeType: "text/markdown", text: GUIDELINES_MARKDOWN }],
|
|
25
|
+
}));
|
|
26
|
+
const checklist = buildChecklistMarkdown(rules);
|
|
27
|
+
server.registerResource("guidelines-checklist", "easyread://guidelines/checklist", { description: "검증 규칙 체크리스트(규칙 ID 표)", mimeType: "text/markdown" }, () => ({
|
|
28
|
+
contents: [{ uri: "easyread://guidelines/checklist", mimeType: "text/markdown", text: checklist }],
|
|
29
|
+
}));
|
|
30
|
+
// easyread://dictionary (주입 사전이 있을 때만, FR-09·T-09)
|
|
31
|
+
if (dictionary) {
|
|
32
|
+
const dictionaryJson = JSON.stringify({ entries: dictionary.entries }, null, 2);
|
|
33
|
+
server.registerResource("dictionary", "easyread://dictionary", { description: "단어 사전 전체(쉬운 대체어·뜻풀이)", mimeType: "application/json" }, () => ({
|
|
34
|
+
contents: [{ uri: "easyread://dictionary", mimeType: "application/json", text: dictionaryJson }],
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
// easyread://resources — Easy-Read 근거·표준·사례 카탈로그(ADR-07, T-14)
|
|
38
|
+
if (catalog) {
|
|
39
|
+
const json = JSON.stringify(catalog, null, 2);
|
|
40
|
+
server.registerResource("resources", "easyread://resources", {
|
|
41
|
+
description: "Easy-Read 근거·표준·사례 카탈로그(66건: 지침·법령·사례·포털)",
|
|
42
|
+
mimeType: "application/json",
|
|
43
|
+
}, () => ({
|
|
44
|
+
contents: [{ uri: "easyread://resources", mimeType: "application/json", text: json }],
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
return server;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACC(사실 보존) 규칙군이 공유하는 값 추출기. 원문·변환문에서 같은 종류의 값을 뽑아
|
|
3
|
+
* "원문에 있으나 변환문에 없는 값 = 사실 누락/왜곡"을 판정하는 데 쓴다(W 정의 인터페이스).
|
|
4
|
+
* v0.1 휴리스틱 — 형태소 분석 없이 정규식·접미 매칭. 과소탐지 가능(문서화된 backlog).
|
|
5
|
+
*/
|
|
6
|
+
// 날짜·시간: "2026년", "3월 2일", "3월", "2일", "3시(3시 30분)". 공백은 정규화로 제거해 비교한다.
|
|
7
|
+
const DATE_RE = /\d+\s*년(?:\s*\d+\s*월)?(?:\s*\d+\s*일)?|\d+\s*월\s*\d+\s*일|\d+\s*월|\d+\s*일|\d+\s*시(?:\s*\d+\s*분)?/g;
|
|
8
|
+
// 금액·수량: 숫자(+콤마) + 단위(만/억/천/백 또는 원/명/개…). 단위가 있어야 매칭(날짜 숫자 오탐 방지).
|
|
9
|
+
const AMOUNT_RE = /\d[\d,]*\s*(?:억|만|천|백)\s*(?:원|명|개|건|가지)?|\d[\d,]*\s*(?:원|명|개|건|가지|권|장|대|마리|시간)/g;
|
|
10
|
+
// 연락처: 전화(\d{2,4}-\d{3,4}-\d{4}) · URL(http(s):// … 또는 www. …).
|
|
11
|
+
const CONTACT_RE = new RegExp("\\d{2,4}-\\d{3,4}-\\d{4}|https?://\\S+|www\\.\\S+", "g");
|
|
12
|
+
// 기관명 후보 접미(휴리스틱). 조사를 떼고 이 접미로 끝나는 어절을 기관명 후보로 본다.
|
|
13
|
+
const INSTITUTION_SUFFIXES = [
|
|
14
|
+
"공단", "공사", "위원회", "구청", "시청", "도청", "교육청",
|
|
15
|
+
"연구원", "진흥원", "재단", "협회", "조합", "센터", "청", "부", "원",
|
|
16
|
+
];
|
|
17
|
+
const TRAILING_JOSA = /(?:은|는|이|가|을|를|에서|에게|에|의|과|와|도|으로|로|까지|부터)$/;
|
|
18
|
+
function stripSpaces(s) {
|
|
19
|
+
return s.replace(/\s+/g, "");
|
|
20
|
+
}
|
|
21
|
+
export function extractDates(text) {
|
|
22
|
+
return [...text.matchAll(DATE_RE)].map((m) => stripSpaces(m[0]));
|
|
23
|
+
}
|
|
24
|
+
export function extractAmounts(text) {
|
|
25
|
+
return [...text.matchAll(AMOUNT_RE)].map((m) => stripSpaces(m[0]));
|
|
26
|
+
}
|
|
27
|
+
export function extractContacts(text) {
|
|
28
|
+
return [...text.matchAll(CONTACT_RE)].map((m) => m[0]);
|
|
29
|
+
}
|
|
30
|
+
export function extractProperNouns(text) {
|
|
31
|
+
const names = [];
|
|
32
|
+
for (const token of text.split(/\s+/)) {
|
|
33
|
+
const word = token.replace(TRAILING_JOSA, "");
|
|
34
|
+
if (word.length >= 2 && INSTITUTION_SUFFIXES.some((suffix) => word.endsWith(suffix))) {
|
|
35
|
+
names.push(word);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return names;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=extractors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractors.js","sourceRoot":"","sources":["../../src/text/extractors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wEAAwE;AACxE,MAAM,OAAO,GACX,iGAAiG,CAAC;AACpG,mEAAmE;AACnE,MAAM,SAAS,GACb,iFAAiF,CAAC;AACpF,gEAAgE;AAChE,MAAM,UAAU,GAAG,IAAI,MAAM,CAAC,mDAAmD,EAAE,GAAG,CAAC,CAAC;AAExF,oDAAoD;AACpD,MAAM,oBAAoB,GAAG;IAC3B,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK;IAC1C,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;CACpD,CAAC;AACF,MAAM,aAAa,GAAG,6CAA6C,CAAC;AAEpE,SAAS,WAAW,CAAC,CAAS;IAC5B,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;QAC9C,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YACrF,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { splitParagraphSpans } from "./paragraphs.js";
|
|
2
|
+
import { splitSentenceSpans } from "./sentences.js";
|
|
3
|
+
import { sliceSpan } from "./spans.js";
|
|
4
|
+
import { tokenizeWords } from "./words.js";
|
|
5
|
+
/**
|
|
6
|
+
* 원문을 문단·문장·어절로 분해한다. text/ 계층의 진입점.
|
|
7
|
+
* 규칙 엔진(rules/)은 이 결과(ParsedText)를 입력으로 받는다.
|
|
8
|
+
* 빈 문자열·공백만 있는 입력은 빈 문단/문장 목록을 돌려준다(오류 아님).
|
|
9
|
+
*/
|
|
10
|
+
export function parseText(raw) {
|
|
11
|
+
const paragraphs = [];
|
|
12
|
+
const sentences = [];
|
|
13
|
+
let sentenceIndex = 0;
|
|
14
|
+
splitParagraphSpans(raw).forEach((pSpan, pIdx) => {
|
|
15
|
+
const pText = sliceSpan(raw, pSpan);
|
|
16
|
+
const pSentences = splitSentenceSpans(pText, pSpan.start).map((sSpan) => {
|
|
17
|
+
const text = sliceSpan(raw, sSpan);
|
|
18
|
+
const words = tokenizeWords(text, sSpan.start);
|
|
19
|
+
const sentence = {
|
|
20
|
+
text,
|
|
21
|
+
span: sSpan,
|
|
22
|
+
words,
|
|
23
|
+
wordCount: words.length,
|
|
24
|
+
index: sentenceIndex++,
|
|
25
|
+
paragraphIndex: pIdx,
|
|
26
|
+
};
|
|
27
|
+
sentences.push(sentence);
|
|
28
|
+
return sentence;
|
|
29
|
+
});
|
|
30
|
+
paragraphs.push({ text: pText, span: pSpan, sentences: pSentences, index: pIdx });
|
|
31
|
+
});
|
|
32
|
+
return { raw, paragraphs, sentences };
|
|
33
|
+
}
|
|
34
|
+
export { splitSentenceSpans } from "./sentences.js";
|
|
35
|
+
export { splitParagraphSpans } from "./paragraphs.js";
|
|
36
|
+
export { tokenizeWords, countWords } from "./words.js";
|
|
37
|
+
export { sliceSpan, spanLength } from "./spans.js";
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/text/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,UAAU,GAAgB,EAAE,CAAC;IACnC,MAAM,SAAS,GAAe,EAAE,CAAC;IACjC,IAAI,aAAa,GAAG,CAAC,CAAC;IAEtB,mBAAmB,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QAC/C,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACpC,MAAM,UAAU,GAAe,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YAClF,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACnC,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;YAC/C,MAAM,QAAQ,GAAa;gBACzB,IAAI;gBACJ,IAAI,EAAE,KAAK;gBACX,KAAK;gBACL,SAAS,EAAE,KAAK,CAAC,MAAM;gBACvB,KAAK,EAAE,aAAa,EAAE;gBACtB,cAAc,EAAE,IAAI;aACrB,CAAC;YACF,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACzB,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;AACxC,CAAC;AAGD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// 빈 줄(공백만 있는 줄 포함) 하나 이상을 문단 구분자로 본다.
|
|
2
|
+
const SEPARATOR = /\n[ \t\r]*\n/g;
|
|
3
|
+
function trimSpan(raw, start, end) {
|
|
4
|
+
let s = start;
|
|
5
|
+
let e = end;
|
|
6
|
+
while (s < e && /\s/.test(raw[s]))
|
|
7
|
+
s++;
|
|
8
|
+
while (e > s && /\s/.test(raw[e - 1]))
|
|
9
|
+
e--;
|
|
10
|
+
return { start: s, end: e };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 원문을 문단 Span 목록으로 나눈다. 빈 문단은 제외한다.
|
|
14
|
+
* 인덱스는 정규화하지 않은 원문 기준이므로 CRLF 입력에서도 span이 정확하다.
|
|
15
|
+
*/
|
|
16
|
+
export function splitParagraphSpans(raw) {
|
|
17
|
+
const spans = [];
|
|
18
|
+
let last = 0;
|
|
19
|
+
let m;
|
|
20
|
+
SEPARATOR.lastIndex = 0;
|
|
21
|
+
while ((m = SEPARATOR.exec(raw)) !== null) {
|
|
22
|
+
spans.push(trimSpan(raw, last, m.index));
|
|
23
|
+
last = m.index + m[0].length;
|
|
24
|
+
}
|
|
25
|
+
spans.push(trimSpan(raw, last, raw.length));
|
|
26
|
+
return spans.filter((s) => s.end > s.start);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=paragraphs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paragraphs.js","sourceRoot":"","sources":["../../src/text/paragraphs.ts"],"names":[],"mappings":"AAEA,sCAAsC;AACtC,MAAM,SAAS,GAAG,eAAe,CAAC;AAElC,SAAS,QAAQ,CAAC,GAAW,EAAE,KAAa,EAAE,GAAW;IACvD,IAAI,CAAC,GAAG,KAAK,CAAC;IACd,IAAI,CAAC,GAAG,GAAG,CAAC;IACZ,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAW,CAAC;QAAE,CAAC,EAAE,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAW,CAAC;QAAE,CAAC,EAAE,CAAC;IACrD,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;AAC9B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,CAAyB,CAAC;IAC9B,SAAS,CAAC,SAAS,GAAG,CAAC,CAAC;IACxB,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACzC,IAAI,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC/B,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 한국어 문장 분리기 (정규식·상태머신 휴리스틱, v0.1).
|
|
3
|
+
*
|
|
4
|
+
* 이 모듈은 의도적으로 격리되어 있다 — 나중에 형태소 분석기 기반으로 교체할 수 있게(ADR-04).
|
|
5
|
+
* 상위 계층은 splitSentenceSpans의 입출력 계약(문자열 → Span[])에만 의존한다.
|
|
6
|
+
*
|
|
7
|
+
* 경계 규칙:
|
|
8
|
+
* - 종결부호: . ! ? … 。 ! ? 뒤에서 문장을 나눈다.
|
|
9
|
+
* - 숫자 마침표: 숫자 바로 뒤의 '.'은 종결부호가 아니다(소수점·날짜 "2026. 8. 9."·버전·순번).
|
|
10
|
+
* - 따옴표/괄호 안의 종결부호로는 나누지 않는다. 닫는 기호 앞이 종결부호이고
|
|
11
|
+
* 그 뒤가 공백/끝이면 그 지점에서 나눈다(예: "신청하세요." | 다음). 뒤에 조사가 붙으면
|
|
12
|
+
* 나누지 않는다(예: "안녕하세요."라고 말했다. → 한 문장).
|
|
13
|
+
* - 개행(\n)은 항상 경계로 본다(문장부호 없는 제목·목록 대응).
|
|
14
|
+
*/
|
|
15
|
+
const TERMINATORS = new Set([".", "!", "?", "…", "。", "!", "?"]);
|
|
16
|
+
const OPEN_TO_CLOSE = new Map([
|
|
17
|
+
["“", "”"], // “ ”
|
|
18
|
+
["‘", "’"], // ‘ ’
|
|
19
|
+
["「", "」"], // 「 」
|
|
20
|
+
["『", "』"], // 『 』
|
|
21
|
+
["《", "》"], // 《 》
|
|
22
|
+
["〈", "〉"], // 〈 〉
|
|
23
|
+
["(", ")"],
|
|
24
|
+
["(", ")"], // ( )
|
|
25
|
+
["[", "]"],
|
|
26
|
+
["【", "】"], // 【 】
|
|
27
|
+
["{", "}"],
|
|
28
|
+
["〔", "〕"], // 〔 〕
|
|
29
|
+
]);
|
|
30
|
+
const CLOSE_CHARS = new Set(OPEN_TO_CLOSE.values());
|
|
31
|
+
const STRAIGHT_QUOTES = new Set(['"', "'"]);
|
|
32
|
+
function isDigit(ch) {
|
|
33
|
+
return ch !== undefined && ch >= "0" && ch <= "9";
|
|
34
|
+
}
|
|
35
|
+
function isSpace(ch) {
|
|
36
|
+
return ch !== undefined && /\s/.test(ch);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 문단 텍스트를 문장 Span 목록으로 나눈다.
|
|
40
|
+
* @param text 문단(또는 전체) 문자열
|
|
41
|
+
* @param offset 원문 기준 시작 오프셋(반환 span에 더해진다)
|
|
42
|
+
*/
|
|
43
|
+
export function splitSentenceSpans(text, offset = 0) {
|
|
44
|
+
const spans = [];
|
|
45
|
+
const stack = []; // 방향성 따옴표·괄호 닫힘 대기
|
|
46
|
+
const straightOpen = new Map([
|
|
47
|
+
['"', false],
|
|
48
|
+
["'", false],
|
|
49
|
+
]);
|
|
50
|
+
let start = 0;
|
|
51
|
+
const push = (end) => {
|
|
52
|
+
let s = start;
|
|
53
|
+
let e = end;
|
|
54
|
+
while (s < e && isSpace(text[s]))
|
|
55
|
+
s++;
|
|
56
|
+
while (e > s && isSpace(text[e - 1]))
|
|
57
|
+
e--;
|
|
58
|
+
if (e > s)
|
|
59
|
+
spans.push({ start: offset + s, end: offset + e });
|
|
60
|
+
start = end;
|
|
61
|
+
};
|
|
62
|
+
const suppressed = () => stack.length > 0 || straightOpen.get('"') === true || straightOpen.get("'") === true;
|
|
63
|
+
// 닫는 따옴표/괄호(index i) 직전이 종결부호이고 뒤가 공백/끝이면 경계로 삼는다.
|
|
64
|
+
const maybeCloseBoundary = (i) => {
|
|
65
|
+
const prev = text[i - 1];
|
|
66
|
+
if (prev === undefined || !TERMINATORS.has(prev))
|
|
67
|
+
return;
|
|
68
|
+
if (prev === "." && isDigit(text[i - 2]))
|
|
69
|
+
return; // 숫자 마침표
|
|
70
|
+
const next = text[i + 1];
|
|
71
|
+
if (next === undefined || isSpace(next))
|
|
72
|
+
push(i + 1);
|
|
73
|
+
};
|
|
74
|
+
for (let i = 0; i < text.length; i++) {
|
|
75
|
+
const ch = text[i];
|
|
76
|
+
if (ch === "\n") {
|
|
77
|
+
push(i + 1);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (OPEN_TO_CLOSE.has(ch)) {
|
|
81
|
+
stack.push(OPEN_TO_CLOSE.get(ch));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (CLOSE_CHARS.has(ch)) {
|
|
85
|
+
if (stack.length > 0 && stack[stack.length - 1] === ch)
|
|
86
|
+
stack.pop();
|
|
87
|
+
maybeCloseBoundary(i);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (STRAIGHT_QUOTES.has(ch)) {
|
|
91
|
+
const wasOpen = straightOpen.get(ch) === true;
|
|
92
|
+
straightOpen.set(ch, !wasOpen);
|
|
93
|
+
if (wasOpen)
|
|
94
|
+
maybeCloseBoundary(i); // 닫히는 경우
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (TERMINATORS.has(ch)) {
|
|
98
|
+
if (ch === "." && isDigit(text[i - 1]))
|
|
99
|
+
continue; // 숫자 마침표
|
|
100
|
+
if (suppressed())
|
|
101
|
+
continue; // 따옴표/괄호 안
|
|
102
|
+
// 연속 종결부호 흡수: "...", "?!" 등
|
|
103
|
+
let j = i;
|
|
104
|
+
while (j + 1 < text.length &&
|
|
105
|
+
TERMINATORS.has(text[j + 1]) &&
|
|
106
|
+
!(text[j + 1] === "." && isDigit(text[j]))) {
|
|
107
|
+
j++;
|
|
108
|
+
}
|
|
109
|
+
push(j + 1);
|
|
110
|
+
i = j;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
push(text.length); // 종결부호 없는 꼬리 문장
|
|
115
|
+
return spans;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=sentences.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentences.js","sourceRoot":"","sources":["../../src/text/sentences.ts"],"names":[],"mappings":"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"}
|