@lifeonlars/prime-yggdrasil 0.3.0 → 0.4.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.
@@ -4,7 +4,7 @@
4
4
 
5
5
  **When to invoke:** When implementing any UI, reviewing code for accessibility, or validating semantic token pairings.
6
6
 
7
- **Status:** 🚧 Phase 6 (Future) - Specification complete, not yet integrated into CLI/ESLint
7
+ **Status:** Active - Integrated into CLI validation and ESLint plugin (Phase 6 complete)
8
8
 
9
9
  **Mandatory References:**
10
10
  - [`docs/AESTHETICS.md`](../../docs/AESTHETICS.md) - Accessibility requirements section
@@ -571,11 +571,18 @@ button:focus-visible {
571
571
 
572
572
  ---
573
573
 
574
- **Status:** 🚧 Phase 6 specification (ready for implementation)
575
- **Next Steps:**
576
- 1. Integrate contrast checking into CLI validation
577
- 2. Add ARIA validation rules to ESLint plugin
578
- 3. Create accessibility testing guide
579
- 4. Add to consumer agent bundle
574
+ **Status:** Phase 6 Active (CLI validation + ESLint plugin integrated)
575
+ **Available Validations:**
576
+ 1. Missing alt text (images, icon-only buttons, avatars)
577
+ 2. Missing form labels (proper htmlFor/id association)
580
578
 
581
- **Last Updated:** 2026-01-10
579
+ **Usage:**
580
+ ```bash
581
+ # CLI validation
582
+ npx @lifeonlars/prime-yggdrasil validate --rules accessibility/missing-alt-text,accessibility/missing-form-labels
583
+ npx @lifeonlars/prime-yggdrasil audit --fix
584
+
585
+ # ESLint (install @lifeonlars/eslint-plugin-yggdrasil)
586
+ ```
587
+
588
+ **Last Updated:** 2026-01-11
@@ -4,7 +4,7 @@
4
4
 
5
5
  **When to invoke:** When implementing interactive features, forms, async operations, or user feedback mechanisms.
6
6
 
7
- **Status:** 🚧 Phase 6 (Future) - Specification complete, not yet integrated into CLI/ESLint
7
+ **Status:** Active - Integrated into CLI validation and ESLint plugin (Phase 6 complete)
8
8
 
9
9
  **Mandatory References:**
10
10
  - [`docs/AESTHETICS.md`](../../docs/AESTHETICS.md) - Interaction principles (subtle motion, clear feedback, functional transparency)
@@ -455,11 +455,19 @@ Before implementing any interactive feature, verify:
455
455
 
456
456
  ---
457
457
 
458
- **Status:** 🚧 Phase 6 specification (ready for implementation)
459
- **Next Steps:**
460
- 1. Integrate into CLI validation
461
- 2. Add to ESLint plugin
462
- 3. Create pattern library examples
463
- 4. Add to consumer agent bundle
458
+ **Status:** Phase 6 Active (CLI validation + ESLint plugin integrated)
459
+ **Available Validations:**
460
+ 1. State completeness (loading/error/empty/disabled)
461
+ 2. Generic copy detection (button labels, messages)
462
+ 3. Focus management (Dialog/Modal patterns)
464
463
 
465
- **Last Updated:** 2026-01-10
464
+ **Usage:**
465
+ ```bash
466
+ # CLI validation
467
+ npx @lifeonlars/prime-yggdrasil validate --rules interaction-patterns/state-completeness
468
+ npx @lifeonlars/prime-yggdrasil audit --fix
469
+
470
+ # ESLint (install @lifeonlars/eslint-plugin-yggdrasil)
471
+ ```
472
+
473
+ **Last Updated:** 2026-01-11
package/README.md CHANGED
@@ -199,11 +199,11 @@ Yggdrasil includes **6 specialized AI agents** that prevent drift, guide composi
199
199
  npx @lifeonlars/prime-yggdrasil init
200
200
  ```
201
201
 
202
- This copies 4 active agents + 2 future specs to your project's `.ai/yggdrasil/` directory.
202
+ This copies all 6 active agents to your project's `.ai/yggdrasil/` directory.
203
203
 
204
204
  ### 📋 The 6 Agents
205
205
 
206
- #### Active Agents (Phase 1-4 Complete)
206
+ #### Active Agents (All 6 Complete - Phase 6 ✅)
207
207
 
208
208
  1. **Block Composer** - Composition-first UI planning
209
209
  - Prevents bespoke component creation
@@ -225,18 +225,17 @@ This copies 4 active agents + 2 future specs to your project's `.ai/yggdrasil/`
225
225
  - ESLint plugin + CLI validation
226
226
  - Autofix capability for safe violations
227
227
 
228
- #### Future Agents (Phase 6 Specs Ready)
228
+ 5. **Interaction Patterns** - Behavioral consistency *(Phase 6 - NEW ✨)*
229
+ - Enforces state completeness (loading/error/empty/disabled)
230
+ - Detects generic copy (button labels, messages)
231
+ - Validates focus management (Dialog/Modal patterns)
232
+ - Ensures keyboard navigation works correctly
229
233
 
230
- 5. **Interaction Patterns** *(specification complete)*
231
- - Standardizes empty/loading/error/success patterns
232
- - Enforces keyboard navigation and focus management
233
- - Ensures copy is clear, pragmatic, non-fluffy
234
-
235
- 6. **Accessibility** *(specification complete)*
236
- - WCAG 2.1 AA minimum compliance
237
- - Contrast ratio validation
234
+ 6. **Accessibility** - WCAG 2.1 AA compliance *(Phase 6 - NEW ✨)*
235
+ - Validates alt text on images and icon-only buttons
236
+ - Enforces proper form label associations (htmlFor/id)
237
+ - Checks contrast ratios for text/surface combinations
238
238
  - Ensures color is not the only cue (icons, text, patterns)
239
- - Validates ARIA and semantic HTML
240
239
 
241
240
  ### 🛠️ Agent Tools
242
241
 
@@ -318,10 +317,10 @@ npx @lifeonlars/prime-yggdrasil validate --format json
318
317
 
319
318
  ### 📊 Enforcement Stats
320
319
 
321
- - **7 ESLint Rules** - Warnings (recommended) or errors (strict)
322
- - **5 CLI Validation Rules** - Report-only or autofix mode
323
- - **4 Active Agents** - Guidance during development
324
- - **2 Future Agents** - Specifications ready for Phase 6
320
+ - **12 CLI Validation Rules** - 7 core + 5 Phase 6 (Interaction Patterns + Accessibility)
321
+ - **6 Active Agents** - All agents operational (Phase 6 complete ✅)
322
+ - **Autofix Support** - State completeness, alt text, form labels, focus management
323
+ - **WCAG 2.1 AA** - Automated accessibility validation
325
324
 
326
325
  ### Example AI Prompt (with Agents)
327
326
 
@@ -1,6 +1,10 @@
1
1
  import { readFileSync, writeFileSync } from 'fs';
2
2
  import { validateCommand } from './validate.js';
3
3
 
4
+ // Import Phase 6 rules for autofix
5
+ import interactionPatternsRules from '../rules/interaction-patterns/index.js';
6
+ import accessibilityRules from '../rules/accessibility/index.js';
7
+
4
8
  /**
5
9
  * Audit command - Detailed analysis with autofix
6
10
  *
@@ -136,7 +140,25 @@ const AUTOFIXES = {
136
140
  return content;
137
141
  },
138
142
  explanation: 'Rounds spacing values to nearest 4px grid value (0, 4, 8, 12, 16, 20, 24, 28, 32px).'
139
- }
143
+ },
144
+
145
+ // Phase 6 Autofixes: Dynamically add from rule definitions
146
+ ...Object.fromEntries(
147
+ Object.entries({ ...interactionPatternsRules, ...accessibilityRules }).map(([key, rule]) => [
148
+ key,
149
+ {
150
+ canAutoFix: typeof rule.autofix === 'function',
151
+ fix: (content, violation) => {
152
+ if (rule.autofix) {
153
+ const result = rule.autofix(content, violation);
154
+ return result.fixed ? result.content : content;
155
+ }
156
+ return content;
157
+ },
158
+ explanation: rule.autofix ? `${rule.description} (Phase 6 rule)` : 'Manual fix required'
159
+ }
160
+ ])
161
+ )
140
162
  };
141
163
 
142
164
  /**
@@ -1,6 +1,10 @@
1
1
  import { readFileSync, readdirSync, statSync } from 'fs';
2
2
  import { join, extname } from 'path';
3
3
 
4
+ // Import Phase 6 rules
5
+ import interactionPatternsRules from '../rules/interaction-patterns/index.js';
6
+ import accessibilityRules from '../rules/accessibility/index.js';
7
+
4
8
  /**
5
9
  * Validate command - Report-only mode
6
10
  *
@@ -241,7 +245,31 @@ const RULES = {
241
245
 
242
246
  return violations;
243
247
  }
244
- }
248
+ },
249
+
250
+ // Phase 6 Rules: Interaction Patterns
251
+ ...Object.fromEntries(
252
+ Object.entries(interactionPatternsRules).map(([key, rule]) => [
253
+ key,
254
+ {
255
+ name: rule.name,
256
+ severity: rule.severity,
257
+ check: (content, filePath) => rule.validate(content, filePath)
258
+ }
259
+ ])
260
+ ),
261
+
262
+ // Phase 6 Rules: Accessibility
263
+ ...Object.fromEntries(
264
+ Object.entries(accessibilityRules).map(([key, rule]) => [
265
+ key,
266
+ {
267
+ name: rule.name,
268
+ severity: rule.severity,
269
+ check: (content, filePath) => rule.validate(content, filePath)
270
+ }
271
+ ])
272
+ )
245
273
  };
246
274
 
247
275
  /**
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Accessibility Rules
3
+ *
4
+ * Phase 6 - WCAG 2.1 AA compliance enforcement
5
+ *
6
+ * All rules enforce patterns from .ai/agents/accessibility.md
7
+ */
8
+
9
+ import missingAltText from './missing-alt-text.js';
10
+ import missingFormLabels from './missing-form-labels.js';
11
+
12
+ export default {
13
+ 'accessibility/missing-alt-text': missingAltText,
14
+ 'accessibility/missing-form-labels': missingFormLabels,
15
+ };
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Accessibility Rule: Missing Alt Text
3
+ *
4
+ * Enforces alt text on images and aria-label on icon-only buttons.
5
+ * WCAG 2.1 Level AA - Guideline 1.1 Text Alternatives
6
+ *
7
+ * Phase 6 - Accessibility Agent
8
+ */
9
+
10
+ export default {
11
+ name: 'accessibility/missing-alt-text',
12
+ description: 'Enforce alt text on images and aria-label on icon-only buttons',
13
+ category: 'accessibility',
14
+ severity: 'error',
15
+ documentation: 'https://github.com/lifeonlars/prime-yggdrasil/blob/master/.ai/agents/accessibility.md#11-text-alternatives',
16
+
17
+ validate(fileContent, filePath) {
18
+ const violations = [];
19
+
20
+ // Skip non-React/TSX files
21
+ if (!filePath.match(/\.(jsx|tsx|js|ts)$/)) {
22
+ return violations;
23
+ }
24
+
25
+ // Pattern 1: Check <img> tags for alt attribute
26
+ const imgRegex = /<img([^>]*)>/g;
27
+ let match;
28
+
29
+ while ((match = imgRegex.exec(fileContent)) !== null) {
30
+ const imgTag = match[1];
31
+ const line = this.getLineNumber(fileContent, match.index);
32
+
33
+ if (!imgTag.includes('alt=')) {
34
+ violations.push({
35
+ line,
36
+ column: 1,
37
+ message: 'Image missing alt attribute. Add alt text or alt="" for decorative images.',
38
+ severity: 'error',
39
+ rule: this.name,
40
+ suggestion: 'alt="Description of image content" or alt="" for decorative',
41
+ });
42
+ }
43
+
44
+ // Check for empty alt with src that suggests non-decorative
45
+ if (imgTag.includes('alt=""') || imgTag.includes("alt=''")) {
46
+ const srcMatch = imgTag.match(/src=["']([^"']+)["']/);
47
+ if (srcMatch) {
48
+ const src = srcMatch[1];
49
+ const nonDecorativeKeywords = ['chart', 'graph', 'diagram', 'photo', 'avatar', 'profile'];
50
+
51
+ if (nonDecorativeKeywords.some(keyword => src.toLowerCase().includes(keyword))) {
52
+ violations.push({
53
+ line,
54
+ column: 1,
55
+ message: 'Image appears non-decorative but has empty alt. Add descriptive alt text.',
56
+ severity: 'warning',
57
+ rule: this.name,
58
+ suggestion: `alt="Description of ${src}"`,
59
+ });
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ // Pattern 2: Check PrimeReact Image component
66
+ const primeImageRegex = /<Image([^>]*)>/g;
67
+
68
+ while ((match = primeImageRegex.exec(fileContent)) !== null) {
69
+ const imageTag = match[1];
70
+ const line = this.getLineNumber(fileContent, match.index);
71
+
72
+ if (!imageTag.includes('alt=')) {
73
+ violations.push({
74
+ line,
75
+ column: 1,
76
+ message: 'PrimeReact Image missing alt attribute.',
77
+ severity: 'error',
78
+ rule: this.name,
79
+ suggestion: 'Add alt="Description" prop',
80
+ });
81
+ }
82
+ }
83
+
84
+ // Pattern 3: Check icon-only buttons for aria-label
85
+ const buttonRegex = /<Button([^>]*)>/g;
86
+
87
+ while ((match = buttonRegex.exec(fileContent)) !== null) {
88
+ const buttonTag = match[1];
89
+ const line = this.getLineNumber(fileContent, match.index);
90
+
91
+ const hasIcon = buttonTag.includes('icon=');
92
+ const hasLabel = buttonTag.includes('label=');
93
+ const hasAriaLabel = buttonTag.includes('ariaLabel=') || buttonTag.includes('aria-label=');
94
+
95
+ // Icon-only button (has icon but no label)
96
+ if (hasIcon && !hasLabel && !hasAriaLabel) {
97
+ violations.push({
98
+ line,
99
+ column: 1,
100
+ message: 'Icon-only button missing ariaLabel. Screen readers need descriptive label.',
101
+ severity: 'error',
102
+ rule: this.name,
103
+ suggestion: 'Add ariaLabel="Description of action"',
104
+ });
105
+ }
106
+ }
107
+
108
+ // Pattern 4: Check custom icon buttons (i tag with onClick)
109
+ const iconClickRegex = /<i[^>]*className=["'][^"']*pi[^"']*["'][^>]*onClick/g;
110
+
111
+ while ((match = iconClickRegex.exec(fileContent)) !== null) {
112
+ const line = this.getLineNumber(fileContent, match.index);
113
+
114
+ violations.push({
115
+ line,
116
+ column: 1,
117
+ message: 'Icon with onClick should be wrapped in Button with ariaLabel, not clickable <i> tag.',
118
+ severity: 'error',
119
+ rule: this.name,
120
+ suggestion: '<Button icon="pi pi-..." ariaLabel="Action" onClick={handler} />',
121
+ });
122
+ }
123
+
124
+ // Pattern 5: Check Avatar component without alt or image
125
+ const avatarRegex = /<Avatar([^>]*)>/g;
126
+
127
+ while ((match = avatarRegex.exec(fileContent)) !== null) {
128
+ const avatarTag = match[1];
129
+ const line = this.getLineNumber(fileContent, match.index);
130
+
131
+ const hasImage = avatarTag.includes('image=');
132
+ const hasLabel = avatarTag.includes('label=');
133
+ const hasAlt = avatarTag.includes('alt=');
134
+ const hasAriaLabel = avatarTag.includes('ariaLabel=') || avatarTag.includes('aria-label=');
135
+
136
+ if (hasImage && !hasAlt && !hasAriaLabel) {
137
+ violations.push({
138
+ line,
139
+ column: 1,
140
+ message: 'Avatar with image should have alt or ariaLabel for accessibility.',
141
+ severity: 'warning',
142
+ rule: this.name,
143
+ suggestion: 'Add alt="User name" or ariaLabel="User name"',
144
+ });
145
+ }
146
+ }
147
+
148
+ return violations;
149
+ },
150
+
151
+ getLineNumber(content, index) {
152
+ return content.substring(0, index).split('\n').length;
153
+ },
154
+
155
+ autofix(fileContent, violation) {
156
+ if (violation.message.includes('Image missing alt attribute')) {
157
+ // Add alt="" as safe default (user must fill in description)
158
+ const imgRegex = /(<img[^>]*)(>)/;
159
+ const match = imgRegex.exec(fileContent);
160
+
161
+ if (match && !match[1].includes('alt=')) {
162
+ const fixedContent = fileContent.replace(
163
+ match[0],
164
+ `${match[1]} alt=""${match[2]}`
165
+ );
166
+
167
+ return {
168
+ fixed: true,
169
+ content: fixedContent,
170
+ message: 'Added empty alt attribute (add description if image is not decorative)',
171
+ };
172
+ }
173
+ }
174
+
175
+ if (violation.message.includes('Icon-only button missing ariaLabel')) {
176
+ const buttonRegex = /(<Button[^>]*icon=["'][^"']+["'][^>]*)(>)/;
177
+ const match = buttonRegex.exec(fileContent);
178
+
179
+ if (match && !match[1].includes('ariaLabel')) {
180
+ const iconMatch = match[1].match(/icon=["']pi pi-([^"']+)["']/);
181
+ const iconName = iconMatch ? iconMatch[1] : 'action';
182
+
183
+ const fixedContent = fileContent.replace(
184
+ match[0],
185
+ `${match[1]} ariaLabel="${iconName}"${match[2]}`
186
+ );
187
+
188
+ return {
189
+ fixed: true,
190
+ content: fixedContent,
191
+ message: `Added ariaLabel="${iconName}" (customize for specific action)`,
192
+ };
193
+ }
194
+ }
195
+
196
+ return {
197
+ fixed: false,
198
+ message: 'Manual fix required. Add descriptive alt text or aria-label.',
199
+ };
200
+ },
201
+ };
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Accessibility Rule: Missing Form Labels
3
+ *
4
+ * Enforces proper label association for form inputs.
5
+ * WCAG 2.1 Level AA - Guideline 3.3 Input Assistance
6
+ *
7
+ * Phase 6 - Accessibility Agent
8
+ */
9
+
10
+ export default {
11
+ name: 'accessibility/missing-form-labels',
12
+ description: 'Enforce proper labels on form inputs with htmlFor/id association',
13
+ category: 'accessibility',
14
+ severity: 'error',
15
+ documentation: 'https://github.com/lifeonlars/prime-yggdrasil/blob/master/.ai/agents/accessibility.md#33-input-assistance',
16
+
17
+ formComponents: [
18
+ 'InputText',
19
+ 'InputNumber',
20
+ 'InputTextarea',
21
+ 'Password',
22
+ 'Dropdown',
23
+ 'MultiSelect',
24
+ 'Calendar',
25
+ 'Checkbox',
26
+ 'RadioButton',
27
+ 'InputSwitch',
28
+ 'Slider',
29
+ 'Rating',
30
+ 'ColorPicker',
31
+ 'Chips',
32
+ 'AutoComplete',
33
+ ],
34
+
35
+ validate(fileContent, filePath) {
36
+ const violations = [];
37
+
38
+ // Skip non-React/TSX files
39
+ if (!filePath.match(/\.(jsx|tsx|js|ts)$/)) {
40
+ return violations;
41
+ }
42
+
43
+ // Extract all form component instances with their IDs
44
+ const componentInstances = this.extractComponentInstances(fileContent);
45
+
46
+ // Extract all label elements with their htmlFor attributes
47
+ const labels = this.extractLabels(fileContent);
48
+
49
+ // Check each form component
50
+ componentInstances.forEach(instance => {
51
+ const line = this.getLineNumber(fileContent, instance.index);
52
+
53
+ // Check 1: Component has id attribute
54
+ if (!instance.id) {
55
+ violations.push({
56
+ line,
57
+ column: 1,
58
+ message: `${instance.component} missing id attribute. Required for label association.`,
59
+ severity: 'error',
60
+ rule: this.name,
61
+ suggestion: `Add id="${this.generateId(instance.component)}"`,
62
+ });
63
+ return;
64
+ }
65
+
66
+ // Check 2: Label exists with matching htmlFor
67
+ const hasLabel = labels.some(label => label.htmlFor === instance.id);
68
+ const hasAriaLabel = instance.tag.includes('aria-label=') || instance.tag.includes('ariaLabel=');
69
+ const hasPlaceholder = instance.tag.includes('placeholder=');
70
+
71
+ if (!hasLabel && !hasAriaLabel) {
72
+ violations.push({
73
+ line,
74
+ column: 1,
75
+ message: `${instance.component} with id="${instance.id}" missing associated label. Add <label htmlFor="${instance.id}">`,
76
+ severity: 'error',
77
+ rule: this.name,
78
+ suggestion: `<label htmlFor="${instance.id}">Label Text</label>`,
79
+ });
80
+ }
81
+
82
+ // Check 3: Placeholder should not replace label
83
+ if (!hasLabel && !hasAriaLabel && hasPlaceholder) {
84
+ violations.push({
85
+ line,
86
+ column: 1,
87
+ message: `${instance.component} uses placeholder but missing label. Placeholder is not a substitute for labels.`,
88
+ severity: 'error',
89
+ rule: this.name,
90
+ suggestion: 'Add proper <label> element in addition to placeholder.',
91
+ });
92
+ }
93
+ });
94
+
95
+ // Check for labels without matching inputs
96
+ labels.forEach(label => {
97
+ if (!label.htmlFor) {
98
+ const line = this.getLineNumber(fileContent, label.index);
99
+
100
+ violations.push({
101
+ line,
102
+ column: 1,
103
+ message: 'Label missing htmlFor attribute. Required for screen reader association.',
104
+ severity: 'warning',
105
+ rule: this.name,
106
+ suggestion: 'Add htmlFor="input-id" matching the input\'s id',
107
+ });
108
+ return;
109
+ }
110
+
111
+ const hasMatchingInput = componentInstances.some(instance => instance.id === label.htmlFor);
112
+
113
+ if (!hasMatchingInput) {
114
+ const line = this.getLineNumber(fileContent, label.index);
115
+
116
+ violations.push({
117
+ line,
118
+ column: 1,
119
+ message: `Label htmlFor="${label.htmlFor}" has no matching input with id="${label.htmlFor}".`,
120
+ severity: 'warning',
121
+ rule: this.name,
122
+ suggestion: `Add id="${label.htmlFor}" to the associated input`,
123
+ });
124
+ }
125
+ });
126
+
127
+ // Check for required field indicators
128
+ const requiredInputs = componentInstances.filter(instance =>
129
+ instance.tag.includes('required') || instance.tag.includes('aria-required')
130
+ );
131
+
132
+ requiredInputs.forEach(instance => {
133
+ const line = this.getLineNumber(fileContent, instance.index);
134
+ const label = labels.find(l => l.htmlFor === instance.id);
135
+
136
+ if (label) {
137
+ const labelContent = fileContent.substring(label.index, label.index + 200);
138
+
139
+ // Check if label indicates required status (*, required text, etc.)
140
+ const hasRequiredIndicator = /(\*|required|Required)/i.test(labelContent);
141
+
142
+ if (!hasRequiredIndicator) {
143
+ violations.push({
144
+ line,
145
+ column: 1,
146
+ message: `Required field "${instance.id}" should have visual indicator in label (e.g., asterisk).`,
147
+ severity: 'warning',
148
+ rule: this.name,
149
+ suggestion: '<span style={{ color: "var(--text-context-danger)" }}>*</span>',
150
+ });
151
+ }
152
+ }
153
+ });
154
+
155
+ return violations;
156
+ },
157
+
158
+ extractComponentInstances(fileContent) {
159
+ const instances = [];
160
+
161
+ this.formComponents.forEach(component => {
162
+ const regex = new RegExp(`<${component}([^>]*)>`, 'g');
163
+ let match;
164
+
165
+ while ((match = regex.exec(fileContent)) !== null) {
166
+ const tag = match[1];
167
+ const idMatch = tag.match(/id=["']([^"']+)["']/);
168
+
169
+ instances.push({
170
+ component,
171
+ tag,
172
+ id: idMatch ? idMatch[1] : null,
173
+ index: match.index,
174
+ });
175
+ }
176
+ });
177
+
178
+ return instances;
179
+ },
180
+
181
+ extractLabels(fileContent) {
182
+ const labels = [];
183
+ const labelRegex = /<label([^>]*)>/g;
184
+ let match;
185
+
186
+ while ((match = labelRegex.exec(fileContent)) !== null) {
187
+ const tag = match[1];
188
+ const htmlForMatch = tag.match(/htmlFor=["']([^"']+)["']/);
189
+
190
+ labels.push({
191
+ tag,
192
+ htmlFor: htmlForMatch ? htmlForMatch[1] : null,
193
+ index: match.index,
194
+ });
195
+ }
196
+
197
+ return labels;
198
+ },
199
+
200
+ generateId(component) {
201
+ const base = component.toLowerCase().replace('input', '');
202
+ return `${base}-${Date.now().toString(36)}`;
203
+ },
204
+
205
+ getLineNumber(content, index) {
206
+ return content.substring(0, index).split('\n').length;
207
+ },
208
+
209
+ autofix(fileContent, violation) {
210
+ if (violation.message.includes('missing id attribute')) {
211
+ const componentMatch = violation.message.match(/^(\w+) missing/);
212
+ if (!componentMatch) return { fixed: false };
213
+
214
+ const component = componentMatch[1];
215
+ const componentRegex = new RegExp(`(<${component}[^>]*)(>)`, 'g');
216
+ const match = componentRegex.exec(fileContent);
217
+
218
+ if (match && !match[1].includes(' id=')) {
219
+ const generatedId = this.generateId(component);
220
+ const fixedContent = fileContent.replace(
221
+ match[0],
222
+ `${match[1]} id="${generatedId}"${match[2]}`
223
+ );
224
+
225
+ return {
226
+ fixed: true,
227
+ content: fixedContent,
228
+ message: `Added id="${generatedId}" (add corresponding <label htmlFor="${generatedId}">)`,
229
+ };
230
+ }
231
+ }
232
+
233
+ return {
234
+ fixed: false,
235
+ message: 'Manual fix required. Add proper label/id association.',
236
+ };
237
+ },
238
+ };