@webability/core 1.17.2 → 1.19.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.
@@ -28,6 +28,14 @@ interface PixelVerdict {
28
28
  bestRatio: number;
29
29
  /** Worst (lowest) contrast ratio found anywhere behind the text. */
30
30
  worstRatio: number;
31
+ /** Glyph-core measurement (text over an image or gradient): min, 2nd/5th/50th percentile, share of glyph pixels below the threshold. */
32
+ glyph?: {
33
+ min: number;
34
+ p02: number;
35
+ p05: number;
36
+ p50: number;
37
+ fractionBelow: number;
38
+ };
31
39
  /** The background pixel behind the text that gave `worstRatio`, as rgb(r,g,b). */
32
40
  worstBg?: string;
33
41
  /** The background pixel behind the text that gave `bestRatio`, as rgb(r,g,b). */
@@ -62,13 +70,14 @@ interface PixelVerdict {
62
70
  * verdict states a ratio the scanner measured, not one the model guessed.
63
71
  *
64
72
  * Models (local, via an OpenAI-compatible chat endpoint — OpenRouter by
65
- * default): vision `anthropic/claude-haiku-4.5`, text-only
73
+ * default): vision `qwen/qwen3.6-35b-a3b` (open weights), text-only
66
74
  * `deepseek/deepseek-v4-flash`. Hosted: POST `${apiUrl}/api/judge-incomplete`.
67
75
  */
68
76
 
69
77
  declare const JUDGE_DEFAULT_TIMEOUT_MS = 20000;
70
78
  declare const DEFAULT_JUDGE_BASE_URL = "https://openrouter.ai/api/v1";
71
- declare const DEFAULT_JUDGE_VISION_MODEL = "anthropic/claude-haiku-4.5";
79
+ /** Open-weights vision model first (standing rule); Anthropic only when WEBABILITY_JUDGE_VISION_MODEL names it. */
80
+ declare const DEFAULT_JUDGE_VISION_MODEL = "qwen/qwen3.6-35b-a3b";
72
81
  declare const DEFAULT_JUDGE_TEXT_MODEL = "deepseek/deepseek-v4-flash";
73
82
  type JudgeVerdictKind = 'pass' | 'violation';
74
83
  /** One judged finding, as recorded on `ScanResult.judged`. Small on purpose. */
@@ -97,6 +106,23 @@ interface JudgeRequest {
97
106
  level: WcagLevel;
98
107
  vision: boolean;
99
108
  items: JudgeItem[];
109
+ /**
110
+ * Rubric requests only (src/rubrics): the rubric's own system prompt and
111
+ * page-level evidence shared by every item. Undecided-finding batches leave
112
+ * both unset and get JUDGE_SYSTEM_PROMPT. The hosted endpoint ignores them.
113
+ */
114
+ system?: string;
115
+ page?: Record<string, unknown>;
116
+ /**
117
+ * Rubric requests only: which rubric this is and a hash of its prompt. The
118
+ * hosted transport sends this INSTEAD of `system`: the platform resolves the
119
+ * prompt from its own @webability/core and refuses a hash it does not have,
120
+ * so a client never chooses the prompt the paid model runs.
121
+ */
122
+ rubric?: {
123
+ id: string;
124
+ hash: string;
125
+ };
100
126
  }
101
127
  interface JudgeVerdict {
102
128
  id: string;
@@ -172,6 +198,9 @@ interface JudgeChatBody {
172
198
  * text model. `disableTextModelReasoning`: the default text model reasons unless
173
199
  * told not to, and on a real 30-item batch spent all 4096 tokens reasoning (no
174
200
  * verdicts, 48 s); set it only for endpoints that accept the `reasoning` field.
201
+ * A rubric request (`req.system`) asks whichever model it uses not to reason:
202
+ * one bounded call per aspect batch, and a reasoning flash model spends ~30x
203
+ * the tokens of its answer on a one-line verdict.
175
204
  */
176
205
  declare function buildJudgeChatBody(req: JudgeRequest, cfg?: {
177
206
  visionModel?: string;
@@ -226,7 +255,7 @@ interface PageLike {
226
255
  axePage?(): unknown;
227
256
  }
228
257
  /** All fix issue types the pipeline can detect and fix. */
229
- type FixIssueType = 'weak_link_name' | 'weak_heading' | 'weak_iframe_title' | 'weak_page_title' | 'weak_svg_name' | 'missing_button_role' | 'missing_semantic_role' | 'missing_document_language' | 'svg_missing_name' | 'aria_hidden_focusable' | 'contrast_insufficient' | 'non_text_contrast_insufficient' | 'focus_not_visible' | 'empty_link' | 'unlabeled_button' | 'missing_label' | 'skip_link' | 'aria_live' | 'missing_alt' | 'identify_input_purpose' | 'redundant_role' | 'new_window_link' | 'missing_fieldset_legend' | 'missing_table_header' | 'missing_table_caption' | 'missing_table_scope' | 'external_link_warning' | 'download_link_warning' | 'form_missing_submit' | 'placeholder_link' | 'missing_autocomplete' | 'missing_media_controls' | 'unsafe_autoplay' | 'zoom_restriction' | 'resize_text_clipped' | 'link_consistency' | 'keyboard_trap' | 'keyboard_trap_self_focus' | 'focus_order_disruption' | 'missing_captions' | 'color_only_link' | 'label_in_name' | 'onfocus_context_change' | 'focus_obscured_by_sticky' | 'text_spacing_blocked' | 'motion_uncontrollable' | 'auto_refresh' | 'motion_without_reduced_motion' | 'flashing_content' | 'animated_gif_no_pause' | 'carousel_autoplay_no_pause' | 'missing_required_indicator' | 'background_image_unlabeled' | 'broken_aria_reference' | 'main_content_hidden' | 'meaningful_sequence' | 'sensory_characteristic' | 'image_of_text' | 'reflow_overflow' | 'missing_audio_description' | 'missing_transcript' | 'character_key_shortcut' | 'on_input_change' | 'pointer_gesture' | 'pointer_cancellation' | 'motion_actuation' | 'error_prevention_missing' | 'inconsistent_navigation' | 'adjacent_redundant_link' | 'missing_button_type' | 'decorative_image_with_title' | 'nested_landmark';
258
+ type FixIssueType = 'weak_link_name' | 'weak_heading' | 'weak_iframe_title' | 'weak_page_title' | 'weak_svg_name' | 'missing_button_role' | 'missing_semantic_role' | 'missing_document_language' | 'svg_missing_name' | 'aria_hidden_focusable' | 'contrast_insufficient' | 'non_text_contrast_insufficient' | 'focus_not_visible' | 'empty_link' | 'unlabeled_button' | 'missing_label' | 'skip_link' | 'aria_live' | 'missing_alt' | 'identify_input_purpose' | 'redundant_role' | 'new_window_link' | 'missing_fieldset_legend' | 'missing_table_header' | 'missing_table_caption' | 'missing_table_scope' | 'external_link_warning' | 'download_link_warning' | 'form_missing_submit' | 'placeholder_link' | 'missing_autocomplete' | 'missing_media_controls' | 'unsafe_autoplay' | 'zoom_restriction' | 'resize_text_clipped' | 'link_consistency' | 'keyboard_trap' | 'keyboard_trap_self_focus' | 'focus_order_disruption' | 'missing_captions' | 'color_only_link' | 'label_in_name' | 'onfocus_context_change' | 'focus_obscured_by_sticky' | 'text_spacing_blocked' | 'motion_uncontrollable' | 'auto_refresh' | 'motion_without_reduced_motion' | 'flashing_content' | 'animated_gif_no_pause' | 'carousel_autoplay_no_pause' | 'missing_required_indicator' | 'background_image_unlabeled' | 'broken_aria_reference' | 'main_content_hidden' | 'meaningful_sequence' | 'sensory_characteristic' | 'image_of_text' | 'reflow_overflow' | 'missing_audio_description' | 'missing_transcript' | 'character_key_shortcut' | 'on_input_change' | 'pointer_gesture' | 'pointer_cancellation' | 'motion_actuation' | 'error_prevention_missing' | 'inconsistent_navigation' | 'image_name_not_descriptive' | 'hidden_image_not_decorative' | 'weak_form_label' | 'page_language_mismatch' | 'element_language_mismatch' | 'error_message_not_descriptive' | 'iframe_same_name_different_content' | 'identical_links_different_purpose' | 'auto_update_no_pause' | 'adjacent_redundant_link' | 'missing_button_type' | 'decorative_image_with_title' | 'nested_landmark';
230
259
  /** WCAG criterion for each issue type. */
231
260
  declare const WCAG_BY_ISSUE: Record<FixIssueType, string>;
232
261
  /** A single accessibility issue detected on the page. */
@@ -265,6 +294,12 @@ interface DetectedIssue {
265
294
  manualFix?: boolean;
266
295
  /** Why this finding needs review — surfaced to MCP/SDK consumers. */
267
296
  reviewReason?: string;
297
+ /**
298
+ * Measurements the detector took for an uncertain verdict (effective
299
+ * opacity, font weights, a ratio, a frame origin). Handed to the judge as
300
+ * context with the row; never a verdict by itself.
301
+ */
302
+ evidence?: Record<string, unknown>;
268
303
  /**
269
304
  * State pseudo-class the fix applies in (`:focus-visible` for
270
305
  * focus_not_visible). `selector` always names the element at rest; a
@@ -340,6 +375,8 @@ interface ScanIssue {
340
375
  confidence?: IssueConfidence;
341
376
  /** Internal: why the engine could not decide; handed to the judge as context, never reported. */
342
377
  reviewReason?: string;
378
+ /** Internal: the detector's measurements for the judge; never reported. */
379
+ evidence?: Record<string, unknown>;
343
380
  fix?: {
344
381
  attribute: string;
345
382
  currentValue: string;
@@ -548,4 +585,4 @@ interface ScanOptions {
548
585
  judge?: JudgeOptions;
549
586
  }
550
587
 
551
- export { type BrowserDetector as B, type DetectedIssue as D, type FixDetector as F, type InternalBug as I, JUDGE_DEFAULT_TIMEOUT_MS as J, type PageLike as P, type ScanOptions as S, VIEWPORT_SIZES as V, type WcagLevel as W, type ScanResult as a, type SiteTheme as b, type ScanIssue as c, DEFAULT_JUDGE_BASE_URL as d, DEFAULT_JUDGE_TEXT_MODEL as e, DEFAULT_JUDGE_VISION_MODEL as f, type FixIssueType as g, type IssueImpact as h, type IssueSource as i, JUDGE_SYSTEM_PROMPT as j, type JudgeChatBody as k, type JudgeItem as l, type JudgeOptions as m, type JudgeRequest as n, type JudgeTransport as o, type JudgeVerdict as p, type JudgedEntry as q, type ViewportPreset as r, WCAG_BY_ISSUE as s, buildJudgeChatBody as t, hostedJudgeTransport as u, judgeUndecided as v, openAiCompatibleTransport as w, resetJudgeCache as x };
588
+ export { type BrowserDetector as B, type DetectedIssue as D, type FixDetector as F, type IssueImpact as I, type JudgeOptions as J, type PageLike as P, type ScanOptions as S, VIEWPORT_SIZES as V, type WcagLevel as W, type ScanResult as a, type ScanIssue as b, type InternalBug as c, type JudgeTransport as d, type SiteTheme as e, DEFAULT_JUDGE_BASE_URL as f, DEFAULT_JUDGE_TEXT_MODEL as g, DEFAULT_JUDGE_VISION_MODEL as h, type FixIssueType as i, type IssueSource as j, JUDGE_DEFAULT_TIMEOUT_MS as k, JUDGE_SYSTEM_PROMPT as l, type JudgeChatBody as m, type JudgeItem as n, type JudgeRequest as o, type JudgeVerdict as p, type JudgedEntry as q, type ViewportPreset as r, WCAG_BY_ISSUE as s, buildJudgeChatBody as t, hostedJudgeTransport as u, judgeUndecided as v, openAiCompatibleTransport as w, resetJudgeCache as x };
@@ -28,6 +28,14 @@ interface PixelVerdict {
28
28
  bestRatio: number;
29
29
  /** Worst (lowest) contrast ratio found anywhere behind the text. */
30
30
  worstRatio: number;
31
+ /** Glyph-core measurement (text over an image or gradient): min, 2nd/5th/50th percentile, share of glyph pixels below the threshold. */
32
+ glyph?: {
33
+ min: number;
34
+ p02: number;
35
+ p05: number;
36
+ p50: number;
37
+ fractionBelow: number;
38
+ };
31
39
  /** The background pixel behind the text that gave `worstRatio`, as rgb(r,g,b). */
32
40
  worstBg?: string;
33
41
  /** The background pixel behind the text that gave `bestRatio`, as rgb(r,g,b). */
@@ -62,13 +70,14 @@ interface PixelVerdict {
62
70
  * verdict states a ratio the scanner measured, not one the model guessed.
63
71
  *
64
72
  * Models (local, via an OpenAI-compatible chat endpoint — OpenRouter by
65
- * default): vision `anthropic/claude-haiku-4.5`, text-only
73
+ * default): vision `qwen/qwen3.6-35b-a3b` (open weights), text-only
66
74
  * `deepseek/deepseek-v4-flash`. Hosted: POST `${apiUrl}/api/judge-incomplete`.
67
75
  */
68
76
 
69
77
  declare const JUDGE_DEFAULT_TIMEOUT_MS = 20000;
70
78
  declare const DEFAULT_JUDGE_BASE_URL = "https://openrouter.ai/api/v1";
71
- declare const DEFAULT_JUDGE_VISION_MODEL = "anthropic/claude-haiku-4.5";
79
+ /** Open-weights vision model first (standing rule); Anthropic only when WEBABILITY_JUDGE_VISION_MODEL names it. */
80
+ declare const DEFAULT_JUDGE_VISION_MODEL = "qwen/qwen3.6-35b-a3b";
72
81
  declare const DEFAULT_JUDGE_TEXT_MODEL = "deepseek/deepseek-v4-flash";
73
82
  type JudgeVerdictKind = 'pass' | 'violation';
74
83
  /** One judged finding, as recorded on `ScanResult.judged`. Small on purpose. */
@@ -97,6 +106,23 @@ interface JudgeRequest {
97
106
  level: WcagLevel;
98
107
  vision: boolean;
99
108
  items: JudgeItem[];
109
+ /**
110
+ * Rubric requests only (src/rubrics): the rubric's own system prompt and
111
+ * page-level evidence shared by every item. Undecided-finding batches leave
112
+ * both unset and get JUDGE_SYSTEM_PROMPT. The hosted endpoint ignores them.
113
+ */
114
+ system?: string;
115
+ page?: Record<string, unknown>;
116
+ /**
117
+ * Rubric requests only: which rubric this is and a hash of its prompt. The
118
+ * hosted transport sends this INSTEAD of `system`: the platform resolves the
119
+ * prompt from its own @webability/core and refuses a hash it does not have,
120
+ * so a client never chooses the prompt the paid model runs.
121
+ */
122
+ rubric?: {
123
+ id: string;
124
+ hash: string;
125
+ };
100
126
  }
101
127
  interface JudgeVerdict {
102
128
  id: string;
@@ -172,6 +198,9 @@ interface JudgeChatBody {
172
198
  * text model. `disableTextModelReasoning`: the default text model reasons unless
173
199
  * told not to, and on a real 30-item batch spent all 4096 tokens reasoning (no
174
200
  * verdicts, 48 s); set it only for endpoints that accept the `reasoning` field.
201
+ * A rubric request (`req.system`) asks whichever model it uses not to reason:
202
+ * one bounded call per aspect batch, and a reasoning flash model spends ~30x
203
+ * the tokens of its answer on a one-line verdict.
175
204
  */
176
205
  declare function buildJudgeChatBody(req: JudgeRequest, cfg?: {
177
206
  visionModel?: string;
@@ -226,7 +255,7 @@ interface PageLike {
226
255
  axePage?(): unknown;
227
256
  }
228
257
  /** All fix issue types the pipeline can detect and fix. */
229
- type FixIssueType = 'weak_link_name' | 'weak_heading' | 'weak_iframe_title' | 'weak_page_title' | 'weak_svg_name' | 'missing_button_role' | 'missing_semantic_role' | 'missing_document_language' | 'svg_missing_name' | 'aria_hidden_focusable' | 'contrast_insufficient' | 'non_text_contrast_insufficient' | 'focus_not_visible' | 'empty_link' | 'unlabeled_button' | 'missing_label' | 'skip_link' | 'aria_live' | 'missing_alt' | 'identify_input_purpose' | 'redundant_role' | 'new_window_link' | 'missing_fieldset_legend' | 'missing_table_header' | 'missing_table_caption' | 'missing_table_scope' | 'external_link_warning' | 'download_link_warning' | 'form_missing_submit' | 'placeholder_link' | 'missing_autocomplete' | 'missing_media_controls' | 'unsafe_autoplay' | 'zoom_restriction' | 'resize_text_clipped' | 'link_consistency' | 'keyboard_trap' | 'keyboard_trap_self_focus' | 'focus_order_disruption' | 'missing_captions' | 'color_only_link' | 'label_in_name' | 'onfocus_context_change' | 'focus_obscured_by_sticky' | 'text_spacing_blocked' | 'motion_uncontrollable' | 'auto_refresh' | 'motion_without_reduced_motion' | 'flashing_content' | 'animated_gif_no_pause' | 'carousel_autoplay_no_pause' | 'missing_required_indicator' | 'background_image_unlabeled' | 'broken_aria_reference' | 'main_content_hidden' | 'meaningful_sequence' | 'sensory_characteristic' | 'image_of_text' | 'reflow_overflow' | 'missing_audio_description' | 'missing_transcript' | 'character_key_shortcut' | 'on_input_change' | 'pointer_gesture' | 'pointer_cancellation' | 'motion_actuation' | 'error_prevention_missing' | 'inconsistent_navigation' | 'adjacent_redundant_link' | 'missing_button_type' | 'decorative_image_with_title' | 'nested_landmark';
258
+ type FixIssueType = 'weak_link_name' | 'weak_heading' | 'weak_iframe_title' | 'weak_page_title' | 'weak_svg_name' | 'missing_button_role' | 'missing_semantic_role' | 'missing_document_language' | 'svg_missing_name' | 'aria_hidden_focusable' | 'contrast_insufficient' | 'non_text_contrast_insufficient' | 'focus_not_visible' | 'empty_link' | 'unlabeled_button' | 'missing_label' | 'skip_link' | 'aria_live' | 'missing_alt' | 'identify_input_purpose' | 'redundant_role' | 'new_window_link' | 'missing_fieldset_legend' | 'missing_table_header' | 'missing_table_caption' | 'missing_table_scope' | 'external_link_warning' | 'download_link_warning' | 'form_missing_submit' | 'placeholder_link' | 'missing_autocomplete' | 'missing_media_controls' | 'unsafe_autoplay' | 'zoom_restriction' | 'resize_text_clipped' | 'link_consistency' | 'keyboard_trap' | 'keyboard_trap_self_focus' | 'focus_order_disruption' | 'missing_captions' | 'color_only_link' | 'label_in_name' | 'onfocus_context_change' | 'focus_obscured_by_sticky' | 'text_spacing_blocked' | 'motion_uncontrollable' | 'auto_refresh' | 'motion_without_reduced_motion' | 'flashing_content' | 'animated_gif_no_pause' | 'carousel_autoplay_no_pause' | 'missing_required_indicator' | 'background_image_unlabeled' | 'broken_aria_reference' | 'main_content_hidden' | 'meaningful_sequence' | 'sensory_characteristic' | 'image_of_text' | 'reflow_overflow' | 'missing_audio_description' | 'missing_transcript' | 'character_key_shortcut' | 'on_input_change' | 'pointer_gesture' | 'pointer_cancellation' | 'motion_actuation' | 'error_prevention_missing' | 'inconsistent_navigation' | 'image_name_not_descriptive' | 'hidden_image_not_decorative' | 'weak_form_label' | 'page_language_mismatch' | 'element_language_mismatch' | 'error_message_not_descriptive' | 'iframe_same_name_different_content' | 'identical_links_different_purpose' | 'auto_update_no_pause' | 'adjacent_redundant_link' | 'missing_button_type' | 'decorative_image_with_title' | 'nested_landmark';
230
259
  /** WCAG criterion for each issue type. */
231
260
  declare const WCAG_BY_ISSUE: Record<FixIssueType, string>;
232
261
  /** A single accessibility issue detected on the page. */
@@ -265,6 +294,12 @@ interface DetectedIssue {
265
294
  manualFix?: boolean;
266
295
  /** Why this finding needs review — surfaced to MCP/SDK consumers. */
267
296
  reviewReason?: string;
297
+ /**
298
+ * Measurements the detector took for an uncertain verdict (effective
299
+ * opacity, font weights, a ratio, a frame origin). Handed to the judge as
300
+ * context with the row; never a verdict by itself.
301
+ */
302
+ evidence?: Record<string, unknown>;
268
303
  /**
269
304
  * State pseudo-class the fix applies in (`:focus-visible` for
270
305
  * focus_not_visible). `selector` always names the element at rest; a
@@ -340,6 +375,8 @@ interface ScanIssue {
340
375
  confidence?: IssueConfidence;
341
376
  /** Internal: why the engine could not decide; handed to the judge as context, never reported. */
342
377
  reviewReason?: string;
378
+ /** Internal: the detector's measurements for the judge; never reported. */
379
+ evidence?: Record<string, unknown>;
343
380
  fix?: {
344
381
  attribute: string;
345
382
  currentValue: string;
@@ -548,4 +585,4 @@ interface ScanOptions {
548
585
  judge?: JudgeOptions;
549
586
  }
550
587
 
551
- export { type BrowserDetector as B, type DetectedIssue as D, type FixDetector as F, type InternalBug as I, JUDGE_DEFAULT_TIMEOUT_MS as J, type PageLike as P, type ScanOptions as S, VIEWPORT_SIZES as V, type WcagLevel as W, type ScanResult as a, type SiteTheme as b, type ScanIssue as c, DEFAULT_JUDGE_BASE_URL as d, DEFAULT_JUDGE_TEXT_MODEL as e, DEFAULT_JUDGE_VISION_MODEL as f, type FixIssueType as g, type IssueImpact as h, type IssueSource as i, JUDGE_SYSTEM_PROMPT as j, type JudgeChatBody as k, type JudgeItem as l, type JudgeOptions as m, type JudgeRequest as n, type JudgeTransport as o, type JudgeVerdict as p, type JudgedEntry as q, type ViewportPreset as r, WCAG_BY_ISSUE as s, buildJudgeChatBody as t, hostedJudgeTransport as u, judgeUndecided as v, openAiCompatibleTransport as w, resetJudgeCache as x };
588
+ export { type BrowserDetector as B, type DetectedIssue as D, type FixDetector as F, type IssueImpact as I, type JudgeOptions as J, type PageLike as P, type ScanOptions as S, VIEWPORT_SIZES as V, type WcagLevel as W, type ScanResult as a, type ScanIssue as b, type InternalBug as c, type JudgeTransport as d, type SiteTheme as e, DEFAULT_JUDGE_BASE_URL as f, DEFAULT_JUDGE_TEXT_MODEL as g, DEFAULT_JUDGE_VISION_MODEL as h, type FixIssueType as i, type IssueSource as j, JUDGE_DEFAULT_TIMEOUT_MS as k, JUDGE_SYSTEM_PROMPT as l, type JudgeChatBody as m, type JudgeItem as n, type JudgeRequest as o, type JudgeVerdict as p, type JudgedEntry as q, type ViewportPreset as r, WCAG_BY_ISSUE as s, buildJudgeChatBody as t, hostedJudgeTransport as u, judgeUndecided as v, openAiCompatibleTransport as w, resetJudgeCache as x };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webability/core",
3
- "version": "1.17.2",
3
+ "version": "1.19.0",
4
4
  "description": "Accessibility scanner engine — WCAG 2.2 AA compliance testing with 200+ rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -40,13 +40,14 @@
40
40
  "NOTICES.md"
41
41
  ],
42
42
  "scripts": {
43
- "build": "npm run gen:dom-kernel && tsup",
43
+ "build": "npm run gen:dom-kernel && npm run gen:rubrics && tsup",
44
44
  "dev": "tsup --watch",
45
45
  "test": "WA_STRICT_STATE=1 vitest run --testTimeout=20000",
46
46
  "test:watch": "vitest",
47
47
  "typecheck": "tsc --noEmit",
48
48
  "clean": "rm -rf dist",
49
- "gen:dom-kernel": "node scripts/gen-dom-kernel.mjs"
49
+ "gen:dom-kernel": "node scripts/gen-dom-kernel.mjs",
50
+ "gen:rubrics": "node scripts/gen-rubrics.mjs"
50
51
  },
51
52
  "keywords": [
52
53
  "a11y",