swwa-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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/assets/alt-text-ko.json +7 -0
  4. package/assets/axe-rule-map.json +196 -0
  5. package/assets/certification.json +47 -0
  6. package/assets/kwcag22-checklist.md +123 -0
  7. package/assets/kwcag22.json +1688 -0
  8. package/assets/link-text-ko.json +8 -0
  9. package/assets/mobile-app-2.0.md +38 -0
  10. package/assets/sources.json +68 -0
  11. package/assets/wcag22.json +780 -0
  12. package/bin/swwa-mcp.mjs +15 -0
  13. package/dist/color/contrast.js +175 -0
  14. package/dist/color/contrast.js.map +1 -0
  15. package/dist/data/axe-map.js +32 -0
  16. package/dist/data/axe-map.js.map +1 -0
  17. package/dist/data/certification.js +54 -0
  18. package/dist/data/certification.js.map +1 -0
  19. package/dist/data/kwcag22.js +91 -0
  20. package/dist/data/kwcag22.js.map +1 -0
  21. package/dist/data/loader.js +18 -0
  22. package/dist/data/loader.js.map +1 -0
  23. package/dist/data/sources.js +44 -0
  24. package/dist/data/sources.js.map +1 -0
  25. package/dist/data/wcag22.js +44 -0
  26. package/dist/data/wcag22.js.map +1 -0
  27. package/dist/data/wordlists.js +40 -0
  28. package/dist/data/wordlists.js.map +1 -0
  29. package/dist/engine/browser-detect.js +44 -0
  30. package/dist/engine/browser-detect.js.map +1 -0
  31. package/dist/engine/browser.js +114 -0
  32. package/dist/engine/browser.js.map +1 -0
  33. package/dist/engine/static-worker.js +18 -0
  34. package/dist/engine/static-worker.js.map +1 -0
  35. package/dist/engine/static.js +139 -0
  36. package/dist/engine/static.js.map +1 -0
  37. package/dist/engine/url-guard.js +37 -0
  38. package/dist/engine/url-guard.js.map +1 -0
  39. package/dist/index.js +23 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/messages.js +176 -0
  42. package/dist/messages.js.map +1 -0
  43. package/dist/normalize/axe.js +72 -0
  44. package/dist/normalize/axe.js.map +1 -0
  45. package/dist/normalize/checkpoints.js +47 -0
  46. package/dist/normalize/checkpoints.js.map +1 -0
  47. package/dist/normalize/finding.js +24 -0
  48. package/dist/normalize/finding.js.map +1 -0
  49. package/dist/normalize/locale.js +20 -0
  50. package/dist/normalize/locale.js.map +1 -0
  51. package/dist/prompts/audit-report.js +45 -0
  52. package/dist/prompts/audit-report.js.map +1 -0
  53. package/dist/prompts/review-markup.js +35 -0
  54. package/dist/prompts/review-markup.js.map +1 -0
  55. package/dist/report/cert.js +72 -0
  56. package/dist/report/cert.js.map +1 -0
  57. package/dist/report/format.js +36 -0
  58. package/dist/report/format.js.map +1 -0
  59. package/dist/report/summarize.js +61 -0
  60. package/dist/report/summarize.js.map +1 -0
  61. package/dist/report/types.js +2 -0
  62. package/dist/report/types.js.map +1 -0
  63. package/dist/resources/index.js +69 -0
  64. package/dist/resources/index.js.map +1 -0
  65. package/dist/rules/b/b-focus-order.js +33 -0
  66. package/dist/rules/b/b-focus-order.js.map +1 -0
  67. package/dist/rules/b/b-focus-visible.js +71 -0
  68. package/dist/rules/b/b-focus-visible.js.map +1 -0
  69. package/dist/rules/b/b-keyboard-reachable.js +82 -0
  70. package/dist/rules/b/b-keyboard-reachable.js.map +1 -0
  71. package/dist/rules/b/b-motion-runtime.js +50 -0
  72. package/dist/rules/b/b-motion-runtime.js.map +1 -0
  73. package/dist/rules/b/b-skip-link-works.js +58 -0
  74. package/dist/rules/b/b-skip-link-works.js.map +1 -0
  75. package/dist/rules/b/b-target-size-6mm.js +54 -0
  76. package/dist/rules/b/b-target-size-6mm.js.map +1 -0
  77. package/dist/rules/b/index.js +7 -0
  78. package/dist/rules/b/index.js.map +1 -0
  79. package/dist/rules/k/index.js +33 -0
  80. package/dist/rules/k/index.js.map +1 -0
  81. package/dist/rules/k/k-accesskey.js +20 -0
  82. package/dist/rules/k/k-accesskey.js.map +1 -0
  83. package/dist/rules/k/k-alt-meaningless.js +35 -0
  84. package/dist/rules/k/k-alt-meaningless.js.map +1 -0
  85. package/dist/rules/k/k-autocomplete-missing.js +36 -0
  86. package/dist/rules/k/k-autocomplete-missing.js.map +1 -0
  87. package/dist/rules/k/k-autoplay-media.js +30 -0
  88. package/dist/rules/k/k-autoplay-media.js.map +1 -0
  89. package/dist/rules/k/k-captcha-detect.js +24 -0
  90. package/dist/rules/k/k-captcha-detect.js.map +1 -0
  91. package/dist/rules/k/k-carousel-no-pause.js +33 -0
  92. package/dist/rules/k/k-carousel-no-pause.js.map +1 -0
  93. package/dist/rules/k/k-device-motion.js +22 -0
  94. package/dist/rules/k/k-device-motion.js.map +1 -0
  95. package/dist/rules/k/k-down-event-action.js +19 -0
  96. package/dist/rules/k/k-down-event-action.js.map +1 -0
  97. package/dist/rules/k/k-error-association.js +31 -0
  98. package/dist/rules/k/k-error-association.js.map +1 -0
  99. package/dist/rules/k/k-flash-animation.js +57 -0
  100. package/dist/rules/k/k-flash-animation.js.map +1 -0
  101. package/dist/rules/k/k-gesture-listener.js +30 -0
  102. package/dist/rules/k/k-gesture-listener.js.map +1 -0
  103. package/dist/rules/k/k-iframe-title.js +23 -0
  104. package/dist/rules/k/k-iframe-title.js.map +1 -0
  105. package/dist/rules/k/k-lang-ko-expected.js +29 -0
  106. package/dist/rules/k/k-lang-ko-expected.js.map +1 -0
  107. package/dist/rules/k/k-link-same-text-diff-href.js +41 -0
  108. package/dist/rules/k/k-link-same-text-diff-href.js.map +1 -0
  109. package/dist/rules/k/k-link-text-generic.js +29 -0
  110. package/dist/rules/k/k-link-text-generic.js.map +1 -0
  111. package/dist/rules/k/k-media-track.js +22 -0
  112. package/dist/rules/k/k-media-track.js.map +1 -0
  113. package/dist/rules/k/k-mouse-only-handler.js +27 -0
  114. package/dist/rules/k/k-mouse-only-handler.js.map +1 -0
  115. package/dist/rules/k/k-new-window-notice.js +27 -0
  116. package/dist/rules/k/k-new-window-notice.js.map +1 -0
  117. package/dist/rules/k/k-onload-popup.js +30 -0
  118. package/dist/rules/k/k-onload-popup.js.map +1 -0
  119. package/dist/rules/k/k-outline-none.js +55 -0
  120. package/dist/rules/k/k-outline-none.js.map +1 -0
  121. package/dist/rules/k/k-parse-errors.js +38 -0
  122. package/dist/rules/k/k-parse-errors.js.map +1 -0
  123. package/dist/rules/k/k-placeholder-only-label.js +32 -0
  124. package/dist/rules/k/k-placeholder-only-label.js.map +1 -0
  125. package/dist/rules/k/k-select-onchange.js +19 -0
  126. package/dist/rules/k/k-select-onchange.js.map +1 -0
  127. package/dist/rules/k/k-sensory-instruction.js +32 -0
  128. package/dist/rules/k/k-sensory-instruction.js.map +1 -0
  129. package/dist/rules/k/k-session-timeout-hint.js +29 -0
  130. package/dist/rules/k/k-session-timeout-hint.js.map +1 -0
  131. package/dist/rules/k/k-skip-link-first.js +23 -0
  132. package/dist/rules/k/k-skip-link-first.js.map +1 -0
  133. package/dist/rules/k/k-skip-target-exists.js +28 -0
  134. package/dist/rules/k/k-skip-target-exists.js.map +1 -0
  135. package/dist/rules/k/k-tabindex-positive.js +23 -0
  136. package/dist/rules/k/k-tabindex-positive.js.map +1 -0
  137. package/dist/rules/k/k-table-caption.js +26 -0
  138. package/dist/rules/k/k-table-caption.js.map +1 -0
  139. package/dist/rules/k/k-table-th-missing.js +28 -0
  140. package/dist/rules/k/k-table-th-missing.js.map +1 -0
  141. package/dist/rules/k/k-title-generic.js +35 -0
  142. package/dist/rules/k/k-title-generic.js.map +1 -0
  143. package/dist/rules/registry.js +62 -0
  144. package/dist/rules/registry.js.map +1 -0
  145. package/dist/rules/types.js +2 -0
  146. package/dist/rules/types.js.map +1 -0
  147. package/dist/rules/util/browser-dom.js +50 -0
  148. package/dist/rules/util/browser-dom.js.map +1 -0
  149. package/dist/rules/util/focusable.js +29 -0
  150. package/dist/rules/util/focusable.js.map +1 -0
  151. package/dist/rules/util/selector.js +19 -0
  152. package/dist/rules/util/selector.js.map +1 -0
  153. package/dist/rules/util/text.js +13 -0
  154. package/dist/rules/util/text.js.map +1 -0
  155. package/dist/schema-dialect.js +41 -0
  156. package/dist/schema-dialect.js.map +1 -0
  157. package/dist/server.js +36 -0
  158. package/dist/server.js.map +1 -0
  159. package/dist/tools/audit-url.js +109 -0
  160. package/dist/tools/audit-url.js.map +1 -0
  161. package/dist/tools/browser-status.js +29 -0
  162. package/dist/tools/browser-status.js.map +1 -0
  163. package/dist/tools/cert-readiness.js +76 -0
  164. package/dist/tools/cert-readiness.js.map +1 -0
  165. package/dist/tools/check-contrast.js +58 -0
  166. package/dist/tools/check-contrast.js.map +1 -0
  167. package/dist/tools/check-html.js +88 -0
  168. package/dist/tools/check-html.js.map +1 -0
  169. package/dist/tools/checklist.js +94 -0
  170. package/dist/tools/checklist.js.map +1 -0
  171. package/dist/tools/lookup.js +70 -0
  172. package/dist/tools/lookup.js.map +1 -0
  173. package/dist/tools/report-schema.js +49 -0
  174. package/dist/tools/report-schema.js.map +1 -0
  175. package/package.json +76 -0
@@ -0,0 +1,29 @@
1
+ import { createFinding } from "../../normalize/finding.js";
2
+ import { normText } from "../util/text.js";
3
+ import { sessionTimeoutHint } from "../../messages.js";
4
+ const PATTERN = /자동\s*로그아웃|세션\s*(만료|종료|연장)|로그인\s*시간|(\d+\s*분)\s*(후|동안).*(로그아웃|만료)/;
5
+ export const rule = {
6
+ id: "k-session-timeout-hint",
7
+ kwcag: "6.2.1",
8
+ wcag: ["2.2.1"],
9
+ engine: "k",
10
+ impact: "moderate",
11
+ confidence: "low",
12
+ tier: "T2",
13
+ run(ctx) {
14
+ const body = ctx.document.body;
15
+ if (!body)
16
+ return [];
17
+ // 문서 전체 텍스트에서 한 번만 판정한다(문구 존재 → 연장 수단 확인 필요).
18
+ for (const el of Array.from(body.querySelectorAll("p, li, span, div, strong, em"))) {
19
+ if (el.children.length > 0)
20
+ continue;
21
+ const text = normText(el.textContent ?? "");
22
+ if (PATTERN.test(text)) {
23
+ return [createFinding(rule, el, { message: sessionTimeoutHint.message, fix: sessionTimeoutHint.fix, outcome: "incomplete" })];
24
+ }
25
+ }
26
+ return [];
27
+ },
28
+ };
29
+ //# sourceMappingURL=k-session-timeout-hint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k-session-timeout-hint.js","sourceRoot":"","sources":["../../../src/rules/k/k-session-timeout-hint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAIvD,MAAM,OAAO,GAAG,kEAAkE,CAAC;AAEnF,MAAM,CAAC,MAAM,IAAI,GAAe;IAC9B,EAAE,EAAE,wBAAwB;IAC5B,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,CAAC,OAAO,CAAC;IACf,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,UAAU;IAClB,UAAU,EAAE,KAAK;IACjB,IAAI,EAAE,IAAI;IACV,GAAG,CAAC,GAAkB;QACpB,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC/B,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,CAAC;QACrB,8CAA8C;QAC9C,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,8BAA8B,CAAC,CAAC,EAAE,CAAC;YACnF,IAAI,EAAE,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YACrC,MAAM,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;YAC5C,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvB,OAAO,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,kBAAkB,CAAC,OAAO,EAAE,GAAG,EAAE,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC;YAChI,CAAC;QACH,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { createFinding } from "../../normalize/finding.js";
2
+ import { firstFocusable } from "../util/focusable.js";
3
+ import { skipLinkFirst } from "../../messages.js";
4
+ export const rule = {
5
+ id: "k-skip-link-first",
6
+ kwcag: "6.4.1",
7
+ wcag: ["2.4.1"],
8
+ engine: "k",
9
+ impact: "serious",
10
+ confidence: "high",
11
+ tier: "T1",
12
+ run(ctx) {
13
+ const first = firstFocusable(ctx.document);
14
+ if (!first)
15
+ return [];
16
+ const href = first.getAttribute("href") ?? "";
17
+ const isSameDocAnchor = first.tagName.toLowerCase() === "a" && href.startsWith("#") && href.length > 1;
18
+ if (isSameDocAnchor)
19
+ return [];
20
+ return [createFinding(rule, first, { message: skipLinkFirst.message, fix: skipLinkFirst.fix, outcome: "fail" })];
21
+ },
22
+ };
23
+ //# sourceMappingURL=k-skip-link-first.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k-skip-link-first.js","sourceRoot":"","sources":["../../../src/rules/k/k-skip-link-first.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIlD,MAAM,CAAC,MAAM,IAAI,GAAe;IAC9B,EAAE,EAAE,mBAAmB;IACvB,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,CAAC,OAAO,CAAC;IACf,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,MAAM;IAClB,IAAI,EAAE,IAAI;IACV,GAAG,CAAC,GAAkB;QACpB,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC9C,MAAM,eAAe,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QACvG,IAAI,eAAe;YAAE,OAAO,EAAE,CAAC;QAC/B,OAAO,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,GAAG,EAAE,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IACnH,CAAC;CACF,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { createFinding } from "../../normalize/finding.js";
2
+ import { firstFocusable } from "../util/focusable.js";
3
+ import { skipTargetExists } from "../../messages.js";
4
+ export const rule = {
5
+ id: "k-skip-target-exists",
6
+ kwcag: "6.4.1",
7
+ wcag: ["2.4.1"],
8
+ engine: "k",
9
+ impact: "serious",
10
+ confidence: "high",
11
+ tier: "T1",
12
+ run(ctx) {
13
+ const first = firstFocusable(ctx.document);
14
+ if (!first || first.tagName.toLowerCase() !== "a")
15
+ return [];
16
+ const href = first.getAttribute("href") ?? "";
17
+ if (!href.startsWith("#") || href.length <= 1)
18
+ return [];
19
+ const targetId = href.slice(1);
20
+ const target = ctx.document.getElementById(targetId) ?? ctx.document.getElementsByName(targetId)[0];
21
+ if (target)
22
+ return [];
23
+ return [
24
+ createFinding(rule, first, { message: skipTargetExists.message(href), fix: skipTargetExists.fix, outcome: "fail" }),
25
+ ];
26
+ },
27
+ };
28
+ //# sourceMappingURL=k-skip-target-exists.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k-skip-target-exists.js","sourceRoot":"","sources":["../../../src/rules/k/k-skip-target-exists.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAIrD,MAAM,CAAC,MAAM,IAAI,GAAe;IAC9B,EAAE,EAAE,sBAAsB;IAC1B,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,CAAC,OAAO,CAAC;IACf,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,MAAM;IAClB,IAAI,EAAE,IAAI;IACV,GAAG,CAAC,GAAkB;QACpB,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,GAAG;YAAE,OAAO,EAAE,CAAC;QAC7D,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC;YAAE,OAAO,EAAE,CAAC;QACzD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACpG,IAAI,MAAM;YAAE,OAAO,EAAE,CAAC;QACtB,OAAO;YACL,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;SACpH,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { createFinding } from "../../normalize/finding.js";
2
+ import { tabindexPositive } from "../../messages.js";
3
+ export const rule = {
4
+ id: "k-tabindex-positive",
5
+ kwcag: "5.3.2",
6
+ wcag: ["1.3.2"],
7
+ engine: "k",
8
+ impact: "moderate",
9
+ confidence: "high",
10
+ tier: "T1",
11
+ run(ctx) {
12
+ const findings = [];
13
+ for (const el of Array.from(ctx.document.querySelectorAll("[tabindex]"))) {
14
+ const value = el.getAttribute("tabindex") ?? "";
15
+ const n = Number(value);
16
+ if (!Number.isNaN(n) && n > 0) {
17
+ findings.push(createFinding(rule, el, { message: tabindexPositive.message(value), fix: tabindexPositive.fix, outcome: "fail" }));
18
+ }
19
+ }
20
+ return findings;
21
+ },
22
+ };
23
+ //# sourceMappingURL=k-tabindex-positive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k-tabindex-positive.js","sourceRoot":"","sources":["../../../src/rules/k/k-tabindex-positive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAIrD,MAAM,CAAC,MAAM,IAAI,GAAe;IAC9B,EAAE,EAAE,qBAAqB;IACzB,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,CAAC,OAAO,CAAC;IACf,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,UAAU;IAClB,UAAU,EAAE,MAAM;IAClB,IAAI,EAAE,IAAI;IACV,GAAG,CAAC,GAAkB;QACpB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC;YACzE,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;YAChD,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;YACxB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC9B,QAAQ,CAAC,IAAI,CACX,aAAa,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAClH,CAAC;YACJ,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { createFinding } from "../../normalize/finding.js";
2
+ import { tableCaption } from "../../messages.js";
3
+ export const rule = {
4
+ id: "k-table-caption",
5
+ kwcag: "5.3.1",
6
+ wcag: ["1.3.1"],
7
+ engine: "k",
8
+ impact: "moderate",
9
+ confidence: "medium",
10
+ tier: "T1",
11
+ run(ctx) {
12
+ const findings = [];
13
+ for (const table of Array.from(ctx.document.querySelectorAll("table"))) {
14
+ const role = table.getAttribute("role");
15
+ if (role === "presentation" || role === "none")
16
+ continue;
17
+ if (!table.querySelector("th"))
18
+ continue; // 데이터 표로 보이지 않으면 판단하지 않는다
19
+ if (!table.querySelector("caption")) {
20
+ findings.push(createFinding(rule, table, { message: tableCaption.message, fix: tableCaption.fix, outcome: "fail" }));
21
+ }
22
+ }
23
+ return findings;
24
+ },
25
+ };
26
+ //# sourceMappingURL=k-table-caption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k-table-caption.js","sourceRoot":"","sources":["../../../src/rules/k/k-table-caption.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIjD,MAAM,CAAC,MAAM,IAAI,GAAe;IAC9B,EAAE,EAAE,iBAAiB;IACrB,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,CAAC,OAAO,CAAC;IACf,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,UAAU;IAClB,UAAU,EAAE,QAAQ;IACpB,IAAI,EAAE,IAAI;IACV,GAAG,CAAC,GAAkB;QACpB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YACvE,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YACxC,IAAI,IAAI,KAAK,cAAc,IAAI,IAAI,KAAK,MAAM;gBAAE,SAAS;YACzD,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC;gBAAE,SAAS,CAAC,0BAA0B;YACpE,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;gBACpC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;YACvH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { createFinding } from "../../normalize/finding.js";
2
+ import { tableThMissing } from "../../messages.js";
3
+ export const rule = {
4
+ id: "k-table-th-missing",
5
+ kwcag: "5.3.1",
6
+ wcag: ["1.3.1"],
7
+ engine: "k",
8
+ impact: "serious",
9
+ confidence: "medium",
10
+ tier: "T1",
11
+ run(ctx) {
12
+ const findings = [];
13
+ for (const table of Array.from(ctx.document.querySelectorAll("table"))) {
14
+ const role = table.getAttribute("role");
15
+ if (role === "presentation" || role === "none")
16
+ continue;
17
+ if (table.querySelector("th"))
18
+ continue;
19
+ const rows = Array.from(table.querySelectorAll("tr"));
20
+ const looksLikeDataTable = rows.length >= 2 && rows.every((r) => r.querySelectorAll("td").length >= 2);
21
+ if (looksLikeDataTable) {
22
+ findings.push(createFinding(rule, table, { message: tableThMissing.message, fix: tableThMissing.fix, outcome: "fail" }));
23
+ }
24
+ }
25
+ return findings;
26
+ },
27
+ };
28
+ //# sourceMappingURL=k-table-th-missing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k-table-th-missing.js","sourceRoot":"","sources":["../../../src/rules/k/k-table-th-missing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAInD,MAAM,CAAC,MAAM,IAAI,GAAe;IAC9B,EAAE,EAAE,oBAAoB;IACxB,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,CAAC,OAAO,CAAC;IACf,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,QAAQ;IACpB,IAAI,EAAE,IAAI;IACV,GAAG,CAAC,GAAkB;QACpB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YACvE,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YACxC,IAAI,IAAI,KAAK,cAAc,IAAI,IAAI,KAAK,MAAM;gBAAE,SAAS;YACzD,IAAI,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC;gBAAE,SAAS;YACxC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;YACtD,MAAM,kBAAkB,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;YACvG,IAAI,kBAAkB,EAAE,CAAC;gBACvB,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,cAAc,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;YAC3H,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { createFinding } from "../../normalize/finding.js";
2
+ import { normText } from "../util/text.js";
3
+ import { titleGeneric } from "../../messages.js";
4
+ function looksLikeSiteNameOnly(text) {
5
+ if (/[:\-–—]/.test(text))
6
+ return false; // "페이지 - 사이트명"처럼 구분자가 있으면 목적이 담긴 것으로 본다
7
+ const words = text.split(/\s+/).filter(Boolean);
8
+ return words.length > 0 && words.length <= 3;
9
+ }
10
+ export const rule = {
11
+ id: "k-title-generic",
12
+ kwcag: "6.4.2",
13
+ wcag: ["2.4.2"],
14
+ engine: "k",
15
+ impact: "moderate",
16
+ confidence: "medium",
17
+ tier: "T1",
18
+ run(ctx) {
19
+ const titleEl = ctx.document.querySelector("title");
20
+ if (!titleEl)
21
+ return [];
22
+ const text = normText(titleEl.textContent ?? "");
23
+ const isGeneric = text === "" || /^(untitled|제목\s*없음)$/i.test(text) || looksLikeSiteNameOnly(text);
24
+ if (!isGeneric)
25
+ return [];
26
+ return [
27
+ createFinding(rule, titleEl, {
28
+ message: titleGeneric.message(text || "(비어 있음)"),
29
+ fix: titleGeneric.fix,
30
+ outcome: "fail",
31
+ }),
32
+ ];
33
+ },
34
+ };
35
+ //# sourceMappingURL=k-title-generic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k-title-generic.js","sourceRoot":"","sources":["../../../src/rules/k/k-title-generic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIjD,SAAS,qBAAqB,CAAC,IAAY;IACzC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,wCAAwC;IAChF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAChD,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,CAAC,MAAM,IAAI,GAAe;IAC9B,EAAE,EAAE,iBAAiB;IACrB,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,CAAC,OAAO,CAAC;IACf,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,UAAU;IAClB,UAAU,EAAE,QAAQ;IACpB,IAAI,EAAE,IAAI;IACV,GAAG,CAAC,GAAkB;QACpB,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;QACjD,MAAM,SAAS,GAAG,IAAI,KAAK,EAAE,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,qBAAqB,CAAC,IAAI,CAAC,CAAC;QACnG,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,CAAC;QAC1B,OAAO;YACL,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE;gBAC3B,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,IAAI,IAAI,SAAS,CAAC;gBAChD,GAAG,EAAE,YAAY,CAAC,GAAG;gBACrB,OAAO,EAAE,MAAM;aAChB,CAAC;SACH,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,62 @@
1
+ import * as k from "./k/index.js";
2
+ import * as b from "./b/index.js";
3
+ /** T1 18개 정적 규칙 전체 목록(단일 소스 §5와 대조된다, ADR-04). */
4
+ export const KRULES = [
5
+ k.altMeaninglessRule,
6
+ k.mediaTrackRule,
7
+ k.tableCaptionRule,
8
+ k.tableThMissingRule,
9
+ k.tabindexPositiveRule,
10
+ k.autoplayMediaRule,
11
+ k.mouseOnlyHandlerRule,
12
+ k.skipLinkFirstRule,
13
+ k.skipTargetExistsRule,
14
+ k.titleGenericRule,
15
+ k.iframeTitleRule,
16
+ k.linkTextGenericRule,
17
+ k.langKoExpectedRule,
18
+ k.newWindowNoticeRule,
19
+ k.selectOnchangeRule,
20
+ k.placeholderOnlyLabelRule,
21
+ k.captchaDetectRule,
22
+ k.parseErrorsRule,
23
+ ];
24
+ /** T2 정적 규칙(휴리스틱, 단일 소스 §5). 대부분 outcome incomplete(사람 확인 필요). */
25
+ export const T2RULES = [
26
+ k.sensoryInstructionRule,
27
+ k.outlineNoneRule,
28
+ k.accesskeyRule,
29
+ k.sessionTimeoutHintRule,
30
+ k.carouselNoPauseRule,
31
+ k.flashAnimationRule,
32
+ k.linkSameTextDiffHrefRule,
33
+ k.gestureListenerRule,
34
+ k.downEventActionRule,
35
+ k.deviceMotionRule,
36
+ k.onloadPopupRule,
37
+ k.errorAssociationRule,
38
+ k.autocompleteMissingRule,
39
+ ];
40
+ /** 정적 엔진이 실행하는 전체 k-규칙(T1 + T2). */
41
+ export const ALL_K_RULES = [...KRULES, ...T2RULES];
42
+ /** 브라우저 규칙 6개(§5 B tier). 백로그 b-widget-keyboard는 아직 구현하지 않는다. */
43
+ export const BRULES = [
44
+ b.focusVisibleRule,
45
+ b.focusOrderRule,
46
+ b.skipLinkWorksRule,
47
+ b.targetSize6mmRule,
48
+ b.keyboardReachableRule,
49
+ b.motionRuntimeRule,
50
+ ];
51
+ /**
52
+ * 규칙의 kwcag 귀속이 단일 소스 데이터(kwcag22.json)에 실재하는지 대조한다.
53
+ * 존재하지 않는 검사항목에 귀속된 규칙이 있으면 던진다 — 기동 중단(ADR-04).
54
+ */
55
+ export function validateRegistry(rules, data) {
56
+ const unknown = rules.filter((r) => !data.kwcag22.findById(r.kwcag));
57
+ if (unknown.length > 0) {
58
+ const ids = unknown.map((r) => `${r.id} → ${r.kwcag}`).join(", ");
59
+ throw new Error(`kwcag22.json에 없는 검사항목에 귀속된 규칙이 있습니다: ${ids}`);
60
+ }
61
+ }
62
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/rules/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAClC,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAIlC,kDAAkD;AAClD,MAAM,CAAC,MAAM,MAAM,GAAiB;IAClC,CAAC,CAAC,kBAAkB;IACpB,CAAC,CAAC,cAAc;IAChB,CAAC,CAAC,gBAAgB;IAClB,CAAC,CAAC,kBAAkB;IACpB,CAAC,CAAC,oBAAoB;IACtB,CAAC,CAAC,iBAAiB;IACnB,CAAC,CAAC,oBAAoB;IACtB,CAAC,CAAC,iBAAiB;IACnB,CAAC,CAAC,oBAAoB;IACtB,CAAC,CAAC,gBAAgB;IAClB,CAAC,CAAC,eAAe;IACjB,CAAC,CAAC,mBAAmB;IACrB,CAAC,CAAC,kBAAkB;IACpB,CAAC,CAAC,mBAAmB;IACrB,CAAC,CAAC,kBAAkB;IACpB,CAAC,CAAC,wBAAwB;IAC1B,CAAC,CAAC,iBAAiB;IACnB,CAAC,CAAC,eAAe;CAClB,CAAC;AAEF,kEAAkE;AAClE,MAAM,CAAC,MAAM,OAAO,GAAiB;IACnC,CAAC,CAAC,sBAAsB;IACxB,CAAC,CAAC,eAAe;IACjB,CAAC,CAAC,aAAa;IACf,CAAC,CAAC,sBAAsB;IACxB,CAAC,CAAC,mBAAmB;IACrB,CAAC,CAAC,kBAAkB;IACpB,CAAC,CAAC,wBAAwB;IAC1B,CAAC,CAAC,mBAAmB;IACrB,CAAC,CAAC,mBAAmB;IACrB,CAAC,CAAC,gBAAgB;IAClB,CAAC,CAAC,eAAe;IACjB,CAAC,CAAC,oBAAoB;IACtB,CAAC,CAAC,uBAAuB;CAC1B,CAAC;AAEF,oCAAoC;AACpC,MAAM,CAAC,MAAM,WAAW,GAAiB,CAAC,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC;AAEjE,iEAAiE;AACjE,MAAM,CAAC,MAAM,MAAM,GAAkB;IACnC,CAAC,CAAC,gBAAgB;IAClB,CAAC,CAAC,cAAc;IAChB,CAAC,CAAC,iBAAiB;IACnB,CAAC,CAAC,iBAAiB;IACnB,CAAC,CAAC,qBAAqB;IACvB,CAAC,CAAC,iBAAiB;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAiB,EAAE,IAAgB;IAClE,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IACrE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,wCAAwC,GAAG,EAAE,CAAC,CAAC;IACjE,CAAC;AACH,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,50 @@
1
+ /**
2
+ * Tab으로 문서를 순회하며 각 정지점의 선택자·HTML·DOM 순서 인덱스를 기록한다(최대 maxStops,
3
+ * 첫 정지점으로 되돌아오거나 진행이 없으면 종료). 03 §6 b-규칙 인터페이스가 공유하는 헬퍼.
4
+ */
5
+ export async function tabTraversal(page, maxStops = 200) {
6
+ await page.evaluate(() => {
7
+ Array.from(document.querySelectorAll("*")).forEach((el, i) => el.setAttribute("data-swwa-order", String(i)));
8
+ });
9
+ const stops = [];
10
+ const seen = new Set();
11
+ for (let i = 0; i < maxStops; i++) {
12
+ await page.keyboard.press("Tab");
13
+ const info = await page.evaluate(() => {
14
+ const active = document.activeElement;
15
+ if (!active || active === document.body)
16
+ return null;
17
+ function cssPath(node) {
18
+ const parts = [];
19
+ let cur = node;
20
+ while (cur) {
21
+ if (cur.id) {
22
+ parts.unshift(`#${cur.id}`);
23
+ break;
24
+ }
25
+ parts.unshift(cur.tagName.toLowerCase());
26
+ cur = cur.parentElement;
27
+ }
28
+ return parts.join(" > ");
29
+ }
30
+ return {
31
+ selector: cssPath(active),
32
+ html: active.outerHTML.slice(0, 300),
33
+ tagName: active.tagName.toLowerCase(),
34
+ domIndex: Number(active.getAttribute("data-swwa-order") ?? "-1"),
35
+ };
36
+ });
37
+ if (!info)
38
+ break;
39
+ const key = `${info.selector}:${info.domIndex}`;
40
+ if (seen.has(key))
41
+ break; // 처음 방문한 정지점으로 돌아왔거나 진행이 없음
42
+ seen.add(key);
43
+ stops.push(info);
44
+ }
45
+ await page.evaluate(() => {
46
+ document.querySelectorAll("[data-swwa-order]").forEach((el) => el.removeAttribute("data-swwa-order"));
47
+ });
48
+ return stops;
49
+ }
50
+ //# sourceMappingURL=browser-dom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-dom.js","sourceRoot":"","sources":["../../../src/rules/util/browser-dom.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAU,EAAE,QAAQ,GAAG,GAAG;IAC3D,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;QACvB,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/G,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;YACpC,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC;YACtC,IAAI,CAAC,MAAM,IAAI,MAAM,KAAK,QAAQ,CAAC,IAAI;gBAAE,OAAO,IAAI,CAAC;YACrD,SAAS,OAAO,CAAC,IAAa;gBAC5B,MAAM,KAAK,GAAa,EAAE,CAAC;gBAC3B,IAAI,GAAG,GAAmB,IAAI,CAAC;gBAC/B,OAAO,GAAG,EAAE,CAAC;oBACX,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;wBACX,KAAK,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;wBAC5B,MAAM;oBACR,CAAC;oBACD,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;oBACzC,GAAG,GAAG,GAAG,CAAC,aAAa,CAAC;gBAC1B,CAAC;gBACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC3B,CAAC;YACD,OAAO;gBACL,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC;gBACzB,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;gBACpC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE;gBACrC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,iBAAiB,CAAC,IAAI,IAAI,CAAC;aACjE,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,IAAI;YAAE,MAAM;QACjB,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChD,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,MAAM,CAAC,4BAA4B;QACtD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAED,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;QACvB,QAAQ,CAAC,gBAAgB,CAAC,mBAAmB,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACxG,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,29 @@
1
+ const NATIVELY_FOCUSABLE_TAGS = new Set(["a", "button", "select", "textarea", "audio", "video"]);
2
+ /** 문서 순서 스캔 등에 쓰는 "초점 가능 요소" 근사 판별(정적 마크업 기준, 실제 렌더링 상태는 모른다). */
3
+ export function isFocusable(el) {
4
+ const tag = el.tagName.toLowerCase();
5
+ const tabindex = el.getAttribute("tabindex");
6
+ if (tabindex !== null && Number(tabindex) < 0)
7
+ return false;
8
+ if (tabindex !== null && !Number.isNaN(Number(tabindex)))
9
+ return true;
10
+ if (tag === "a" || tag === "area")
11
+ return el.hasAttribute("href");
12
+ if (tag === "input")
13
+ return el.getAttribute("type") !== "hidden" && !el.hasAttribute("disabled");
14
+ if (NATIVELY_FOCUSABLE_TAGS.has(tag))
15
+ return !el.hasAttribute("disabled");
16
+ return false;
17
+ }
18
+ /** 문서 순서상 첫 초점 가능 요소를 찾는다(k-skip-link-first). */
19
+ export function firstFocusable(document) {
20
+ const walker = document.createTreeWalker(document.body ?? document, 1 /* NodeFilter.SHOW_ELEMENT */);
21
+ let node = walker.nextNode();
22
+ while (node) {
23
+ if (isFocusable(node))
24
+ return node;
25
+ node = walker.nextNode();
26
+ }
27
+ return null;
28
+ }
29
+ //# sourceMappingURL=focusable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focusable.js","sourceRoot":"","sources":["../../../src/rules/util/focusable.ts"],"names":[],"mappings":"AAAA,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAEjG,kEAAkE;AAClE,MAAM,UAAU,WAAW,CAAC,EAAW;IACrC,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAC7C,IAAI,QAAQ,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5D,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACtE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IAClE,IAAI,GAAG,KAAK,OAAO;QAAE,OAAO,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,QAAQ,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IACjG,IAAI,uBAAuB,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAC1E,OAAO,KAAK,CAAC;AACf,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,cAAc,CAAC,QAAkB;IAC/C,MAAM,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,IAAI,QAAQ,EAAE,CAAC,CAAC,6BAA6B,CAAC,CAAC;IACrG,IAAI,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAoB,CAAC;IAC/C,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,WAAW,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACnC,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAoB,CAAC;IAC7C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 요소를 가리키는 CSS 선택자를 만든다. id가 있으면 그 즉시 `#id`로 끝내고, 없으면 조상으로
3
+ * 올라가며 태그명을 이어 붙인다(예: `html > head > title`). 픽스처가 대부분 id를 갖도록 작성돼
4
+ * 있어 규칙 결과가 안정적으로 스냅샷된다(04-qa-plan §1).
5
+ */
6
+ export function cssPath(el) {
7
+ const parts = [];
8
+ let node = el;
9
+ while (node) {
10
+ if (node.id) {
11
+ parts.unshift(`#${node.id}`);
12
+ break;
13
+ }
14
+ parts.unshift(node.tagName.toLowerCase());
15
+ node = node.parentElement;
16
+ }
17
+ return parts.join(" > ");
18
+ }
19
+ //# sourceMappingURL=selector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selector.js","sourceRoot":"","sources":["../../../src/rules/util/selector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,EAAW;IACjC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,IAAI,GAAmB,EAAE,CAAC;IAC9B,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;YACZ,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;YAC7B,MAAM;QACR,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;QAC1C,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,13 @@
1
+ /** 공백을 정규화한다(연속 공백·줄바꿈 → 단일 스페이스, 양끝 trim). */
2
+ export function normText(text) {
3
+ return text.replace(/\s+/g, " ").trim();
4
+ }
5
+ /** 한글(가-힣) 문자 비율을 계산한다(공백·구두점 제외 문자 기준). */
6
+ export function hangulRatio(text) {
7
+ const chars = text.replace(/\s/g, "");
8
+ if (chars.length === 0)
9
+ return 0;
10
+ const hangul = chars.match(/[가-힣]/g)?.length ?? 0;
11
+ return hangul / chars.length;
12
+ }
13
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../../../src/rules/util/text.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAC1C,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACtC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC;IAClD,OAAO,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 도구 스키마의 JSON Schema 방언을 최신 MCP 클라이언트와 호환되게 맞춘다.
3
+ *
4
+ * 배경: `@modelcontextprotocol/sdk` 1.30.0의 `McpServer`는 zod 스키마를 JSON Schema로
5
+ * 변환할 때 방언 target을 지정하지 않아 `zod-to-json-schema` 기본값인 **draft-07**로
6
+ * `$schema`를 방출한다(server/mcp.js의 tools/list 핸들러). 최신 MCP 클라이언트는 JSON
7
+ * Schema **2020-12**만 검증하므로, draft-07을 선언한 도구 outputSchema를 거부한다
8
+ * ("unsupported dialect ($schema: http://json-schema.org/draft-07/schema#)").
9
+ *
10
+ * 도구 스키마 본문은 두 방언에서 동일한 키워드(type·properties·required·enum·min/max
11
+ * 등)만 쓰므로, tools/list 응답에서 최상위 `$schema` 선언만 제거하면 클라이언트가 기본
12
+ * 방언(2020-12)으로 해석해 통과한다. SDK가 변환 target을 지정하는 공개 API를 노출하지
13
+ * 않으므로, 응답을 후처리하는 이 방식이 가장 작은 수정이다(EASYREAD 동일 이슈 대응).
14
+ *
15
+ * 반드시 도구 등록 **전**에 호출한다. SDK가 tools/list 핸들러를 설치하는 순간을 가로채
16
+ * 응답을 감싸야 하기 때문이다.
17
+ */
18
+ export function normalizeToolSchemaDialect(server) {
19
+ const low = server.server;
20
+ const originalSetRequestHandler = low.setRequestHandler.bind(low);
21
+ low.setRequestHandler = (schema, handler) => {
22
+ originalSetRequestHandler(schema, async (...args) => {
23
+ const result = await handler(...args);
24
+ const tools = result?.tools;
25
+ if (Array.isArray(tools)) {
26
+ for (const tool of tools) {
27
+ stripDollarSchema(tool.inputSchema);
28
+ stripDollarSchema(tool.outputSchema);
29
+ }
30
+ }
31
+ return result;
32
+ });
33
+ };
34
+ }
35
+ /** JSON Schema 객체의 최상위 `$schema` 방언 선언을 제거한다(있을 때만). */
36
+ function stripDollarSchema(schema) {
37
+ if (typeof schema === "object" && schema !== null && "$schema" in schema) {
38
+ delete schema.$schema;
39
+ }
40
+ }
41
+ //# sourceMappingURL=schema-dialect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-dialect.js","sourceRoot":"","sources":["../src/schema-dialect.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,0BAA0B,CAAC,MAAiB;IAK1D,MAAM,GAAG,GAAG,MAAM,CAAC,MAAmC,CAAC;IACvD,MAAM,yBAAyB,GAAG,GAAG,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClE,GAAG,CAAC,iBAAiB,GAAG,CAAC,MAAe,EAAE,OAAuB,EAAQ,EAAE;QACzE,yBAAyB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,IAAe,EAAoB,EAAE;YAC/E,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;YACtC,MAAM,KAAK,GAAI,MAAmD,EAAE,KAAK,CAAC;YAC1E,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBACzB,iBAAiB,CAAE,IAAkC,CAAC,WAAW,CAAC,CAAC;oBACnE,iBAAiB,CAAE,IAAmC,CAAC,YAAY,CAAC,CAAC;gBACvE,CAAC;YACH,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED,wDAAwD;AACxD,SAAS,iBAAiB,CAAC,MAAe;IACxC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,SAAS,IAAI,MAAM,EAAE,CAAC;QACzE,OAAQ,MAAkC,CAAC,OAAO,CAAC;IACrD,CAAC;AACH,CAAC"}
package/dist/server.js ADDED
@@ -0,0 +1,36 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { normalizeToolSchemaDialect } from "./schema-dialect.js";
3
+ import { registerLookupTool } from "./tools/lookup.js";
4
+ import { registerChecklistTool } from "./tools/checklist.js";
5
+ import { registerCheckHtmlTool } from "./tools/check-html.js";
6
+ import { registerCheckContrastTool } from "./tools/check-contrast.js";
7
+ import { registerAuditUrlTool } from "./tools/audit-url.js";
8
+ import { registerBrowserStatusTool } from "./tools/browser-status.js";
9
+ import { registerCertReadinessTool } from "./tools/cert-readiness.js";
10
+ import { registerReviewMarkupPrompt } from "./prompts/review-markup.js";
11
+ import { registerAuditReportPrompt } from "./prompts/audit-report.js";
12
+ import { registerResources } from "./resources/index.js";
13
+ /**
14
+ * SWWA MCP 서버를 조립한다. 도구·프롬프트·리소스 등록만 담당하고 로직은 갖지 않는다(03 §1).
15
+ * 도구 7개(lookup_checkpoint·get_checklist·check_html·check_contrast·audit_url·browser_status·
16
+ * estimate_cert_readiness)·프롬프트 2종(review-markup·audit-report)·리소스 6종을 등록한다
17
+ * (계약: docs/plan/02-architecture.md §3).
18
+ */
19
+ export function createServer(data) {
20
+ const server = new McpServer({ name: "swwa", version: "0.1.0" });
21
+ // SDK가 도구 스키마를 draft-07로 방출 → 최신 클라이언트(2020-12) 호환되게 정규화.
22
+ // 반드시 도구 등록 전에 호출한다(tools/list 핸들러 설치 시점을 가로채기 위함).
23
+ normalizeToolSchemaDialect(server);
24
+ registerLookupTool(server, data);
25
+ registerChecklistTool(server, data);
26
+ registerCheckHtmlTool(server, data);
27
+ registerCheckContrastTool(server);
28
+ registerAuditUrlTool(server, data);
29
+ registerBrowserStatusTool(server);
30
+ registerCertReadinessTool(server, data);
31
+ registerReviewMarkupPrompt(server);
32
+ registerAuditReportPrompt(server);
33
+ registerResources(server, data);
34
+ return server;
35
+ }
36
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,IAAgB;IAC3C,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACjE,0DAA0D;IAC1D,oDAAoD;IACpD,0BAA0B,CAAC,MAAM,CAAC,CAAC;IAEnC,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACjC,qBAAqB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACpC,qBAAqB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACpC,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAClC,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACnC,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAClC,yBAAyB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAExC,0BAA0B,CAAC,MAAM,CAAC,CAAC;IACnC,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAElC,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAEhC,OAAO,MAAM,CAAC;AAChB,CAAC"}