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,114 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { chromium } from "playwright-core";
4
+ import { AxeBuilder } from "@axe-core/playwright";
5
+ import { detectBrowser } from "./browser-detect.js";
6
+ import { normalizeAxeResults } from "../normalize/axe.js";
7
+ import { BRULES } from "../rules/registry.js";
8
+ /** "keyboard" 체크 = Tab 순회 기반 초점 표시·순서·바로가기 동작(§3.1 audit_url). 나머지는 "b-rules". */
9
+ const KEYBOARD_RULE_IDS = new Set(["b-focus-visible", "b-focus-order", "b-skip-link-works"]);
10
+ const EXPERIMENTAL_AXE_RULES = ["label-content-name-mismatch"];
11
+ const TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"];
12
+ const VIEWPORTS = {
13
+ desktop: { width: 1280, height: 800 },
14
+ mobile: { width: 375, height: 812 },
15
+ };
16
+ export class NoBrowserError extends Error {
17
+ }
18
+ export class NavigationError extends Error {
19
+ }
20
+ export class BrowserTimeoutError extends Error {
21
+ }
22
+ /**
23
+ * playwright-core + @axe-core/playwright + b-규칙으로 브라우저 감사를 수행한다(03 §6).
24
+ * 도구 호출마다 브라우저를 새로 열고 닫는다(단순·안전, 성능 개선은 백로그).
25
+ */
26
+ export async function runBrowser(opts) {
27
+ const status = await detectBrowser();
28
+ if (!status.available || !status.channel) {
29
+ throw new NoBrowserError(status.installHint);
30
+ }
31
+ const browser = await chromium.launch(status.channel === "chromium" ? { headless: true } : { channel: status.channel, headless: true });
32
+ try {
33
+ const viewport = opts.viewport ?? "desktop";
34
+ const context = await browser.newContext({
35
+ viewport: VIEWPORTS[viewport],
36
+ isMobile: viewport === "mobile",
37
+ hasTouch: viewport === "mobile",
38
+ extraHTTPHeaders: opts.headers,
39
+ ignoreHTTPSErrors: false,
40
+ });
41
+ const page = await context.newPage();
42
+ const timeoutMs = opts.timeoutMs ?? 30000;
43
+ try {
44
+ const waitUntil = opts.waitFor === "networkidle" ? "networkidle" : "load";
45
+ await page.goto(opts.url, { timeout: timeoutMs, waitUntil });
46
+ if (opts.waitFor && opts.waitFor !== "load" && opts.waitFor !== "networkidle") {
47
+ await page.waitForSelector(opts.waitFor, { timeout: timeoutMs });
48
+ }
49
+ await page.waitForTimeout(500); // waitFor 이후 렌더링 안정화 여유
50
+ }
51
+ catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ if (/timeout/i.test(message))
54
+ throw new BrowserTimeoutError(message);
55
+ throw new NavigationError(message);
56
+ }
57
+ const checks = opts.checks ?? ["axe", "b-rules", "keyboard"];
58
+ const findings = [];
59
+ const excludeRules = opts.excludeRules ?? [];
60
+ if (checks.includes("axe")) {
61
+ const mainResults = await new AxeBuilder({ page })
62
+ .withTags(TAGS)
63
+ .disableRules(excludeRules)
64
+ .options({ resultTypes: ["violations", "incomplete"] })
65
+ .analyze();
66
+ findings.push(...normalizeAxeResults(mainResults, opts.data.axeRuleMap, opts.data.wcag22));
67
+ const experimentalIds = EXPERIMENTAL_AXE_RULES.filter((id) => !excludeRules.includes(id));
68
+ if (experimentalIds.length > 0) {
69
+ const experimentalResults = await new AxeBuilder({ page })
70
+ .withRules(experimentalIds)
71
+ .options({ resultTypes: ["violations", "incomplete"] })
72
+ .analyze();
73
+ findings.push(...normalizeAxeResults(experimentalResults, opts.data.axeRuleMap, opts.data.wcag22));
74
+ }
75
+ }
76
+ const activeRuleIds = new Set();
77
+ if (checks.includes("keyboard"))
78
+ for (const id of KEYBOARD_RULE_IDS)
79
+ activeRuleIds.add(id);
80
+ if (checks.includes("b-rules")) {
81
+ for (const rule of BRULES)
82
+ if (!KEYBOARD_RULE_IDS.has(rule.id))
83
+ activeRuleIds.add(rule.id);
84
+ }
85
+ const ruleCtx = { page, data: opts.data, viewport };
86
+ for (const rule of BRULES) {
87
+ if (!activeRuleIds.has(rule.id) || excludeRules.includes(rule.id))
88
+ continue;
89
+ try {
90
+ findings.push(...(await rule.run(ruleCtx)));
91
+ }
92
+ catch {
93
+ // 규칙별 실패 격리(03 §6) — 개별 규칙 오류가 전체 리포트를 막지 않는다.
94
+ }
95
+ }
96
+ let screenshotPath;
97
+ if (opts.screenshot && opts.outputDir) {
98
+ mkdirSync(opts.outputDir, { recursive: true });
99
+ screenshotPath = join(opts.outputDir, `screenshot-${Date.now()}.png`);
100
+ await page.screenshot({ path: screenshotPath, fullPage: true });
101
+ }
102
+ return {
103
+ findings,
104
+ title: (await page.title()) || undefined,
105
+ finalUrl: page.url(),
106
+ browser: { channel: status.channel, version: status.version ?? "" },
107
+ screenshotPath,
108
+ };
109
+ }
110
+ finally {
111
+ await browser.close();
112
+ }
113
+ }
114
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/engine/browser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ // 정적 검사 워커 엔트리(03 §5.1). runStatic이 dist/engine/static-worker.js로 스폰한다.
2
+ // workerData로 입력을 받아 검사 후 결과를 한 번 postMessage하고 종료한다. 타임아웃 초과 시
3
+ // 메인 스레드가 worker.terminate()로 강제 종료하므로, 여기서는 별도 타임아웃을 두지 않는다.
4
+ import { parentPort, workerData } from "node:worker_threads";
5
+ import { runStaticFromWorkerInput } from "./static.js";
6
+ async function main() {
7
+ if (!parentPort)
8
+ return;
9
+ try {
10
+ const result = await runStaticFromWorkerInput(workerData);
11
+ parentPort.postMessage({ ok: true, result });
12
+ }
13
+ catch (err) {
14
+ parentPort.postMessage({ ok: false, error: err instanceof Error ? err.message : String(err) });
15
+ }
16
+ }
17
+ void main();
18
+ //# sourceMappingURL=static-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-worker.js","sourceRoot":"","sources":["../../src/engine/static-worker.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,gEAAgE;AAChE,8DAA8D;AAC9D,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAA0B,MAAM,aAAa,CAAC;AAE/E,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC,UAAU;QAAE,OAAO;IACxB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAAC,UAA+B,CAAC,CAAC;QAC/E,UAAU,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,UAAU,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACjG,CAAC;AACH,CAAC;AAED,KAAK,IAAI,EAAE,CAAC"}
@@ -0,0 +1,139 @@
1
+ import { existsSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { Worker } from "node:worker_threads";
4
+ import { JSDOM } from "jsdom";
5
+ import axeCore from "axe-core";
6
+ import { ALL_K_RULES } from "../rules/registry.js";
7
+ import { normalizeAxeResults } from "../normalize/axe.js";
8
+ import { loadDataBundle } from "../data/loader.js";
9
+ /** 렌더링이 필요해 정적(jsdom) 모드에서 판정할 수 없는 axe 규칙(02-architecture §3.1). */
10
+ export const STATIC_DISABLED_RULES = [
11
+ "color-contrast",
12
+ "color-contrast-enhanced",
13
+ "link-in-text-block",
14
+ "target-size",
15
+ "scrollable-region-focusable",
16
+ "no-autoplay-audio",
17
+ "frame-tested",
18
+ "css-orientation-lock",
19
+ ];
20
+ const TAGS = {
21
+ kwcag22: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"],
22
+ wcag22aa: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"],
23
+ };
24
+ const ENGINE_TIMEOUT_MS = 10_000;
25
+ class EngineTimeoutError extends Error {
26
+ }
27
+ function withTimeout(promise, ms) {
28
+ return Promise.race([
29
+ promise,
30
+ new Promise((_, reject) => {
31
+ setTimeout(() => reject(new EngineTimeoutError("정적 검사 시간이 초과되었습니다.")), ms);
32
+ }),
33
+ ]);
34
+ }
35
+ export { EngineTimeoutError };
36
+ /**
37
+ * jsdom + axe-core(ko) + k-규칙으로 정적 검사를 수행한다(03 §5). 페이지 <script>는 실행하지
38
+ * 않고(runScripts: "outside-only") axe만 주입한다. 규칙별 실패는 개별 try/catch로 격리해
39
+ * 한 규칙의 오류가 전체 리포트를 막지 않는다.
40
+ *
41
+ * 이 함수 자체는 하드 타임아웃을 갖지 않는다 — axe가 이벤트 루프를 동기적으로 점유하면
42
+ * setTimeout이 선점하지 못하기 때문이다. 강제 종료 가능한 타임아웃은 워커에서 실행하는
43
+ * `runStatic`이 담당한다(static-worker.ts).
44
+ */
45
+ export async function runStaticCore(opts) {
46
+ const ruleset = opts.ruleset ?? "kwcag22";
47
+ const dom = new JSDOM(opts.html, {
48
+ url: opts.baseUrl ?? "http://localhost/",
49
+ runScripts: "outside-only",
50
+ pretendToBeVisual: true,
51
+ });
52
+ const { window } = dom;
53
+ const notices = [];
54
+ try {
55
+ window.eval(axeCore.source);
56
+ const axe = window.axe;
57
+ const disabledRules = new Set([...STATIC_DISABLED_RULES, ...(opts.excludeRules ?? [])]);
58
+ const rulesConfig = Object.fromEntries([...disabledRules].map((id) => [id, { enabled: false }]));
59
+ // resultTypes에 "passes"를 넣으면 axe-core가 모든 통과 사례까지 리포트하느라 대규모 문서에서
60
+ // 수십 배 느려진다(500행 표 기준 <1초 → 50초+ 실측). checkpoint 상태 계산은 단일 소스의
61
+ // axeRules 목록으로 "실행 가능 여부"를 판단하므로(normalize/checkpoints.ts) axe의 실제
62
+ // passes 결과가 필요 없다 — violations·incomplete만 요청한다.
63
+ const axeResults = await axe.run(window.document, {
64
+ runOnly: { type: "tag", values: TAGS[ruleset] },
65
+ rules: rulesConfig,
66
+ resultTypes: ["violations", "incomplete"],
67
+ });
68
+ // axe-core의 실제 결과 타입(AxeResults)은 target을 크로스트리·섀도우 DOM까지 포괄하는
69
+ // 복잡한 유니언으로 선언한다. 이 프로젝트는 일반적인 문자열 선택자 배열만 다루므로 우리
70
+ // 쪽 좁은 타입(AxeRunResults)으로 단언한다.
71
+ const axeFindings = normalizeAxeResults(axeResults, opts.data.axeRuleMap, opts.data.wcag22);
72
+ const kFindings = [];
73
+ if (ruleset === "kwcag22") {
74
+ const ctx = {
75
+ document: window.document,
76
+ window,
77
+ html: opts.html,
78
+ baseUrl: opts.baseUrl,
79
+ data: opts.data,
80
+ };
81
+ for (const rule of ALL_K_RULES) {
82
+ if (opts.excludeRules?.includes(rule.id))
83
+ continue;
84
+ try {
85
+ kFindings.push(...rule.run(ctx));
86
+ }
87
+ catch (err) {
88
+ notices.push(`규칙 ${rule.id} 실행 중 오류가 발생해 건너뜁니다: ${err instanceof Error ? err.message : String(err)}`);
89
+ }
90
+ }
91
+ }
92
+ const title = window.document.title || undefined;
93
+ return { findings: [...axeFindings, ...kFindings], title, notices };
94
+ }
95
+ finally {
96
+ window.close();
97
+ }
98
+ }
99
+ function runStaticInWorker(workerPath, input, timeoutMs) {
100
+ return new Promise((resolve, reject) => {
101
+ const worker = new Worker(workerPath, { workerData: input });
102
+ let settled = false;
103
+ const finish = (fn) => {
104
+ if (settled)
105
+ return;
106
+ settled = true;
107
+ clearTimeout(timer);
108
+ void worker.terminate();
109
+ fn();
110
+ };
111
+ const timer = setTimeout(() => {
112
+ finish(() => reject(new EngineTimeoutError("정적 검사 시간이 초과되었습니다.")));
113
+ }, timeoutMs);
114
+ worker.once("message", (msg) => {
115
+ finish(() => (msg.ok ? resolve(msg.result) : reject(new Error(msg.error))));
116
+ });
117
+ worker.once("error", (err) => finish(() => reject(err)));
118
+ });
119
+ }
120
+ /**
121
+ * 정적 검사를 실행한다. 컴파일된 워커(dist/engine/static-worker.js)가 있으면 워커에서 실행하고
122
+ * 하드 타임아웃 초과 시 worker.terminate()로 강제 종료한다(동기 점유도 확실히 중단). 워커가 없는
123
+ * 개발(tsx)·테스트(vitest src 실행) 환경에서는 인라인으로 실행하되 best-effort 소프트 타임아웃만
124
+ * 적용한다(03 §5.1). 어느 경로든 초과 시 EngineTimeoutError를 던진다.
125
+ */
126
+ export async function runStatic(opts) {
127
+ const timeoutMs = opts.timeoutMs ?? ENGINE_TIMEOUT_MS;
128
+ const workerPath = fileURLToPath(new URL("./static-worker.js", import.meta.url));
129
+ if (existsSync(workerPath)) {
130
+ return runStaticInWorker(workerPath, { html: opts.html, baseUrl: opts.baseUrl, ruleset: opts.ruleset, excludeRules: opts.excludeRules }, timeoutMs);
131
+ }
132
+ return withTimeout(runStaticCore(opts), timeoutMs);
133
+ }
134
+ /** 워커 엔트리(static-worker.ts)가 호출한다. 자체적으로 DataBundle을 로드해 클론 불가 문제를 피한다. */
135
+ export async function runStaticFromWorkerInput(input) {
136
+ const data = loadDataBundle();
137
+ return runStaticCore({ ...input, data });
138
+ }
139
+ //# sourceMappingURL=static.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static.js","sourceRoot":"","sources":["../../src/engine/static.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ // audit_url이 허용하는 스킴·호스트를 검사한다(NFR-07). localhost·사설 IP는 로컬 개발 서버
2
+ // 감사가 1차 시나리오이므로 허용하고, 링크-로컬·클라우드 메타데이터 호스트만 차단한다.
3
+ const BLOCKED_HOSTS = new Set(["metadata.google.internal"]);
4
+ const ALLOWED_PROTOCOLS = new Set(["http:", "https:", "file:"]);
5
+ export class BlockedUrlError extends Error {
6
+ }
7
+ function isLinkLocalV4(hostname) {
8
+ return /^169\.254\.\d{1,3}\.\d{1,3}$/.test(hostname);
9
+ }
10
+ function isBlockedV6(hostname) {
11
+ const h = hostname.toLowerCase();
12
+ return h.startsWith("fe80:") || /^fd[0-9a-f]{2}:/.test(h) || h === "::ffff:169.254.169.254";
13
+ }
14
+ /** 스킴·호스트를 검사하고 통과하면 URL을 반환한다. 차단 시 BlockedUrlError를 던진다. */
15
+ export function assertUrlAllowed(rawUrl) {
16
+ let url;
17
+ try {
18
+ url = new URL(rawUrl);
19
+ }
20
+ catch {
21
+ throw new BlockedUrlError(`URL을 해석할 수 없습니다: ${rawUrl}`);
22
+ }
23
+ if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
24
+ throw new BlockedUrlError(`허용되지 않는 스킴입니다: ${url.protocol}`);
25
+ }
26
+ if (url.protocol === "file:")
27
+ return url; // 로컬 파일은 호스트 개념이 없다
28
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
29
+ if (BLOCKED_HOSTS.has(hostname.toLowerCase())) {
30
+ throw new BlockedUrlError(`차단된 호스트입니다: ${hostname}`);
31
+ }
32
+ if (isLinkLocalV4(hostname) || isBlockedV6(hostname)) {
33
+ throw new BlockedUrlError(`링크-로컬·메타데이터 호스트는 차단됩니다: ${hostname}`);
34
+ }
35
+ return url;
36
+ }
37
+ //# sourceMappingURL=url-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-guard.js","sourceRoot":"","sources":["../../src/engine/url-guard.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,mDAAmD;AACnD,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC;AAC5D,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAEhE,MAAM,OAAO,eAAgB,SAAQ,KAAK;CAAG;AAE7C,SAAS,aAAa,CAAC,QAAgB;IACrC,OAAO,8BAA8B,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACvD,CAAC;AAED,SAAS,WAAW,CAAC,QAAgB;IACnC,MAAM,CAAC,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACjC,OAAO,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,wBAAwB,CAAC;AAC9F,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,eAAe,CAAC,oBAAoB,MAAM,EAAE,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,eAAe,CAAC,kBAAkB,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,GAAG,CAAC,CAAC,oBAAoB;IAE9D,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IACtD,IAAI,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,eAAe,CAAC,eAAe,QAAQ,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,aAAa,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,eAAe,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SWWA MCP 서버 엔트리. stdio 트랜스포트로 기동한다.
4
+ * 계약: docs/plan/02-architecture.md §3 · 구현 방침: docs/plan/03-backend-plan.md
5
+ *
6
+ * 로깅 규약: stdout은 JSON-RPC 채널이므로 절대 쓰지 않는다. 로그는 stderr(console.error)만,
7
+ * 그리고 입력·페이지 내용은 저장·전송하지 않는다(NFR-01·03).
8
+ */
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { loadDataBundle } from "./data/loader.js";
11
+ import { createServer } from "./server.js";
12
+ async function main() {
13
+ const data = loadDataBundle();
14
+ const server = createServer(data);
15
+ const transport = new StdioServerTransport();
16
+ await server.connect(transport);
17
+ }
18
+ main().catch((error) => {
19
+ const detail = error instanceof Error ? error.message : "";
20
+ console.error(`[swwa] 서버를 시작하지 못했습니다. ${detail}`);
21
+ process.exit(1);
22
+ });
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3D,OAAO,CAAC,KAAK,CAAC,0BAA0B,MAAM,EAAE,CAAC,CAAC;IAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,176 @@
1
+ /**
2
+ * 사용자 대면 문자열(규칙 메시지·fix 안내)을 이 파일에 집중시킨다(03 §1). 도구 오류 문구는
3
+ * 각 tools/*.ts가 직접 담당한다(오류 코드별로 짧고 도구에 종속적이라 여기 두지 않는다).
4
+ */
5
+ export const altMeaningless = {
6
+ message: (alt) => `이미지의 대체 텍스트가 적절하지 않습니다("${alt}").`,
7
+ fix: 'alt 속성에 이미지의 의미나 용도를 설명하는 텍스트를 넣으세요. 순수 장식용 이미지라면 alt=""로 비워 두세요.',
8
+ };
9
+ export const mediaTrack = {
10
+ message: "동영상에 자막 트랙이 없습니다.",
11
+ fix: '<track kind="captions" src="자막.vtt" srclang="ko">를 <video> 안에 추가하세요.',
12
+ };
13
+ export const tableCaption = {
14
+ message: "데이터 표에 caption이 없습니다.",
15
+ fix: "<table> 바로 안에 <caption>표 제목</caption>을 추가하세요.",
16
+ };
17
+ export const tableThMissing = {
18
+ message: "데이터 표에 제목 셀(th)이 없습니다.",
19
+ fix: "열 또는 행의 첫 셀을 <th scope=\"col\">(또는 scope=\"row\")로 바꾸세요.",
20
+ };
21
+ export const tabindexPositive = {
22
+ message: (value) => `tabindex="${value}"처럼 양수 tabindex는 초점 순서를 예측하기 어렵게 만듭니다.`,
23
+ fix: "tabindex는 0 또는 -1만 사용하고, 초점 순서는 DOM 순서로 조정하세요.",
24
+ };
25
+ export const autoplayMedia = {
26
+ message: "음소거 없이 자동 재생되는 미디어가 있습니다.",
27
+ fix: "autoplay를 제거하거나 muted 속성을 추가하세요.",
28
+ };
29
+ export const mouseOnlyHandler = {
30
+ message: "클릭 핸들러만 있고 키보드로 접근할 수 없는 요소입니다.",
31
+ fix: "button 등 네이티브 상호작용 요소를 쓰거나, tabindex=\"0\"과 role·keydown 핸들러를 추가하세요.",
32
+ };
33
+ export const skipLinkFirst = {
34
+ message: "문서의 첫 초점 요소가 본문 바로가기 링크가 아닙니다.",
35
+ fix: "페이지 최상단에 본문(#main 등)으로 이동하는 바로가기 링크를 첫 초점 요소로 추가하세요.",
36
+ };
37
+ export const skipTargetExists = {
38
+ message: (href) => `바로가기 링크의 대상(${href})이 문서에 없습니다.`,
39
+ fix: "링크 href가 가리키는 id를 실제 요소에 부여하세요.",
40
+ };
41
+ export const titleGeneric = {
42
+ message: (title) => `문서 제목이 비어 있거나 무의미합니다("${title}").`,
43
+ fix: "페이지 내용을 설명하는 구체적인 제목을 <title>에 넣으세요(예: \"2026년 예산안 - 사이트명\").",
44
+ };
45
+ export const iframeTitle = {
46
+ message: "iframe에 title이 없거나 무의미합니다.",
47
+ fix: 'iframe이 담고 있는 내용을 설명하는 title 속성을 추가하세요(예: title="찾아오시는 길 지도").',
48
+ };
49
+ export const linkTextGeneric = {
50
+ message: (text) => `링크 텍스트가 일반적이라 목적을 알 수 없습니다("${text}").`,
51
+ fix: "링크 텍스트에 목적지·내용을 포함시키거나 aria-label로 보완하세요.",
52
+ };
53
+ export const langKoExpected = {
54
+ message: (lang) => `본문이 대부분 한국어인데 html lang="${lang}"입니다.`,
55
+ fix: '<html lang="ko">로 지정하세요.',
56
+ };
57
+ export const newWindowNotice = {
58
+ message: "새 창으로 열리는 링크에 안내가 없습니다.",
59
+ fix: '링크 텍스트·aria-label 등에 "(새 창)" 안내를 추가하세요.',
60
+ };
61
+ export const selectOnchange = {
62
+ message: "select의 onchange가 페이지 이동·제출을 일으키는 것으로 보입니다(수동 확인 필요).",
63
+ fix: "select 변경만으로 이동·제출하지 말고, 별도의 확인 버튼을 함께 제공하세요.",
64
+ };
65
+ export const placeholderOnlyLabel = {
66
+ message: "입력 필드에 placeholder만 있고 레이블이 없습니다.",
67
+ fix: "<label for>·aria-label·aria-labelledby 중 하나로 레이블을 연결하세요.",
68
+ };
69
+ export const captchaDetect = {
70
+ message: "캡차로 보이는 인증 수단이 감지되었습니다(대체 인증 수단 수동 확인 필요).",
71
+ fix: "이미지 캡차만 제공하지 말고 오디오 캡차 등 대체 인증 수단을 함께 제공하세요.",
72
+ };
73
+ // --- T2 규칙(휴리스틱, 대부분 outcome incomplete = 사람 확인 필요) ---
74
+ export const sensoryInstruction = {
75
+ message: (word) => `감각적 특성("${word}")에만 의존한 지시일 수 있습니다.`,
76
+ fix: "모양·위치·색·소리 대신(또는 함께) 텍스트 라벨로 대상을 지칭하세요.",
77
+ };
78
+ export const outlineNone = {
79
+ message: "초점 스타일(:focus)에서 outline을 제거하고 대안이 없어 보입니다.",
80
+ fix: ":focus/:focus-visible에 outline 또는 box-shadow·border 등 대체 초점 표시를 제공하세요.",
81
+ };
82
+ export const accesskey = {
83
+ message: (key) => `accesskey="${key}"가 사용되었습니다(단축키 충돌·오동작 검토 필요).`,
84
+ fix: "문자 단축키는 끄거나 재설정할 수 있게 하고, 다른 단축키와 충돌하지 않는지 확인하세요.",
85
+ };
86
+ export const sessionTimeoutHint = {
87
+ message: "세션 만료·자동 로그아웃 안내로 보이는 문구가 있습니다.",
88
+ fix: "시간제한이 있다면 연장·해제 수단(경고 후 연장 버튼 등)을 제공하는지 확인하세요.",
89
+ };
90
+ export const carouselNoPause = {
91
+ message: "자동으로 넘어가는 슬라이더/캐러셀에 정지 컨트롤이 없어 보입니다.",
92
+ fix: "일시정지/정지 버튼을 제공하세요. (실제 자동 전환 여부는 브라우저 감사로 확인)",
93
+ };
94
+ export const flashAnimation = {
95
+ message: "짧은 주기로 무한 반복되는 애니메이션이 있습니다(깜빡임 검토 필요).",
96
+ fix: "초당 3~50회 점멸에 해당하지 않는지 확인하고, 필요하면 주기를 늘리거나 반복을 제한하세요.",
97
+ };
98
+ export const linkSameTextDiffHref = {
99
+ message: (text) => `같은 링크 텍스트("${text}")가 서로 다른 곳을 가리킵니다.`,
100
+ fix: "링크 텍스트에 목적지를 구분하는 정보를 넣거나 aria-label로 구별하세요.",
101
+ };
102
+ export const gestureListener = {
103
+ message: "다중 포인터·경로 기반 제스처 처리로 보이는 코드가 있습니다.",
104
+ fix: "스와이프·핀치 등 제스처 전용 기능에 단일 탭·버튼 대안을 제공하세요.",
105
+ };
106
+ export const downEventAction = {
107
+ message: "포인터를 누르는 시점(down)에 기능이 실행될 수 있습니다.",
108
+ fix: "down이 아니라 클릭 완료(up) 시점에 실행하고, 이동해 벗어나면 취소되게 하세요.",
109
+ };
110
+ export const deviceMotion = {
111
+ message: "기기 동작(흔들기·기울이기) 이벤트를 사용하는 것으로 보입니다.",
112
+ fix: "동작 기반 기능에 UI 컨트롤 대안을 제공하고 비활성화할 수 있게 하세요.",
113
+ };
114
+ export const onloadPopup = {
115
+ message: "페이지 로드 시 새 창(window.open)을 여는 코드로 보입니다.",
116
+ fix: "사용자가 의도하지 않은 새 창을 자동으로 열지 마세요.",
117
+ };
118
+ export const errorAssociation = {
119
+ message: "aria-invalid 입력에 오류 메시지 연결(aria-describedby)이 없습니다.",
120
+ fix: "오류 메시지 요소의 id를 입력의 aria-describedby로 연결하세요.",
121
+ };
122
+ export const autocompleteMissing = {
123
+ message: (field) => `개인정보 입력 필드(${field})에 autocomplete 속성이 없습니다.`,
124
+ fix: "이름·이메일·전화·주소 등에는 표준 autocomplete 값을 지정해 자동 입력을 돕습니다.",
125
+ };
126
+ export const bFocusVisible = {
127
+ message: "초점을 받아도 시각적으로 구별되는 스타일 변화가 없습니다.",
128
+ fix: ":focus 또는 :focus-visible에 outline·box-shadow 등 눈에 띄는 스타일을 추가하세요.",
129
+ };
130
+ export const bFocusOrder = {
131
+ message: "키보드 초점 순서가 문서(DOM) 순서와 어긋납니다.",
132
+ fix: "tabindex 양수값 사용을 피하고 DOM 순서로 자연스러운 초점 이동이 되도록 조정하세요.",
133
+ };
134
+ export const bSkipLinkWorks = {
135
+ message: "본문 바로가기 링크를 실행해도 초점이 대상으로 이동하지 않습니다.",
136
+ fix: '바로가기 대상 요소에 tabindex="-1"을 추가해 프로그래밍적으로 초점을 받을 수 있게 하세요.',
137
+ };
138
+ export const bTargetSize = {
139
+ message: (px) => `상호작용 요소의 클릭 가능 영역이 너무 작습니다(대각선 약 ${px}px, 기준 22.7px 미만).`,
140
+ fix: "버튼·링크 등의 클릭 가능 영역을 최소 24×24px(6mm) 이상으로 키우세요.",
141
+ };
142
+ export const bKeyboardReachable = {
143
+ message: "클릭·역할(role)만으로 상호작용하는 요소가 Tab 순회로 접근되지 않습니다.",
144
+ fix: 'tabindex="0"을 추가하고 키보드 이벤트(Enter·Space) 핸들러를 함께 구현하세요.',
145
+ };
146
+ export const bMotionRuntime = {
147
+ message: "자동으로 계속 변경되는 콘텐츠에 정지·일시정지 컨트롤이 없습니다.",
148
+ fix: '"일시정지" 버튼 등 사용자가 자동 변경을 멈출 수 있는 컨트롤을 추가하세요.',
149
+ };
150
+ export const parseErrors = {
151
+ "duplicate-attribute": {
152
+ message: "같은 속성이 한 태그에 중복 선언되었습니다.",
153
+ fix: "중복된 속성 중 하나만 남기세요.",
154
+ },
155
+ "end-tag-with-attributes": {
156
+ message: "닫는 태그에 속성이 있습니다.",
157
+ fix: "닫는 태그(</tag>)에는 속성을 쓰지 마세요.",
158
+ },
159
+ "end-tag-without-matching-open-element": {
160
+ message: "짝이 맞지 않는 닫는 태그가 있습니다.",
161
+ fix: "여는 태그와 순서·짝을 맞추세요.",
162
+ },
163
+ "closing-of-element-with-open-child-elements": {
164
+ message: "자식 요소가 열려 있는 상태에서 부모 요소가 닫혔습니다(태그 중첩 오류).",
165
+ fix: "태그를 연 순서의 역순으로 닫으세요.",
166
+ },
167
+ "unexpected-character-in-attribute-name": {
168
+ message: "속성 이름에 예상치 못한 문자가 있습니다.",
169
+ fix: "속성 이름·값 앞뒤에 공백·따옴표를 올바르게 넣으세요.",
170
+ },
171
+ "missing-attribute-value": {
172
+ message: "속성 값이 비어 있습니다.",
173
+ fix: '속성에 값을 지정하세요(예: attr="value").',
174
+ },
175
+ };
176
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import { localizeAxeMessage } from "./locale.js";
2
+ const HTML_SNIPPET_MAX = 300;
3
+ /** "1.4.11" → "wcag1411"(axe 태그 관례). 실제 SC 목록에서만 역산하므로 자리수 모호성이 없다. */
4
+ function tagForSc(sc) {
5
+ return `wcag${sc.replace(/\./g, "")}`;
6
+ }
7
+ /**
8
+ * axe 규칙의 KWCAG 귀속을 정한다(ADR-06). axe-rule-map.json에 오버라이드가 있으면 그것을
9
+ * 그대로 쓰고(명시적 null 포함 — "매핑 없음"으로 확정된 경우), 없으면 규칙의 wcag 태그로
10
+ * kwcagIds를 역추적한다. 그래도 없으면 null("기타(WCAG)" 그룹).
11
+ */
12
+ export function resolveKwcag(ruleId, tags, axeRuleMap, wcag22) {
13
+ const override = axeRuleMap[ruleId];
14
+ if (override && override.kwcag !== undefined)
15
+ return override.kwcag;
16
+ const tagSet = new Set(tags);
17
+ for (const criterion of wcag22.criteria) {
18
+ if (criterion.kwcagIds.length === 0)
19
+ continue;
20
+ if (tagSet.has(tagForSc(criterion.sc)))
21
+ return criterion.kwcagIds[0] ?? null;
22
+ }
23
+ return null;
24
+ }
25
+ const IMPACT_FALLBACK = "moderate";
26
+ const VALID_IMPACTS = new Set(["critical", "serious", "moderate", "minor"]);
27
+ function normalizeImpact(impact) {
28
+ return impact && VALID_IMPACTS.has(impact) ? impact : IMPACT_FALLBACK;
29
+ }
30
+ /** wcag 태그("wcag111" 등)를 실제 SC 목록에 대조해 "1.1.1" 형태로 되돌린다(자리수 모호성 회피). */
31
+ function tagsToScList(tags, wcag22) {
32
+ const tagSet = new Set(tags);
33
+ const scs = [];
34
+ for (const criterion of wcag22.criteria) {
35
+ if (tagSet.has(tagForSc(criterion.sc)))
36
+ scs.push(criterion.sc);
37
+ }
38
+ return scs;
39
+ }
40
+ function toFindings(results, outcome, confidence, axeRuleMap, wcag22) {
41
+ const findings = [];
42
+ for (const result of results) {
43
+ const kwcag = resolveKwcag(result.id, result.tags, axeRuleMap, wcag22);
44
+ const wcag = tagsToScList(result.tags, wcag22);
45
+ const { help, description } = localizeAxeMessage(result.id, result.help, result.description);
46
+ for (const node of result.nodes) {
47
+ findings.push({
48
+ ruleId: result.id,
49
+ engine: "axe",
50
+ kwcag,
51
+ wcag,
52
+ impact: normalizeImpact(result.impact),
53
+ outcome,
54
+ confidence,
55
+ selector: node.target.join(" "),
56
+ html: node.html.slice(0, HTML_SNIPPET_MAX),
57
+ message: help,
58
+ fix: description,
59
+ helpUrl: result.helpUrl,
60
+ });
61
+ }
62
+ }
63
+ return findings;
64
+ }
65
+ /** axe.run() 결과(violations/incomplete)를 Finding[]으로 정규화한다. passes는 집계에만 쓰인다. */
66
+ export function normalizeAxeResults(axeResults, axeRuleMap, wcag22) {
67
+ return [
68
+ ...toFindings(axeResults.violations, "fail", "high", axeRuleMap, wcag22),
69
+ ...toFindings(axeResults.incomplete, "incomplete", "medium", axeRuleMap, wcag22),
70
+ ];
71
+ }
72
+ //# sourceMappingURL=axe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"axe.js","sourceRoot":"","sources":["../../src/normalize/axe.ts"],"names":[],"mappings":"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"}