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,27 @@
1
+ // NUM-01 한글 수사: 한자어 수사(2자 이상) 또는 고유어 수사 어절을 아라비아 숫자로 바꾸도록 warning.
2
+ // 근거: 국내 지침(한국어 특화) · Inclusion Europe(숫자는 아라비아 숫자로).
3
+ // 단일 글자('이','삼')는 오탐이 커 제외. v0.1은 어절 정확 일치(조사 결합·복합어는 backlog).
4
+ const SINO = /^[영일이삼사오육칠팔구십백천만]{2,}$/;
5
+ const NATIVE = new Set(["열", "스물", "서른", "마흔", "쉰", "예순", "일흔", "여든", "아흔"]);
6
+ export const num01 = {
7
+ id: "NUM-01",
8
+ group: "NUM",
9
+ defaultSeverity: "warning",
10
+ check(ctx) {
11
+ const findings = [];
12
+ for (const s of ctx.sentences) {
13
+ for (const w of s.words) {
14
+ if (SINO.test(w.text) || NATIVE.has(w.text)) {
15
+ findings.push({
16
+ ruleId: "NUM-01",
17
+ message: `'${w.text}'은(는) 한글로 쓴 숫자입니다. 아라비아 숫자로 쓰면 더 쉽습니다.`,
18
+ span: w.span,
19
+ suggestion: "예: 삼십 → 30",
20
+ });
21
+ }
22
+ }
23
+ }
24
+ return findings;
25
+ },
26
+ };
27
+ //# sourceMappingURL=num-01.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"num-01.js","sourceRoot":"","sources":["../../../src/rules/num/num-01.ts"],"names":[],"mappings":"AAEA,mEAAmE;AACnE,sDAAsD;AACtD,gEAAgE;AAChE,MAAM,IAAI,GAAG,wBAAwB,CAAC;AACtC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAE7E,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,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC5C,QAAQ,CAAC,IAAI,CAAC;wBACZ,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,wCAAwC;wBAC3D,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,UAAU,EAAE,YAAY;qBACzB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,21 @@
1
+ // NUM-02 복잡한 비율: 소수점 백분율(예 47.3%)마다 info. '10명 중 몇 명'식으로 풀어쓰기 제안.
2
+ // 근거: Inclusion Europe #13(백분율 지양). 정수 백분율('50%')은 v0.1 대상 아님.
3
+ const DECIMAL_PERCENT = /\d+\.\d+\s*%/g;
4
+ export const num02 = {
5
+ id: "NUM-02",
6
+ group: "NUM",
7
+ defaultSeverity: "info",
8
+ check(ctx) {
9
+ const findings = [];
10
+ for (const m of ctx.raw.matchAll(DECIMAL_PERCENT)) {
11
+ const start = m.index ?? 0;
12
+ findings.push({
13
+ ruleId: "NUM-02",
14
+ message: `'${m[0]}'처럼 복잡한 비율은 이해하기 어렵습니다. '10명 중 몇 명'처럼 쉽게 쓰세요.`,
15
+ span: { start, end: start + m[0].length },
16
+ });
17
+ }
18
+ return findings;
19
+ },
20
+ };
21
+ //# sourceMappingURL=num-02.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"num-02.js","sourceRoot":"","sources":["../../../src/rules/num/num-02.ts"],"names":[],"mappings":"AAEA,kEAAkE;AAClE,+DAA+D;AAC/D,MAAM,eAAe,GAAG,eAAe,CAAC;AAExC,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,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;YAClD,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;YAC3B,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,+CAA+C;gBAChE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE;aAC1C,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,24 @@
1
+ // NUM-03 상대적 날짜: '익일·금주·전월'처럼 기준이 불분명한 상대 날짜 어절을 구체적 날짜로 바꾸도록 warning.
2
+ // 근거: Inclusion Europe(날짜는 분명하게) · 국내 지침(완전한 날짜). 조사 결합('금주에')은 startsWith로 대응.
3
+ const RELATIVE = ["익일", "명일", "작일", "금일", "금주", "차주", "전주", "익월", "차월", "전월", "금년", "익년"];
4
+ export const num03 = {
5
+ id: "NUM-03",
6
+ group: "NUM",
7
+ defaultSeverity: "warning",
8
+ check(ctx) {
9
+ const findings = [];
10
+ for (const s of ctx.sentences) {
11
+ for (const w of s.words) {
12
+ if (RELATIVE.some((r) => w.text.startsWith(r))) {
13
+ findings.push({
14
+ ruleId: "NUM-03",
15
+ message: `'${w.text}'은(는) 기준이 불분명한 날짜입니다. 정확한 날짜(예: 4월 3일)로 쓰세요.`,
16
+ span: w.span,
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return findings;
22
+ },
23
+ };
24
+ //# sourceMappingURL=num-03.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"num-03.js","sourceRoot":"","sources":["../../../src/rules/num/num-03.ts"],"names":[],"mappings":"AAEA,uEAAuE;AACvE,gFAAgF;AAChF,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAE1F,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,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC/C,QAAQ,CAAC,IAAI,CAAC;wBACZ,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,8CAA8C;wBACjE,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,21 @@
1
+ // NUM-04 읽기 어려운 큰 수: 천단위 콤마 숫자(예 1,000,000)마다 info. '100만 원'식 단위 끊어쓰기 제안.
2
+ // 근거: Inclusion Europe #13(큰 수 지양) · 국내 지침(만/억 단위). 콤마 없는 '1000'은 대상 아님.
3
+ const BIG_NUMBER = /\d{1,3}(,\d{3})+/g;
4
+ export const num04 = {
5
+ id: "NUM-04",
6
+ group: "NUM",
7
+ defaultSeverity: "info",
8
+ check(ctx) {
9
+ const findings = [];
10
+ for (const m of ctx.raw.matchAll(BIG_NUMBER)) {
11
+ const start = m.index ?? 0;
12
+ findings.push({
13
+ ruleId: "NUM-04",
14
+ message: `'${m[0]}'은(는) 읽기 어려운 큰 수입니다. '100만 원'처럼 단위로 끊어 쓰세요.`,
15
+ span: { start, end: start + m[0].length },
16
+ });
17
+ }
18
+ return findings;
19
+ },
20
+ };
21
+ //# sourceMappingURL=num-04.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"num-04.js","sourceRoot":"","sources":["../../../src/rules/num/num-04.ts"],"names":[],"mappings":"AAEA,0EAA0E;AAC1E,yEAAyE;AACzE,MAAM,UAAU,GAAG,mBAAmB,CAAC;AAEvC,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,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAC7C,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;YAC3B,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,6CAA6C;gBAC9D,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE;aAC1C,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { accRules } from "./acc/index.js";
2
+ import { numRules } from "./num/index.js";
3
+ import { senRules } from "./sen/index.js";
4
+ import { strRules } from "./str/index.js";
5
+ import { typRules } from "./typ/index.js";
6
+ import { vocRules } from "./voc/index.js";
7
+ /**
8
+ * 전체 규칙 목록. 각 규칙군을 **정적 import**로 조립한다(동적 로딩 금지 — 번들·기동 단순성).
9
+ * ACC군은 requiresOriginal:true라 원문이 있을 때만 getActiveRules가 활성화한다.
10
+ */
11
+ export const rules = [
12
+ ...senRules,
13
+ ...vocRules,
14
+ ...numRules,
15
+ ...strRules,
16
+ ...typRules,
17
+ ...accRules,
18
+ ];
19
+ /** excludeRules와 원문 유무(ACC 활성)를 반영해 실행할 규칙만 고른다. */
20
+ export function getActiveRules(source, opts = {}) {
21
+ const exclude = new Set(opts.excludeRules ?? []);
22
+ return source.filter((rule) => {
23
+ if (exclude.has(rule.id))
24
+ return false;
25
+ if (rule.requiresOriginal === true && opts.hasOriginal !== true)
26
+ return false;
27
+ return true;
28
+ });
29
+ }
30
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/rules/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG1C;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAoB;IACpC,GAAG,QAAQ;IACX,GAAG,QAAQ;IACX,GAAG,QAAQ;IACX,GAAG,QAAQ;IACX,GAAG,QAAQ;IACX,GAAG,QAAQ;CACZ,CAAC;AAQF,oDAAoD;AACpD,MAAM,UAAU,cAAc,CAAC,MAAuB,EAAE,OAA0B,EAAE;IAClF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;IACjD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAC5B,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAAE,OAAO,KAAK,CAAC;QACvC,IAAI,IAAI,CAAC,gBAAgB,KAAK,IAAI,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC9E,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_NOTICES } from "../messages.js";
3
+ /** 초장문 입력 보호: 위반은 이 개수에서 절단하고 summary.truncated로 표시(03 §6). */
4
+ export const MAX_VIOLATIONS = 200;
5
+ const spanSchema = z.object({
6
+ start: z.number().int().nonnegative(),
7
+ end: z.number().int().nonnegative(),
8
+ });
9
+ const violationSchema = z.object({
10
+ ruleId: z.string(),
11
+ severity: z.enum(["error", "warning", "info"]),
12
+ message: z.string(),
13
+ span: spanSchema,
14
+ excerpt: z.string().optional(),
15
+ suggestion: z.string().optional(),
16
+ });
17
+ /** validate_easy_read의 structuredContent 스키마(02 §3.1). 도구 outputSchema로도 쓴다. */
18
+ export const validationReportSchema = z.object({
19
+ verdict: z.enum(["pass", "needs-review", "fail"]),
20
+ summary: z.object({
21
+ errors: z.number().int().nonnegative(),
22
+ warnings: z.number().int().nonnegative(),
23
+ infos: z.number().int().nonnegative(),
24
+ byGroup: z.record(z.string(), z.number().int().nonnegative()),
25
+ truncated: z.boolean().optional(),
26
+ }),
27
+ violations: z.array(violationSchema),
28
+ notices: z.array(z.string()),
29
+ });
30
+ /** 리포트·메시지에 넣을 짧은 발췌. 공백을 정리하고 maxLen에서 자른다. */
31
+ export function makeExcerpt(raw, span, maxLen = 60) {
32
+ const text = raw.slice(span.start, span.end).replace(/\s+/g, " ").trim();
33
+ if (text.length <= maxLen)
34
+ return text;
35
+ return `${text.slice(0, maxLen - 1)}…`;
36
+ }
37
+ /**
38
+ * 위반 목록 → 리포트. verdict: error≥1 → fail, warning≥1 → needs-review, 그 외 pass(03 §3).
39
+ * summary 수치·byGroup은 절단 전 전체를 반영하고, violations 배열만 절단한다.
40
+ */
41
+ export function assembleReport(violations, opts = {}) {
42
+ const truncated = violations.length > MAX_VIOLATIONS;
43
+ const kept = truncated ? violations.slice(0, MAX_VIOLATIONS) : violations;
44
+ let errors = 0;
45
+ let warnings = 0;
46
+ let infos = 0;
47
+ const byGroup = {};
48
+ for (const v of violations) {
49
+ if (v.severity === "error")
50
+ errors++;
51
+ else if (v.severity === "warning")
52
+ warnings++;
53
+ else
54
+ infos++;
55
+ const group = v.ruleId.split("-")[0] ?? v.ruleId;
56
+ byGroup[group] = (byGroup[group] ?? 0) + 1;
57
+ }
58
+ const verdict = errors > 0 ? "fail" : warnings > 0 ? "needs-review" : "pass";
59
+ const summary = { errors, warnings, infos, byGroup };
60
+ if (truncated)
61
+ summary.truncated = true;
62
+ return {
63
+ verdict,
64
+ summary,
65
+ violations: kept.map((v) => ({ ...v })),
66
+ notices: [...(opts.notices ?? DEFAULT_NOTICES)],
67
+ };
68
+ }
69
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/rules/report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { sen01 } from "./sen-01.js";
2
+ import { sen02 } from "./sen-02.js";
3
+ import { sen03 } from "./sen-03.js";
4
+ import { sen04 } from "./sen-04.js";
5
+ import { sen05 } from "./sen-05.js";
6
+ import { sen07 } from "./sen-07.js";
7
+ export { sen01, sen02, sen03, sen04, sen05, sen07 };
8
+ /** SEN 규칙군 전체. registry가 정적으로 조립한다. */
9
+ export const senRules = [sen01, sen02, sen03, sen04, sen05, sen07];
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/sen/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,uCAAuC;AACvC,MAAM,CAAC,MAAM,QAAQ,GAAoB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC"}
@@ -0,0 +1,33 @@
1
+ // SEN-01 문장이 너무 길다: 어절 수 임계값 초과(기본 >10 warning, >15 error, 설정 가능).
2
+ // 근거: Inclusion Europe #14(짧은 문장) · ISO 24495-1(이해 쉬움) · 국내 지침 문장쓰기.
3
+ export const sen01 = {
4
+ id: "SEN-01",
5
+ group: "SEN",
6
+ defaultSeverity: "warning",
7
+ check(ctx) {
8
+ const findings = [];
9
+ for (const s of ctx.sentences) {
10
+ const n = s.wordCount;
11
+ if (n > ctx.config.maxWordsError) {
12
+ findings.push({
13
+ ruleId: "SEN-01",
14
+ severity: "error",
15
+ message: `문장이 너무 깁니다(어절 ${n}개). 두 문장 이상으로 나누세요.`,
16
+ span: s.span,
17
+ suggestion: "한 문장에 한 가지 정보만 담으세요.",
18
+ });
19
+ }
20
+ else if (n > ctx.config.maxWordsWarning) {
21
+ findings.push({
22
+ ruleId: "SEN-01",
23
+ severity: "warning",
24
+ message: `문장이 조금 깁니다(어절 ${n}개). 나누면 더 쉽습니다.`,
25
+ span: s.span,
26
+ suggestion: "한 문장에 한 가지 정보만 담으세요.",
27
+ });
28
+ }
29
+ }
30
+ return findings;
31
+ },
32
+ };
33
+ //# sourceMappingURL=sen-01.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sen-01.js","sourceRoot":"","sources":["../../../src/rules/sen/sen-01.ts"],"names":[],"mappings":"AAEA,mEAAmE;AACnE,qEAAqE;AACrE,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,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC;YACtB,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;gBACjC,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,QAAQ;oBAChB,QAAQ,EAAE,OAAO;oBACjB,OAAO,EAAE,iBAAiB,CAAC,qBAAqB;oBAChD,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,UAAU,EAAE,sBAAsB;iBACnC,CAAC,CAAC;YACL,CAAC;iBAAM,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;gBAC1C,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,QAAQ;oBAChB,QAAQ,EAAE,SAAS;oBACnB,OAAO,EAAE,iBAAiB,CAAC,iBAAiB;oBAC5C,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,UAAU,EAAE,sBAAsB;iBACnC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,40 @@
1
+ // SEN-02 복문/이어진 문장: 연결어미로 절 2개 이상 결합. 보조(오탐 있음 → warning).
2
+ // 근거: Inclusion Europe #14 · 국내 지침(한 문장 한 정보).
3
+ // 어절 기반 탐지: 접속부사(그리고/하지만 등)는 통째로 제외해 오탐을 줄인다.
4
+ const CONNECTIVE_ENDINGS = ["고", "며", "지만", "는데", "으며", "면서", "거나", "든지", "든가", "는데도"];
5
+ const EXCLUDE_WORDS = new Set([
6
+ "하지만",
7
+ "그렇지만",
8
+ "그리고",
9
+ "그러고",
10
+ "그러며",
11
+ "말고",
12
+ "말며",
13
+ "및",
14
+ ]);
15
+ export const sen02 = {
16
+ id: "SEN-02",
17
+ group: "SEN",
18
+ defaultSeverity: "warning",
19
+ check(ctx) {
20
+ const findings = [];
21
+ for (const s of ctx.sentences) {
22
+ if (s.words.length < 2)
23
+ continue;
24
+ const nonLast = s.words.slice(0, -1);
25
+ const hasConnective = nonLast.some((w) => w.text.length >= 2 &&
26
+ !EXCLUDE_WORDS.has(w.text) &&
27
+ CONNECTIVE_ENDINGS.some((end) => w.text.endsWith(end)));
28
+ if (hasConnective) {
29
+ findings.push({
30
+ ruleId: "SEN-02",
31
+ message: "여러 절이 이어진 복문입니다. 짧은 문장으로 나누세요.",
32
+ span: s.span,
33
+ suggestion: "'-고, -며, -지만, -는데'로 잇지 말고 문장을 끊으세요.",
34
+ });
35
+ }
36
+ }
37
+ return findings;
38
+ },
39
+ };
40
+ //# sourceMappingURL=sen-02.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sen-02.js","sourceRoot":"","sources":["../../../src/rules/sen/sen-02.ts"],"names":[],"mappings":"AAEA,2DAA2D;AAC3D,+CAA+C;AAC/C,8CAA8C;AAC9C,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AACvF,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,KAAK;IACL,MAAM;IACN,KAAK;IACL,KAAK;IACL,KAAK;IACL,IAAI;IACJ,IAAI;IACJ,GAAG;CACJ,CAAC,CAAC;AAEH,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,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YACjC,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAChC,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC;gBAClB,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC1B,kBAAkB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CACzD,CAAC;YACF,IAAI,aAAa,EAAE,CAAC;gBAClB,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,QAAQ;oBAChB,OAAO,EAAE,gCAAgC;oBACzC,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,UAAU,EAAE,qCAAqC;iBAClD,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,33 @@
1
+ // SEN-03 피동 표현: '-되다', '-어지다', '-당하다' 등. 보조(패턴 매칭 → warning).
2
+ // 근거: Inclusion Europe #17(능동태 사용).
3
+ // '담당하다'는 '당하다' 오탐이 되지 않게 (?<!담)으로 제외한다.
4
+ const PASSIVE_PATTERNS = [
5
+ /(되어|되었|되는|되고|되며|된다|됩니다|됐|돼서|돼요)/g,
6
+ /(아|어|여)(지다|집니다|졌|지는|지고|지며|져서|져요)/g,
7
+ /(?<!담)당(하|했|한|할|해|합니다)/g,
8
+ ];
9
+ export const sen03 = {
10
+ id: "SEN-03",
11
+ group: "SEN",
12
+ defaultSeverity: "warning",
13
+ check(ctx) {
14
+ const findings = [];
15
+ for (const s of ctx.sentences) {
16
+ for (const pattern of PASSIVE_PATTERNS) {
17
+ pattern.lastIndex = 0;
18
+ let m;
19
+ while ((m = pattern.exec(s.text)) !== null) {
20
+ const start = s.span.start + m.index;
21
+ findings.push({
22
+ ruleId: "SEN-03",
23
+ message: "피동 표현입니다. 누가 하는지 드러내는 능동 표현이 더 쉽습니다.",
24
+ span: { start, end: start + m[0].length },
25
+ suggestion: "능동태로 바꿔 보세요. 예: '지급됩니다' → '구청이 줍니다'.",
26
+ });
27
+ }
28
+ }
29
+ }
30
+ return findings;
31
+ },
32
+ };
33
+ //# sourceMappingURL=sen-03.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sen-03.js","sourceRoot":"","sources":["../../../src/rules/sen/sen-03.ts"],"names":[],"mappings":"AAEA,8DAA8D;AAC9D,oCAAoC;AACpC,yCAAyC;AACzC,MAAM,gBAAgB,GAAsB;IAC1C,kCAAkC;IAClC,mCAAmC;IACnC,yBAAyB;CAC1B,CAAC;AAEF,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,OAAO,IAAI,gBAAgB,EAAE,CAAC;gBACvC,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC;gBACtB,IAAI,CAAyB,CAAC;gBAC9B,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;oBAC3C,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;oBACrC,QAAQ,CAAC,IAAI,CAAC;wBACZ,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE,sCAAsC;wBAC/C,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE;wBACzC,UAAU,EAAE,sCAAsC;qBACnD,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,31 @@
1
+ // SEN-04 이중 부정: 한 문장에 부정 표현 2회 이상. 자동(error).
2
+ // 근거: Inclusion Europe #16(부정문보다 긍정문).
3
+ // 절 단위 부정 마커('-지 않/못/마', '없', '아니')만 센다 — '안'/'못' 단독은 '안전/안내'
4
+ // 같은 오탐이 커서 제외한다(정밀도 우선).
5
+ const NEGATION_PATTERNS = [/지\s*않/g, /지\s*못/g, /지\s*마/g, /없/g, /아니/g];
6
+ export const sen04 = {
7
+ id: "SEN-04",
8
+ group: "SEN",
9
+ defaultSeverity: "error",
10
+ check(ctx) {
11
+ const findings = [];
12
+ for (const s of ctx.sentences) {
13
+ let count = 0;
14
+ for (const pattern of NEGATION_PATTERNS) {
15
+ const matches = s.text.match(pattern);
16
+ if (matches !== null)
17
+ count += matches.length;
18
+ }
19
+ if (count >= 2) {
20
+ findings.push({
21
+ ruleId: "SEN-04",
22
+ message: "부정 표현이 여러 번 있습니다(이중 부정). 긍정문으로 바꾸세요.",
23
+ span: s.span,
24
+ suggestion: "예: '신청하지 않으면 받을 수 없습니다' → '신청하면 받을 수 있습니다'.",
25
+ });
26
+ }
27
+ }
28
+ return findings;
29
+ },
30
+ };
31
+ //# sourceMappingURL=sen-04.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sen-04.js","sourceRoot":"","sources":["../../../src/rules/sen/sen-04.ts"],"names":[],"mappings":"AAEA,8CAA8C;AAC9C,uCAAuC;AACvC,8DAA8D;AAC9D,0BAA0B;AAC1B,MAAM,iBAAiB,GAAsB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AAEzF,MAAM,CAAC,MAAM,KAAK,GAAS;IACzB,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,OAAO;IACxB,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAkB,EAAE,CAAC;QACnC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YAC9B,IAAI,KAAK,GAAG,CAAC,CAAC;YACd,KAAK,MAAM,OAAO,IAAI,iBAAiB,EAAE,CAAC;gBACxC,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBACtC,IAAI,OAAO,KAAK,IAAI;oBAAE,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;YAChD,CAAC;YACD,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;gBACf,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,QAAQ;oBAChB,OAAO,EAAE,sCAAsC;oBAC/C,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,UAAU,EAAE,6CAA6C;iBAC1D,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,27 @@
1
+ // SEN-05 한 문장에 여러 정보: 병렬 나열 3개 이상(구분자 2개 이상). 보조(info).
2
+ // 근거: Inclusion Europe(한 문장에 한 가지 생각) · 국내 지침 내용조직화.
3
+ // 천 단위 콤마('1,000,000')는 나열이 아니므로 숫자 사이 콤마를 먼저 제거한다.
4
+ const ENUM_MARKS = /[,,、·]/g;
5
+ export const sen05 = {
6
+ id: "SEN-05",
7
+ group: "SEN",
8
+ defaultSeverity: "info",
9
+ check(ctx) {
10
+ const findings = [];
11
+ for (const s of ctx.sentences) {
12
+ const withoutNumberCommas = s.text.replace(/(?<=\d),(?=\d)/g, "");
13
+ const marks = withoutNumberCommas.match(ENUM_MARKS)?.length ?? 0;
14
+ const ands = withoutNumberCommas.match(/및/g)?.length ?? 0;
15
+ if (marks + ands >= 2) {
16
+ findings.push({
17
+ ruleId: "SEN-05",
18
+ message: "한 문장에 여러 가지가 나열되어 있습니다. 목록으로 나누면 읽기 쉽습니다.",
19
+ span: s.span,
20
+ suggestion: "세 가지 이상은 글머리표 목록으로 만드세요.",
21
+ });
22
+ }
23
+ }
24
+ return findings;
25
+ },
26
+ };
27
+ //# sourceMappingURL=sen-05.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sen-05.js","sourceRoot":"","sources":["../../../src/rules/sen/sen-05.ts"],"names":[],"mappings":"AAEA,wDAAwD;AACxD,qDAAqD;AACrD,oDAAoD;AACpD,MAAM,UAAU,GAAG,SAAS,CAAC;AAE7B,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,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;YAClE,MAAM,KAAK,GAAG,mBAAmB,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC;YACjE,MAAM,IAAI,GAAG,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC;YAC1D,IAAI,KAAK,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC;gBACtB,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,QAAQ;oBAChB,OAAO,EAAE,2CAA2C;oBACpD,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,UAAU,EAAE,0BAA0B;iBACvC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,58 @@
1
+ // SEN-07 명사화·긴 수식: '~하는 것'·'~의 여부'·'~에 대한/관한/통한'·'~함으로써' 같은
2
+ // 명사화·관형 수식을 어절 경계로 탐지해 문장당 1건 경고. 동사가 드러나는 짧은 문장을 권한다.
3
+ // 보조 규칙(오탐 있음 → warning). 형태소 분석 도입 시 정밀화(backlog).
4
+ // 근거: 소소한소통 「쉬운정보 가이드라인 1.0」 §8.3.6 · Inclusion Europe #14(짧고 간결).
5
+ /** 어절 양끝의 문장부호·따옴표·괄호를 떼어 핵심만 남긴다(마커 판정용). */
6
+ function core(text) {
7
+ return text.replace(/^[^\p{L}\p{N}]+/u, "").replace(/[^\p{L}\p{N}]+$/u, "");
8
+ }
9
+ /** 관형형 종결인가: '는/은/을'로 끝나거나 종성이 ㄴ·ㄹ인 음절로 끝난다('갈 것'·'만들 것'). */
10
+ function endsAdnominal(word) {
11
+ if (word.endsWith("는") || word.endsWith("은") || word.endsWith("을"))
12
+ return true;
13
+ const code = word.charCodeAt(word.length - 1);
14
+ if (code < 0xac00 || code > 0xd7a3)
15
+ return false; // 한글 음절이 아님
16
+ const jong = (code - 0xac00) % 28;
17
+ return jong === 4 /* ㄴ */ || jong === 8 /* ㄹ */;
18
+ }
19
+ /** '~에 대한' 류: '대한민국' 같은 다른 어절을 잡지 않도록 정확 일치로만 판정. */
20
+ const ADNOMINAL_EXACT = new Set(["대한", "관한", "통한", "대하여", "관하여"]);
21
+ const BY_DOING_ENDINGS = ["함으로써", "됨으로써", "음으로써"];
22
+ export const sen07 = {
23
+ id: "SEN-07",
24
+ group: "SEN",
25
+ defaultSeverity: "warning",
26
+ check(ctx) {
27
+ const findings = [];
28
+ for (const s of ctx.sentences) {
29
+ const cores = s.words.map((w) => core(w.text));
30
+ const hasMarker = cores.some((c, i) => {
31
+ // (a) 명사화 '것': 관형형 어절 바로 뒤에 '것…' 어절이 온다.
32
+ const next = cores[i + 1];
33
+ if (next !== undefined && endsAdnominal(c) && next.startsWith("것"))
34
+ return true;
35
+ // (b) '여부…'로 시작하는 어절.
36
+ if (c.startsWith("여부"))
37
+ return true;
38
+ // (c) '~에 대한/관한/통한/대하여/관하여' (정확 일치).
39
+ if (ADNOMINAL_EXACT.has(c))
40
+ return true;
41
+ // (d) '함으로써/됨으로써/음으로써'로 끝나는 어절.
42
+ if (BY_DOING_ENDINGS.some((e) => c.endsWith(e)))
43
+ return true;
44
+ return false;
45
+ });
46
+ if (hasMarker) {
47
+ findings.push({
48
+ ruleId: "SEN-07",
49
+ message: "명사화·긴 수식 표현이 있습니다. 동사를 살려 짧게 풀어 쓰세요.",
50
+ span: s.span,
51
+ suggestion: "'~하는 것'·'~의 여부'·'~에 대한' 대신 동사를 드러내세요. 예: '확인하는 것이 필요합니다' → '확인하세요'.",
52
+ });
53
+ }
54
+ }
55
+ return findings;
56
+ },
57
+ };
58
+ //# sourceMappingURL=sen-07.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sen-07.js","sourceRoot":"","sources":["../../../src/rules/sen/sen-07.ts"],"names":[],"mappings":"AAEA,4DAA4D;AAC5D,wDAAwD;AACxD,oDAAoD;AACpD,mEAAmE;AAEnE,8CAA8C;AAC9C,SAAS,IAAI,CAAC,IAAY;IACxB,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,8DAA8D;AAC9D,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAChF,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC9C,IAAI,IAAI,GAAG,MAAM,IAAI,IAAI,GAAG,MAAM;QAAE,OAAO,KAAK,CAAC,CAAC,YAAY;IAC9D,MAAM,IAAI,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;IAClC,OAAO,IAAI,KAAK,CAAC,CAAC,OAAO,IAAI,IAAI,KAAK,CAAC,CAAC,OAAO,CAAC;AAClD,CAAC;AAED,qDAAqD;AACrD,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAClE,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAElD,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,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAC/C,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;gBACpC,yCAAyC;gBACzC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC1B,IAAI,IAAI,KAAK,SAAS,IAAI,aAAa,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;oBAAE,OAAO,IAAI,CAAC;gBAChF,sBAAsB;gBACtB,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;oBAAE,OAAO,IAAI,CAAC;gBACpC,qCAAqC;gBACrC,IAAI,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC;oBAAE,OAAO,IAAI,CAAC;gBACxC,gCAAgC;gBAChC,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;oBAAE,OAAO,IAAI,CAAC;gBAC7D,OAAO,KAAK,CAAC;YACf,CAAC,CAAC,CAAC;YACH,IAAI,SAAS,EAAE,CAAC;gBACd,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,QAAQ;oBAChB,OAAO,EAAE,sCAAsC;oBAC/C,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,UAAU,EACR,qEAAqE;iBACxE,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { str02 } from "./str-02.js";
2
+ import { str03 } from "./str-03.js";
3
+ export { str02, str03 };
4
+ /**
5
+ * STR 규칙군. registry가 정적으로 조립한다.
6
+ * STR-01(결론 먼저)·STR-04(절차 번호)는 자동 탐지 규칙이 아니라 리포트 점검 안내(PROC류)로 다룬다.
7
+ */
8
+ export const strRules = [str02, str03];
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/str/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAExB;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAoB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC"}
@@ -0,0 +1,22 @@
1
+ // STR-02 단락이 너무 길다: 한 문단의 문장 수가 임계값(5)을 넘으면 문단마다 info.
2
+ // 근거: Inclusion Europe(짧은 단락) · guidelines §4(한 단락 한 주제). 임계값은 상수로 조정 가능.
3
+ const MAX_SENTENCES = 5;
4
+ export const str02 = {
5
+ id: "STR-02",
6
+ group: "STR",
7
+ defaultSeverity: "info",
8
+ check(ctx) {
9
+ const findings = [];
10
+ for (const p of ctx.paragraphs) {
11
+ if (p.sentences.length > MAX_SENTENCES) {
12
+ findings.push({
13
+ ruleId: "STR-02",
14
+ message: `한 단락에 문장이 ${p.sentences.length}개입니다. ${MAX_SENTENCES}개 이하로 나누면 더 쉽습니다.`,
15
+ span: p.span,
16
+ });
17
+ }
18
+ }
19
+ return findings;
20
+ },
21
+ };
22
+ //# sourceMappingURL=str-02.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"str-02.js","sourceRoot":"","sources":["../../../src/rules/str/str-02.ts"],"names":[],"mappings":"AAEA,uDAAuD;AACvD,0EAA0E;AAC1E,MAAM,aAAa,GAAG,CAAC,CAAC;AAExB,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,UAAU,EAAE,CAAC;YAC/B,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;gBACvC,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,QAAQ;oBAChB,OAAO,EAAE,aAAa,CAAC,CAAC,SAAS,CAAC,MAAM,SAAS,aAAa,mBAAmB;oBACjF,IAAI,EAAE,CAAC,CAAC,IAAI;iBACb,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,24 @@
1
+ // STR-03 문장 내 나열: 한 문장의 나열 구분자(',' '·' '、')가 2개 이상(항목 3개 이상)이면 문장당 info.
2
+ // 근거: Inclusion Europe #19(같은 주제 묶기) · guidelines §4(나열은 목록으로). 오탐 있음 → info.
3
+ const LIST_SEPARATOR = /[,·、]/g;
4
+ const MIN_SEPARATORS = 2;
5
+ export const str03 = {
6
+ id: "STR-03",
7
+ group: "STR",
8
+ defaultSeverity: "info",
9
+ check(ctx) {
10
+ const findings = [];
11
+ for (const s of ctx.sentences) {
12
+ const count = (s.text.match(LIST_SEPARATOR) ?? []).length;
13
+ if (count >= MIN_SEPARATORS) {
14
+ findings.push({
15
+ ruleId: "STR-03",
16
+ message: "한 문장에 여러 항목이 나열돼 있습니다. 글머리표 목록으로 나누면 더 쉽습니다.",
17
+ span: s.span,
18
+ });
19
+ }
20
+ }
21
+ return findings;
22
+ },
23
+ };
24
+ //# sourceMappingURL=str-03.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"str-03.js","sourceRoot":"","sources":["../../../src/rules/str/str-03.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,8EAA8E;AAC9E,MAAM,cAAc,GAAG,QAAQ,CAAC;AAChC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEzB,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,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;YAC1D,IAAI,KAAK,IAAI,cAAc,EAAE,CAAC;gBAC5B,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,QAAQ;oBAChB,OAAO,EAAE,8CAA8C;oBACvD,IAAI,EAAE,CAAC,CAAC,IAAI;iBACb,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { typ01 } from "./typ-01.js";
2
+ import { typ02 } from "./typ-02.js";
3
+ import { typ03 } from "./typ-03.js";
4
+ export { typ01, typ02, typ03 };
5
+ /** TYP 규칙군. registry가 정적으로 조립한다. */
6
+ export const typRules = [typ01, typ02, typ03];
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/typ/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;AAEpC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAE/B,oCAAoC;AACpC,MAAM,CAAC,MAAM,QAAQ,GAAoB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ // TYP-01 풀어 쓰지 않은 기호: {※, ~, &, /} 출현마다 warning. 말로 풀어쓰기 제안.
2
+ // 근거: Inclusion Europe(특수기호 대신 낱말) · guidelines §5('~'는 '부터/까지').
3
+ const SYMBOLS = new RegExp("[※~&/]", "g");
4
+ export const typ01 = {
5
+ id: "TYP-01",
6
+ group: "TYP",
7
+ defaultSeverity: "warning",
8
+ check(ctx) {
9
+ const findings = [];
10
+ for (const m of ctx.raw.matchAll(SYMBOLS)) {
11
+ const start = m.index ?? 0;
12
+ findings.push({
13
+ ruleId: "TYP-01",
14
+ message: `기호 '${m[0]}'은(는) 말로 풀어 쓰세요. 예: '~'→'부터/까지', '&'→'그리고', '/'→'또는'.`,
15
+ span: { start, end: start + 1 },
16
+ });
17
+ }
18
+ return findings;
19
+ },
20
+ };
21
+ //# sourceMappingURL=typ-01.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typ-01.js","sourceRoot":"","sources":["../../../src/rules/typ/typ-01.ts"],"names":[],"mappings":"AAEA,6DAA6D;AAC7D,kEAAkE;AAClE,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AAE1C,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,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1C,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;YAC3B,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,uDAAuD;gBAC3E,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE;aAChC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}