hauntview 0.0.0-stage → 0.0.6

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +358 -2
  3. package/VERSION +1 -0
  4. package/dist/cli.js +8184 -0
  5. package/package.json +120 -4
  6. package/scripts/postinstall.mjs +47 -0
  7. package/src/core/browser-launch.ts +60 -0
  8. package/src/core/browser-pool.ts +267 -0
  9. package/src/core/concurrency-resolver.ts +154 -0
  10. package/src/core/finding-classifier.ts +346 -0
  11. package/src/core/index.ts +8 -0
  12. package/src/core/profile-manager.ts +499 -0
  13. package/src/core/report-store.ts +117 -0
  14. package/src/core/screenshot-manager.ts +312 -0
  15. package/src/core/system-capability.ts +161 -0
  16. package/src/core/visual-diff.ts +230 -0
  17. package/src/engine.ts +473 -0
  18. package/src/profiles/accessibility-cognitive.ts +261 -0
  19. package/src/profiles/accessibility-keyboard.ts +225 -0
  20. package/src/profiles/accessibility-screenreader.ts +243 -0
  21. package/src/profiles/accessibility-visual.ts +257 -0
  22. package/src/profiles/broken-links.ts +261 -0
  23. package/src/profiles/browser-edge.ts +192 -0
  24. package/src/profiles/browser-firefox.ts +254 -0
  25. package/src/profiles/browser-legacy.ts +213 -0
  26. package/src/profiles/console-errors.ts +192 -0
  27. package/src/profiles/desktop-4k.ts +273 -0
  28. package/src/profiles/desktop.ts +192 -0
  29. package/src/profiles/font-failure.ts +148 -0
  30. package/src/profiles/form-resilience.ts +285 -0
  31. package/src/profiles/i18n-cjk.ts +218 -0
  32. package/src/profiles/i18n-mixed.ts +255 -0
  33. package/src/profiles/i18n-rtl.ts +236 -0
  34. package/src/profiles/image-failure.ts +178 -0
  35. package/src/profiles/index.ts +42 -0
  36. package/src/profiles/js-disabled.ts +179 -0
  37. package/src/profiles/mobile.ts +175 -0
  38. package/src/profiles/network-resilience.ts +208 -0
  39. package/src/profiles/offline.ts +165 -0
  40. package/src/profiles/performance.ts +244 -0
  41. package/src/profiles/reduced-motion.ts +144 -0
  42. package/src/profiles/security-hygiene.ts +206 -0
  43. package/src/profiles/tablet.ts +176 -0
  44. package/src/reports/factory.ts +56 -0
  45. package/src/reports/html-generator.ts +1105 -0
  46. package/src/reports/index.ts +9 -0
  47. package/src/reports/markdown-generator.ts +349 -0
  48. package/src/reports/types.ts +34 -0
  49. package/src/reports/zip-generator.ts +85 -0
  50. package/src/types/contracts.ts +238 -0
  51. package/src/types/finding-utils.ts +121 -0
  52. package/src/types/index.ts +8 -0
  53. package/src/types/profiles.ts +175 -0
  54. package/src/types/results.ts +135 -0
  55. package/src/version.ts +5 -0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * FindingClassifier - Classifies and canonicalizes findings across profiles
3
+ * Handles finding normalization, deduplication, and categorization
4
+ */
5
+
6
+ import * as crypto from 'crypto';
7
+ import { Finding, ClassificationResult, CacheEntry } from '../types';
8
+
9
+ interface ClassificationRule {
10
+ pattern: RegExp;
11
+ category: string;
12
+ severity: 'error' | 'warning' | 'info';
13
+ canonical: string;
14
+ }
15
+
16
+ export class FindingClassifier {
17
+ private rules: ClassificationRule[] = [];
18
+ private cache: Map<string, CacheEntry> = new Map();
19
+ private seenFindings: Map<string, Finding> = new Map(); // hash -> canonical finding
20
+ private cacheTTL: number = 60 * 60 * 1000; // 1 hour
21
+
22
+ constructor() {
23
+ this.initializeClassificationRules();
24
+ }
25
+
26
+ /**
27
+ * Classify a finding
28
+ */
29
+ async classify(finding: Finding): Promise<ClassificationResult> {
30
+ // Check cache first
31
+ const cacheKey = this.generateFindingHash(finding);
32
+ const cached = this.cache.get(cacheKey);
33
+
34
+ if (cached && !this.isCacheExpired(cached)) {
35
+ return {
36
+ originalFinding: finding,
37
+ canonical: cached.finding,
38
+ classifier: 'cache',
39
+ confidence: 1.0,
40
+ };
41
+ }
42
+
43
+ // Match against classification rules
44
+ for (const rule of this.rules) {
45
+ if (rule.pattern.test(finding.message)) {
46
+ const canonical: Finding = {
47
+ ...finding,
48
+ category: rule.category,
49
+ severity: rule.severity,
50
+ message: rule.canonical,
51
+ canonicalHash: this.hashFinding(rule.canonical),
52
+ };
53
+
54
+ // Store in deduplication index
55
+ this.seenFindings.set(canonical.canonicalHash!, canonical);
56
+
57
+ // Cache result
58
+ this.cacheClassification(cacheKey, canonical);
59
+
60
+ return {
61
+ originalFinding: finding,
62
+ canonical,
63
+ classifier: 'rule-based',
64
+ confidence: 0.95,
65
+ };
66
+ }
67
+ }
68
+
69
+ // If no rule matches, use heuristic classification
70
+ return this.classifyByHeuristic(finding, cacheKey);
71
+ }
72
+
73
+ /**
74
+ * Batch classify findings
75
+ */
76
+ async classifyBatch(findings: Finding[]): Promise<ClassificationResult[]> {
77
+ return Promise.all(findings.map((f) => this.classify(f)));
78
+ }
79
+
80
+ /**
81
+ * Deduplicate findings
82
+ */
83
+ deduplicateFindings(findings: Finding[]): Finding[] {
84
+ const deduplicated: Finding[] = [];
85
+ const seen = new Set<string>();
86
+
87
+ for (const finding of findings) {
88
+ const hash = finding.canonicalHash || this.hashFinding(finding.message);
89
+
90
+ if (!seen.has(hash)) {
91
+ seen.add(hash);
92
+ deduplicated.push(finding);
93
+ }
94
+ }
95
+
96
+ return deduplicated;
97
+ }
98
+
99
+ /**
100
+ * Group findings by category
101
+ */
102
+ groupByCategory(findings: Finding[]): Map<string, Finding[]> {
103
+ const grouped = new Map<string, Finding[]>();
104
+
105
+ for (const finding of findings) {
106
+ const category = finding.category || 'uncategorized';
107
+
108
+ if (!grouped.has(category)) {
109
+ grouped.set(category, []);
110
+ }
111
+
112
+ grouped.get(category)!.push(finding);
113
+ }
114
+
115
+ return grouped;
116
+ }
117
+
118
+ /**
119
+ * Group findings by severity
120
+ */
121
+ groupBySeverity(findings: Finding[]): Map<string, Finding[]> {
122
+ const grouped = new Map<string, Finding[]>();
123
+
124
+ for (const finding of findings) {
125
+ if (!grouped.has(finding.severity)) {
126
+ grouped.set(finding.severity, []);
127
+ }
128
+
129
+ grouped.get(finding.severity)!.push(finding);
130
+ }
131
+
132
+ return grouped;
133
+ }
134
+
135
+ /**
136
+ * Analyze finding trends
137
+ */
138
+ analyzeTrends(
139
+ currentFindings: Finding[],
140
+ previousFindings?: Finding[]
141
+ ): {
142
+ newFindings: Finding[];
143
+ resolvedFindings: Finding[];
144
+ changedFindings: Finding[];
145
+ } {
146
+ if (!previousFindings) {
147
+ return {
148
+ newFindings: currentFindings,
149
+ resolvedFindings: [],
150
+ changedFindings: [],
151
+ };
152
+ }
153
+
154
+ const currentHashes = new Set(
155
+ currentFindings.map((f) => f.canonicalHash || this.hashFinding(f.message))
156
+ );
157
+ const previousHashes = new Set(
158
+ previousFindings.map((f) => f.canonicalHash || this.hashFinding(f.message))
159
+ );
160
+
161
+ const newFindings = currentFindings.filter(
162
+ (f) => !previousHashes.has(f.canonicalHash || this.hashFinding(f.message))
163
+ );
164
+
165
+ const resolvedFindings = previousFindings.filter(
166
+ (f) => !currentHashes.has(f.canonicalHash || this.hashFinding(f.message))
167
+ );
168
+
169
+ const changedFindings = currentFindings.filter((f) => {
170
+ const hash = f.canonicalHash || this.hashFinding(f.message);
171
+ if (!previousHashes.has(hash)) return false;
172
+
173
+ const prevFinding = previousFindings.find(
174
+ (pf) => (pf.canonicalHash || this.hashFinding(pf.message)) === hash
175
+ );
176
+
177
+ return prevFinding && prevFinding.severity !== f.severity;
178
+ });
179
+
180
+ return {
181
+ newFindings,
182
+ resolvedFindings,
183
+ changedFindings,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Clear classification cache
189
+ */
190
+ clearCache(): void {
191
+ this.cache.clear();
192
+ }
193
+
194
+ /**
195
+ * Initialize classification rules
196
+ */
197
+ private initializeClassificationRules(): void {
198
+ this.rules = [
199
+ // Accessibility rules
200
+ {
201
+ pattern: /alt.{0,50}text|missing.{0,30}alt/i,
202
+ category: 'accessibility',
203
+ severity: 'error',
204
+ canonical: 'Missing or invalid alt text on image element',
205
+ },
206
+ {
207
+ pattern: /aria.{0,50}label|missing.{0,30}aria.{0,20}label/i,
208
+ category: 'accessibility',
209
+ severity: 'warning',
210
+ canonical: 'Element missing ARIA label',
211
+ },
212
+ {
213
+ pattern: /contrast|wcag|color.{0,30}contrast/i,
214
+ category: 'accessibility',
215
+ severity: 'warning',
216
+ canonical: 'Insufficient color contrast ratio',
217
+ },
218
+
219
+ // Performance rules
220
+ {
221
+ pattern: /layout.{0,30}shift|cumulative.{0,30}layout|cls/i,
222
+ category: 'performance',
223
+ severity: 'warning',
224
+ canonical: 'Cumulative Layout Shift detected',
225
+ },
226
+ {
227
+ pattern: /large.{0,30}image|unoptimized.{0,30}image|image.{0,30}size/i,
228
+ category: 'performance',
229
+ severity: 'info',
230
+ canonical: 'Image optimization opportunity detected',
231
+ },
232
+
233
+ // Best practices rules
234
+ {
235
+ pattern: /console.{0,50}error|javascript.{0,30}error|uncaught/i,
236
+ category: 'errors',
237
+ severity: 'error',
238
+ canonical: 'JavaScript error in console',
239
+ },
240
+ {
241
+ pattern: /console.{0,50}warn|javascript.{0,30}warning/i,
242
+ category: 'errors',
243
+ severity: 'warning',
244
+ canonical: 'JavaScript warning in console',
245
+ },
246
+
247
+ // Security rules
248
+ {
249
+ pattern: /mixed.{0,30}content|http.{0,30}https|insecure/i,
250
+ category: 'security',
251
+ severity: 'error',
252
+ canonical: 'Mixed content (HTTP/HTTPS) detected',
253
+ },
254
+
255
+ // Responsive design rules
256
+ {
257
+ pattern: /text.{0,30}small|font.{0,30}size|readability/i,
258
+ category: 'responsive',
259
+ severity: 'warning',
260
+ canonical: 'Text size may be too small for readability',
261
+ },
262
+ ];
263
+ }
264
+
265
+ /**
266
+ * Classify by heuristic when no rule matches
267
+ */
268
+ private classifyByHeuristic(finding: Finding, cacheKey: string): ClassificationResult {
269
+ // Infer category from message content
270
+ const category = finding.category || 'general';
271
+ let severity = finding.severity || 'info';
272
+
273
+ if (finding.message.toLowerCase().includes('error')) {
274
+ severity = 'error';
275
+ } else if (finding.message.toLowerCase().includes('warning')) {
276
+ severity = 'warning';
277
+ }
278
+
279
+ const canonical: Finding = {
280
+ ...finding,
281
+ category,
282
+ severity,
283
+ canonicalHash: this.hashFinding(finding.message),
284
+ isClassified: true,
285
+ };
286
+
287
+ this.cacheClassification(cacheKey, canonical);
288
+ return {
289
+ originalFinding: finding,
290
+ canonical,
291
+ classifier: 'heuristic',
292
+ confidence: 0.6,
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Generate cache key for a finding
298
+ */
299
+ private generateFindingHash(finding: Finding): string {
300
+ const key = `${finding.profileId}:${finding.message}:${finding.category}`;
301
+ return crypto.createHash('sha256').update(key).digest('hex');
302
+ }
303
+
304
+ /**
305
+ * Hash finding message
306
+ */
307
+ private hashFinding(message: string): string {
308
+ return crypto.createHash('sha256').update(message).digest('hex');
309
+ }
310
+
311
+ /**
312
+ * Cache a classification result
313
+ */
314
+ private cacheClassification(key: string, finding: Finding): void {
315
+ const now = Date.now();
316
+ this.cache.set(key, {
317
+ key,
318
+ finding,
319
+ ttl: this.cacheTTL,
320
+ createdAt: now,
321
+ expiresAt: now + this.cacheTTL,
322
+ });
323
+ }
324
+
325
+ /**
326
+ * Check if cache entry is expired
327
+ */
328
+ private isCacheExpired(entry: CacheEntry): boolean {
329
+ return Date.now() > entry.expiresAt;
330
+ }
331
+
332
+ /**
333
+ * Get cache statistics
334
+ */
335
+ getStats(): {
336
+ cacheSize: number;
337
+ seenFindingsCount: number;
338
+ rulesCount: number;
339
+ } {
340
+ return {
341
+ cacheSize: this.cache.size,
342
+ seenFindingsCount: this.seenFindings.size,
343
+ rulesCount: this.rules.length,
344
+ };
345
+ }
346
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Core engine exports for HauntView
3
+ */
4
+
5
+ export { BrowserPool } from './browser-pool';
6
+ export { ScreenshotManager } from './screenshot-manager';
7
+ export { FindingClassifier } from './finding-classifier';
8
+ export { ProfileManager } from './profile-manager';