tessera-learn 0.0.10 → 0.0.13

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 (79) hide show
  1. package/README.md +1 -0
  2. package/dist/audit-BBJpQGqb.js +204 -0
  3. package/dist/audit-BBJpQGqb.js.map +1 -0
  4. package/dist/plugin/a11y-cli.d.ts +1 -0
  5. package/dist/plugin/a11y-cli.js +36 -0
  6. package/dist/plugin/a11y-cli.js.map +1 -0
  7. package/dist/plugin/cli.js +6 -3
  8. package/dist/plugin/cli.js.map +1 -1
  9. package/dist/plugin/index.d.ts +16 -1
  10. package/dist/plugin/index.d.ts.map +1 -1
  11. package/dist/plugin/index.js +171 -140
  12. package/dist/plugin/index.js.map +1 -1
  13. package/dist/{validation-BxWAMMnJ.js → validation-B-xTvM9B.js} +417 -81
  14. package/dist/validation-B-xTvM9B.js.map +1 -0
  15. package/package.json +17 -2
  16. package/src/components/Accordion.svelte +3 -1
  17. package/src/components/AccordionItem.svelte +1 -5
  18. package/src/components/Audio.svelte +22 -5
  19. package/src/components/Callout.svelte +5 -1
  20. package/src/components/Carousel.svelte +24 -8
  21. package/src/components/DefaultLayout.svelte +41 -12
  22. package/src/components/FillInTheBlank.svelte +75 -103
  23. package/src/components/Image.svelte +14 -10
  24. package/src/components/LockedBanner.svelte +5 -5
  25. package/src/components/Matching.svelte +48 -19
  26. package/src/components/MediaTracks.svelte +21 -0
  27. package/src/components/MultipleChoice.svelte +81 -102
  28. package/src/components/Quiz.svelte +63 -21
  29. package/src/components/ResultIcon.svelte +20 -4
  30. package/src/components/RevealModal.svelte +25 -22
  31. package/src/components/Sorting.svelte +61 -26
  32. package/src/components/Transcript.svelte +37 -0
  33. package/src/components/Video.svelte +25 -20
  34. package/src/components/util.ts +4 -1
  35. package/src/components/video-embed.ts +25 -0
  36. package/src/index.ts +2 -7
  37. package/src/plugin/a11y/audit.ts +299 -0
  38. package/src/plugin/a11y/contrast.ts +67 -0
  39. package/src/plugin/a11y-cli.ts +35 -0
  40. package/src/plugin/cli.ts +6 -8
  41. package/src/plugin/export.ts +60 -50
  42. package/src/plugin/index.ts +244 -101
  43. package/src/plugin/layout.ts +6 -51
  44. package/src/plugin/manifest.ts +90 -24
  45. package/src/plugin/override-plugin.ts +68 -0
  46. package/src/plugin/quiz.ts +9 -54
  47. package/src/plugin/validation.ts +768 -183
  48. package/src/runtime/App.svelte +128 -64
  49. package/src/runtime/LoadingBar.svelte +12 -3
  50. package/src/runtime/Sidebar.svelte +24 -8
  51. package/src/runtime/access.ts +15 -3
  52. package/src/runtime/adapters/cmi5.ts +68 -116
  53. package/src/runtime/adapters/format.ts +67 -0
  54. package/src/runtime/adapters/index.ts +45 -34
  55. package/src/runtime/adapters/retry.ts +25 -84
  56. package/src/runtime/adapters/scorm-base.ts +19 -15
  57. package/src/runtime/adapters/scorm12.ts +8 -9
  58. package/src/runtime/adapters/scorm2004.ts +22 -30
  59. package/src/runtime/adapters/web.ts +1 -1
  60. package/src/runtime/hooks.svelte.ts +152 -328
  61. package/src/runtime/interaction-format.ts +30 -12
  62. package/src/runtime/interaction.ts +44 -11
  63. package/src/runtime/navigation.svelte.ts +29 -40
  64. package/src/runtime/persistence.ts +2 -2
  65. package/src/runtime/progress.svelte.ts +22 -9
  66. package/src/runtime/quiz-engine.svelte.ts +361 -0
  67. package/src/runtime/quiz-policy.ts +28 -179
  68. package/src/runtime/types.ts +24 -2
  69. package/src/runtime/xapi/agent-rules.ts +11 -3
  70. package/src/runtime/xapi/client.ts +5 -5
  71. package/src/runtime/xapi/derive-actor.ts +2 -2
  72. package/src/runtime/xapi/publisher.ts +33 -40
  73. package/src/runtime/xapi/setup.ts +18 -15
  74. package/src/runtime/xapi/validation.ts +15 -6
  75. package/src/virtual.d.ts +4 -1
  76. package/styles/base.css +32 -11
  77. package/styles/layout.css +39 -18
  78. package/styles/theme.css +15 -3
  79. package/dist/validation-BxWAMMnJ.js.map +0 -1
package/README.md CHANGED
@@ -20,6 +20,7 @@ That creates a project with Tessera wired up, a starter page structure, and the
20
20
  - **Vite plugin** (`tessera-learn/plugin`): `tesseraPlugin()` — wires page/layout discovery, the LMS adapter, and the export pipeline. Used in your project's `vite.config.js`.
21
21
  - **Built-in components** (`tessera-learn`): `Callout`, `Image`, `Audio`, `Video`, `Accordion` / `AccordionItem`, `Carousel` / `CarouselSlide`, `RevealModal`, `Quiz`, `MultipleChoice`, `FillInTheBlank`, `Matching`, `Sorting`, `DefaultLayout`.
22
22
  - **LMS adapters**: SCORM 1.2, SCORM 2004 4th Edition, cmi5, static Web — selected via `course.config.js` `export.standard`.
23
+ - **Accessibility checks**: static rules (alt text, media titles/captions, heading order, contrast, `<html lang>`) run inside validation and the build with zero extra dependencies; an opt-in runtime audit (`tessera-a11y`, with `playwright` + `@axe-core/playwright` as optional peers) renders every page and gates on axe-core violations.
23
24
 
24
25
  See `AGENTS.md` for usage, signatures, and authoring conventions.
25
26
 
@@ -0,0 +1,204 @@
1
+ import { o as generateManifest, r as normalizeA11y, s as readCourseConfig } from "./validation-B-xTvM9B.js";
2
+ import { resolve } from "node:path";
3
+ import { existsSync, writeFileSync } from "node:fs";
4
+ //#region src/plugin/a11y/audit.ts
5
+ const IMPACT_RANK = {
6
+ minor: 1,
7
+ moderate: 2,
8
+ serious: 3,
9
+ critical: 4
10
+ };
11
+ const AUDIT_ENV_FLAG = "TESSERA_A11Y_AUDIT";
12
+ /** Map the `a11y.standard` enum to axe's cumulative `runOnly` tag list. */
13
+ function axeTags(standard) {
14
+ switch (standard) {
15
+ case "wcag2a": return ["wcag2a"];
16
+ case "wcag21aa": return [
17
+ "wcag2a",
18
+ "wcag2aa",
19
+ "wcag21a",
20
+ "wcag21aa"
21
+ ];
22
+ default: return ["wcag2a", "wcag2aa"];
23
+ }
24
+ }
25
+ /** axe-applicable ignore entries: drop the Tier-1a/1b namespaces. */
26
+ function axeIgnoreRules(ignore) {
27
+ return ignore.filter((id) => !id.startsWith("tessera/") && !id.startsWith("a11y_"));
28
+ }
29
+ function isFailing(v, thresholdRank) {
30
+ return !v.impact || IMPACT_RANK[v.impact] >= thresholdRank;
31
+ }
32
+ async function tryImport(specifier) {
33
+ return import(specifier);
34
+ }
35
+ async function loadDeps() {
36
+ let chromium;
37
+ for (const spec of ["playwright", "@playwright/test"]) try {
38
+ const mod = await tryImport(spec);
39
+ if (mod.chromium) {
40
+ chromium = mod.chromium;
41
+ break;
42
+ }
43
+ } catch {}
44
+ if (!chromium) return {
45
+ ok: false,
46
+ missing: "playwright"
47
+ };
48
+ try {
49
+ const mod = await tryImport("@axe-core/playwright");
50
+ if (!mod.default) return {
51
+ ok: false,
52
+ missing: "@axe-core/playwright"
53
+ };
54
+ return {
55
+ ok: true,
56
+ deps: {
57
+ chromium,
58
+ AxeBuilder: mod.default
59
+ }
60
+ };
61
+ } catch {
62
+ return {
63
+ ok: false,
64
+ missing: "@axe-core/playwright"
65
+ };
66
+ }
67
+ }
68
+ /**
69
+ * Run the Tier-2 runtime accessibility audit against a built course. Builds (or
70
+ * reuses) dist/, serves it, drives Playwright + axe-core over each page, writes
71
+ * a11y-report.json, and returns a process exit code (0 pass, 1 fail/error).
72
+ */
73
+ async function runAudit(projectRoot, options = {}) {
74
+ const threshold = options.threshold ?? "serious";
75
+ const deps = await loadDeps();
76
+ if (!deps.ok) {
77
+ console.error("\x1B[31m[tessera a11y]\x1B[0m Tier 2 needs Playwright + axe-core, which aren't installed.\n Install them to run the runtime audit:\n npm i -D playwright @axe-core/playwright\n npx playwright install chromium");
78
+ return 1;
79
+ }
80
+ const { chromium, AxeBuilder } = deps.deps;
81
+ const read = readCourseConfig(projectRoot);
82
+ const settings = normalizeA11y(read.ok ? read.config.a11y : void 0);
83
+ const tags = axeTags(settings.standard);
84
+ const disableRules = axeIgnoreRules(settings.ignore);
85
+ const manifest = generateManifest(resolve(projectRoot, "pages"));
86
+ const vite = await import("vite");
87
+ const auditDist = resolve(projectRoot, "node_modules", ".tessera-a11y");
88
+ const distHtml = resolve(auditDist, "index.html");
89
+ const prevEnv = process.env[AUDIT_ENV_FLAG];
90
+ process.env[AUDIT_ENV_FLAG] = "1";
91
+ let server;
92
+ try {
93
+ if (options.rebuild || !existsSync(distHtml)) {
94
+ console.log("[tessera a11y] Building course…");
95
+ await vite.build({
96
+ root: projectRoot,
97
+ build: {
98
+ outDir: auditDist,
99
+ emptyOutDir: true
100
+ },
101
+ logLevel: "warn"
102
+ });
103
+ }
104
+ server = await vite.preview({
105
+ root: projectRoot,
106
+ build: { outDir: auditDist },
107
+ preview: {
108
+ port: 0,
109
+ host: "127.0.0.1"
110
+ },
111
+ logLevel: "warn"
112
+ });
113
+ const baseUrl = server.resolvedUrls?.local?.[0];
114
+ if (!baseUrl) {
115
+ console.error("[tessera a11y] Could not determine preview server URL.");
116
+ return 1;
117
+ }
118
+ const browser = await chromium.launch();
119
+ const pages = [];
120
+ try {
121
+ const page = await (await browser.newContext()).newPage();
122
+ const auditUrl = new URL(baseUrl);
123
+ auditUrl.searchParams.set("__tessera_audit", "1");
124
+ await page.goto(auditUrl.href, { waitUntil: "networkidle" });
125
+ await page.waitForSelector("#tessera-app", { timeout: 2e4 });
126
+ const scan = async () => {
127
+ const builder = new AxeBuilder({ page }).withTags(tags);
128
+ if (disableRules.length > 0) builder.disableRules(disableRules);
129
+ return (await builder.analyze()).violations.map((v) => ({
130
+ id: v.id,
131
+ impact: v.impact ?? null,
132
+ help: v.help,
133
+ helpUrl: v.helpUrl,
134
+ nodes: v.nodes.length
135
+ }));
136
+ };
137
+ const navCount = await page.locator("button.tessera-nav-page").count();
138
+ if (navCount === 0) pages.push({
139
+ index: 0,
140
+ title: manifest.pages[0]?.title ?? "(entry)",
141
+ violations: await scan()
142
+ });
143
+ else for (let i = 0; i < navCount; i++) {
144
+ const btn = page.locator("button.tessera-nav-page").nth(i);
145
+ const title = (await btn.textContent())?.trim() || `Page ${i + 1}`;
146
+ await btn.click();
147
+ await page.waitForFunction((idx) => document.querySelectorAll("button.tessera-nav-page")[idx]?.getAttribute("aria-current") === "page", i, { timeout: 2e4 });
148
+ await page.waitForLoadState("networkidle");
149
+ pages.push({
150
+ index: i,
151
+ title,
152
+ violations: await scan()
153
+ });
154
+ }
155
+ } finally {
156
+ await browser.close();
157
+ }
158
+ const thresholdRank = IMPACT_RANK[threshold];
159
+ let totalViolations = 0;
160
+ let failingViolations = 0;
161
+ for (const p of pages) for (const v of p.violations) {
162
+ totalViolations++;
163
+ if (isFailing(v, thresholdRank)) failingViolations++;
164
+ }
165
+ const report = {
166
+ standard: settings.standard,
167
+ threshold,
168
+ pages,
169
+ totalViolations,
170
+ failingViolations,
171
+ passed: failingViolations === 0
172
+ };
173
+ const reportPath = resolve(projectRoot, "a11y-report.json");
174
+ writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
175
+ printSummary(report, reportPath);
176
+ return report.passed ? 0 : 1;
177
+ } catch (err) {
178
+ console.error(`\x1b[31m[tessera a11y]\x1b[0m Audit could not complete: ${err instanceof Error ? err.message : String(err)}`);
179
+ return 1;
180
+ } finally {
181
+ server?.httpServer?.close?.();
182
+ if (prevEnv === void 0) delete process.env[AUDIT_ENV_FLAG];
183
+ else process.env[AUDIT_ENV_FLAG] = prevEnv;
184
+ }
185
+ }
186
+ function printSummary(report, reportPath) {
187
+ const thresholdRank = IMPACT_RANK[report.threshold];
188
+ for (const p of report.pages) {
189
+ if (p.violations.length === 0) {
190
+ console.log(`\x1b[32m ✓\x1b[0m ${p.title}`);
191
+ continue;
192
+ }
193
+ const mark = p.violations.some((v) => isFailing(v, thresholdRank)) ? "\x1B[31m ✗\x1B[0m" : "\x1B[33m ⚠\x1B[0m";
194
+ console.log(`${mark} ${p.title}`);
195
+ for (const v of p.violations) console.log(` [${v.impact ?? "n/a"}] ${v.id} — ${v.help} (${v.nodes} node${v.nodes === 1 ? "" : "s"})`);
196
+ }
197
+ console.log(`\n[tessera a11y] Report written to ${reportPath}`);
198
+ if (report.passed) console.log(`\x1b[32m[tessera a11y] Passed\x1b[0m — ${report.totalViolations} total finding(s), none at/above "${report.threshold}".`);
199
+ else console.log(`\x1b[31m[tessera a11y] Failed\x1b[0m — ${report.failingViolations} finding(s) at/above "${report.threshold}" (of ${report.totalViolations} total).`);
200
+ }
201
+ //#endregion
202
+ export { runAudit as n, AUDIT_ENV_FLAG as t };
203
+
204
+ //# sourceMappingURL=audit-BBJpQGqb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-BBJpQGqb.js","names":[],"sources":["../src/plugin/a11y/audit.ts"],"sourcesContent":["import { existsSync, writeFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { generateManifest, readCourseConfig } from '../manifest.js';\nimport { normalizeA11y, type A11ySettings } from '../validation.js';\n\nexport interface AuditOptions {\n /** Minimum violation impact that fails the run (CI gate). Default 'serious'. */\n threshold?: ImpactLevel;\n /** Force a fresh `vite build` even if dist/ exists. */\n rebuild?: boolean;\n}\n\nexport type ImpactLevel = 'minor' | 'moderate' | 'serious' | 'critical';\n\nconst IMPACT_RANK: Record<ImpactLevel, number> = {\n minor: 1,\n moderate: 2,\n serious: 3,\n critical: 4,\n};\n\n// Set by runAudit during its build/preview; the plugin forces the WebAdapter,\n// skips export packaging, and stubs xAPI while it's set. See plugin/index.ts.\nexport const AUDIT_ENV_FLAG = 'TESSERA_A11Y_AUDIT';\n\ninterface AxeViolation {\n id: string;\n impact: ImpactLevel | null;\n help: string;\n helpUrl: string;\n nodes: number;\n}\n\ninterface PageAuditResult {\n index: number;\n title: string;\n violations: AxeViolation[];\n}\n\ninterface AuditReport {\n standard: A11ySettings['standard'];\n threshold: ImpactLevel;\n pages: PageAuditResult[];\n totalViolations: number;\n failingViolations: number;\n passed: boolean;\n}\n\n/** Map the `a11y.standard` enum to axe's cumulative `runOnly` tag list. */\nexport function axeTags(standard: A11ySettings['standard']): string[] {\n switch (standard) {\n case 'wcag2a':\n return ['wcag2a'];\n case 'wcag21aa':\n return ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];\n case 'wcag2aa':\n default:\n return ['wcag2a', 'wcag2aa'];\n }\n}\n\n/** axe-applicable ignore entries: drop the Tier-1a/1b namespaces. */\nexport function axeIgnoreRules(ignore: string[]): string[] {\n return ignore.filter(\n (id) => !id.startsWith('tessera/') && !id.startsWith('a11y_'),\n );\n}\n\n// A violation with no impact is treated as failing rather than slipping the\n// gate at every threshold.\nfunction isFailing(v: AxeViolation, thresholdRank: number): boolean {\n return !v.impact || IMPACT_RANK[v.impact] >= thresholdRank;\n}\n\n// Optional deps loaded by variable specifier so tsc doesn't require them to be\n// installed — Tier 2 is opt-in and the absence is handled with a clear message.\nasync function tryImport(specifier: string): Promise<unknown> {\n return import(specifier);\n}\n\ninterface LoadedDeps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n chromium: any;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n AxeBuilder: any;\n}\n\nasync function loadDeps(): Promise<\n { ok: true; deps: LoadedDeps } | { ok: false; missing: string }\n> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let chromium: any;\n for (const spec of ['playwright', '@playwright/test']) {\n try {\n const mod = (await tryImport(spec)) as { chromium?: unknown };\n if (mod.chromium) {\n chromium = mod.chromium;\n break;\n }\n } catch {\n // try the next specifier\n }\n }\n if (!chromium) return { ok: false, missing: 'playwright' };\n\n try {\n const mod = (await tryImport('@axe-core/playwright')) as {\n default?: unknown;\n };\n if (!mod.default) return { ok: false, missing: '@axe-core/playwright' };\n return { ok: true, deps: { chromium, AxeBuilder: mod.default } };\n } catch {\n return { ok: false, missing: '@axe-core/playwright' };\n }\n}\n\n/**\n * Run the Tier-2 runtime accessibility audit against a built course. Builds (or\n * reuses) dist/, serves it, drives Playwright + axe-core over each page, writes\n * a11y-report.json, and returns a process exit code (0 pass, 1 fail/error).\n */\nexport async function runAudit(\n projectRoot: string,\n options: AuditOptions = {},\n): Promise<number> {\n const threshold: ImpactLevel = options.threshold ?? 'serious';\n\n const deps = await loadDeps();\n if (!deps.ok) {\n console.error(\n `\\x1b[31m[tessera a11y]\\x1b[0m Tier 2 needs Playwright + axe-core, which aren't installed.\\n` +\n ` Install them to run the runtime audit:\\n` +\n ` npm i -D playwright @axe-core/playwright\\n` +\n ` npx playwright install chromium`,\n );\n return 1;\n }\n const { chromium, AxeBuilder } = deps.deps;\n\n const read = readCourseConfig(projectRoot);\n const settings = normalizeA11y(read.ok ? read.config.a11y : undefined);\n const tags = axeTags(settings.standard);\n const disableRules = axeIgnoreRules(settings.ignore);\n\n const manifest = generateManifest(resolve(projectRoot, 'pages'));\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const vite = (await import('vite')) as any;\n\n // A throwaway web build, kept out of dist/ so a real LMS export is untouched.\n const auditDist = resolve(projectRoot, 'node_modules', '.tessera-a11y');\n const distHtml = resolve(auditDist, 'index.html');\n\n const prevEnv = process.env[AUDIT_ENV_FLAG];\n process.env[AUDIT_ENV_FLAG] = '1';\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let server: any;\n try {\n if (options.rebuild || !existsSync(distHtml)) {\n console.log('[tessera a11y] Building course…');\n await vite.build({\n root: projectRoot,\n build: { outDir: auditDist, emptyOutDir: true },\n logLevel: 'warn',\n });\n }\n\n server = await vite.preview({\n root: projectRoot,\n build: { outDir: auditDist },\n preview: { port: 0, host: '127.0.0.1' },\n logLevel: 'warn',\n });\n const baseUrl: string | undefined = server.resolvedUrls?.local?.[0];\n if (!baseUrl) {\n console.error('[tessera a11y] Could not determine preview server URL.');\n return 1;\n }\n\n const browser = await chromium.launch();\n const pages: PageAuditResult[] = [];\n try {\n // axe-core/playwright requires a page from an explicit context.\n const context = await browser.newContext();\n const page = await context.newPage();\n // ?__tessera_audit unlocks navigation so quiz-gated pages can be scanned.\n const auditUrl = new URL(baseUrl);\n auditUrl.searchParams.set('__tessera_audit', '1');\n await page.goto(auditUrl.href, { waitUntil: 'networkidle' });\n await page.waitForSelector('#tessera-app', { timeout: 20_000 });\n\n const scan = async (): Promise<AxeViolation[]> => {\n const builder = new AxeBuilder({ page }).withTags(tags);\n if (disableRules.length > 0) builder.disableRules(disableRules);\n const out = await builder.analyze();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return out.violations.map((v: any) => ({\n id: v.id,\n impact: v.impact ?? null,\n help: v.help,\n helpUrl: v.helpUrl,\n nodes: v.nodes.length,\n }));\n };\n\n const navCount = await page.locator('button.tessera-nav-page').count();\n if (navCount === 0) {\n // No sidebar (custom chrome) — audit whatever is rendered at the entry.\n pages.push({\n index: 0,\n title: manifest.pages[0]?.title ?? '(entry)',\n violations: await scan(),\n });\n } else {\n for (let i = 0; i < navCount; i++) {\n const btn = page.locator('button.tessera-nav-page').nth(i);\n const title = (await btn.textContent())?.trim() || `Page ${i + 1}`;\n await btn.click();\n await page.waitForFunction(\n (idx: number) =>\n document\n .querySelectorAll('button.tessera-nav-page')\n [idx]?.getAttribute('aria-current') === 'page',\n i,\n { timeout: 20_000 },\n );\n await page.waitForLoadState('networkidle');\n pages.push({ index: i, title, violations: await scan() });\n }\n }\n } finally {\n await browser.close();\n }\n\n const thresholdRank = IMPACT_RANK[threshold];\n let totalViolations = 0;\n let failingViolations = 0;\n for (const p of pages) {\n for (const v of p.violations) {\n totalViolations++;\n if (isFailing(v, thresholdRank)) failingViolations++;\n }\n }\n\n const report: AuditReport = {\n standard: settings.standard,\n threshold,\n pages,\n totalViolations,\n failingViolations,\n passed: failingViolations === 0,\n };\n const reportPath = resolve(projectRoot, 'a11y-report.json');\n writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');\n\n printSummary(report, reportPath);\n return report.passed ? 0 : 1;\n } catch (err) {\n console.error(\n `\\x1b[31m[tessera a11y]\\x1b[0m Audit could not complete: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n return 1;\n } finally {\n server?.httpServer?.close?.();\n if (prevEnv === undefined) delete process.env[AUDIT_ENV_FLAG];\n else process.env[AUDIT_ENV_FLAG] = prevEnv;\n }\n}\n\nfunction printSummary(report: AuditReport, reportPath: string): void {\n const thresholdRank = IMPACT_RANK[report.threshold];\n for (const p of report.pages) {\n if (p.violations.length === 0) {\n console.log(`\\x1b[32m ✓\\x1b[0m ${p.title}`);\n continue;\n }\n const failing = p.violations.some((v) => isFailing(v, thresholdRank));\n const mark = failing ? '\\x1b[31m ✗\\x1b[0m' : '\\x1b[33m ⚠\\x1b[0m';\n console.log(`${mark} ${p.title}`);\n for (const v of p.violations) {\n console.log(\n ` [${v.impact ?? 'n/a'}] ${v.id} — ${v.help} (${v.nodes} node${v.nodes === 1 ? '' : 's'})`,\n );\n }\n }\n console.log(`\\n[tessera a11y] Report written to ${reportPath}`);\n if (report.passed) {\n console.log(\n `\\x1b[32m[tessera a11y] Passed\\x1b[0m — ${report.totalViolations} total finding(s), none at/above \"${report.threshold}\".`,\n );\n } else {\n console.log(\n `\\x1b[31m[tessera a11y] Failed\\x1b[0m — ${report.failingViolations} finding(s) at/above \"${report.threshold}\" (of ${report.totalViolations} total).`,\n );\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { n as runAudit } from "../audit-BBJpQGqb.js";
3
+ //#region src/plugin/a11y-cli.ts
4
+ const VALID_THRESHOLDS = [
5
+ "minor",
6
+ "moderate",
7
+ "serious",
8
+ "critical"
9
+ ];
10
+ const args = process.argv.slice(2);
11
+ let threshold;
12
+ let rebuild = false;
13
+ for (let i = 0; i < args.length; i++) {
14
+ const arg = args[i];
15
+ if (arg === "--threshold") {
16
+ const value = args[++i];
17
+ if (!VALID_THRESHOLDS.includes(value)) {
18
+ console.error(`[tessera a11y] --threshold must be one of: ${VALID_THRESHOLDS.join(", ")}`);
19
+ process.exit(1);
20
+ }
21
+ threshold = value;
22
+ } else if (arg === "--build") rebuild = true;
23
+ else {
24
+ console.error(`[tessera a11y] Unknown argument: ${arg}`);
25
+ process.exit(1);
26
+ }
27
+ }
28
+ const code = await runAudit(process.cwd(), {
29
+ threshold,
30
+ rebuild
31
+ });
32
+ process.exit(code);
33
+ //#endregion
34
+ export {};
35
+
36
+ //# sourceMappingURL=a11y-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a11y-cli.js","names":[],"sources":["../../src/plugin/a11y-cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { runAudit, type ImpactLevel } from './a11y/audit.js';\n\nconst VALID_THRESHOLDS: ImpactLevel[] = [\n 'minor',\n 'moderate',\n 'serious',\n 'critical',\n];\n\nconst args = process.argv.slice(2);\nlet threshold: ImpactLevel | undefined;\nlet rebuild = false;\n\nfor (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === '--threshold') {\n const value = args[++i] as ImpactLevel;\n if (!VALID_THRESHOLDS.includes(value)) {\n console.error(\n `[tessera a11y] --threshold must be one of: ${VALID_THRESHOLDS.join(', ')}`,\n );\n process.exit(1);\n }\n threshold = value;\n } else if (arg === '--build') {\n rebuild = true;\n } else {\n console.error(`[tessera a11y] Unknown argument: ${arg}`);\n process.exit(1);\n }\n}\n\nconst code = await runAudit(process.cwd(), { threshold, rebuild });\nprocess.exit(code);\n"],"mappings":";;;AAGA,MAAM,mBAAkC;CACtC;CACA;CACA;CACA;CACD;AAED,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAClC,IAAI;AACJ,IAAI,UAAU;AAEd,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;CACpC,MAAM,MAAM,KAAK;CACjB,IAAI,QAAQ,eAAe;EACzB,MAAM,QAAQ,KAAK,EAAE;EACrB,IAAI,CAAC,iBAAiB,SAAS,MAAM,EAAE;GACrC,QAAQ,MACN,8CAA8C,iBAAiB,KAAK,KAAK,GAC1E;GACD,QAAQ,KAAK,EAAE;;EAEjB,YAAY;QACP,IAAI,QAAQ,WACjB,UAAU;MACL;EACL,QAAQ,MAAM,oCAAoC,MAAM;EACxD,QAAQ,KAAK,EAAE;;;AAInB,MAAM,OAAO,MAAM,SAAS,QAAQ,KAAK,EAAE;CAAE;CAAW;CAAS,CAAC;AAClE,QAAQ,KAAK,KAAK"}
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { t as validateProject } from "../validation-BxWAMMnJ.js";
2
+ import { a as validateProject, i as reportValidationIssues } from "../validation-B-xTvM9B.js";
3
3
  //#region src/plugin/cli.ts
4
4
  const { errors, warnings } = validateProject(process.cwd());
5
- for (const warning of warnings) console.warn(`\x1b[33m[tessera warning]\x1b[0m ${warning}`);
6
- for (const error of errors) console.error(`\x1b[31m[tessera error]\x1b[0m ${error}`);
5
+ reportValidationIssues({
6
+ errors,
7
+ warnings
8
+ });
7
9
  if (errors.length > 0) {
8
10
  const summary = `Validation failed with ${errors.length} error(s)` + (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : "") + ".";
9
11
  console.error(`\n\x1b[31m${summary}\x1b[0m`);
@@ -11,6 +13,7 @@ if (errors.length > 0) {
11
13
  }
12
14
  if (warnings.length > 0) console.log(`\n\x1b[33mValidation passed with ${warnings.length} warning(s).\x1b[0m`);
13
15
  else console.log("\x1B[32m[tessera]\x1B[0m Validation passed — no issues found.");
16
+ console.log("\x1B[2m[tessera] Static checks only. For a full runtime accessibility audit, run: npm run accessibility-check\x1B[0m");
14
17
  process.exit(0);
15
18
  //#endregion
16
19
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../../src/plugin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { validateProject } from './validation.js';\n\nconst projectRoot = process.cwd();\nconst { errors, warnings } = validateProject(projectRoot);\n\nfor (const warning of warnings) {\n console.warn(`\\x1b[33m[tessera warning]\\x1b[0m ${warning}`);\n}\nfor (const error of errors) {\n console.error(`\\x1b[31m[tessera error]\\x1b[0m ${error}`);\n}\n\nif (errors.length > 0) {\n const summary =\n `Validation failed with ${errors.length} error(s)` +\n (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +\n '.';\n console.error(`\\n\\x1b[31m${summary}\\x1b[0m`);\n process.exit(1);\n}\n\nif (warnings.length > 0) {\n console.log(\n `\\n\\x1b[33mValidation passed with ${warnings.length} warning(s).\\x1b[0m`\n );\n} else {\n console.log('\\x1b[32m[tessera]\\x1b[0m Validation passed — no issues found.');\n}\nprocess.exit(0);\n"],"mappings":";;;AAIA,MAAM,EAAE,QAAQ,aAAa,gBADT,QAAQ,KAC4B,CAAC;AAEzD,KAAK,MAAM,WAAW,UACpB,QAAQ,KAAK,oCAAoC,UAAU;AAE7D,KAAK,MAAM,SAAS,QAClB,QAAQ,MAAM,kCAAkC,QAAQ;AAG1D,IAAI,OAAO,SAAS,GAAG;CACrB,MAAM,UACJ,0BAA0B,OAAO,OAAO,cACvC,SAAS,SAAS,IAAI,QAAQ,SAAS,OAAO,eAAe,MAC9D;CACF,QAAQ,MAAM,aAAa,QAAQ,SAAS;CAC5C,QAAQ,KAAK,EAAE;;AAGjB,IAAI,SAAS,SAAS,GACpB,QAAQ,IACN,oCAAoC,SAAS,OAAO,qBACrD;KAED,QAAQ,IAAI,gEAAgE;AAE9E,QAAQ,KAAK,EAAE"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../../src/plugin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { validateProject, reportValidationIssues } from './validation.js';\n\nconst projectRoot = process.cwd();\nconst { errors, warnings } = validateProject(projectRoot);\n\nreportValidationIssues({ errors, warnings });\n\nif (errors.length > 0) {\n const summary =\n `Validation failed with ${errors.length} error(s)` +\n (warnings.length > 0 ? ` and ${warnings.length} warning(s)` : '') +\n '.';\n console.error(`\\n\\x1b[31m${summary}\\x1b[0m`);\n process.exit(1);\n}\n\nif (warnings.length > 0) {\n console.log(\n `\\n\\x1b[33mValidation passed with ${warnings.length} warning(s).\\x1b[0m`,\n );\n} else {\n console.log('\\x1b[32m[tessera]\\x1b[0m Validation passed — no issues found.');\n}\nconsole.log(\n '\\x1b[2m[tessera] Static checks only. For a full runtime accessibility audit, run: npm run accessibility-check\\x1b[0m',\n);\nprocess.exit(0);\n"],"mappings":";;;AAIA,MAAM,EAAE,QAAQ,aAAa,gBADT,QAAQ,KAC4B,CAAC;AAEzD,uBAAuB;CAAE;CAAQ;CAAU,CAAC;AAE5C,IAAI,OAAO,SAAS,GAAG;CACrB,MAAM,UACJ,0BAA0B,OAAO,OAAO,cACvC,SAAS,SAAS,IAAI,QAAQ,SAAS,OAAO,eAAe,MAC9D;CACF,QAAQ,MAAM,aAAa,QAAQ,SAAS;CAC5C,QAAQ,KAAK,EAAE;;AAGjB,IAAI,SAAS,SAAS,GACpB,QAAQ,IACN,oCAAoC,SAAS,OAAO,qBACrD;KAED,QAAQ,IAAI,gEAAgE;AAE9E,QAAQ,IACN,uHACD;AACD,QAAQ,KAAK,EAAE"}
@@ -1,7 +1,22 @@
1
1
  import { Plugin } from "vite";
2
2
 
3
+ //#region src/plugin/a11y/audit.d.ts
4
+ interface AuditOptions {
5
+ /** Minimum violation impact that fails the run (CI gate). Default 'serious'. */
6
+ threshold?: ImpactLevel;
7
+ /** Force a fresh `vite build` even if dist/ exists. */
8
+ rebuild?: boolean;
9
+ }
10
+ type ImpactLevel = 'minor' | 'moderate' | 'serious' | 'critical';
11
+ /**
12
+ * Run the Tier-2 runtime accessibility audit against a built course. Builds (or
13
+ * reuses) dist/, serves it, drives Playwright + axe-core over each page, writes
14
+ * a11y-report.json, and returns a process exit code (0 pass, 1 fail/error).
15
+ */
16
+ declare function runAudit(projectRoot: string, options?: AuditOptions): Promise<number>;
17
+ //#endregion
3
18
  //#region src/plugin/index.d.ts
4
19
  declare function tesseraPlugin(): (Plugin<any> | Plugin<any>[])[];
5
20
  //#endregion
6
- export { tesseraPlugin };
21
+ export { type AuditOptions, type ImpactLevel, runAudit, tesseraPlugin };
7
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/plugin/index.ts"],"mappings":";;;iBA4BgB,aAAA,CAAA,IAAa,MAAA,QAAA,MAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/plugin/a11y/audit.ts","../../src/plugin/index.ts"],"mappings":";;;UAKiB,YAAA;;EAEf,SAAA,GAAY,WAAA;EAFe;EAI3B,OAAA;AAAA;AAAA,KAGU,WAAA;;;;;AA6GZ;iBAAsB,QAAA,CACpB,WAAA,UACA,OAAA,GAAS,YAAA,GACR,OAAA;;;iBCtCa,aAAA,CAAA,IAAa,MAAA,QAAA,MAAA"}