@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.
Files changed (239) hide show
  1. package/README.md +109 -0
  2. package/dist/analyze/drift-detector.d.ts +30 -0
  3. package/dist/analyze/drift-detector.d.ts.map +1 -0
  4. package/dist/analyze/drift-detector.js +310 -0
  5. package/dist/analyze/drift-detector.js.map +1 -0
  6. package/dist/analyze/health-scorer.d.ts +71 -0
  7. package/dist/analyze/health-scorer.d.ts.map +1 -0
  8. package/dist/analyze/health-scorer.js +420 -0
  9. package/dist/analyze/health-scorer.js.map +1 -0
  10. package/dist/analyze/index.d.ts +11 -0
  11. package/dist/analyze/index.d.ts.map +1 -0
  12. package/dist/analyze/index.js +11 -0
  13. package/dist/analyze/index.js.map +1 -0
  14. package/dist/analyze/naming-validator.d.ts +99 -0
  15. package/dist/analyze/naming-validator.d.ts.map +1 -0
  16. package/dist/analyze/naming-validator.js +430 -0
  17. package/dist/analyze/naming-validator.js.map +1 -0
  18. package/dist/analyze/slot-contract-validator.d.ts +68 -0
  19. package/dist/analyze/slot-contract-validator.d.ts.map +1 -0
  20. package/dist/analyze/slot-contract-validator.js +232 -0
  21. package/dist/analyze/slot-contract-validator.js.map +1 -0
  22. package/dist/analyze/token-validator.d.ts +62 -0
  23. package/dist/analyze/token-validator.d.ts.map +1 -0
  24. package/dist/analyze/token-validator.js +348 -0
  25. package/dist/analyze/token-validator.js.map +1 -0
  26. package/dist/cli/brain.d.ts +12 -0
  27. package/dist/cli/brain.d.ts.map +1 -0
  28. package/dist/cli/brain.js +641 -0
  29. package/dist/cli/brain.js.map +1 -0
  30. package/dist/cli/formatters/json.d.ts +15 -0
  31. package/dist/cli/formatters/json.d.ts.map +1 -0
  32. package/dist/cli/formatters/json.js +18 -0
  33. package/dist/cli/formatters/json.js.map +1 -0
  34. package/dist/cli/formatters/terminal.d.ts +19 -0
  35. package/dist/cli/formatters/terminal.d.ts.map +1 -0
  36. package/dist/cli/formatters/terminal.js +125 -0
  37. package/dist/cli/formatters/terminal.js.map +1 -0
  38. package/dist/cli/index.d.ts +7 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +7 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/data/governance-rules.json +94 -0
  43. package/dist/governance/audit-log.d.ts +17 -0
  44. package/dist/governance/audit-log.d.ts.map +1 -0
  45. package/dist/governance/audit-log.js +44 -0
  46. package/dist/governance/audit-log.js.map +1 -0
  47. package/dist/governance/index.d.ts +8 -0
  48. package/dist/governance/index.d.ts.map +1 -0
  49. package/dist/governance/index.js +8 -0
  50. package/dist/governance/index.js.map +1 -0
  51. package/dist/governance/permissions.d.ts +26 -0
  52. package/dist/governance/permissions.d.ts.map +1 -0
  53. package/dist/governance/permissions.js +75 -0
  54. package/dist/governance/permissions.js.map +1 -0
  55. package/dist/governance/rules-engine.d.ts +24 -0
  56. package/dist/governance/rules-engine.d.ts.map +1 -0
  57. package/dist/governance/rules-engine.js +111 -0
  58. package/dist/governance/rules-engine.js.map +1 -0
  59. package/dist/governance/trust-manager.d.ts +34 -0
  60. package/dist/governance/trust-manager.d.ts.map +1 -0
  61. package/dist/governance/trust-manager.js +148 -0
  62. package/dist/governance/trust-manager.js.map +1 -0
  63. package/dist/index.d.ts +23 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +28 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/knowledge-graph/component-index.d.ts +320 -0
  68. package/dist/knowledge-graph/component-index.d.ts.map +1 -0
  69. package/dist/knowledge-graph/component-index.js +1033 -0
  70. package/dist/knowledge-graph/component-index.js.map +1 -0
  71. package/dist/knowledge-graph/index.d.ts +134 -0
  72. package/dist/knowledge-graph/index.d.ts.map +1 -0
  73. package/dist/knowledge-graph/index.js +249 -0
  74. package/dist/knowledge-graph/index.js.map +1 -0
  75. package/dist/knowledge-graph/learning-history.d.ts +77 -0
  76. package/dist/knowledge-graph/learning-history.d.ts.map +1 -0
  77. package/dist/knowledge-graph/learning-history.js +187 -0
  78. package/dist/knowledge-graph/learning-history.js.map +1 -0
  79. package/dist/knowledge-graph/relationship-map.d.ts +55 -0
  80. package/dist/knowledge-graph/relationship-map.d.ts.map +1 -0
  81. package/dist/knowledge-graph/relationship-map.js +238 -0
  82. package/dist/knowledge-graph/relationship-map.js.map +1 -0
  83. package/dist/knowledge-graph/token-taxonomy.d.ts +127 -0
  84. package/dist/knowledge-graph/token-taxonomy.d.ts.map +1 -0
  85. package/dist/knowledge-graph/token-taxonomy.js +357 -0
  86. package/dist/knowledge-graph/token-taxonomy.js.map +1 -0
  87. package/dist/loop/fix-agent.d.ts +55 -0
  88. package/dist/loop/fix-agent.d.ts.map +1 -0
  89. package/dist/loop/fix-agent.js +344 -0
  90. package/dist/loop/fix-agent.js.map +1 -0
  91. package/dist/loop/index.d.ts +8 -0
  92. package/dist/loop/index.d.ts.map +1 -0
  93. package/dist/loop/index.js +8 -0
  94. package/dist/loop/index.js.map +1 -0
  95. package/dist/loop/issue-fetcher.d.ts +51 -0
  96. package/dist/loop/issue-fetcher.d.ts.map +1 -0
  97. package/dist/loop/issue-fetcher.js +188 -0
  98. package/dist/loop/issue-fetcher.js.map +1 -0
  99. package/dist/loop/observer.d.ts +42 -0
  100. package/dist/loop/observer.d.ts.map +1 -0
  101. package/dist/loop/observer.js +220 -0
  102. package/dist/loop/observer.js.map +1 -0
  103. package/dist/loop/pacer.d.ts +44 -0
  104. package/dist/loop/pacer.d.ts.map +1 -0
  105. package/dist/loop/pacer.js +90 -0
  106. package/dist/loop/pacer.js.map +1 -0
  107. package/dist/loop/reporter.d.ts +9 -0
  108. package/dist/loop/reporter.d.ts.map +1 -0
  109. package/dist/loop/reporter.js +119 -0
  110. package/dist/loop/reporter.js.map +1 -0
  111. package/dist/loop/runner.d.ts +57 -0
  112. package/dist/loop/runner.d.ts.map +1 -0
  113. package/dist/loop/runner.js +390 -0
  114. package/dist/loop/runner.js.map +1 -0
  115. package/dist/loop/types.d.ts +151 -0
  116. package/dist/loop/types.d.ts.map +1 -0
  117. package/dist/loop/types.js +22 -0
  118. package/dist/loop/types.js.map +1 -0
  119. package/dist/mcp/index.d.ts +7 -0
  120. package/dist/mcp/index.d.ts.map +1 -0
  121. package/dist/mcp/index.js +7 -0
  122. package/dist/mcp/index.js.map +1 -0
  123. package/dist/mcp/server.d.ts +12 -0
  124. package/dist/mcp/server.d.ts.map +1 -0
  125. package/dist/mcp/server.js +618 -0
  126. package/dist/mcp/server.js.map +1 -0
  127. package/dist/pipeline/agent-runner.d.ts +34 -0
  128. package/dist/pipeline/agent-runner.d.ts.map +1 -0
  129. package/dist/pipeline/agent-runner.js +323 -0
  130. package/dist/pipeline/agent-runner.js.map +1 -0
  131. package/dist/pipeline/agents/accessibility-auditor.d.ts +10 -0
  132. package/dist/pipeline/agents/accessibility-auditor.d.ts.map +1 -0
  133. package/dist/pipeline/agents/accessibility-auditor.js +69 -0
  134. package/dist/pipeline/agents/accessibility-auditor.js.map +1 -0
  135. package/dist/pipeline/agents/code-reviewer.d.ts +10 -0
  136. package/dist/pipeline/agents/code-reviewer.d.ts.map +1 -0
  137. package/dist/pipeline/agents/code-reviewer.js +75 -0
  138. package/dist/pipeline/agents/code-reviewer.js.map +1 -0
  139. package/dist/pipeline/agents/code-writer.d.ts +10 -0
  140. package/dist/pipeline/agents/code-writer.d.ts.map +1 -0
  141. package/dist/pipeline/agents/code-writer.js +103 -0
  142. package/dist/pipeline/agents/code-writer.js.map +1 -0
  143. package/dist/pipeline/agents/component-architect.d.ts +13 -0
  144. package/dist/pipeline/agents/component-architect.d.ts.map +1 -0
  145. package/dist/pipeline/agents/component-architect.js +81 -0
  146. package/dist/pipeline/agents/component-architect.js.map +1 -0
  147. package/dist/pipeline/agents/index.d.ts +16 -0
  148. package/dist/pipeline/agents/index.d.ts.map +1 -0
  149. package/dist/pipeline/agents/index.js +24 -0
  150. package/dist/pipeline/agents/index.js.map +1 -0
  151. package/dist/pipeline/agents/library-researcher.d.ts +12 -0
  152. package/dist/pipeline/agents/library-researcher.d.ts.map +1 -0
  153. package/dist/pipeline/agents/library-researcher.js +85 -0
  154. package/dist/pipeline/agents/library-researcher.js.map +1 -0
  155. package/dist/pipeline/agents/quality-gate.d.ts +9 -0
  156. package/dist/pipeline/agents/quality-gate.d.ts.map +1 -0
  157. package/dist/pipeline/agents/quality-gate.js +71 -0
  158. package/dist/pipeline/agents/quality-gate.js.map +1 -0
  159. package/dist/pipeline/agents/spec-analyst.d.ts +10 -0
  160. package/dist/pipeline/agents/spec-analyst.d.ts.map +1 -0
  161. package/dist/pipeline/agents/spec-analyst.js +72 -0
  162. package/dist/pipeline/agents/spec-analyst.js.map +1 -0
  163. package/dist/pipeline/agents/story-author.d.ts +9 -0
  164. package/dist/pipeline/agents/story-author.d.ts.map +1 -0
  165. package/dist/pipeline/agents/story-author.js +65 -0
  166. package/dist/pipeline/agents/story-author.js.map +1 -0
  167. package/dist/pipeline/artifact-store.d.ts +27 -0
  168. package/dist/pipeline/artifact-store.d.ts.map +1 -0
  169. package/dist/pipeline/artifact-store.js +77 -0
  170. package/dist/pipeline/artifact-store.js.map +1 -0
  171. package/dist/pipeline/conversational-gate.d.ts +26 -0
  172. package/dist/pipeline/conversational-gate.d.ts.map +1 -0
  173. package/dist/pipeline/conversational-gate.js +122 -0
  174. package/dist/pipeline/conversational-gate.js.map +1 -0
  175. package/dist/pipeline/index.d.ts +14 -0
  176. package/dist/pipeline/index.d.ts.map +1 -0
  177. package/dist/pipeline/index.js +17 -0
  178. package/dist/pipeline/index.js.map +1 -0
  179. package/dist/pipeline/iteration-tracker.d.ts +29 -0
  180. package/dist/pipeline/iteration-tracker.d.ts.map +1 -0
  181. package/dist/pipeline/iteration-tracker.js +102 -0
  182. package/dist/pipeline/iteration-tracker.js.map +1 -0
  183. package/dist/pipeline/learning-bridge.d.ts +37 -0
  184. package/dist/pipeline/learning-bridge.d.ts.map +1 -0
  185. package/dist/pipeline/learning-bridge.js +118 -0
  186. package/dist/pipeline/learning-bridge.js.map +1 -0
  187. package/dist/pipeline/orchestrator.d.ts +45 -0
  188. package/dist/pipeline/orchestrator.d.ts.map +1 -0
  189. package/dist/pipeline/orchestrator.js +473 -0
  190. package/dist/pipeline/orchestrator.js.map +1 -0
  191. package/dist/pipeline/templates/architecture.d.ts +27 -0
  192. package/dist/pipeline/templates/architecture.d.ts.map +1 -0
  193. package/dist/pipeline/templates/architecture.js +111 -0
  194. package/dist/pipeline/templates/architecture.js.map +1 -0
  195. package/dist/pipeline/templates/brief.d.ts +22 -0
  196. package/dist/pipeline/templates/brief.d.ts.map +1 -0
  197. package/dist/pipeline/templates/brief.js +121 -0
  198. package/dist/pipeline/templates/brief.js.map +1 -0
  199. package/dist/pipeline/templates/component-rules.d.ts +25 -0
  200. package/dist/pipeline/templates/component-rules.d.ts.map +1 -0
  201. package/dist/pipeline/templates/component-rules.js +93 -0
  202. package/dist/pipeline/templates/component-rules.js.map +1 -0
  203. package/dist/pipeline/templates/index.d.ts +9 -0
  204. package/dist/pipeline/templates/index.d.ts.map +1 -0
  205. package/dist/pipeline/templates/index.js +7 -0
  206. package/dist/pipeline/templates/index.js.map +1 -0
  207. package/dist/pipeline/tool-handler.d.ts +25 -0
  208. package/dist/pipeline/tool-handler.d.ts.map +1 -0
  209. package/dist/pipeline/tool-handler.js +392 -0
  210. package/dist/pipeline/tool-handler.js.map +1 -0
  211. package/dist/pipeline/types.d.ts +146 -0
  212. package/dist/pipeline/types.d.ts.map +1 -0
  213. package/dist/pipeline/types.js +27 -0
  214. package/dist/pipeline/types.js.map +1 -0
  215. package/dist/plan/action-types.d.ts +31 -0
  216. package/dist/plan/action-types.d.ts.map +1 -0
  217. package/dist/plan/action-types.js +83 -0
  218. package/dist/plan/action-types.js.map +1 -0
  219. package/dist/plan/decision-engine.d.ts +57 -0
  220. package/dist/plan/decision-engine.d.ts.map +1 -0
  221. package/dist/plan/decision-engine.js +162 -0
  222. package/dist/plan/decision-engine.js.map +1 -0
  223. package/dist/plan/index.d.ts +6 -0
  224. package/dist/plan/index.d.ts.map +1 -0
  225. package/dist/plan/index.js +6 -0
  226. package/dist/plan/index.js.map +1 -0
  227. package/dist/types.d.ts +351 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +26 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/utils/anthropic.d.ts +15 -0
  232. package/dist/utils/anthropic.d.ts.map +1 -0
  233. package/dist/utils/anthropic.js +40 -0
  234. package/dist/utils/anthropic.js.map +1 -0
  235. package/dist/utils/id.d.ts +8 -0
  236. package/dist/utils/id.d.ts.map +1 -0
  237. package/dist/utils/id.js +14 -0
  238. package/dist/utils/id.js.map +1 -0
  239. 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":"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"}
@@ -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