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.
Files changed (117) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +41 -0
  3. package/assets/dictionary.json +118 -0
  4. package/assets/resources.json +1149 -0
  5. package/dist/data/dictionary.js +61 -0
  6. package/dist/data/dictionary.js.map +1 -0
  7. package/dist/data/guidelines.js +63 -0
  8. package/dist/data/guidelines.js.map +1 -0
  9. package/dist/data/resources.js +76 -0
  10. package/dist/data/resources.js.map +1 -0
  11. package/dist/index.js +26 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/messages.js +9 -0
  14. package/dist/messages.js.map +1 -0
  15. package/dist/prompts/review.js +30 -0
  16. package/dist/prompts/review.js.map +1 -0
  17. package/dist/prompts/simplify.js +40 -0
  18. package/dist/prompts/simplify.js.map +1 -0
  19. package/dist/rules/acc/acc-01.js +28 -0
  20. package/dist/rules/acc/acc-01.js.map +1 -0
  21. package/dist/rules/acc/acc-02.js +28 -0
  22. package/dist/rules/acc/acc-02.js.map +1 -0
  23. package/dist/rules/acc/acc-03.js +28 -0
  24. package/dist/rules/acc/acc-03.js.map +1 -0
  25. package/dist/rules/acc/acc-04.js +28 -0
  26. package/dist/rules/acc/acc-04.js.map +1 -0
  27. package/dist/rules/acc/index.js +8 -0
  28. package/dist/rules/acc/index.js.map +1 -0
  29. package/dist/rules/config.js +20 -0
  30. package/dist/rules/config.js.map +1 -0
  31. package/dist/rules/engine.js +49 -0
  32. package/dist/rules/engine.js.map +1 -0
  33. package/dist/rules/index.js +11 -0
  34. package/dist/rules/index.js.map +1 -0
  35. package/dist/rules/num/index.js +8 -0
  36. package/dist/rules/num/index.js.map +1 -0
  37. package/dist/rules/num/num-01.js +27 -0
  38. package/dist/rules/num/num-01.js.map +1 -0
  39. package/dist/rules/num/num-02.js +21 -0
  40. package/dist/rules/num/num-02.js.map +1 -0
  41. package/dist/rules/num/num-03.js +24 -0
  42. package/dist/rules/num/num-03.js.map +1 -0
  43. package/dist/rules/num/num-04.js +21 -0
  44. package/dist/rules/num/num-04.js.map +1 -0
  45. package/dist/rules/registry.js +30 -0
  46. package/dist/rules/registry.js.map +1 -0
  47. package/dist/rules/report.js +69 -0
  48. package/dist/rules/report.js.map +1 -0
  49. package/dist/rules/sen/index.js +10 -0
  50. package/dist/rules/sen/index.js.map +1 -0
  51. package/dist/rules/sen/sen-01.js +33 -0
  52. package/dist/rules/sen/sen-01.js.map +1 -0
  53. package/dist/rules/sen/sen-02.js +40 -0
  54. package/dist/rules/sen/sen-02.js.map +1 -0
  55. package/dist/rules/sen/sen-03.js +33 -0
  56. package/dist/rules/sen/sen-03.js.map +1 -0
  57. package/dist/rules/sen/sen-04.js +31 -0
  58. package/dist/rules/sen/sen-04.js.map +1 -0
  59. package/dist/rules/sen/sen-05.js +27 -0
  60. package/dist/rules/sen/sen-05.js.map +1 -0
  61. package/dist/rules/sen/sen-07.js +58 -0
  62. package/dist/rules/sen/sen-07.js.map +1 -0
  63. package/dist/rules/str/index.js +9 -0
  64. package/dist/rules/str/index.js.map +1 -0
  65. package/dist/rules/str/str-02.js +22 -0
  66. package/dist/rules/str/str-02.js.map +1 -0
  67. package/dist/rules/str/str-03.js +24 -0
  68. package/dist/rules/str/str-03.js.map +1 -0
  69. package/dist/rules/typ/index.js +7 -0
  70. package/dist/rules/typ/index.js.map +1 -0
  71. package/dist/rules/typ/typ-01.js +21 -0
  72. package/dist/rules/typ/typ-01.js.map +1 -0
  73. package/dist/rules/typ/typ-02.js +23 -0
  74. package/dist/rules/typ/typ-02.js.map +1 -0
  75. package/dist/rules/typ/typ-03.js +28 -0
  76. package/dist/rules/typ/typ-03.js.map +1 -0
  77. package/dist/rules/types.js +2 -0
  78. package/dist/rules/types.js.map +1 -0
  79. package/dist/rules/voc/index.js +10 -0
  80. package/dist/rules/voc/index.js.map +1 -0
  81. package/dist/rules/voc/voc-01.js +27 -0
  82. package/dist/rules/voc/voc-01.js.map +1 -0
  83. package/dist/rules/voc/voc-02.js +38 -0
  84. package/dist/rules/voc/voc-02.js.map +1 -0
  85. package/dist/rules/voc/voc-03.js +30 -0
  86. package/dist/rules/voc/voc-03.js.map +1 -0
  87. package/dist/rules/voc/voc-04.js +26 -0
  88. package/dist/rules/voc/voc-04.js.map +1 -0
  89. package/dist/rules/voc/voc-05.js +34 -0
  90. package/dist/rules/voc/voc-05.js.map +1 -0
  91. package/dist/rules/voc/voc-06.js +36 -0
  92. package/dist/rules/voc/voc-06.js.map +1 -0
  93. package/dist/server.js +49 -0
  94. package/dist/server.js.map +1 -0
  95. package/dist/text/extractors.js +40 -0
  96. package/dist/text/extractors.js.map +1 -0
  97. package/dist/text/index.js +38 -0
  98. package/dist/text/index.js.map +1 -0
  99. package/dist/text/paragraphs.js +28 -0
  100. package/dist/text/paragraphs.js.map +1 -0
  101. package/dist/text/sentences.js +117 -0
  102. package/dist/text/sentences.js.map +1 -0
  103. package/dist/text/spans.js +9 -0
  104. package/dist/text/spans.js.map +1 -0
  105. package/dist/text/types.js +6 -0
  106. package/dist/text/types.js.map +1 -0
  107. package/dist/text/words.js +20 -0
  108. package/dist/text/words.js.map +1 -0
  109. package/dist/tools/analyze.js +73 -0
  110. package/dist/tools/analyze.js.map +1 -0
  111. package/dist/tools/guidelines.js +55 -0
  112. package/dist/tools/guidelines.js.map +1 -0
  113. package/dist/tools/lookup.js +79 -0
  114. package/dist/tools/lookup.js.map +1 -0
  115. package/dist/tools/validate.js +79 -0
  116. package/dist/tools/validate.js.map +1 -0
  117. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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":"AAEA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAEjE,MAAM,aAAa,GAAG,IAAI,GAAG,CAAiB;IAC5C,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM;IAClB,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM;IAClB,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM;IAClB,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM;IAClB,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM;IAClB,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM;IAClB,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM;IAClB,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM;IAClB,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM;CACnB,CAAC,CAAC;AACH,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC;AACpD,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAE5C,SAAS,OAAO,CAAC,EAAsB;IACrC,OAAO,EAAE,KAAK,SAAS,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,IAAI,GAAG,CAAC;AACpD,CAAC;AAED,SAAS,OAAO,CAAC,EAAsB;IACrC,OAAO,EAAE,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC3C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,MAAM,GAAG,CAAC;IACzD,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,MAAM,KAAK,GAAa,EAAE,CAAC,CAAC,mBAAmB;IAC/C,MAAM,YAAY,GAAG,IAAI,GAAG,CAAkB;QAC5C,CAAC,GAAG,EAAE,KAAK,CAAC;QACZ,CAAC,GAAG,EAAE,KAAK,CAAC;KACb,CAAC,CAAC;IACH,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,MAAM,IAAI,GAAG,CAAC,GAAW,EAAQ,EAAE;QACjC,IAAI,CAAC,GAAG,KAAK,CAAC;QACd,IAAI,CAAC,GAAG,GAAG,CAAC;QACZ,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAAE,CAAC,EAAE,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9D,KAAK,GAAG,GAAG,CAAC;IACd,CAAC,CAAC;IAEF,MAAM,UAAU,GAAG,GAAY,EAAE,CAC/B,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC;IAEvF,mDAAmD;IACnD,MAAM,kBAAkB,GAAG,CAAC,CAAS,EAAQ,EAAE;QAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACzB,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO;QACzD,IAAI,IAAI,KAAK,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,SAAS;QAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACzB,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;YAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,CAAC,CAAC;IAEF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAW,CAAC;QAE7B,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACZ,SAAS;QACX,CAAC;QACD,IAAI,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAW,CAAC,CAAC;YAC5C,SAAS;QACX,CAAC;QACD,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACxB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE;gBAAE,KAAK,CAAC,GAAG,EAAE,CAAC;YACpE,kBAAkB,CAAC,CAAC,CAAC,CAAC;YACtB,SAAS;QACX,CAAC;QACD,IAAI,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAC5B,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC;YAC9C,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;YAC/B,IAAI,OAAO;gBAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;YAC7C,SAAS;QACX,CAAC;QACD,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACxB,IAAI,EAAE,KAAK,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAE,SAAS,CAAC,SAAS;YAC3D,IAAI,UAAU,EAAE;gBAAE,SAAS,CAAC,WAAW;YACvC,4BAA4B;YAC5B,IAAI,CAAC,GAAG,CAAC,CAAC;YACV,OACE,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM;gBACnB,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAW,CAAC;gBACtC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAC1C,CAAC;gBACD,CAAC,EAAE,CAAC;YACN,CAAC;YACD,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACZ,CAAC,GAAG,CAAC,CAAC;YACN,SAAS;QACX,CAAC;IACH,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB;IACnC,OAAO,KAAK,CAAC;AACf,CAAC"}