@brad-frost-web/eddie-brain 0.32.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.
- package/README.md +109 -0
- package/dist/analyze/drift-detector.d.ts +30 -0
- package/dist/analyze/drift-detector.d.ts.map +1 -0
- package/dist/analyze/drift-detector.js +310 -0
- package/dist/analyze/drift-detector.js.map +1 -0
- package/dist/analyze/health-scorer.d.ts +71 -0
- package/dist/analyze/health-scorer.d.ts.map +1 -0
- package/dist/analyze/health-scorer.js +420 -0
- package/dist/analyze/health-scorer.js.map +1 -0
- package/dist/analyze/index.d.ts +11 -0
- package/dist/analyze/index.d.ts.map +1 -0
- package/dist/analyze/index.js +11 -0
- package/dist/analyze/index.js.map +1 -0
- package/dist/analyze/naming-validator.d.ts +99 -0
- package/dist/analyze/naming-validator.d.ts.map +1 -0
- package/dist/analyze/naming-validator.js +430 -0
- package/dist/analyze/naming-validator.js.map +1 -0
- package/dist/analyze/slot-contract-validator.d.ts +68 -0
- package/dist/analyze/slot-contract-validator.d.ts.map +1 -0
- package/dist/analyze/slot-contract-validator.js +232 -0
- package/dist/analyze/slot-contract-validator.js.map +1 -0
- package/dist/analyze/token-validator.d.ts +62 -0
- package/dist/analyze/token-validator.d.ts.map +1 -0
- package/dist/analyze/token-validator.js +348 -0
- package/dist/analyze/token-validator.js.map +1 -0
- package/dist/cli/brain.d.ts +12 -0
- package/dist/cli/brain.d.ts.map +1 -0
- package/dist/cli/brain.js +641 -0
- package/dist/cli/brain.js.map +1 -0
- package/dist/cli/formatters/json.d.ts +15 -0
- package/dist/cli/formatters/json.d.ts.map +1 -0
- package/dist/cli/formatters/json.js +18 -0
- package/dist/cli/formatters/json.js.map +1 -0
- package/dist/cli/formatters/terminal.d.ts +19 -0
- package/dist/cli/formatters/terminal.d.ts.map +1 -0
- package/dist/cli/formatters/terminal.js +125 -0
- package/dist/cli/formatters/terminal.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/data/governance-rules.json +94 -0
- package/dist/governance/audit-log.d.ts +17 -0
- package/dist/governance/audit-log.d.ts.map +1 -0
- package/dist/governance/audit-log.js +44 -0
- package/dist/governance/audit-log.js.map +1 -0
- package/dist/governance/index.d.ts +8 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/index.js +8 -0
- package/dist/governance/index.js.map +1 -0
- package/dist/governance/permissions.d.ts +26 -0
- package/dist/governance/permissions.d.ts.map +1 -0
- package/dist/governance/permissions.js +75 -0
- package/dist/governance/permissions.js.map +1 -0
- package/dist/governance/rules-engine.d.ts +24 -0
- package/dist/governance/rules-engine.d.ts.map +1 -0
- package/dist/governance/rules-engine.js +111 -0
- package/dist/governance/rules-engine.js.map +1 -0
- package/dist/governance/trust-manager.d.ts +34 -0
- package/dist/governance/trust-manager.d.ts.map +1 -0
- package/dist/governance/trust-manager.js +148 -0
- package/dist/governance/trust-manager.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge-graph/component-index.d.ts +320 -0
- package/dist/knowledge-graph/component-index.d.ts.map +1 -0
- package/dist/knowledge-graph/component-index.js +1033 -0
- package/dist/knowledge-graph/component-index.js.map +1 -0
- package/dist/knowledge-graph/index.d.ts +134 -0
- package/dist/knowledge-graph/index.d.ts.map +1 -0
- package/dist/knowledge-graph/index.js +249 -0
- package/dist/knowledge-graph/index.js.map +1 -0
- package/dist/knowledge-graph/learning-history.d.ts +77 -0
- package/dist/knowledge-graph/learning-history.d.ts.map +1 -0
- package/dist/knowledge-graph/learning-history.js +187 -0
- package/dist/knowledge-graph/learning-history.js.map +1 -0
- package/dist/knowledge-graph/relationship-map.d.ts +55 -0
- package/dist/knowledge-graph/relationship-map.d.ts.map +1 -0
- package/dist/knowledge-graph/relationship-map.js +238 -0
- package/dist/knowledge-graph/relationship-map.js.map +1 -0
- package/dist/knowledge-graph/token-taxonomy.d.ts +127 -0
- package/dist/knowledge-graph/token-taxonomy.d.ts.map +1 -0
- package/dist/knowledge-graph/token-taxonomy.js +357 -0
- package/dist/knowledge-graph/token-taxonomy.js.map +1 -0
- package/dist/loop/fix-agent.d.ts +55 -0
- package/dist/loop/fix-agent.d.ts.map +1 -0
- package/dist/loop/fix-agent.js +344 -0
- package/dist/loop/fix-agent.js.map +1 -0
- package/dist/loop/index.d.ts +8 -0
- package/dist/loop/index.d.ts.map +1 -0
- package/dist/loop/index.js +8 -0
- package/dist/loop/index.js.map +1 -0
- package/dist/loop/issue-fetcher.d.ts +51 -0
- package/dist/loop/issue-fetcher.d.ts.map +1 -0
- package/dist/loop/issue-fetcher.js +188 -0
- package/dist/loop/issue-fetcher.js.map +1 -0
- package/dist/loop/observer.d.ts +42 -0
- package/dist/loop/observer.d.ts.map +1 -0
- package/dist/loop/observer.js +220 -0
- package/dist/loop/observer.js.map +1 -0
- package/dist/loop/pacer.d.ts +44 -0
- package/dist/loop/pacer.d.ts.map +1 -0
- package/dist/loop/pacer.js +90 -0
- package/dist/loop/pacer.js.map +1 -0
- package/dist/loop/reporter.d.ts +9 -0
- package/dist/loop/reporter.d.ts.map +1 -0
- package/dist/loop/reporter.js +119 -0
- package/dist/loop/reporter.js.map +1 -0
- package/dist/loop/runner.d.ts +57 -0
- package/dist/loop/runner.d.ts.map +1 -0
- package/dist/loop/runner.js +390 -0
- package/dist/loop/runner.js.map +1 -0
- package/dist/loop/types.d.ts +151 -0
- package/dist/loop/types.d.ts.map +1 -0
- package/dist/loop/types.js +22 -0
- package/dist/loop/types.js.map +1 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +12 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +618 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/pipeline/agent-runner.d.ts +34 -0
- package/dist/pipeline/agent-runner.d.ts.map +1 -0
- package/dist/pipeline/agent-runner.js +323 -0
- package/dist/pipeline/agent-runner.js.map +1 -0
- package/dist/pipeline/agents/accessibility-auditor.d.ts +10 -0
- package/dist/pipeline/agents/accessibility-auditor.d.ts.map +1 -0
- package/dist/pipeline/agents/accessibility-auditor.js +69 -0
- package/dist/pipeline/agents/accessibility-auditor.js.map +1 -0
- package/dist/pipeline/agents/code-reviewer.d.ts +10 -0
- package/dist/pipeline/agents/code-reviewer.d.ts.map +1 -0
- package/dist/pipeline/agents/code-reviewer.js +75 -0
- package/dist/pipeline/agents/code-reviewer.js.map +1 -0
- package/dist/pipeline/agents/code-writer.d.ts +10 -0
- package/dist/pipeline/agents/code-writer.d.ts.map +1 -0
- package/dist/pipeline/agents/code-writer.js +103 -0
- package/dist/pipeline/agents/code-writer.js.map +1 -0
- package/dist/pipeline/agents/component-architect.d.ts +13 -0
- package/dist/pipeline/agents/component-architect.d.ts.map +1 -0
- package/dist/pipeline/agents/component-architect.js +81 -0
- package/dist/pipeline/agents/component-architect.js.map +1 -0
- package/dist/pipeline/agents/index.d.ts +16 -0
- package/dist/pipeline/agents/index.d.ts.map +1 -0
- package/dist/pipeline/agents/index.js +24 -0
- package/dist/pipeline/agents/index.js.map +1 -0
- package/dist/pipeline/agents/library-researcher.d.ts +12 -0
- package/dist/pipeline/agents/library-researcher.d.ts.map +1 -0
- package/dist/pipeline/agents/library-researcher.js +85 -0
- package/dist/pipeline/agents/library-researcher.js.map +1 -0
- package/dist/pipeline/agents/quality-gate.d.ts +9 -0
- package/dist/pipeline/agents/quality-gate.d.ts.map +1 -0
- package/dist/pipeline/agents/quality-gate.js +71 -0
- package/dist/pipeline/agents/quality-gate.js.map +1 -0
- package/dist/pipeline/agents/spec-analyst.d.ts +10 -0
- package/dist/pipeline/agents/spec-analyst.d.ts.map +1 -0
- package/dist/pipeline/agents/spec-analyst.js +72 -0
- package/dist/pipeline/agents/spec-analyst.js.map +1 -0
- package/dist/pipeline/agents/story-author.d.ts +9 -0
- package/dist/pipeline/agents/story-author.d.ts.map +1 -0
- package/dist/pipeline/agents/story-author.js +65 -0
- package/dist/pipeline/agents/story-author.js.map +1 -0
- package/dist/pipeline/artifact-store.d.ts +27 -0
- package/dist/pipeline/artifact-store.d.ts.map +1 -0
- package/dist/pipeline/artifact-store.js +77 -0
- package/dist/pipeline/artifact-store.js.map +1 -0
- package/dist/pipeline/conversational-gate.d.ts +26 -0
- package/dist/pipeline/conversational-gate.d.ts.map +1 -0
- package/dist/pipeline/conversational-gate.js +122 -0
- package/dist/pipeline/conversational-gate.js.map +1 -0
- package/dist/pipeline/index.d.ts +14 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +17 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/iteration-tracker.d.ts +29 -0
- package/dist/pipeline/iteration-tracker.d.ts.map +1 -0
- package/dist/pipeline/iteration-tracker.js +102 -0
- package/dist/pipeline/iteration-tracker.js.map +1 -0
- package/dist/pipeline/learning-bridge.d.ts +37 -0
- package/dist/pipeline/learning-bridge.d.ts.map +1 -0
- package/dist/pipeline/learning-bridge.js +118 -0
- package/dist/pipeline/learning-bridge.js.map +1 -0
- package/dist/pipeline/orchestrator.d.ts +45 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -0
- package/dist/pipeline/orchestrator.js +473 -0
- package/dist/pipeline/orchestrator.js.map +1 -0
- package/dist/pipeline/templates/architecture.d.ts +27 -0
- package/dist/pipeline/templates/architecture.d.ts.map +1 -0
- package/dist/pipeline/templates/architecture.js +111 -0
- package/dist/pipeline/templates/architecture.js.map +1 -0
- package/dist/pipeline/templates/brief.d.ts +22 -0
- package/dist/pipeline/templates/brief.d.ts.map +1 -0
- package/dist/pipeline/templates/brief.js +121 -0
- package/dist/pipeline/templates/brief.js.map +1 -0
- package/dist/pipeline/templates/component-rules.d.ts +25 -0
- package/dist/pipeline/templates/component-rules.d.ts.map +1 -0
- package/dist/pipeline/templates/component-rules.js +93 -0
- package/dist/pipeline/templates/component-rules.js.map +1 -0
- package/dist/pipeline/templates/index.d.ts +9 -0
- package/dist/pipeline/templates/index.d.ts.map +1 -0
- package/dist/pipeline/templates/index.js +7 -0
- package/dist/pipeline/templates/index.js.map +1 -0
- package/dist/pipeline/tool-handler.d.ts +25 -0
- package/dist/pipeline/tool-handler.d.ts.map +1 -0
- package/dist/pipeline/tool-handler.js +392 -0
- package/dist/pipeline/tool-handler.js.map +1 -0
- package/dist/pipeline/types.d.ts +146 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +27 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/plan/action-types.d.ts +31 -0
- package/dist/plan/action-types.d.ts.map +1 -0
- package/dist/plan/action-types.js +83 -0
- package/dist/plan/action-types.js.map +1 -0
- package/dist/plan/decision-engine.d.ts +57 -0
- package/dist/plan/decision-engine.d.ts.map +1 -0
- package/dist/plan/decision-engine.js +162 -0
- package/dist/plan/decision-engine.js.map +1 -0
- package/dist/plan/index.d.ts +6 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/types.d.ts +351 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/anthropic.d.ts +15 -0
- package/dist/utils/anthropic.d.ts.map +1 -0
- package/dist/utils/anthropic.js +40 -0
- package/dist/utils/anthropic.js.map +1 -0
- package/dist/utils/id.d.ts +8 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +14 -0
- package/dist/utils/id.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot Contract Validator
|
|
3
|
+
*
|
|
4
|
+
* Detects the silent-drop class of bug filed under #595, #627, #639: Eddie
|
|
5
|
+
* markup that uses a `slot="X"` attribute referencing a slot the component
|
|
6
|
+
* doesn't declare. The unknown slot reaches the shadow DOM with no matching
|
|
7
|
+
* `<slot name="X">`, the content is dropped, and nothing in the browser
|
|
8
|
+
* surfaces the failure.
|
|
9
|
+
*
|
|
10
|
+
* Scope:
|
|
11
|
+
* - `.html` files (consumer projects, boilerplates)
|
|
12
|
+
* - Lit `html\`...\`` template regions inside `.ts` / `.tsx` files
|
|
13
|
+
*
|
|
14
|
+
* Strategy:
|
|
15
|
+
* - Find every `<ed-...>` opening tag
|
|
16
|
+
* - Walk forward to its matching close tag (depth-tracked)
|
|
17
|
+
* - Inside that range, find every immediate-child element with a
|
|
18
|
+
* `slot="NAME"` attribute
|
|
19
|
+
* - Look up the parent component in the index; if NAME is not in the
|
|
20
|
+
* declared slot list, emit a finding
|
|
21
|
+
*
|
|
22
|
+
* The check is intentionally conservative: false positives are worse than
|
|
23
|
+
* false negatives because validators run in tight CI loops and noisy
|
|
24
|
+
* output gets ignored. Components missing from the index are skipped
|
|
25
|
+
* (rather than flagged as unknown), and the regex is forgiving about
|
|
26
|
+
* whitespace and quote style.
|
|
27
|
+
*/
|
|
28
|
+
import { readFile } from 'fs/promises';
|
|
29
|
+
export class SlotContractValidator {
|
|
30
|
+
index;
|
|
31
|
+
constructor(index) {
|
|
32
|
+
this.index = index;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate a single file for slot-contract violations.
|
|
36
|
+
* Returns an empty array for files we don't scan (e.g. .scss).
|
|
37
|
+
*/
|
|
38
|
+
async validateFile(filePath) {
|
|
39
|
+
let content;
|
|
40
|
+
try {
|
|
41
|
+
content = await readFile(filePath, 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
id: `slot-contract-read-error-${Date.now()}`,
|
|
47
|
+
category: 'consistency',
|
|
48
|
+
severity: 'error',
|
|
49
|
+
message: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
50
|
+
file: filePath,
|
|
51
|
+
autoFixable: false,
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
if (filePath.endsWith('.html')) {
|
|
56
|
+
return this.scanRegion(filePath, content, 0);
|
|
57
|
+
}
|
|
58
|
+
if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
|
|
59
|
+
const issues = [];
|
|
60
|
+
for (const region of this.extractLitTemplates(content)) {
|
|
61
|
+
issues.push(...this.scanRegion(filePath, region.content, region.startLine));
|
|
62
|
+
}
|
|
63
|
+
return issues;
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Scan one chunk of markup for slot-contract violations.
|
|
69
|
+
* `startLine` is the 0-based line offset of the chunk within the file —
|
|
70
|
+
* issue line numbers are reported relative to the file, not the chunk.
|
|
71
|
+
*/
|
|
72
|
+
scanRegion(filePath, region, startLine) {
|
|
73
|
+
const issues = [];
|
|
74
|
+
// Find every <ed-* ...> opening tag (not self-closing). Walk forward to
|
|
75
|
+
// the matching close, then scan that range for slotted children.
|
|
76
|
+
const openTagRegex = /<(ed-[a-z][a-z0-9-]*)\b([^>]*?)>/gi;
|
|
77
|
+
let match;
|
|
78
|
+
while ((match = openTagRegex.exec(region)) !== null) {
|
|
79
|
+
const tagName = match[1].toLowerCase();
|
|
80
|
+
const attrText = match[2];
|
|
81
|
+
// Skip self-closing tags — they have no children to slot into.
|
|
82
|
+
if (attrText.trim().endsWith('/'))
|
|
83
|
+
continue;
|
|
84
|
+
const openEnd = match.index + match[0].length;
|
|
85
|
+
const closeIdx = this.findMatchingClose(region, tagName, openEnd);
|
|
86
|
+
if (closeIdx === -1)
|
|
87
|
+
continue;
|
|
88
|
+
const innerMarkup = region.substring(openEnd, closeIdx);
|
|
89
|
+
issues.push(...this.scanInner(filePath, tagName, innerMarkup, startLine + this.lineOf(region, openEnd)));
|
|
90
|
+
}
|
|
91
|
+
return issues;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Look up the component, then scan the inner markup for any
|
|
95
|
+
* `slot="NAME"` attribute. Emit a finding for each NAME that the
|
|
96
|
+
* component doesn't declare.
|
|
97
|
+
*/
|
|
98
|
+
scanInner(filePath, parentTag, inner, innerStartLine) {
|
|
99
|
+
const component = this.index.getComponent(parentTag);
|
|
100
|
+
if (!component)
|
|
101
|
+
return [];
|
|
102
|
+
const declaredSlots = new Set(component.slots.map((s) => s.name));
|
|
103
|
+
// The "default" slot is always reachable as unnamed children — anything
|
|
104
|
+
// without a slot attribute lands there. Only NAMED slot violations count.
|
|
105
|
+
declaredSlots.delete('default');
|
|
106
|
+
const issues = [];
|
|
107
|
+
const slotAttrRegex = /\bslot\s*=\s*(?:"([^"]+)"|'([^']+)')/g;
|
|
108
|
+
let m;
|
|
109
|
+
while ((m = slotAttrRegex.exec(inner)) !== null) {
|
|
110
|
+
const slotName = m[1] ?? m[2];
|
|
111
|
+
if (!slotName || slotName === 'default')
|
|
112
|
+
continue;
|
|
113
|
+
if (declaredSlots.has(slotName))
|
|
114
|
+
continue;
|
|
115
|
+
const lineWithinInner = this.lineOf(inner, m.index);
|
|
116
|
+
const declared = Array.from(declaredSlots).sort();
|
|
117
|
+
const declaredHint = declared.length === 0
|
|
118
|
+
? `${parentTag} declares no named slots — content with a slot attribute will be dropped`
|
|
119
|
+
: `declared slots: ${declared.join(', ')}`;
|
|
120
|
+
issues.push({
|
|
121
|
+
id: `slot-contract-${parentTag}-${slotName}-${innerStartLine + lineWithinInner}`,
|
|
122
|
+
category: 'consistency',
|
|
123
|
+
severity: 'error',
|
|
124
|
+
message: `<${parentTag}> has no slot named "${slotName}" — slotted content will be silently dropped (${declaredHint})`,
|
|
125
|
+
file: filePath,
|
|
126
|
+
line: innerStartLine + lineWithinInner + 1,
|
|
127
|
+
actual: `slot="${slotName}"`,
|
|
128
|
+
expected: declared.length > 0 ? `slot="${declared[0]}" (or another declared slot)` : 'remove the slot attribute',
|
|
129
|
+
autoFixable: false,
|
|
130
|
+
suggestion: `Call eddie_get_component("${parentTag}") to see the real slot contract; consider whether a property API is the right path instead.`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return issues;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Walk forward from `startIdx` to find the matching `</tag>` for an
|
|
137
|
+
* already-opened `<tag …>`. Tracks open/close depth so nested same-named
|
|
138
|
+
* tags don't fool the matcher.
|
|
139
|
+
*/
|
|
140
|
+
findMatchingClose(content, tagName, startIdx) {
|
|
141
|
+
const openRe = new RegExp(`<${tagName}\\b[^>]*>`, 'gi');
|
|
142
|
+
const closeRe = new RegExp(`<\\/${tagName}\\s*>`, 'gi');
|
|
143
|
+
openRe.lastIndex = startIdx;
|
|
144
|
+
closeRe.lastIndex = startIdx;
|
|
145
|
+
let depth = 1;
|
|
146
|
+
while (depth > 0) {
|
|
147
|
+
const nextOpen = openRe.exec(content);
|
|
148
|
+
const nextClose = closeRe.exec(content);
|
|
149
|
+
if (!nextClose)
|
|
150
|
+
return -1;
|
|
151
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
152
|
+
if (!nextOpen[0].endsWith('/>'))
|
|
153
|
+
depth++;
|
|
154
|
+
// Keep closeRe.lastIndex where it is — we'll re-scan from after the open.
|
|
155
|
+
closeRe.lastIndex = nextOpen.index + nextOpen[0].length;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
depth--;
|
|
159
|
+
if (depth === 0)
|
|
160
|
+
return nextClose.index;
|
|
161
|
+
openRe.lastIndex = nextClose.index + nextClose[0].length;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return -1;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Extract Lit `html\`...\`` template regions from a TS/TSX file.
|
|
168
|
+
* Mirrors NamingValidator.extractLitTemplates, but stripped down — we
|
|
169
|
+
* don't need to handle every nesting edge case, just the common ones.
|
|
170
|
+
*/
|
|
171
|
+
extractLitTemplates(content) {
|
|
172
|
+
const regions = [];
|
|
173
|
+
const lines = content.split('\n');
|
|
174
|
+
let inTemplate = false;
|
|
175
|
+
let templateLines = [];
|
|
176
|
+
let templateStartLine = 0;
|
|
177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
178
|
+
const line = lines[i];
|
|
179
|
+
const trimmed = line.trim();
|
|
180
|
+
if (trimmed.startsWith('//'))
|
|
181
|
+
continue;
|
|
182
|
+
if (!inTemplate) {
|
|
183
|
+
const idx = line.indexOf('html`');
|
|
184
|
+
if (idx !== -1) {
|
|
185
|
+
inTemplate = true;
|
|
186
|
+
templateStartLine = i;
|
|
187
|
+
const after = line.substring(idx + 5);
|
|
188
|
+
templateLines = [after];
|
|
189
|
+
if (this.templateClosesOnLine(after)) {
|
|
190
|
+
inTemplate = false;
|
|
191
|
+
regions.push({ content: templateLines.join('\n'), startLine: templateStartLine });
|
|
192
|
+
templateLines = [];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
templateLines.push(line);
|
|
198
|
+
if (this.templateClosesOnLine(line)) {
|
|
199
|
+
inTemplate = false;
|
|
200
|
+
regions.push({ content: templateLines.join('\n'), startLine: templateStartLine });
|
|
201
|
+
templateLines = [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return regions;
|
|
206
|
+
}
|
|
207
|
+
templateClosesOnLine(line) {
|
|
208
|
+
let inExpr = 0;
|
|
209
|
+
for (let i = 0; i < line.length; i++) {
|
|
210
|
+
if (line[i] === '$' && line[i + 1] === '{') {
|
|
211
|
+
inExpr++;
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
214
|
+
else if (line[i] === '}' && inExpr > 0) {
|
|
215
|
+
inExpr--;
|
|
216
|
+
}
|
|
217
|
+
else if (line[i] === '`' && inExpr === 0) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
lineOf(content, offset) {
|
|
224
|
+
let line = 0;
|
|
225
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
226
|
+
if (content[i] === '\n')
|
|
227
|
+
line++;
|
|
228
|
+
}
|
|
229
|
+
return line;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=slot-contract-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-contract-validator.js","sourceRoot":"","sources":["../../src/analyze/slot-contract-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAOvC,MAAM,OAAO,qBAAqB;IACH;IAA7B,YAA6B,KAAsB;QAAtB,UAAK,GAAL,KAAK,CAAiB;IAAG,CAAC;IAEvD;;;OAGG;IACH,KAAK,CAAC,YAAY,CAAC,QAAgB;QACjC,IAAI,OAAe,CAAC;QACpB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO;gBACL;oBACE,EAAE,EAAE,4BAA4B,IAAI,CAAC,GAAG,EAAE,EAAE;oBAC5C,QAAQ,EAAE,aAAa;oBACvB,QAAQ,EAAE,OAAO;oBACjB,OAAO,EAAE,wBAAwB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;oBACzF,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,KAAK;iBACnB;aACF,CAAC;QACJ,CAAC;QAED,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/B,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QAC/C,CAAC;QAED,IAAI,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1D,MAAM,MAAM,GAAkB,EAAE,CAAC;YACjC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,EAAE,CAAC;gBACvD,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;YAC9E,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,OAAO,EAAE,CAAC;IACZ,CAAC;IAED;;;;OAIG;IACK,UAAU,CAAC,QAAgB,EAAE,MAAc,EAAE,SAAiB;QACpE,MAAM,MAAM,GAAkB,EAAE,CAAC;QAEjC,wEAAwE;QACxE,iEAAiE;QACjE,MAAM,YAAY,GAAG,oCAAoC,CAAC;QAC1D,IAAI,KAA6B,CAAC;QAClC,OAAO,CAAC,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACpD,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;YACvC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YAC1B,+DAA+D;YAC/D,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAAE,SAAS;YAE5C,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;YAClE,IAAI,QAAQ,KAAK,CAAC,CAAC;gBAAE,SAAS;YAE9B,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YACxD,MAAM,CAAC,IAAI,CACT,GAAG,IAAI,CAAC,SAAS,CACf,QAAQ,EACR,OAAO,EACP,WAAW,EACX,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACzC,CACF,CAAC;QACJ,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACK,SAAS,CACf,QAAgB,EAChB,SAAiB,EACjB,KAAa,EACb,cAAsB;QAEtB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;QACrD,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,CAAC;QAE1B,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAClE,wEAAwE;QACxE,0EAA0E;QAC1E,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAEhC,MAAM,MAAM,GAAkB,EAAE,CAAC;QACjC,MAAM,aAAa,GAAG,uCAAuC,CAAC;QAC9D,IAAI,CAAyB,CAAC;QAC9B,OAAO,CAAC,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAChD,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,CAAC,QAAQ,IAAI,QAAQ,KAAK,SAAS;gBAAE,SAAS;YAClD,IAAI,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAE1C,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;YACpD,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,IAAI,EAAE,CAAC;YAClD,MAAM,YAAY,GAChB,QAAQ,CAAC,MAAM,KAAK,CAAC;gBACnB,CAAC,CAAC,GAAG,SAAS,0EAA0E;gBACxF,CAAC,CAAC,mBAAmB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAE/C,MAAM,CAAC,IAAI,CAAC;gBACV,EAAE,EAAE,iBAAiB,SAAS,IAAI,QAAQ,IAAI,cAAc,GAAG,eAAe,EAAE;gBAChF,QAAQ,EAAE,aAAa;gBACvB,QAAQ,EAAE,OAAO;gBACjB,OAAO,EAAE,IAAI,SAAS,wBAAwB,QAAQ,iDAAiD,YAAY,GAAG;gBACtH,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,cAAc,GAAG,eAAe,GAAG,CAAC;gBAC1C,MAAM,EAAE,SAAS,QAAQ,GAAG;gBAC5B,QAAQ,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,2BAA2B;gBAChH,WAAW,EAAE,KAAK;gBAClB,UAAU,EAAE,6BAA6B,SAAS,8FAA8F;aACjJ,CAAC,CAAC;QACL,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACK,iBAAiB,CAAC,OAAe,EAAE,OAAe,EAAE,QAAgB;QAC1E,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,OAAO,WAAW,EAAE,IAAI,CAAC,CAAC;QACxD,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,OAAO,OAAO,OAAO,EAAE,IAAI,CAAC,CAAC;QACxD,MAAM,CAAC,SAAS,GAAG,QAAQ,CAAC;QAC5B,OAAO,CAAC,SAAS,GAAG,QAAQ,CAAC;QAE7B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,OAAO,KAAK,GAAG,CAAC,EAAE,CAAC;YACjB,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtC,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACxC,IAAI,CAAC,SAAS;gBAAE,OAAO,CAAC,CAAC,CAAC;YAC1B,IAAI,QAAQ,IAAI,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC;gBACjD,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAAE,KAAK,EAAE,CAAC;gBACzC,0EAA0E;gBAC1E,OAAO,CAAC,SAAS,GAAG,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YAC1D,CAAC;iBAAM,CAAC;gBACN,KAAK,EAAE,CAAC;gBACR,IAAI,KAAK,KAAK,CAAC;oBAAE,OAAO,SAAS,CAAC,KAAK,CAAC;gBACxC,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YAC3D,CAAC;QACH,CAAC;QACD,OAAO,CAAC,CAAC,CAAC;IACZ,CAAC;IAED;;;;OAIG;IACK,mBAAmB,CAAC,OAAe;QACzC,MAAM,OAAO,GAAkD,EAAE,CAAC;QAClE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,UAAU,GAAG,KAAK,CAAC;QACvB,IAAI,aAAa,GAAa,EAAE,CAAC;QACjC,IAAI,iBAAiB,GAAG,CAAC,CAAC;QAE1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;gBAAE,SAAS;YAEvC,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;gBAClC,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;oBACf,UAAU,GAAG,IAAI,CAAC;oBAClB,iBAAiB,GAAG,CAAC,CAAC;oBACtB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;oBACtC,aAAa,GAAG,CAAC,KAAK,CAAC,CAAC;oBACxB,IAAI,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC,EAAE,CAAC;wBACrC,UAAU,GAAG,KAAK,CAAC;wBACnB,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC,CAAC;wBAClF,aAAa,GAAG,EAAE,CAAC;oBACrB,CAAC;gBACH,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACzB,IAAI,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;oBACpC,UAAU,GAAG,KAAK,CAAC;oBACnB,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC,CAAC;oBAClF,aAAa,GAAG,EAAE,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,oBAAoB,CAAC,IAAY;QACvC,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC3C,MAAM,EAAE,CAAC;gBACT,CAAC,EAAE,CAAC;YACN,CAAC;iBAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzC,MAAM,EAAE,CAAC;YACX,CAAC;iBAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC3C,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,MAAM,CAAC,OAAe,EAAE,MAAc;QAC5C,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtD,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI;gBAAE,IAAI,EAAE,CAAC;QAClC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates token usage across SCSS files, enforcing the 3-tier token architecture,
|
|
5
|
+
* naming conventions, intent matching, theme coverage, and deprecation rules.
|
|
6
|
+
*/
|
|
7
|
+
import { HealthIssue } from '../types.js';
|
|
8
|
+
import type { TokenTaxonomy } from '../knowledge-graph/token-taxonomy.js';
|
|
9
|
+
export declare class TokenValidator {
|
|
10
|
+
private readonly tokenPatterns;
|
|
11
|
+
/**
|
|
12
|
+
* Validate a single SCSS file for token usage violations
|
|
13
|
+
*/
|
|
14
|
+
validateFile(filePath: string, taxonomy: TokenTaxonomy): Promise<HealthIssue[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Validate all SCSS files in a directory
|
|
17
|
+
*/
|
|
18
|
+
validateDirectory(dirPath: string, taxonomy: TokenTaxonomy): Promise<HealthIssue[]>;
|
|
19
|
+
/**
|
|
20
|
+
* Check for raw color, spacing, and font values
|
|
21
|
+
*/
|
|
22
|
+
private checkRawValues;
|
|
23
|
+
/**
|
|
24
|
+
* Validate that component code uses tier 2+ tokens, not tier 1 directly
|
|
25
|
+
*/
|
|
26
|
+
private validateTokenTier;
|
|
27
|
+
/**
|
|
28
|
+
* Validate that token usage matches its intent (e.g., don't use background token as text color)
|
|
29
|
+
*/
|
|
30
|
+
private validateIntentMatch;
|
|
31
|
+
/**
|
|
32
|
+
* Pull the color subcategory ("background" | "content" | "border") out of
|
|
33
|
+
* a token name like `--ed-theme-color-content-subtle`. Returns undefined
|
|
34
|
+
* for non-color tokens or color tokens that don't carry a subcategory
|
|
35
|
+
* segment in the name (definition-tier raw colors, etc.).
|
|
36
|
+
*/
|
|
37
|
+
private colorSubcategoryFromTokenName;
|
|
38
|
+
/**
|
|
39
|
+
* Bucket a CSS property name into one of the three color subcategory
|
|
40
|
+
* families. Returns undefined for properties that aren't color-bearing
|
|
41
|
+
* (we only enforce the rule on color properties; spacing, typography,
|
|
42
|
+
* etc. take other tokens entirely).
|
|
43
|
+
*
|
|
44
|
+
* background-* / background-color → "background"
|
|
45
|
+
* color, fill, caret-color, accent-color → "content"
|
|
46
|
+
* border-*-color / outline-*-color → "border"
|
|
47
|
+
*/
|
|
48
|
+
private colorPropertyFamily;
|
|
49
|
+
/**
|
|
50
|
+
* Check if a token is deprecated
|
|
51
|
+
*/
|
|
52
|
+
private checkDeprecation;
|
|
53
|
+
/**
|
|
54
|
+
* Check that tokens used in one theme exist in all themes.
|
|
55
|
+
*
|
|
56
|
+
* Only checks semantic/component tokens (--ed-theme-*), not definition-tier tokens.
|
|
57
|
+
* Strips comments before matching. Handles var() fallback values correctly
|
|
58
|
+
* (e.g. var(--ed-foo, none) extracts only --ed-foo).
|
|
59
|
+
*/
|
|
60
|
+
private checkThemeCoverage;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=token-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-validator.d.ts","sourceRoot":"","sources":["../../src/analyze/token-validator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,EACL,WAAW,EAIZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAS1E,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAsB5B;IAEF;;OAEG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IA2ErF;;OAEG;IACG,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAezF;;OAEG;IACH,OAAO,CAAC,cAAc;IA0EtB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkCzB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IA2C3B;;;;;OAKG;IACH,OAAO,CAAC,6BAA6B;IAOrC;;;;;;;;;OASG;IACH,OAAO,CAAC,mBAAmB;IAS3B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA0BxB;;;;;;OAMG;YACW,kBAAkB;CA0DjC"}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates token usage across SCSS files, enforcing the 3-tier token architecture,
|
|
5
|
+
* naming conventions, intent matching, theme coverage, and deprecation rules.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile } from 'fs/promises';
|
|
8
|
+
import scss from 'postcss-scss';
|
|
9
|
+
import glob from 'fast-glob';
|
|
10
|
+
export class TokenValidator {
|
|
11
|
+
tokenPatterns = [
|
|
12
|
+
// Raw hex colors
|
|
13
|
+
{
|
|
14
|
+
pattern: /#[0-9a-fA-F]{3,6}(?!\w)/g,
|
|
15
|
+
description: 'Raw hex color value found. Should use --ed-theme-color-* token',
|
|
16
|
+
category: 'tokens',
|
|
17
|
+
severity: 'error',
|
|
18
|
+
},
|
|
19
|
+
// Raw px values for spacing (outside of calculations/comments)
|
|
20
|
+
{
|
|
21
|
+
pattern: /(?<!\/\/.*):\s*\d+px\s*(?:;|!important)/g,
|
|
22
|
+
description: 'Raw pixel value for spacing/sizing. Should use --ed-spacing-* token',
|
|
23
|
+
category: 'tokens',
|
|
24
|
+
severity: 'error',
|
|
25
|
+
},
|
|
26
|
+
// Raw font-family values
|
|
27
|
+
{
|
|
28
|
+
pattern: /font-family\s*:\s*[^;]*(?!var\(--ed)/g,
|
|
29
|
+
description: 'Raw font-family value. Should use --ed-typography-font-* token',
|
|
30
|
+
category: 'tokens',
|
|
31
|
+
severity: 'error',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Validate a single SCSS file for token usage violations
|
|
36
|
+
*/
|
|
37
|
+
async validateFile(filePath, taxonomy) {
|
|
38
|
+
try {
|
|
39
|
+
const content = await readFile(filePath, 'utf-8');
|
|
40
|
+
const issues = [];
|
|
41
|
+
// Parse SCSS
|
|
42
|
+
const root = scss.parse(content);
|
|
43
|
+
let lineNumber = 0;
|
|
44
|
+
// Check for raw values using regex patterns
|
|
45
|
+
const lines = content.split('\n');
|
|
46
|
+
issues.push(...this.checkRawValues(filePath, lines));
|
|
47
|
+
// Check CSS variable declarations and usage
|
|
48
|
+
root.walkDecls((decl) => {
|
|
49
|
+
const declLine = decl.source?.start?.line || lineNumber;
|
|
50
|
+
// Check if using a token (has var(--ed-...))
|
|
51
|
+
if (decl.value.includes('var(--ed-')) {
|
|
52
|
+
const matches = decl.value.match(/var\((--ed-[^)]+)\)/g);
|
|
53
|
+
if (matches) {
|
|
54
|
+
for (const match of matches) {
|
|
55
|
+
const tokenName = match.replace(/var\(|\)/g, '');
|
|
56
|
+
// Validate token tier
|
|
57
|
+
const tierIssue = this.validateTokenTier(filePath, declLine, tokenName, taxonomy);
|
|
58
|
+
if (tierIssue)
|
|
59
|
+
issues.push(tierIssue);
|
|
60
|
+
// Validate intent match (e.g., background token for color property)
|
|
61
|
+
const intentIssue = this.validateIntentMatch(filePath, declLine, decl.prop, tokenName, taxonomy);
|
|
62
|
+
if (intentIssue)
|
|
63
|
+
issues.push(intentIssue);
|
|
64
|
+
// Check for deprecation
|
|
65
|
+
const deprecationIssue = this.checkDeprecation(filePath, declLine, tokenName, taxonomy);
|
|
66
|
+
if (deprecationIssue)
|
|
67
|
+
issues.push(deprecationIssue);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// Check theme coverage
|
|
73
|
+
const coverageIssues = await this.checkThemeCoverage(filePath, taxonomy);
|
|
74
|
+
issues.push(...coverageIssues);
|
|
75
|
+
return issues;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
id: `token-validate-error-${Date.now()}`,
|
|
81
|
+
category: 'tokens',
|
|
82
|
+
severity: 'error',
|
|
83
|
+
message: `Failed to validate file: ${error instanceof Error ? error.message : String(error)}`,
|
|
84
|
+
file: filePath,
|
|
85
|
+
autoFixable: false,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Validate all SCSS files in a directory
|
|
92
|
+
*/
|
|
93
|
+
async validateDirectory(dirPath, taxonomy) {
|
|
94
|
+
const scssFiles = await glob('**/*.scss', {
|
|
95
|
+
cwd: dirPath,
|
|
96
|
+
absolute: true,
|
|
97
|
+
});
|
|
98
|
+
const allIssues = [];
|
|
99
|
+
for (const file of scssFiles) {
|
|
100
|
+
const issues = await this.validateFile(file, taxonomy);
|
|
101
|
+
allIssues.push(...issues);
|
|
102
|
+
}
|
|
103
|
+
return allIssues;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check for raw color, spacing, and font values
|
|
107
|
+
*/
|
|
108
|
+
checkRawValues(filePath, lines) {
|
|
109
|
+
const issues = [];
|
|
110
|
+
let inBlockComment = false;
|
|
111
|
+
lines.forEach((line, index) => {
|
|
112
|
+
const trimmed = line.trim();
|
|
113
|
+
// Track block comments
|
|
114
|
+
if (trimmed.includes('/*'))
|
|
115
|
+
inBlockComment = true;
|
|
116
|
+
if (trimmed.includes('*/')) {
|
|
117
|
+
inBlockComment = false;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (inBlockComment)
|
|
121
|
+
return;
|
|
122
|
+
// Skip single-line comments
|
|
123
|
+
if (trimmed.startsWith('//'))
|
|
124
|
+
return;
|
|
125
|
+
// Skip lines that are token definitions (CSS custom property declarations)
|
|
126
|
+
if (trimmed.startsWith('--ed-'))
|
|
127
|
+
return;
|
|
128
|
+
// Check for raw hex colors
|
|
129
|
+
const hexMatches = line.matchAll(/#[0-9a-fA-F]{3,6}(?!\w)/g);
|
|
130
|
+
for (const match of hexMatches) {
|
|
131
|
+
issues.push({
|
|
132
|
+
id: `token-raw-hex-${index}-${match.index}`,
|
|
133
|
+
category: 'tokens',
|
|
134
|
+
severity: 'error',
|
|
135
|
+
message: `Raw hex color value "${match[0]}" found. Use --ed-theme-color-* token`,
|
|
136
|
+
file: filePath,
|
|
137
|
+
line: index + 1,
|
|
138
|
+
column: (match.index || 0) + 1,
|
|
139
|
+
actual: match[0],
|
|
140
|
+
autoFixable: false,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Check for raw px values (not in calculations)
|
|
144
|
+
if (!line.includes('calc(') && !line.includes('//')) {
|
|
145
|
+
const pxMatches = line.matchAll(/:\s*(\d+)px\s*(?:;|!important)/g);
|
|
146
|
+
for (const match of pxMatches) {
|
|
147
|
+
// Skip if it's a component value (like border-width: 2px)
|
|
148
|
+
const isBorderWidth = line.includes('border-width');
|
|
149
|
+
if (!isBorderWidth) {
|
|
150
|
+
issues.push({
|
|
151
|
+
id: `token-raw-px-${index}-${match.index}`,
|
|
152
|
+
category: 'tokens',
|
|
153
|
+
severity: 'error',
|
|
154
|
+
message: `Raw pixel value "${match[0]}" for spacing/sizing. Use --ed-spacing-* token`,
|
|
155
|
+
file: filePath,
|
|
156
|
+
line: index + 1,
|
|
157
|
+
column: (match.index || 0) + 1,
|
|
158
|
+
actual: match[1],
|
|
159
|
+
autoFixable: false,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Check for raw font-family
|
|
165
|
+
if (line.includes('font-family') && !line.includes('var(--ed')) {
|
|
166
|
+
const fontMatch = line.match(/font-family\s*:\s*([^;]+)/);
|
|
167
|
+
if (fontMatch && !fontMatch[1].includes('inherit') && !fontMatch[1].includes('initial')) {
|
|
168
|
+
issues.push({
|
|
169
|
+
id: `token-raw-font-${index}`,
|
|
170
|
+
category: 'tokens',
|
|
171
|
+
severity: 'error',
|
|
172
|
+
message: 'Raw font-family value found. Use --ed-typography-font-* token',
|
|
173
|
+
file: filePath,
|
|
174
|
+
line: index + 1,
|
|
175
|
+
actual: fontMatch[1].trim(),
|
|
176
|
+
autoFixable: false,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return issues;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Validate that component code uses tier 2+ tokens, not tier 1 directly
|
|
185
|
+
*/
|
|
186
|
+
validateTokenTier(filePath, line, tokenName, taxonomy) {
|
|
187
|
+
// Only enforce this if we're in a component directory (not in token definitions)
|
|
188
|
+
if (filePath.includes('/tokens/') || filePath.includes('/design-tokens/')) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
// Extract tier from token name: --ed-{tier}-...
|
|
192
|
+
// tier 1: --ed-color-*, --ed-spacing-*
|
|
193
|
+
// tier 2: --ed-theme-*, --ed-semantic-*
|
|
194
|
+
// tier 3: --ed-component-*
|
|
195
|
+
const isTier1 = tokenName.match(/--ed-(color|spacing|typography|shadow|border|motion|opacity)-/);
|
|
196
|
+
if (isTier1 && !filePath.includes('token-definitions')) {
|
|
197
|
+
return {
|
|
198
|
+
id: `token-tier-violation-${Date.now()}`,
|
|
199
|
+
category: 'tokens',
|
|
200
|
+
severity: 'warning',
|
|
201
|
+
message: `Component should not directly use tier 1 token "${tokenName}". Use tier 2 (--ed-theme-*) or tier 3 (--ed-component-*) instead`,
|
|
202
|
+
file: filePath,
|
|
203
|
+
line,
|
|
204
|
+
actual: tokenName,
|
|
205
|
+
expected: tokenName.replace(/--ed-(color|spacing)/, '--ed-theme-$1'),
|
|
206
|
+
autoFixable: false,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Validate that token usage matches its intent (e.g., don't use background token as text color)
|
|
213
|
+
*/
|
|
214
|
+
validateIntentMatch(filePath, line, cssProperty, tokenName, taxonomy) {
|
|
215
|
+
// Token-category-mismatch check (#631): every color token has a
|
|
216
|
+
// subcategory (background / content / border) that maps 1:1 to a CSS
|
|
217
|
+
// property family. Crossing the boundary — e.g.
|
|
218
|
+
// `background: var(--ed-theme-color-content-subtle)` — looks subtle in
|
|
219
|
+
// one theme and wrong in another because the token tiers are tuned
|
|
220
|
+
// independently per category.
|
|
221
|
+
const tokenSubcategory = this.colorSubcategoryFromTokenName(tokenName);
|
|
222
|
+
const propertyFamily = this.colorPropertyFamily(cssProperty);
|
|
223
|
+
if (tokenSubcategory && propertyFamily && tokenSubcategory !== propertyFamily) {
|
|
224
|
+
const sibling = tokenName.replace(`color-${tokenSubcategory}-`, `color-${propertyFamily}-`);
|
|
225
|
+
const siblingExists = !!taxonomy.getToken?.(sibling);
|
|
226
|
+
const suggestionLine = siblingExists
|
|
227
|
+
? `Did you mean \`${sibling}\`?`
|
|
228
|
+
: `Use a \`color-${propertyFamily}-*\` token here.`;
|
|
229
|
+
return {
|
|
230
|
+
id: `token-category-mismatch-${tokenName}-${cssProperty}-${line}`,
|
|
231
|
+
category: 'tokens',
|
|
232
|
+
severity: 'error',
|
|
233
|
+
message: `Token category mismatch: \`${cssProperty}\` is a ${propertyFamily} property but \`${tokenName}\` is a ${tokenSubcategory} token. ${suggestionLine}`,
|
|
234
|
+
file: filePath,
|
|
235
|
+
line,
|
|
236
|
+
actual: `${cssProperty}: var(${tokenName})`,
|
|
237
|
+
expected: siblingExists ? `${cssProperty}: var(${sibling})` : undefined,
|
|
238
|
+
autoFixable: siblingExists,
|
|
239
|
+
suggestion: siblingExists ? sibling : undefined,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Pull the color subcategory ("background" | "content" | "border") out of
|
|
246
|
+
* a token name like `--ed-theme-color-content-subtle`. Returns undefined
|
|
247
|
+
* for non-color tokens or color tokens that don't carry a subcategory
|
|
248
|
+
* segment in the name (definition-tier raw colors, etc.).
|
|
249
|
+
*/
|
|
250
|
+
colorSubcategoryFromTokenName(tokenName) {
|
|
251
|
+
const m = tokenName.match(/--ed-(?:theme-)?color-(background|content|border)-/);
|
|
252
|
+
return m ? m[1] : undefined;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Bucket a CSS property name into one of the three color subcategory
|
|
256
|
+
* families. Returns undefined for properties that aren't color-bearing
|
|
257
|
+
* (we only enforce the rule on color properties; spacing, typography,
|
|
258
|
+
* etc. take other tokens entirely).
|
|
259
|
+
*
|
|
260
|
+
* background-* / background-color → "background"
|
|
261
|
+
* color, fill, caret-color, accent-color → "content"
|
|
262
|
+
* border-*-color / outline-*-color → "border"
|
|
263
|
+
*/
|
|
264
|
+
colorPropertyFamily(property) {
|
|
265
|
+
if (/^background(-color)?$/.test(property))
|
|
266
|
+
return 'background';
|
|
267
|
+
if (/^(color|fill|caret-color|accent-color)$/.test(property))
|
|
268
|
+
return 'content';
|
|
269
|
+
if (/(^border|outline)([-a-z]*-)?color$/.test(property))
|
|
270
|
+
return 'border';
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Check if a token is deprecated
|
|
275
|
+
*/
|
|
276
|
+
checkDeprecation(filePath, line, tokenName, taxonomy) {
|
|
277
|
+
// Check taxonomy for deprecated tokens
|
|
278
|
+
const allTokens = taxonomy.getAll?.() || [];
|
|
279
|
+
const isDeprecated = allTokens.some((t) => t.name === tokenName && t.deprecated);
|
|
280
|
+
if (isDeprecated) {
|
|
281
|
+
return {
|
|
282
|
+
id: `token-deprecated-${Date.now()}`,
|
|
283
|
+
category: 'tokens',
|
|
284
|
+
severity: 'warning',
|
|
285
|
+
message: `Token "${tokenName}" is deprecated and should not be used`,
|
|
286
|
+
file: filePath,
|
|
287
|
+
line,
|
|
288
|
+
actual: tokenName,
|
|
289
|
+
autoFixable: false,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Check that tokens used in one theme exist in all themes.
|
|
296
|
+
*
|
|
297
|
+
* Only checks semantic/component tokens (--ed-theme-*), not definition-tier tokens.
|
|
298
|
+
* Strips comments before matching. Handles var() fallback values correctly
|
|
299
|
+
* (e.g. var(--ed-foo, none) extracts only --ed-foo).
|
|
300
|
+
*/
|
|
301
|
+
async checkThemeCoverage(filePath, taxonomy) {
|
|
302
|
+
const issues = [];
|
|
303
|
+
let content = await readFile(filePath, 'utf-8');
|
|
304
|
+
// Strip block comments to avoid scanning JSDoc/documentation
|
|
305
|
+
content = content.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
306
|
+
// Strip single-line comments
|
|
307
|
+
content = content.replace(/\/\/.*$/gm, '');
|
|
308
|
+
// Match var(--ed-...) and extract just the token name (before any comma fallback)
|
|
309
|
+
const tokenMatches = content.match(/var\(\s*(--ed-[^),\s]+)/g) || [];
|
|
310
|
+
const uniqueTokens = [...new Set(tokenMatches.map((m) => {
|
|
311
|
+
// Extract token name: "var(--ed-foo" → "--ed-foo"
|
|
312
|
+
const match = m.match(/var\(\s*(--ed-[^),\s]+)/);
|
|
313
|
+
return match ? match[1] : '';
|
|
314
|
+
}).filter(Boolean))];
|
|
315
|
+
// Only check theme coverage for semantic tokens (--ed-theme-*)
|
|
316
|
+
// Component-local custom properties (--ed-button-*) are defined in SCSS, not in themes
|
|
317
|
+
const themeTokens = uniqueTokens.filter((t) => t.startsWith('--ed-theme-'));
|
|
318
|
+
const allThemes = taxonomy.getThemes?.() || [];
|
|
319
|
+
if (allThemes.length === 0)
|
|
320
|
+
return issues;
|
|
321
|
+
for (const token of themeTokens) {
|
|
322
|
+
const missingThemes = [];
|
|
323
|
+
for (const theme of allThemes) {
|
|
324
|
+
const themeTokenList = taxonomy.getByTheme?.(theme) || [];
|
|
325
|
+
const hasToken = themeTokenList.some((t) => t.name === token);
|
|
326
|
+
if (!hasToken) {
|
|
327
|
+
missingThemes.push(theme);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Only flag if missing from ALL themes (likely a typo) or some themes (actual gap)
|
|
331
|
+
// If missing from all, it might be a valid component-level override — downgrade to warning
|
|
332
|
+
if (missingThemes.length > 0 && missingThemes.length < allThemes.length) {
|
|
333
|
+
issues.push({
|
|
334
|
+
id: `token-coverage-${token}`,
|
|
335
|
+
category: 'tokens',
|
|
336
|
+
severity: 'warning',
|
|
337
|
+
message: `Token "${token}" missing from ${missingThemes.length}/${allThemes.length} themes: ${missingThemes.join(', ')}`,
|
|
338
|
+
file: filePath,
|
|
339
|
+
actual: token,
|
|
340
|
+
expected: `Token should exist in all themes`,
|
|
341
|
+
autoFixable: false,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return issues;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
//# sourceMappingURL=token-validator.js.map
|