chaincss 2.1.39 → 2.3.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 (33) hide show
  1. package/dist/compiler/accessibility-engine.d.ts +57 -0
  2. package/dist/compiler/constraint-solver.d.ts +85 -0
  3. package/dist/compiler/css-if-transpiler.d.ts +33 -0
  4. package/dist/compiler/design-orchestrator.d.ts +119 -0
  5. package/dist/compiler/intent-api.d.ts +73 -0
  6. package/dist/compiler/intent-engine.d.ts +19 -1
  7. package/dist/compiler/layout-intelligence.d.ts +71 -0
  8. package/dist/compiler/pass-manager.d.ts +157 -0
  9. package/dist/compiler/pattern-learner.d.ts +112 -0
  10. package/dist/compiler/responsive-inference.d.ts +63 -0
  11. package/dist/compiler/scroll-timeline.d.ts +91 -0
  12. package/dist/compiler/semantic-tokens.d.ts +57 -0
  13. package/dist/compiler/source-optimizer.d.ts +109 -0
  14. package/dist/compiler/style-ir.d.ts +183 -0
  15. package/dist/index.d.ts +23 -0
  16. package/dist/index.js +4126 -2
  17. package/package.json +1 -1
  18. package/src/compiler/accessibility-engine.ts +502 -0
  19. package/src/compiler/constraint-solver.ts +407 -0
  20. package/src/compiler/css-if-transpiler.ts +117 -0
  21. package/src/compiler/design-orchestrator.ts +322 -0
  22. package/src/compiler/intent-api.ts +505 -0
  23. package/src/compiler/intent-engine.ts +291 -1
  24. package/src/compiler/layout-intelligence.ts +697 -0
  25. package/src/compiler/pass-manager.ts +657 -0
  26. package/src/compiler/pattern-learner.ts +398 -0
  27. package/src/compiler/responsive-inference.ts +415 -0
  28. package/src/compiler/scroll-timeline.ts +284 -0
  29. package/src/compiler/semantic-tokens.ts +468 -0
  30. package/src/compiler/source-optimizer.ts +541 -0
  31. package/src/compiler/style-ir.ts +495 -0
  32. package/src/index.ts +209 -0
  33. package/ROADMAP.md +0 -31
@@ -0,0 +1,398 @@
1
+ // src/compiler/pattern-learner.ts
2
+ /**
3
+ * Style Pattern Learner
4
+ *
5
+ * Observes all styles across a codebase and:
6
+ * 1. DETECTS repeated patterns by content-hashing style blocks
7
+ * 2. RANKS by frequency × property count
8
+ * 3. SUGGESTS extraction as chain.recipe() or intent macros
9
+ * 4. REPORTS savings (lines eliminated, bundle size reduction)
10
+ *
11
+ * This is compiler-assisted design system extraction.
12
+ */
13
+
14
+ import type { StyleIR, IRRule, IRPass } from './style-ir.js';
15
+ import crypto from 'crypto';
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ export interface StyleFingerprint {
22
+ /** Content hash of the property set */
23
+ hash: string;
24
+ /** Human-readable signature */
25
+ signature: string;
26
+ /** Properties and their values */
27
+ properties: Record<string, string | number>;
28
+ /** Number of properties */
29
+ propertyCount: number;
30
+ }
31
+
32
+ export interface PatternCluster {
33
+ /** The fingerprint that defines this cluster */
34
+ fingerprint: StyleFingerprint;
35
+ /** All rules that match this pattern */
36
+ occurrences: Array<{
37
+ ruleId: string;
38
+ selector: string;
39
+ sourceFile?: string;
40
+ component?: string;
41
+ }>;
42
+ /** How many times it appears */
43
+ frequency: number;
44
+ /** Across how many files */
45
+ fileCount: number;
46
+ /** Importance score (frequency × propertyCount) */
47
+ score: number;
48
+ /** Suggested recipe name */
49
+ suggestedName: string;
50
+ /** Suggested extraction as recipe */
51
+ suggestedRecipe: string;
52
+ /** Estimated savings */
53
+ savings: {
54
+ declarations: number;
55
+ linesEliminated: number;
56
+ bundleReduction: string;
57
+ };
58
+ }
59
+
60
+ export interface LearningReport {
61
+ clusters: PatternCluster[];
62
+ totalPatterns: number;
63
+ highValuePatterns: PatternCluster[];
64
+ totalSavings: {
65
+ declarations: number;
66
+ estimatedBytes: number;
67
+ };
68
+ summary: string;
69
+ }
70
+
71
+ // ============================================================================
72
+ // Fingerprinting
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Create a content hash from a set of declarations.
77
+ * Ignores selectors, focuses on the actual style properties.
78
+ */
79
+ function fingerprintDeclarations(
80
+ declarations: Array<{ property: string; value: string | number }>
81
+ ): StyleFingerprint {
82
+ // Sort properties alphabetically for consistent hashing
83
+ const sorted = [...declarations].sort((a, b) => a.property.localeCompare(b.property));
84
+
85
+ const properties: Record<string, string | number> = {};
86
+ const propertyList: string[] = [];
87
+
88
+ for (const decl of sorted) {
89
+ properties[decl.property] = decl.value;
90
+ propertyList.push(decl.property + ':' + decl.value);
91
+ }
92
+
93
+ const signature = propertyList.join('; ');
94
+ const hash = crypto.createHash('md5').update(signature).digest('hex').slice(0, 12);
95
+
96
+ return {
97
+ hash,
98
+ signature,
99
+ properties,
100
+ propertyCount: sorted.length,
101
+ };
102
+ }
103
+
104
+ // ============================================================================
105
+ // Pattern Clustering
106
+ // ============================================================================
107
+
108
+ /**
109
+ * Cluster rules by their style fingerprint.
110
+ * Only considers rules with a minimum number of declarations.
111
+ */
112
+ function clusterPatterns(
113
+ rules: IRRule[],
114
+ options: { minProperties?: number; minFrequency?: number } = {}
115
+ ): PatternCluster[] {
116
+ const minProperties = options.minProperties || 3;
117
+ const minFrequency = options.minFrequency || 2;
118
+
119
+ // Group by hash
120
+ const groups = new Map<string, {
121
+ fingerprint: StyleFingerprint;
122
+ occurrences: PatternCluster['occurrences'];
123
+ files: Set<string>;
124
+ }>();
125
+
126
+ for (const rule of rules) {
127
+ if (rule.isDead) continue;
128
+ if (rule.declarations.length < minProperties) continue;
129
+
130
+ const fp = fingerprintDeclarations(rule.declarations);
131
+
132
+ const existing = groups.get(fp.hash);
133
+ if (existing) {
134
+ existing.occurrences.push({
135
+ ruleId: rule.id,
136
+ selector: rule.selector,
137
+ sourceFile: rule.source.file,
138
+ component: rule.source.component,
139
+ });
140
+ if (rule.source.file) existing.files.add(rule.source.file);
141
+ } else {
142
+ groups.set(fp.hash, {
143
+ fingerprint: fp,
144
+ occurrences: [{
145
+ ruleId: rule.id,
146
+ selector: rule.selector,
147
+ sourceFile: rule.source.file,
148
+ component: rule.source.component,
149
+ }],
150
+ files: new Set(rule.source.file ? [rule.source.file] : []),
151
+ });
152
+ }
153
+ }
154
+
155
+ // Filter by frequency and convert to clusters
156
+ const clusters: PatternCluster[] = [];
157
+
158
+ for (const [, group] of groups) {
159
+ if (group.occurrences.length < minFrequency) continue;
160
+
161
+ const frequency = group.occurrences.length;
162
+ const score = frequency * group.fingerprint.propertyCount;
163
+
164
+ // Generate a suggested name from the properties
165
+ const suggestedName = generatePatternName(group.fingerprint.properties);
166
+
167
+ // Calculate savings
168
+ const declarations = frequency * group.fingerprint.propertyCount;
169
+ const linesEliminated = declarations - 1; // Replaced by 1 macro call
170
+ const bundleReduction = estimateBundleSavings(declarations, frequency);
171
+
172
+ clusters.push({
173
+ fingerprint: group.fingerprint,
174
+ occurrences: group.occurrences,
175
+ frequency,
176
+ fileCount: group.files.size,
177
+ score,
178
+ suggestedName,
179
+ suggestedRecipe: generateRecipeCode(suggestedName, group.fingerprint.properties),
180
+ savings: {
181
+ declarations,
182
+ linesEliminated,
183
+ bundleReduction,
184
+ },
185
+ });
186
+ }
187
+
188
+ // Sort by score descending
189
+ clusters.sort((a, b) => b.score - a.score);
190
+
191
+ return clusters;
192
+ }
193
+
194
+ // ============================================================================
195
+ // Name Generation
196
+ // ============================================================================
197
+
198
+ /**
199
+ * Generate a human-readable pattern name from properties.
200
+ */
201
+ function generatePatternName(properties: Record<string, string | number>): string {
202
+ const keys = Object.keys(properties);
203
+
204
+ // Try to infer semantic name from common property combinations
205
+ if (hasAll(keys, ['display', 'justifyContent', 'alignItems']) && properties['display'] === 'flex') {
206
+ return 'flexCenter';
207
+ }
208
+ if (hasAll(keys, ['display', 'flexDirection', 'justifyContent', 'alignItems']) && properties['flexDirection'] === 'column') {
209
+ return 'stack';
210
+ }
211
+ if (hasAll(keys, ['padding', 'borderRadius', 'backgroundColor', 'color'])) {
212
+ const bg = String(properties['backgroundColor'] || '');
213
+ if (bg.includes('2563eb') || bg.includes('blue')) return 'primaryButton';
214
+ if (bg.includes('e5e7eb') || bg.includes('gray')) return 'secondaryButton';
215
+ return 'button';
216
+ }
217
+ if (hasAll(keys, ['position', 'top']) && properties['position'] === 'sticky') {
218
+ return 'stickyHeader';
219
+ }
220
+ if (hasAll(keys, ['position', 'top', 'left']) && properties['position'] === 'absolute') {
221
+ return 'absoluteOverlay';
222
+ }
223
+ if (hasAll(keys, ['borderRadius']) && properties['borderRadius'] === '9999px') {
224
+ return 'pill';
225
+ }
226
+ if (hasAll(keys, ['overflow', 'textOverflow', 'whiteSpace'])) {
227
+ return 'truncate';
228
+ }
229
+ if (hasAll(keys, ['backdropFilter'])) {
230
+ return 'glass';
231
+ }
232
+
233
+ // Fallback: use most significant property
234
+ if (keys.includes('display')) return String(properties['display']) + 'Layout';
235
+ if (keys.includes('position')) return String(properties['position']) + 'Element';
236
+ return 'pattern-' + keys.slice(0, 3).join('-');
237
+ }
238
+
239
+ function hasAll(haystack: string[], needles: string[]): boolean {
240
+ return needles.every(n => haystack.includes(n));
241
+ }
242
+
243
+ /**
244
+ * Generate recipe code from a pattern.
245
+ */
246
+ function generateRecipeCode(
247
+ name: string,
248
+ properties: Record<string, string | number>
249
+ ): string {
250
+ const lines = Object.entries(properties).map(
251
+ ([prop, value]) => ' ' + prop + ": '" + value + "',"
252
+ );
253
+ return "chain.recipe('" + name + "', {\n" + lines.join('\n') + "\n})";
254
+ }
255
+
256
+ /**
257
+ * Estimate bundle size savings.
258
+ */
259
+ function estimateBundleSavings(declarations: number, frequency: number): string {
260
+ const avgBytesPerDecl = 25; // ~25 bytes per CSS declaration
261
+ const totalBytes = declarations * avgBytesPerDecl;
262
+ if (totalBytes > 10000) return '~' + Math.round(totalBytes / 1000) + 'KB';
263
+ return '~' + totalBytes + 'B';
264
+ }
265
+
266
+ // ============================================================================
267
+ // Learning Report
268
+ // ============================================================================
269
+
270
+ /**
271
+ * Generate a full learning report from clustered patterns.
272
+ */
273
+ function generateReport(clusters: PatternCluster[]): LearningReport {
274
+ const highValue = clusters.filter(c => c.score >= 10);
275
+
276
+ const totalDeclarations = clusters.reduce((sum, c) => sum + c.savings.declarations, 0);
277
+ const totalBytes = totalDeclarations * 25;
278
+
279
+ let summary: string;
280
+ if (clusters.length === 0) {
281
+ summary = 'No repeated patterns found. Your styles are already unique!';
282
+ } else if (highValue.length > 0) {
283
+ summary = 'Found ' + clusters.length + ' patterns. ' + highValue.length + ' high-value patterns worth extracting.';
284
+ } else {
285
+ summary = 'Found ' + clusters.length + ' patterns. None high-value enough to suggest extraction yet.';
286
+ }
287
+
288
+ return {
289
+ clusters,
290
+ totalPatterns: clusters.length,
291
+ highValuePatterns: highValue,
292
+ totalSavings: {
293
+ declarations: totalDeclarations,
294
+ estimatedBytes: totalBytes,
295
+ },
296
+ summary,
297
+ };
298
+ }
299
+
300
+ // ============================================================================
301
+ // IR Pass
302
+ // ============================================================================
303
+
304
+ /**
305
+ * Pattern Learning IR pass.
306
+ * Analyzes rules, clusters patterns, and adds diagnostics.
307
+ */
308
+ export const patternLearningPass: IRPass = (ir: StyleIR): StyleIR => {
309
+ const clusters = clusterPatterns(ir.rules, {
310
+ minProperties: 3,
311
+ minFrequency: 2,
312
+ });
313
+
314
+ if (clusters.length === 0) return ir;
315
+
316
+ // Report top patterns as diagnostics
317
+ for (const cluster of clusters.slice(0, 5)) {
318
+ ir.diagnostics.push({
319
+ id: 'pattern-' + cluster.fingerprint.hash,
320
+ nodeId: ir.rules[0]?.id || ir.id,
321
+ severity: 'info',
322
+ message: 'Pattern "' + cluster.suggestedName + '" found ' + cluster.frequency + ' times across ' + cluster.fileCount + ' files. ' + cluster.savings.bundleReduction + ' potential savings.',
323
+ suggestion: cluster.suggestedRecipe,
324
+ pass: 'pattern-learner',
325
+ });
326
+ }
327
+
328
+ // Store clusters for reporting
329
+ ir.meta = ir.meta || {};
330
+ (ir.meta as any).patternClusters = clusters;
331
+ (ir.meta as any).learningReport = generateReport(clusters);
332
+
333
+ return ir;
334
+ };
335
+
336
+ // ============================================================================
337
+ // Standalone API
338
+ // ============================================================================
339
+
340
+ /**
341
+ * Learn patterns from a set of style rules.
342
+ */
343
+ export function learnPatterns(
344
+ rules: Array<{ selector: string; declarations: Record<string, string | number>; sourceFile?: string }>,
345
+ options?: { minProperties?: number; minFrequency?: number }
346
+ ): LearningReport {
347
+ const irRules: IRRule[] = rules.map(r => ({
348
+ id: 'learn-' + Math.random().toString(36).slice(2, 8),
349
+ selector: r.selector,
350
+ declarations: Object.entries(r.declarations).map(([prop, value]) => ({
351
+ id: 'learn-decl-' + prop,
352
+ property: prop,
353
+ value,
354
+ history: [],
355
+ meta: {},
356
+ })),
357
+ pseudoClasses: [],
358
+ atRules: [],
359
+ nestedRules: [],
360
+ conditions: [],
361
+ isDead: false,
362
+ specificity: 0,
363
+ hash: '',
364
+ source: { file: r.sourceFile },
365
+ history: [],
366
+ meta: {},
367
+ }));
368
+
369
+ const clusters = clusterPatterns(irRules, options);
370
+ return generateReport(clusters);
371
+ }
372
+
373
+ /**
374
+ * Get the top patterns worth extracting.
375
+ */
376
+ export function getExtractionCandidates(
377
+ rules: Array<{ selector: string; declarations: Record<string, string | number>; sourceFile?: string }>,
378
+ minScore?: number
379
+ ): PatternCluster[] {
380
+ const report = learnPatterns(rules);
381
+ const threshold = minScore || 10;
382
+ return report.clusters.filter(c => c.score >= threshold);
383
+ }
384
+
385
+ // ============================================================================
386
+ // Quick API
387
+ // ============================================================================
388
+
389
+ export const patternLearner = {
390
+ learn: learnPatterns,
391
+ extract: getExtractionCandidates,
392
+ fingerprint: fingerprintDeclarations,
393
+ cluster: clusterPatterns,
394
+ report: generateReport,
395
+ pass: patternLearningPass,
396
+ };
397
+
398
+ export default patternLearner;