@wix/agent-skills 1.18.2 → 1.19.1

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 (68) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +42 -40
  3. package/skills/wix-app/references/BACKEND_API.md +151 -152
  4. package/skills/wix-app/references/DASHBOARD_PAGE.md +6 -10
  5. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +16 -7
  6. package/skills/wix-app/references/EXTENSION_REGISTRATION.md +12 -8
  7. package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +53 -131
  8. package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +1 -2
  9. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +83 -73
  10. package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +3 -2
  11. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +9 -0
  12. package/skills/wix-app/references/editor-react-component/PARTS.md +5 -2
  13. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +1 -1
  14. package/skills/wix-app/scripts/pkg-root.cjs +73 -0
  15. package/skills/wix-app/scripts/scan-a11y-code.cjs +310 -40
  16. package/skills/wix-app/scripts/scan-a11y-eslint.cjs +118 -24
  17. package/skills/wix-app/scripts/scan-a11y-render.cjs +551 -0
  18. package/skills/wix-app/scripts/scan-a11y-review.cjs +373 -0
  19. package/skills/wix-base44-connector/SKILL.md +24 -22
  20. package/skills/wix-base44-connector/scripts/utils.cjs +39 -3
  21. package/skills/wix-docs/SKILL.md +47 -20
  22. package/skills/wix-headless/entry/bootstrap.mjs +106 -50
  23. package/skills/wix-headless/entry/skill.md +5 -3
  24. package/skills/wix-headless/references/managed/AUTHENTICATION.md +25 -5
  25. package/skills/wix-headless-fast/SKILL.md +25 -7
  26. package/skills/wix-headless-fast/entry/skill.md +23 -14
  27. package/skills/wix-headless-fast/install/fast-path.mjs +48 -5
  28. package/skills/wix-headless-fast/install/pin-agents-md.mjs +57 -0
  29. package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +3 -3
  30. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +204 -35
  31. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +146 -36
  32. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +8 -2
  33. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +95 -20
  34. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +21 -2
  35. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +104 -18
  36. package/skills/wix-manage/SKILL.md +14 -8
  37. package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +59 -33
  38. package/skills/wix-manage/references/bookings/bookings-staff-setup.md +45 -22
  39. package/skills/wix-manage/references/bookings/multi-resource-service-creation.md +70 -12
  40. package/skills/wix-manage/references/calendar/configure-default-business-hours.md +53 -17
  41. package/skills/wix-manage/references/google-ads/billing-and-payment.md +3 -3
  42. package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +8 -8
  43. package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +4 -4
  44. package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +1 -1
  45. package/skills/wix-manage/references/google-ads/install-and-create-account.md +9 -9
  46. package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +7 -7
  47. package/skills/wix-manage/references/seo/manage-content-plan.md +181 -135
  48. package/skills/wix-manage/references/seo/manage-google-search-console.md +272 -0
  49. package/skills/wix-manage/references/sites/upload-static-site.md +53 -0
  50. package/skills/wix-manage/references/stores/change-store-currency.md +1 -1
  51. package/skills/wix-manage/references/stores/create-product-catalog-v3.md +17 -1
  52. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +12 -9
  53. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +12 -9
  54. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
  55. package/skills/wix-vibe-headless/platforms/base44.md +11 -3
  56. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +5 -1
  57. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +5 -4
  58. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +33 -5
  59. package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +0 -80
  60. package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +0 -140
  61. package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +0 -112
  62. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +0 -100
  63. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +0 -130
  64. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +0 -48
  65. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +0 -66
  66. package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +0 -103
  67. package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +0 -56
  68. package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +0 -23
@@ -195,6 +195,12 @@ and do not expose a visual prop solely for that purpose.
195
195
  }
196
196
  ```
197
197
 
198
+ The same override risk applies to paintable properties. A layout-only inner
199
+ wrapper must stay visually transparent; if it isn't, promote it to its own
200
+ named part instead of hardcoding an opaque style on an unexposed node, since
201
+ that style would otherwise mask the parent part's own
202
+ `background`/`border`/`box-shadow`.
203
+
198
204
  ## Interaction and Motion
199
205
 
200
206
  - Set `pointer-events: auto` on the root and each interactive element.
@@ -254,3 +260,6 @@ contract; the contrast relationship is.
254
260
  - [ ] Visual containers have deliberate hierarchy rather than bare scaffolding.
255
261
  - [ ] Every explicit foreground has a known contrasting background; transparent
256
262
  components inherit foregrounds from their host.
263
+ - [ ] No unexposed descendant inside a named part paints its own opaque
264
+ `background`/`border`/`box-shadow`; a wrapper needing its own surface is
265
+ promoted to a named part instead.
@@ -24,14 +24,17 @@ would plausibly control independently in the editor. It receives:
24
24
 
25
25
  - one prefixed global class: `'<component-name>-<part-name>'`
26
26
  - one CSS Module class
27
- - one matching `elementProps` entry, spread onto the element
27
+ - one matching `elementProps` entry, spread onto the element. Declare `a11y` on
28
+ the entry only when the part reads `ariaLabel`, and destructure it out before
29
+ spreading (see `ACCESSIBILITY.md`)
28
30
 
29
31
  Apply this filter to every candidate:
30
32
 
31
33
  - A state or variant (`selected`, `active`, `open`, `disabled`) is not a part.
32
34
  Implement it as a prefixed design-state modifier on the affected part.
33
35
  - A hidden and shown version of the same element is one part, not two.
34
- - A grouping or layout-only wrapper is not a part.
36
+ - A grouping or layout-only wrapper is not a part. A wrapper that isn't
37
+ visually transparent is no longer layout-only — promote it to a named part.
35
38
  - A static child whose styling and data are fully owned by its parent is not a
36
39
  part. For example, an image whose source and appearance both belong to its
37
40
  carousel slide can remain module-class-only.
@@ -86,4 +86,4 @@ export const ProfileCard: React.FC<ProfileCardProps> = (props) => {
86
86
  - [ ] Direction and accessibility contracts are wired to the correct elements.
87
87
  - [ ] Public handlers use supported SDK names; internal handlers stay internal.
88
88
  - [ ] CSS changed by the request keeps editable selectors flat and scoped.
89
- - [ ] The accessibility review runs after JSX edits.
89
+ - [ ] The accessibility review command passes, or its remaining findings are triaged.
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // Prints the install directory of a package — the folder holding its
3
+ // package.json — resolved from the current working directory.
4
+ //
5
+ // node pkg-root.cjs @wix/patterns
6
+ //
7
+ // Takes the package name as an argument and knows nothing about any particular
8
+ // package, so it stays correct as the packages it is pointed at change.
9
+ //
10
+ // Three steps, because one is not enough in the repos this runs in:
11
+ // 1. Activate Yarn PnP if a .pnp.cjs exists at or above cwd. A bare
12
+ // require.resolve throws in a Yarn Berry project even when the package is
13
+ // installed, and in a monorepo the .pnp.cjs sits at the repo root rather
14
+ // than beside the project being built.
15
+ // 2. require.resolve, which handles npm, pnpm and workspace layouts.
16
+ // 3. Failing that, walk up looking for node_modules/<pkg> directly, which
17
+ // covers a partially-installed tree where resolution fails but the files
18
+ // are on disk.
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const pkg = process.argv[2];
23
+ if (!pkg) {
24
+ console.error('usage: pkg-root.cjs <package-name>');
25
+ process.exit(2);
26
+ }
27
+
28
+ function activatePnp() {
29
+ let dir = process.cwd();
30
+ for (;;) {
31
+ const pnp = path.join(dir, '.pnp.cjs');
32
+ if (fs.existsSync(pnp)) {
33
+ try {
34
+ require(pnp).setup();
35
+ } catch {
36
+ // An unusable .pnp.cjs is not fatal: the plain resolve below may still
37
+ // succeed against a node_modules tree.
38
+ }
39
+ return;
40
+ }
41
+ const parent = path.dirname(dir);
42
+ if (parent === dir) return;
43
+ dir = parent;
44
+ }
45
+ }
46
+
47
+ activatePnp();
48
+
49
+ try {
50
+ console.log(
51
+ path.dirname(
52
+ require.resolve(`${pkg}/package.json`, { paths: [process.cwd()] }),
53
+ ),
54
+ );
55
+ process.exit(0);
56
+ } catch {
57
+ // fall through to the directory walk
58
+ }
59
+
60
+ let dir = process.cwd();
61
+ for (;;) {
62
+ const candidate = path.join(dir, 'node_modules', ...pkg.split('/'));
63
+ if (fs.existsSync(path.join(candidate, 'package.json'))) {
64
+ console.log(candidate);
65
+ process.exit(0);
66
+ }
67
+ const parent = path.dirname(dir);
68
+ if (parent === dir) {
69
+ console.error(`${pkg} not found from ${process.cwd()}`);
70
+ process.exit(1);
71
+ }
72
+ dir = parent;
73
+ }
@@ -157,6 +157,21 @@ const UNSUPPORTED_ARIA_ELEMENTS = new Set([
157
157
  ]);
158
158
 
159
159
  const FILE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs'];
160
+ /** `A11y` fields a component may read. ZeroConfig turns each read into an editor control. */
161
+ const A11Y_FIELDS = new Set(['ariaLabel']);
162
+ const A11Y_FIELDS_LABEL = [...A11Y_FIELDS].join(', ');
163
+ const A11Y_CONVERTER = 'convertA11yKeysToHtmlFormat';
164
+ const HARDCODED_LABEL_ATTRIBUTES = new Set(['aria-label', 'aria-description']);
165
+ const SUPPORTED_RULES = [
166
+ 'alt-text',
167
+ 'anchor-is-valid',
168
+ 'aria-props',
169
+ 'aria-role',
170
+ 'aria-unsupported-elements',
171
+ 'a11y-whole-object',
172
+ 'a11y-disallowed-field',
173
+ 'hardcoded-aria-label',
174
+ ];
160
175
  const parseCache = new Map();
161
176
  const resolutionWarnings = [];
162
177
 
@@ -189,12 +204,8 @@ function parseFile(filePath) {
189
204
  'optionalChaining',
190
205
  'nullishCoalescingOperator',
191
206
  ],
192
- errorRecovery: true,
193
207
  });
194
- const recoverableErrors = Array.isArray(ast.errors)
195
- ? ast.errors.map((error) => error.message)
196
- : [];
197
- const parsed = { ok: true, ast, code, recoverableErrors };
208
+ const parsed = { ok: true, ast, code };
198
209
  parseCache.set(filePath, parsed);
199
210
  return parsed;
200
211
  } catch (error) {
@@ -557,6 +568,132 @@ function toRelative(filePath) {
557
568
  return path.relative(ROOT, filePath) || filePath;
558
569
  }
559
570
 
571
+ // ─────────────────────────────────────────────────────────────────────────────
572
+ // Per-part a11y contract helpers
573
+ // ─────────────────────────────────────────────────────────────────────────────
574
+
575
+ /** Peel type assertions, parentheses, and guards such as `a11y && x` or `x ?? {}`. */
576
+ function unwrapExpression(node) {
577
+ let current = node;
578
+ for (let guard = 0; current && guard < 20; guard++) {
579
+ if (
580
+ t.isTSAsExpression(current) ||
581
+ t.isTSNonNullExpression(current) ||
582
+ t.isTSTypeAssertion(current) ||
583
+ t.isParenthesizedExpression(current) ||
584
+ (t.isTSSatisfiesExpression && t.isTSSatisfiesExpression(current))
585
+ ) {
586
+ current = current.expression;
587
+ } else if (t.isLogicalExpression(current)) {
588
+ current = current.operator === '&&' ? current.right : current.left;
589
+ } else {
590
+ break;
591
+ }
592
+ }
593
+ return current;
594
+ }
595
+
596
+ function isA11yIdentifierName(name) {
597
+ return name === 'a11y' || name.endsWith('A11y');
598
+ }
599
+
600
+ /** `a11y`, `toggleA11y`, `props.a11y`, `elementProps?.toggle?.a11y`. */
601
+ function isA11yLike(node) {
602
+ const expr = unwrapExpression(node);
603
+ if (t.isIdentifier(expr)) return isA11yIdentifierName(expr.name);
604
+ if ((t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) && !expr.computed) {
605
+ return t.isIdentifier(expr.property, { name: 'a11y' });
606
+ }
607
+ return false;
608
+ }
609
+
610
+ /** `props.elementProps?.toggle` → ['props', 'elementProps', 'toggle']; null for other shapes. */
611
+ function memberChain(node) {
612
+ const expr = unwrapExpression(node);
613
+ if (t.isIdentifier(expr)) return [expr.name];
614
+ if ((t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) && !expr.computed) {
615
+ const parent = memberChain(expr.object);
616
+ if (!parent || !t.isIdentifier(expr.property)) return null;
617
+ return [...parent, expr.property.name];
618
+ }
619
+ return null;
620
+ }
621
+
622
+ function isElementPropsPartChain(chain, partsWithA11y) {
623
+ if (!chain || chain.length < 2) return false;
624
+ const index = chain.indexOf('elementProps');
625
+ return index !== -1 && index === chain.length - 2 && partsWithA11y.has(chain[chain.length - 1]);
626
+ }
627
+
628
+ function typeLiteralHasMember(typeNode, memberName) {
629
+ return (
630
+ t.isTSTypeLiteral(typeNode) &&
631
+ typeNode.members.some(
632
+ (member) =>
633
+ t.isTSPropertySignature(member) &&
634
+ ((t.isIdentifier(member.key) && member.key.name === memberName) ||
635
+ (t.isStringLiteral(member.key) && member.key.value === memberName)),
636
+ )
637
+ );
638
+ }
639
+
640
+ /**
641
+ * Names of `elementProps` parts whose declared type carries an `a11y` field,
642
+ * keyed by folder so one component's contract never applies to another.
643
+ * Reads every `*.props.ts` next to the scanned files plus the files themselves.
644
+ */
645
+ function collectPropsTypes(files) {
646
+ const partsByDir = new Map();
647
+ const candidates = new Set(files);
648
+ for (const file of files) {
649
+ const dir = path.dirname(file);
650
+ if (!partsByDir.has(dir)) partsByDir.set(dir, new Set());
651
+ if (!fs.existsSync(dir)) continue;
652
+ for (const name of fs.readdirSync(dir)) {
653
+ if (name.endsWith('.props.ts')) candidates.add(path.join(dir, name));
654
+ }
655
+ }
656
+
657
+ for (const file of candidates) {
658
+ const parsed = parseFile(file);
659
+ if (!parsed.ok) continue;
660
+ const partsWithA11y = partsByDir.get(path.dirname(file));
661
+ traverse(parsed.ast, {
662
+ TSPropertySignature(path) {
663
+ const { key, typeAnnotation } = path.node;
664
+ if (!t.isIdentifier(key, { name: 'elementProps' }) || !typeAnnotation) return;
665
+ const literal = typeAnnotation.typeAnnotation;
666
+ if (!t.isTSTypeLiteral(literal)) return;
667
+ for (const member of literal.members) {
668
+ if (!t.isTSPropertySignature(member) || !member.typeAnnotation) continue;
669
+ const partName = t.isIdentifier(member.key)
670
+ ? member.key.name
671
+ : t.isStringLiteral(member.key)
672
+ ? member.key.value
673
+ : null;
674
+ if (partName && typeLiteralHasMember(member.typeAnnotation.typeAnnotation, 'a11y')) {
675
+ partsWithA11y.add(partName);
676
+ }
677
+ }
678
+ },
679
+ });
680
+ }
681
+
682
+ return partsByDir;
683
+ }
684
+
685
+ function contractFinding(filePath, node, rule, confidence, message) {
686
+ return findingFromNode(filePath, node, {
687
+ rule,
688
+ confidence,
689
+ message,
690
+ componentName: null,
691
+ semanticType: 'a11y-contract',
692
+ evidence: 'Per-part accessibility contract (ACCESSIBILITY.md).',
693
+ sourceKind: 'contract',
694
+ });
695
+ }
696
+
560
697
  function findingFromNode(filePath, node, data) {
561
698
  return {
562
699
  file: toRelative(filePath),
@@ -572,7 +709,7 @@ function findingFromNode(filePath, node, data) {
572
709
  };
573
710
  }
574
711
 
575
- function scanFile(filePath) {
712
+ function scanFile(filePath, options = {}) {
576
713
  const parsed = parseFile(filePath);
577
714
  if (!parsed.ok) {
578
715
  return {
@@ -586,9 +723,124 @@ function scanFile(filePath) {
586
723
 
587
724
  const imports = getImportMap(parsed.ast);
588
725
  const findings = [];
589
- const parseWarnings = parsed.recoverableErrors || [];
726
+ const partsWithA11y = options.partsWithA11y || new Set();
727
+
728
+ const reportDisallowedField = (node, fieldName) => {
729
+ findings.push(
730
+ contractFinding(
731
+ filePath,
732
+ node,
733
+ 'a11y-disallowed-field',
734
+ 'high',
735
+ `a11y.${fieldName} is read; only ${A11Y_FIELDS_LABEL} may be read. Keep roles, state, and structure in component code.`,
736
+ ),
737
+ );
738
+ };
590
739
 
591
740
  traverse(parsed.ast, {
741
+ JSXSpreadAttribute(path) {
742
+ const argument = unwrapExpression(path.node.argument);
743
+
744
+ if (isA11yLike(argument)) {
745
+ findings.push(
746
+ contractFinding(
747
+ filePath,
748
+ path.node,
749
+ 'a11y-whole-object',
750
+ 'high',
751
+ 'The whole a11y object is spread onto an element; every field becomes an editor control.',
752
+ ),
753
+ );
754
+ return;
755
+ }
756
+
757
+ // A spread identifier may alias `a11y` or an `elementProps` part; follow its declaration.
758
+ let chain = memberChain(argument);
759
+ if (t.isIdentifier(argument)) {
760
+ const binding = path.scope.getBinding(argument.name);
761
+ const declarator = binding && binding.path && binding.path.node;
762
+ if (declarator && t.isVariableDeclarator(declarator) && t.isIdentifier(declarator.id)) {
763
+ if (declarator.init && isA11yLike(declarator.init)) {
764
+ findings.push(
765
+ contractFinding(
766
+ filePath,
767
+ path.node,
768
+ 'a11y-whole-object',
769
+ 'high',
770
+ `${argument.name} aliases the a11y object and is spread onto an element; read a11y.ariaLabel instead.`,
771
+ ),
772
+ );
773
+ return;
774
+ }
775
+ chain = memberChain(declarator.init);
776
+ }
777
+ }
778
+ if (isElementPropsPartChain(chain, partsWithA11y)) {
779
+ findings.push(
780
+ contractFinding(
781
+ filePath,
782
+ path.node,
783
+ 'a11y-whole-object',
784
+ 'medium',
785
+ `elementProps.${chain[chain.length - 1]} is spread although its type declares a11y; destructure a11y out first.`,
786
+ ),
787
+ );
788
+ }
789
+ },
790
+
791
+ CallExpression(path) {
792
+ const callee = path.node.callee;
793
+ const isConverter =
794
+ t.isIdentifier(callee, { name: A11Y_CONVERTER }) ||
795
+ ((t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
796
+ t.isIdentifier(callee.property, { name: A11Y_CONVERTER }));
797
+ if (isConverter) {
798
+ findings.push(
799
+ contractFinding(
800
+ filePath,
801
+ path.node,
802
+ 'a11y-whole-object',
803
+ 'high',
804
+ `${A11Y_CONVERTER} writes every a11y field to the DOM; read only the field the part needs.`,
805
+ ),
806
+ );
807
+ }
808
+ },
809
+
810
+ 'MemberExpression|OptionalMemberExpression'(path) {
811
+ const { node } = path;
812
+ if (node.computed || !t.isIdentifier(node.property)) return;
813
+ if (!isA11yLike(node.object)) return;
814
+ if (A11Y_FIELDS.has(node.property.name)) return;
815
+ reportDisallowedField(node.property, node.property.name);
816
+ },
817
+
818
+ VariableDeclarator(path) {
819
+ const { id, init } = path.node;
820
+ if (!t.isObjectPattern(id) || !init || !isA11yLike(init)) return;
821
+ for (const property of id.properties) {
822
+ if (t.isRestElement(property)) {
823
+ findings.push(
824
+ contractFinding(
825
+ filePath,
826
+ property,
827
+ 'a11y-whole-object',
828
+ 'high',
829
+ 'Rest-destructuring a11y keeps every field; pick the single field the part needs.',
830
+ ),
831
+ );
832
+ continue;
833
+ }
834
+ if (!t.isObjectProperty(property) || property.computed) continue;
835
+ const fieldName = t.isIdentifier(property.key)
836
+ ? property.key.name
837
+ : t.isStringLiteral(property.key)
838
+ ? property.key.value
839
+ : null;
840
+ if (fieldName && !A11Y_FIELDS.has(fieldName)) reportDisallowedField(property, fieldName);
841
+ }
842
+ },
843
+
592
844
  JSXOpeningElement(path) {
593
845
  const node = path.node;
594
846
  const name = getJsxName(node.name);
@@ -600,6 +852,23 @@ function scanFile(filePath) {
600
852
  );
601
853
  const roleAttr = getAttribute(node, 'role');
602
854
 
855
+ for (const attr of ariaAttrs) {
856
+ const attrName = getJsxName(attr.name);
857
+ if (!HARDCODED_LABEL_ATTRIBUTES.has(attrName)) continue;
858
+ const literal = getLiteralAttributeValue(attr);
859
+ if (typeof literal === 'string' && literal.trim() !== '') {
860
+ findings.push(
861
+ contractFinding(
862
+ filePath,
863
+ attr,
864
+ 'hardcoded-aria-label',
865
+ 'high',
866
+ `${attrName}="${literal}" is a hardcoded string; use visible text, a11y.ariaLabel, or a constants.ts label.`,
867
+ ),
868
+ );
869
+ }
870
+ }
871
+
603
872
  if (semantic.semanticType === 'img' && semantic.confidence !== 'low') {
604
873
  const altAttr = getAttribute(node, 'alt');
605
874
  if (!altAttr) {
@@ -714,35 +983,19 @@ function scanFile(filePath) {
714
983
  },
715
984
  });
716
985
 
717
- return { parseError: null, findings, parseWarnings };
986
+ return { parseError: null, findings };
718
987
  }
719
988
 
720
- function main() {
721
- const files = process.argv.slice(2);
722
- if (files.length === 0) {
723
- console.log(
724
- JSON.stringify(
725
- {
726
- error: 'No files specified.',
727
- usage: 'node <SKILL_ROOT>/scripts/scan-a11y-code.cjs <file1> [file2] ...',
728
- },
729
- null,
730
- 2,
731
- ),
732
- );
733
- process.exit(1);
734
- }
989
+ /** Scan the given files. Any syntax error lands in `meta.parseErrors`; the review treats it as fatal. */
990
+ function scan(files) {
991
+ const absoluteFiles = files.map((file) => path.resolve(file));
992
+ const partsByDir = collectPropsTypes(absoluteFiles);
735
993
 
736
994
  const meta = {
737
995
  filesScanned: files.length,
738
996
  parser: '@babel/parser',
739
- supportedRules: [
740
- 'alt-text',
741
- 'anchor-is-valid',
742
- 'aria-props',
743
- 'aria-role',
744
- 'aria-unsupported-elements',
745
- ],
997
+ supportedRules: SUPPORTED_RULES,
998
+ a11yFields: [...A11Y_FIELDS],
746
999
  confidenceModel: ['high', 'medium', 'low', 'unknown'],
747
1000
  parseErrors: [],
748
1001
  resolutionWarnings,
@@ -750,15 +1003,9 @@ function main() {
750
1003
 
751
1004
  const findings = [];
752
1005
 
753
- for (const file of files.map((file) => path.resolve(file))) {
754
- const result = scanFile(file);
1006
+ for (const file of absoluteFiles) {
1007
+ const result = scanFile(file, { partsWithA11y: partsByDir.get(path.dirname(file)) });
755
1008
  if (result.parseError) meta.parseErrors.push(result.parseError);
756
- for (const warning of result.parseWarnings || []) {
757
- meta.parseErrors.push({
758
- file: toRelative(file),
759
- message: warning,
760
- });
761
- }
762
1009
  findings.push(...result.findings);
763
1010
  }
764
1011
 
@@ -771,7 +1018,30 @@ function main() {
771
1018
  cleanFiles: files.length - new Set(findings.map((item) => item.file)).size,
772
1019
  };
773
1020
 
774
- console.log(JSON.stringify({ meta, findings, summary }, null, 2));
1021
+ return { meta, findings, summary };
775
1022
  }
776
1023
 
777
- main();
1024
+ function main() {
1025
+ const files = process.argv.slice(2);
1026
+ if (files.length === 0) {
1027
+ console.log(
1028
+ JSON.stringify(
1029
+ {
1030
+ error: 'No files specified.',
1031
+ usage: 'node <SKILL_ROOT>/scripts/scan-a11y-code.cjs <file1> [file2] ...',
1032
+ },
1033
+ null,
1034
+ 2,
1035
+ ),
1036
+ );
1037
+ process.exit(1);
1038
+ }
1039
+
1040
+ console.log(JSON.stringify(scan(files), null, 2));
1041
+ }
1042
+
1043
+ module.exports = { scan };
1044
+
1045
+ if (require.main === module) {
1046
+ main();
1047
+ }