har-o-scope 0.1.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 (75) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +179 -0
  3. package/completions/har-o-scope.bash +64 -0
  4. package/completions/har-o-scope.fish +43 -0
  5. package/completions/har-o-scope.zsh +63 -0
  6. package/dist/cli/colors.d.ts +17 -0
  7. package/dist/cli/colors.d.ts.map +1 -0
  8. package/dist/cli/colors.js +54 -0
  9. package/dist/cli/demo.d.ts +7 -0
  10. package/dist/cli/demo.d.ts.map +1 -0
  11. package/dist/cli/demo.js +62 -0
  12. package/dist/cli/formatters.d.ts +12 -0
  13. package/dist/cli/formatters.d.ts.map +1 -0
  14. package/dist/cli/formatters.js +249 -0
  15. package/dist/cli/index.d.ts +3 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +260 -0
  18. package/dist/cli/rules.d.ts +3 -0
  19. package/dist/cli/rules.d.ts.map +1 -0
  20. package/dist/cli/rules.js +36 -0
  21. package/dist/cli/sarif.d.ts +9 -0
  22. package/dist/cli/sarif.d.ts.map +1 -0
  23. package/dist/cli/sarif.js +104 -0
  24. package/dist/lib/analyze.d.ts +10 -0
  25. package/dist/lib/analyze.d.ts.map +1 -0
  26. package/dist/lib/analyze.js +83 -0
  27. package/dist/lib/classifier.d.ts +8 -0
  28. package/dist/lib/classifier.d.ts.map +1 -0
  29. package/dist/lib/classifier.js +74 -0
  30. package/dist/lib/diff.d.ts +15 -0
  31. package/dist/lib/diff.d.ts.map +1 -0
  32. package/dist/lib/diff.js +130 -0
  33. package/dist/lib/errors.d.ts +56 -0
  34. package/dist/lib/errors.d.ts.map +1 -0
  35. package/dist/lib/errors.js +65 -0
  36. package/dist/lib/evaluate.d.ts +19 -0
  37. package/dist/lib/evaluate.d.ts.map +1 -0
  38. package/dist/lib/evaluate.js +189 -0
  39. package/dist/lib/health-score.d.ts +18 -0
  40. package/dist/lib/health-score.d.ts.map +1 -0
  41. package/dist/lib/health-score.js +74 -0
  42. package/dist/lib/html-report.d.ts +15 -0
  43. package/dist/lib/html-report.d.ts.map +1 -0
  44. package/dist/lib/html-report.js +299 -0
  45. package/dist/lib/index.d.ts +26 -0
  46. package/dist/lib/index.d.ts.map +1 -0
  47. package/dist/lib/index.js +24 -0
  48. package/dist/lib/normalizer.d.ts +18 -0
  49. package/dist/lib/normalizer.d.ts.map +1 -0
  50. package/dist/lib/normalizer.js +201 -0
  51. package/dist/lib/rule-engine.d.ts +12 -0
  52. package/dist/lib/rule-engine.d.ts.map +1 -0
  53. package/dist/lib/rule-engine.js +122 -0
  54. package/dist/lib/sanitizer.d.ts +10 -0
  55. package/dist/lib/sanitizer.d.ts.map +1 -0
  56. package/dist/lib/sanitizer.js +129 -0
  57. package/dist/lib/schema.d.ts +85 -0
  58. package/dist/lib/schema.d.ts.map +1 -0
  59. package/dist/lib/schema.js +1 -0
  60. package/dist/lib/trace-sanitizer.d.ts +30 -0
  61. package/dist/lib/trace-sanitizer.d.ts.map +1 -0
  62. package/dist/lib/trace-sanitizer.js +85 -0
  63. package/dist/lib/types.d.ts +161 -0
  64. package/dist/lib/types.d.ts.map +1 -0
  65. package/dist/lib/types.js +1 -0
  66. package/dist/lib/unbatched-detect.d.ts +7 -0
  67. package/dist/lib/unbatched-detect.d.ts.map +1 -0
  68. package/dist/lib/unbatched-detect.js +59 -0
  69. package/dist/lib/validator.d.ts +4 -0
  70. package/dist/lib/validator.d.ts.map +1 -0
  71. package/dist/lib/validator.js +409 -0
  72. package/package.json +98 -0
  73. package/rules/generic/issue-rules.yaml +292 -0
  74. package/rules/generic/shared/base-conditions.yaml +28 -0
  75. package/rules/generic/shared/filters.yaml +12 -0
@@ -0,0 +1,189 @@
1
+ // ── Field accessor ──────────────────────────────────────────────
2
+ const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
3
+ export function getField(obj, path) {
4
+ const parts = path.split('.');
5
+ let current = obj;
6
+ for (const part of parts) {
7
+ if (current == null || typeof current !== 'object')
8
+ return undefined;
9
+ if (BLOCKED_KEYS.has(part))
10
+ return undefined;
11
+ current = current[part];
12
+ }
13
+ return current;
14
+ }
15
+ /**
16
+ * Resolve a field value with optional fallback.
17
+ * Fallback is used if the primary value is null, undefined, or zero.
18
+ * Zero triggers fallback because the main use case is transferSize
19
+ * falling back to contentSize where 0 means unavailable.
20
+ */
21
+ function resolveFieldValue(entry, field, fieldFallback) {
22
+ const value = getField(entry, field);
23
+ if (fieldFallback !== undefined && (value === 0 || value == null)) {
24
+ return getField(entry, fieldFallback);
25
+ }
26
+ return value;
27
+ }
28
+ // ── Regex cache ─────────────────────────────────────────────────
29
+ const regexCache = new Map();
30
+ function getCachedRegex(pattern, flags) {
31
+ const key = `${pattern}\0${flags}`;
32
+ let re = regexCache.get(key);
33
+ if (!re) {
34
+ re = new RegExp(pattern, flags);
35
+ regexCache.set(key, re);
36
+ }
37
+ return re;
38
+ }
39
+ // ── Type guards ─────────────────────────────────────────────────
40
+ export function isFieldCondition(node) {
41
+ return 'field' in node;
42
+ }
43
+ export function isHeaderCondition(node) {
44
+ return 'has_response_header' in node || 'no_response_header' in node;
45
+ }
46
+ export function isConditionGroup(node) {
47
+ return 'match_all' in node || 'match_any' in node;
48
+ }
49
+ // ── Response header access ──────────────────────────────────────
50
+ function getResponseHeaders(entry) {
51
+ const headers = getField(entry, 'entry.response.headers');
52
+ return Array.isArray(headers) ? headers : [];
53
+ }
54
+ // ── Condition evaluation ────────────────────────────────────────
55
+ export function evaluateFieldCondition(entry, cond) {
56
+ const value = resolveFieldValue(entry, cond.field, cond.field_fallback);
57
+ if (cond.equals !== undefined)
58
+ return value === cond.equals;
59
+ if (cond.not_equals !== undefined)
60
+ return value !== cond.not_equals;
61
+ if (cond.in !== undefined)
62
+ return cond.in.includes(value);
63
+ if (cond.not_in !== undefined)
64
+ return !cond.not_in.includes(value);
65
+ if (cond.gt !== undefined)
66
+ return typeof value === 'number' && value > cond.gt;
67
+ if (cond.gte !== undefined)
68
+ return typeof value === 'number' && value >= cond.gte;
69
+ if (cond.lt !== undefined)
70
+ return typeof value === 'number' && value < cond.lt;
71
+ if (cond.lte !== undefined)
72
+ return typeof value === 'number' && value <= cond.lte;
73
+ if (cond.matches !== undefined) {
74
+ const str = value == null ? '' : String(value);
75
+ return getCachedRegex(cond.matches, 'i').test(str);
76
+ }
77
+ if (cond.not_matches !== undefined) {
78
+ const str = value == null ? '' : String(value);
79
+ return !getCachedRegex(cond.not_matches, 'i').test(str);
80
+ }
81
+ return true;
82
+ }
83
+ function evaluateHeaderCondition(entry, cond) {
84
+ const headers = getResponseHeaders(entry);
85
+ if (cond.has_response_header) {
86
+ const { name, value_matches, value_gt, value_lt } = cond.has_response_header;
87
+ return headers.some((h) => {
88
+ if (h.name.toLowerCase() !== name.toLowerCase())
89
+ return false;
90
+ if (value_matches && !getCachedRegex(value_matches, 'i').test(h.value))
91
+ return false;
92
+ if (value_gt !== undefined) {
93
+ const num = parseFloat(h.value);
94
+ if (isNaN(num) || num <= value_gt)
95
+ return false;
96
+ }
97
+ if (value_lt !== undefined) {
98
+ const num = parseFloat(h.value);
99
+ if (isNaN(num) || num >= value_lt)
100
+ return false;
101
+ }
102
+ return true;
103
+ });
104
+ }
105
+ if (cond.no_response_header) {
106
+ const { name, value_matches } = cond.no_response_header;
107
+ const matchingHeader = headers.some((h) => {
108
+ if (h.name.toLowerCase() !== name.toLowerCase())
109
+ return false;
110
+ if (value_matches)
111
+ return getCachedRegex(value_matches, 'i').test(h.value);
112
+ return true;
113
+ });
114
+ return !matchingHeader;
115
+ }
116
+ return true;
117
+ }
118
+ export function evaluateCondition(entry, node) {
119
+ if (isFieldCondition(node))
120
+ return evaluateFieldCondition(entry, node);
121
+ if (isHeaderCondition(node))
122
+ return evaluateHeaderCondition(entry, node);
123
+ if (isConditionGroup(node)) {
124
+ if (node.match_all)
125
+ return node.match_all.every((child) => evaluateCondition(entry, child));
126
+ if (node.match_any)
127
+ return node.match_any.some((child) => evaluateCondition(entry, child));
128
+ }
129
+ return true;
130
+ }
131
+ // ── Severity computation ────────────────────────────────────────
132
+ const SEVERITY_ORDER = { info: 0, warning: 1, critical: 2 };
133
+ function escalateIfHigher(current, candidate) {
134
+ return (SEVERITY_ORDER[candidate] ?? 0) > (SEVERITY_ORDER[current] ?? 0) ? candidate : current;
135
+ }
136
+ export function computeSeverity(baseSeverity, escalation, affectedCount, totalCount) {
137
+ if (!escalation)
138
+ return baseSeverity;
139
+ let severity = baseSeverity;
140
+ const ratio = totalCount > 0 ? affectedCount / totalCount : 0;
141
+ if (escalation.warning_threshold !== undefined && affectedCount >= escalation.warning_threshold) {
142
+ severity = escalateIfHigher(severity, 'warning');
143
+ }
144
+ if (escalation.warning_ratio !== undefined && ratio > escalation.warning_ratio) {
145
+ severity = escalateIfHigher(severity, 'warning');
146
+ }
147
+ if (escalation.critical_threshold !== undefined && affectedCount >= escalation.critical_threshold) {
148
+ severity = escalateIfHigher(severity, 'critical');
149
+ }
150
+ if (escalation.critical_ratio !== undefined && ratio > escalation.critical_ratio) {
151
+ severity = escalateIfHigher(severity, 'critical');
152
+ }
153
+ return severity;
154
+ }
155
+ // ── Impact computation ──────────────────────────────────────────
156
+ export function computeImpact(entries, affectedIndices, impactSpec) {
157
+ if (!impactSpec)
158
+ return 0;
159
+ if (impactSpec.value !== undefined)
160
+ return impactSpec.value;
161
+ let total = 0;
162
+ const baseline = impactSpec.baseline ?? 0;
163
+ for (const idx of affectedIndices) {
164
+ const entry = entries[idx];
165
+ if (impactSpec.field) {
166
+ const val = getField(entry, impactSpec.field);
167
+ if (typeof val === 'number')
168
+ total += val - baseline;
169
+ }
170
+ if (impactSpec.fields) {
171
+ let entrySum = 0;
172
+ for (const f of impactSpec.fields) {
173
+ const val = getField(entry, f);
174
+ if (typeof val === 'number')
175
+ entrySum += val;
176
+ }
177
+ total += entrySum - baseline;
178
+ }
179
+ }
180
+ return total;
181
+ }
182
+ // ── Template interpolation ──────────────────────────────────────
183
+ export function interpolate(template, vars) {
184
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
185
+ if (key in vars)
186
+ return String(vars[key]);
187
+ return `{${key}}`;
188
+ });
189
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Health score: 0-100 pure function.
3
+ *
4
+ * Scoring formula (from CEO plan):
5
+ * Base: 100
6
+ * Deductions:
7
+ * - critical finding: -15 * confidenceMultiplier
8
+ * - warning finding: -5 * confidenceMultiplier
9
+ * - info finding: -1 * confidenceMultiplier
10
+ * - Confidence multiplier from root cause: >=0.6 high (1.0x), >=0.4 medium (0.7x), <0.4 low (0.5x)
11
+ * - Timing penalty: median TTFB >1000ms: -10, >2000ms: -20
12
+ * - Volume penalty: >200 requests: -5, >500 requests: -10
13
+ * Floor: 0, Ceiling: 100
14
+ */
15
+ import type { AnalysisResult, HealthScore, NormalizedEntry, RootCauseResult, Finding } from './types.js';
16
+ export declare function computeHealthScore(result: AnalysisResult): HealthScore;
17
+ export declare function computeHealthScoreFromParts(findings: Finding[], rootCause: RootCauseResult, entries: NormalizedEntry[]): HealthScore;
18
+ //# sourceMappingURL=health-score.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-score.d.ts","sourceRoot":"","sources":["../../src/lib/health-score.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EAGX,eAAe,EACf,eAAe,EACf,OAAO,EACR,MAAM,YAAY,CAAA;AAEnB,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,WAAW,CAGtE;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,OAAO,EAAE,EACnB,SAAS,EAAE,eAAe,EAC1B,OAAO,EAAE,eAAe,EAAE,GACzB,WAAW,CA2Cb"}
@@ -0,0 +1,74 @@
1
+ export function computeHealthScore(result) {
2
+ const { findings, rootCause, entries } = result;
3
+ return computeHealthScoreFromParts(findings, rootCause, entries);
4
+ }
5
+ export function computeHealthScoreFromParts(findings, rootCause, entries) {
6
+ const confidenceMultiplier = getConfidenceMultiplier(rootCause);
7
+ const findingDeductions = [];
8
+ let findingPoints = 0;
9
+ for (const finding of findings) {
10
+ let points;
11
+ switch (finding.severity) {
12
+ case 'critical':
13
+ points = 15;
14
+ break;
15
+ case 'warning':
16
+ points = 5;
17
+ break;
18
+ case 'info':
19
+ points = 1;
20
+ break;
21
+ default:
22
+ points = 0;
23
+ }
24
+ if (points > 0) {
25
+ const adjusted = Math.round(points * confidenceMultiplier * 10) / 10;
26
+ findingDeductions.push({ reason: `${finding.severity}: ${finding.ruleId}`, points: adjusted });
27
+ findingPoints += adjusted;
28
+ }
29
+ }
30
+ const timingPenalty = computeTimingPenalty(entries);
31
+ const volumePenalty = computeVolumePenalty(entries.length);
32
+ const totalDeductions = findingPoints + timingPenalty + volumePenalty;
33
+ const score = Math.max(0, Math.min(100, Math.round(100 - totalDeductions)));
34
+ return {
35
+ score,
36
+ breakdown: {
37
+ findingDeductions,
38
+ timingPenalty,
39
+ volumePenalty,
40
+ confidenceMultiplier,
41
+ totalDeductions,
42
+ },
43
+ };
44
+ }
45
+ function getConfidenceMultiplier(rootCause) {
46
+ const maxWeight = Math.max(rootCause.client, rootCause.network, rootCause.server);
47
+ if (maxWeight >= 0.6)
48
+ return 1.0;
49
+ if (maxWeight >= 0.4)
50
+ return 0.7;
51
+ return 0.5;
52
+ }
53
+ function computeTimingPenalty(entries) {
54
+ // Filter to non-WebSocket, non-long-poll entries with wait > 0
55
+ const waits = entries
56
+ .filter((e) => !e.isWebSocket && !e.isLongPoll && e.timings.wait > 0)
57
+ .map((e) => e.timings.wait)
58
+ .sort((a, b) => a - b);
59
+ if (waits.length === 0)
60
+ return 0;
61
+ const median = waits[Math.floor(waits.length / 2)];
62
+ if (median > 2000)
63
+ return 20;
64
+ if (median > 1000)
65
+ return 10;
66
+ return 0;
67
+ }
68
+ function computeVolumePenalty(count) {
69
+ if (count > 500)
70
+ return 10;
71
+ if (count > 200)
72
+ return 5;
73
+ return 0;
74
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Self-contained HTML report generator.
3
+ *
4
+ * Produces a standalone HTML file with inlined CSS. No JavaScript, no external
5
+ * dependencies. Opens in any browser. Dark mode via prefers-color-scheme.
6
+ *
7
+ * Content: health score donut, root cause, finding count by severity,
8
+ * all findings with descriptions and recommendations, timing summary.
9
+ */
10
+ import type { AnalysisResult, HealthScore } from './types.js';
11
+ export interface HtmlReportOptions {
12
+ title?: string;
13
+ }
14
+ export declare function generateHtmlReport(result: AnalysisResult, healthScore: HealthScore, options?: HtmlReportOptions): string;
15
+ //# sourceMappingURL=html-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-report.d.ts","sourceRoot":"","sources":["../../src/lib/html-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAW,MAAM,YAAY,CAAA;AAEtE,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,WAAW,EACxB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,MAAM,CAmHR"}
@@ -0,0 +1,299 @@
1
+ export function generateHtmlReport(result, healthScore, options) {
2
+ const title = options?.title ?? 'HAR Analysis Report';
3
+ const timestamp = new Date().toISOString();
4
+ const { findings, rootCause, metadata, entries } = result;
5
+ const score = healthScore.score;
6
+ // Sort findings: critical first, then warning, then info
7
+ const sorted = [...findings].sort((a, b) => {
8
+ const order = { critical: 0, warning: 1, info: 2 };
9
+ return order[a.severity] - order[b.severity];
10
+ });
11
+ const counts = {
12
+ critical: findings.filter(f => f.severity === 'critical').length,
13
+ warning: findings.filter(f => f.severity === 'warning').length,
14
+ info: findings.filter(f => f.severity === 'info').length,
15
+ };
16
+ // Root cause
17
+ const rcEntries = Object.entries(rootCause);
18
+ rcEntries.sort((a, b) => b[1] - a[1]);
19
+ const topCause = rcEntries[0];
20
+ const rootCauseText = topCause[1] === 0
21
+ ? 'None (no findings)'
22
+ : `${topCause[0].charAt(0).toUpperCase() + topCause[0].slice(1)} (${Math.round(topCause[1] * 100)}%)`;
23
+ // Timing
24
+ const totalTimeMs = metadata.totalTimeMs;
25
+ const totalTimeStr = totalTimeMs >= 10_000
26
+ ? `${(totalTimeMs / 1000).toFixed(1)}s`
27
+ : `${Math.round(totalTimeMs)}ms`;
28
+ // Health score donut SVG
29
+ const circumference = 2 * Math.PI * 45;
30
+ const offset = circumference * (1 - score / 100);
31
+ const scoreColor = score >= 90 ? 'var(--good)' : score >= 50 ? 'var(--ok)' : 'var(--bad)';
32
+ const donutSvg = `<svg viewBox="0 0 100 100" class="donut">
33
+ <circle cx="50" cy="50" r="45" fill="none" stroke="var(--surface-hover)" stroke-width="8"/>
34
+ <circle cx="50" cy="50" r="45" fill="none" stroke="${scoreColor}" stroke-width="8"
35
+ stroke-dasharray="${circumference.toFixed(1)}" stroke-dashoffset="${offset.toFixed(1)}"
36
+ stroke-linecap="round" transform="rotate(-90 50 50)"/>
37
+ <text x="50" y="50" text-anchor="middle" dominant-baseline="central"
38
+ fill="var(--text)" font-size="24" font-weight="700">${score}</text>
39
+ <text x="50" y="66" text-anchor="middle" fill="var(--text-3)" font-size="8">/100</text>
40
+ </svg>`;
41
+ // Findings HTML
42
+ const findingsHtml = sorted.length > 0
43
+ ? sorted.map(f => renderFinding(f)).join('\n')
44
+ : '<p class="no-findings">No issues found.</p>';
45
+ // Severity summary badges
46
+ const badgesHtml = [
47
+ counts.critical > 0 ? `<span class="badge critical">${counts.critical} critical</span>` : '',
48
+ counts.warning > 0 ? `<span class="badge warning">${counts.warning} warning</span>` : '',
49
+ counts.info > 0 ? `<span class="badge info">${counts.info} info</span>` : '',
50
+ ].filter(Boolean).join('\n ');
51
+ return `<!DOCTYPE html>
52
+ <html lang="en">
53
+ <head>
54
+ <meta charset="UTF-8">
55
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
56
+ <title>${escapeHtml(title)}</title>
57
+ <style>${CSS}</style>
58
+ </head>
59
+ <body>
60
+ <div class="report">
61
+ <header class="report-header">
62
+ <h1>${escapeHtml(title)}</h1>
63
+ <time datetime="${timestamp}">${new Date(timestamp).toLocaleString()}</time>
64
+ </header>
65
+
66
+ <section class="summary">
67
+ <div class="score-card">
68
+ ${donutSvg}
69
+ </div>
70
+ <div class="stats">
71
+ <div class="stat">
72
+ <span class="stat-label">Requests</span>
73
+ <span class="stat-value">${metadata.totalRequests}</span>
74
+ </div>
75
+ <div class="stat">
76
+ <span class="stat-label">Total Time</span>
77
+ <span class="stat-value">${totalTimeStr}</span>
78
+ </div>
79
+ <div class="stat">
80
+ <span class="stat-label">Root Cause</span>
81
+ <span class="stat-value">${rootCauseText}</span>
82
+ </div>
83
+ <div class="stat">
84
+ <span class="stat-label">Analysis</span>
85
+ <span class="stat-value">${metadata.analysisTimeMs}ms</span>
86
+ </div>
87
+ </div>
88
+ </section>
89
+
90
+ <section class="findings-section">
91
+ <h2>Findings (${findings.length})</h2>
92
+ <div class="badges">
93
+ ${badgesHtml || '<span class="badge info">0 issues</span>'}
94
+ </div>
95
+ <div class="findings">
96
+ ${findingsHtml}
97
+ </div>
98
+ </section>
99
+
100
+ <footer class="report-footer">
101
+ Generated by <a href="https://github.com/vegaPDX/har-o-scope">har-o-scope</a>
102
+ </footer>
103
+ </div>
104
+ </body>
105
+ </html>`;
106
+ }
107
+ function renderFinding(f) {
108
+ return `<div class="finding ${f.severity}">
109
+ <div class="finding-header">
110
+ <span class="severity-badge ${f.severity}">${f.severity.toUpperCase()}</span>
111
+ <span class="finding-title">${escapeHtml(f.title)}</span>
112
+ </div>
113
+ <p class="finding-desc">${escapeHtml(f.description)}</p>
114
+ <p class="finding-fix">${escapeHtml(f.recommendation)}</p>
115
+ <span class="finding-meta">Rule: ${escapeHtml(f.ruleId)} · Category: ${escapeHtml(f.category)} · Affected: ${f.affectedEntries.length} entries</span>
116
+ </div>`;
117
+ }
118
+ function escapeHtml(s) {
119
+ return s
120
+ .replace(/&/g, '&amp;')
121
+ .replace(/</g, '&lt;')
122
+ .replace(/>/g, '&gt;')
123
+ .replace(/"/g, '&quot;');
124
+ }
125
+ // ── Inlined CSS ─────────────────────────────────────────────────
126
+ const CSS = `
127
+ :root {
128
+ --bg: #0f1117;
129
+ --surface: #1a1d27;
130
+ --surface-hover: #252830;
131
+ --border: #2f323b;
132
+ --text: #e4e4e7;
133
+ --text-2: #a1a1aa;
134
+ --text-3: #71717a;
135
+ --accent: #3b82f6;
136
+ --critical: #ef4444;
137
+ --critical-bg: #451a1a;
138
+ --warning: #f59e0b;
139
+ --warning-bg: #451a00;
140
+ --info: #3b82f6;
141
+ --info-bg: #1a2744;
142
+ --good: #22c55e;
143
+ --ok: #f59e0b;
144
+ --bad: #ef4444;
145
+ }
146
+ @media (prefers-color-scheme: light) {
147
+ :root {
148
+ --bg: #ffffff;
149
+ --surface: #f9fafb;
150
+ --surface-hover: #f3f4f6;
151
+ --border: #e5e7eb;
152
+ --text: #18181b;
153
+ --text-2: #52525b;
154
+ --text-3: #a1a1aa;
155
+ --critical-bg: #fef2f2;
156
+ --warning-bg: #fffbeb;
157
+ --info-bg: #eff6ff;
158
+ }
159
+ }
160
+ * { margin: 0; padding: 0; box-sizing: border-box; }
161
+ body {
162
+ font-family: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
163
+ font-size: 14px;
164
+ line-height: 1.5;
165
+ color: var(--text);
166
+ background: var(--bg);
167
+ }
168
+ .report {
169
+ max-width: 720px;
170
+ margin: 0 auto;
171
+ padding: 32px 24px;
172
+ }
173
+ .report-header {
174
+ margin-bottom: 32px;
175
+ }
176
+ .report-header h1 {
177
+ font-size: 20px;
178
+ font-weight: 700;
179
+ margin-bottom: 4px;
180
+ }
181
+ .report-header time {
182
+ font-size: 12px;
183
+ color: var(--text-3);
184
+ }
185
+ .summary {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 32px;
189
+ padding: 24px;
190
+ background: var(--surface);
191
+ border: 1px solid var(--border);
192
+ border-radius: 8px;
193
+ margin-bottom: 32px;
194
+ }
195
+ .score-card { flex-shrink: 0; }
196
+ .donut { width: 100px; height: 100px; }
197
+ .stats {
198
+ display: grid;
199
+ grid-template-columns: 1fr 1fr;
200
+ gap: 16px;
201
+ flex: 1;
202
+ }
203
+ .stat-label {
204
+ display: block;
205
+ font-size: 11px;
206
+ font-weight: 500;
207
+ color: var(--text-3);
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.05em;
210
+ margin-bottom: 2px;
211
+ }
212
+ .stat-value {
213
+ font-size: 16px;
214
+ font-weight: 600;
215
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
216
+ }
217
+ .findings-section { margin-bottom: 32px; }
218
+ .findings-section h2 {
219
+ font-size: 16px;
220
+ font-weight: 600;
221
+ margin-bottom: 8px;
222
+ }
223
+ .badges { display: flex; gap: 8px; margin-bottom: 16px; }
224
+ .badge {
225
+ font-size: 11px;
226
+ font-weight: 600;
227
+ padding: 2px 8px;
228
+ border-radius: 4px;
229
+ }
230
+ .badge.critical { color: var(--critical); background: var(--critical-bg); }
231
+ .badge.warning { color: var(--warning); background: var(--warning-bg); }
232
+ .badge.info { color: var(--info); background: var(--info-bg); }
233
+ .finding {
234
+ padding: 16px;
235
+ border: 1px solid var(--border);
236
+ border-radius: 8px;
237
+ margin-bottom: 12px;
238
+ }
239
+ .finding.critical { border-left: 3px solid var(--critical); }
240
+ .finding.warning { border-left: 3px solid var(--warning); }
241
+ .finding.info { border-left: 3px solid var(--info); }
242
+ .finding-header {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 8px;
246
+ margin-bottom: 8px;
247
+ }
248
+ .severity-badge {
249
+ font-size: 10px;
250
+ font-weight: 700;
251
+ padding: 1px 6px;
252
+ border-radius: 3px;
253
+ letter-spacing: 0.03em;
254
+ }
255
+ .severity-badge.critical { color: var(--critical); background: var(--critical-bg); }
256
+ .severity-badge.warning { color: var(--warning); background: var(--warning-bg); }
257
+ .severity-badge.info { color: var(--info); background: var(--info-bg); }
258
+ .finding-title {
259
+ font-size: 14px;
260
+ font-weight: 600;
261
+ }
262
+ .finding-desc {
263
+ font-size: 13px;
264
+ color: var(--text-2);
265
+ margin-bottom: 8px;
266
+ }
267
+ .finding-fix {
268
+ font-size: 13px;
269
+ color: var(--text-2);
270
+ margin-bottom: 8px;
271
+ padding-left: 12px;
272
+ border-left: 2px solid var(--accent);
273
+ }
274
+ .finding-meta {
275
+ font-size: 11px;
276
+ color: var(--text-3);
277
+ font-family: 'IBM Plex Mono', ui-monospace, monospace;
278
+ }
279
+ .no-findings {
280
+ color: var(--text-2);
281
+ padding: 24px;
282
+ text-align: center;
283
+ }
284
+ .report-footer {
285
+ text-align: center;
286
+ font-size: 11px;
287
+ color: var(--text-3);
288
+ padding-top: 24px;
289
+ border-top: 1px solid var(--border);
290
+ }
291
+ .report-footer a {
292
+ color: var(--accent);
293
+ text-decoration: none;
294
+ }
295
+ @media (max-width: 500px) {
296
+ .summary { flex-direction: column; gap: 16px; }
297
+ .stats { grid-template-columns: 1fr 1fr; }
298
+ }
299
+ `;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * har-o-scope public API.
3
+ *
4
+ * This barrel file IS the API contract. Anything not re-exported here is internal.
5
+ * ~15 public exports specified in eng review #2.
6
+ */
7
+ export { analyze, setBuiltinRules } from './analyze.js';
8
+ export type { AnalysisResult, AnalysisOptions } from './types.js';
9
+ export { diff } from './diff.js';
10
+ export { normalizeUrlForGrouping } from './diff.js';
11
+ export type { DiffResult, TimingDelta, FindingDelta } from './types.js';
12
+ export { computeHealthScore, computeHealthScoreFromParts } from './health-score.js';
13
+ export type { HealthScore, ScoreBreakdown, ScoreDeduction } from './types.js';
14
+ export { sanitize } from './sanitizer.js';
15
+ export { sanitizeTrace, sanitizeUrl, sanitizeHeaderValue } from './trace-sanitizer.js';
16
+ export type { SanitizeOptions, SanitizeCategory, SanitizeMode } from './types.js';
17
+ export { validate } from './validator.js';
18
+ export type { ValidationResult, ValidationError } from './types.js';
19
+ export { parseHar, normalizeHar } from './normalizer.js';
20
+ export { generateHtmlReport } from './html-report.js';
21
+ export type { HtmlReportOptions } from './html-report.js';
22
+ export { HarError, HAR_ERRORS, RULE_ERRORS, CLI_ERRORS, createWarning } from './errors.js';
23
+ export type { AnalysisWarning } from './types.js';
24
+ export type { NormalizedEntry, NormalizedTimings, ResourceType, Finding, RootCauseResult, IssueSeverity, IssueCategory, AnalysisMetadata, Har, Entry, Header, } from './types.js';
25
+ export type { YamlRule, IssueRulesFile, SharedConditionsFile, FiltersFile, FieldCondition, ConditionNode, ConditionGroup, ResponseHeaderCondition, SeverityEscalation, ImpactSpec, RootCauseWeight, } from './schema.js';
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACvD,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAGjE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AACnD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAGvE,OAAO,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAA;AACnF,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAG7E,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AACtF,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAGjF,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAGnE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAGxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAGzD,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC1F,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAGjD,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,OAAO,EACP,eAAe,EACf,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,GAAG,EACH,KAAK,EACL,MAAM,GACP,MAAM,YAAY,CAAA;AAGnB,YAAY,EACV,QAAQ,EACR,cAAc,EACd,oBAAoB,EACpB,WAAW,EACX,cAAc,EACd,aAAa,EACb,cAAc,EACd,uBAAuB,EACvB,kBAAkB,EAClB,UAAU,EACV,eAAe,GAChB,MAAM,aAAa,CAAA"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * har-o-scope public API.
3
+ *
4
+ * This barrel file IS the API contract. Anything not re-exported here is internal.
5
+ * ~15 public exports specified in eng review #2.
6
+ */
7
+ // Analysis
8
+ export { analyze, setBuiltinRules } from './analyze.js';
9
+ // Diff
10
+ export { diff } from './diff.js';
11
+ export { normalizeUrlForGrouping } from './diff.js';
12
+ // Health Score
13
+ export { computeHealthScore, computeHealthScoreFromParts } from './health-score.js';
14
+ // Sanitization
15
+ export { sanitize } from './sanitizer.js';
16
+ export { sanitizeTrace, sanitizeUrl, sanitizeHeaderValue } from './trace-sanitizer.js';
17
+ // Validation
18
+ export { validate } from './validator.js';
19
+ // Parsing / Normalization (useful for advanced consumers)
20
+ export { parseHar, normalizeHar } from './normalizer.js';
21
+ // HTML Report
22
+ export { generateHtmlReport } from './html-report.js';
23
+ // Errors
24
+ export { HarError, HAR_ERRORS, RULE_ERRORS, CLI_ERRORS, createWarning } from './errors.js';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * HAR normalizer: parses HAR JSON and produces NormalizedEntry[].
3
+ *
4
+ * Pre-computes fields needed by the rule engine: startTimeMs, totalDuration,
5
+ * transferSizeResolved, isLongPoll, isWebSocket, resourceType.
6
+ * Handles edge cases: missing fields, sanitized Chrome exports (transferSize=0),
7
+ * WebSocket entries, long-polls.
8
+ */
9
+ import type { Har } from 'har-format';
10
+ import type { NormalizedEntry } from './types.js';
11
+ import type { AnalysisWarning } from './types.js';
12
+ export interface NormalizeResult {
13
+ entries: NormalizedEntry[];
14
+ warnings: AnalysisWarning[];
15
+ }
16
+ export declare function normalizeHar(har: Har): NormalizeResult;
17
+ export declare function parseHar(input: string): Har;
18
+ //# sourceMappingURL=normalizer.d.ts.map