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,409 @@
1
+ /**
2
+ * YAML rule validator: full semantic validation.
3
+ *
4
+ * Three levels:
5
+ * 1. YAML syntax (valid YAML?)
6
+ * 2. Schema conformance (required fields, valid operators, valid values)
7
+ * 3. Semantic validation (field paths, contradictions, inheritance)
8
+ *
9
+ * Decidable contradiction detection:
10
+ * - Numeric range conflicts (gt/lt, gte/lte)
11
+ * - Impossible enum combos (in/not_in)
12
+ * - Composed inheritance conflicts
13
+ * - Skip: regex intersection (undecidable)
14
+ * - Bound nesting at 5 levels (RULE006)
15
+ * - Circular inheritance at depth 10 (RULE003)
16
+ */
17
+ import yaml from 'js-yaml';
18
+ import { isFieldCondition, isConditionGroup, isHeaderCondition } from './evaluate.js';
19
+ import { RULE_ERRORS } from './errors.js';
20
+ const DOCS_BASE = 'https://github.com/vegaPDX/har-o-scope/blob/main/docs/errors';
21
+ // ── Known field paths on NormalizedEntry ─────────────────────────
22
+ const VALID_FIELD_PREFIXES = new Set([
23
+ 'entry.request',
24
+ 'entry.response',
25
+ 'timings',
26
+ 'startTimeMs',
27
+ 'totalDuration',
28
+ 'transferSizeResolved',
29
+ 'contentSize',
30
+ 'resourceType',
31
+ 'isLongPoll',
32
+ 'isWebSocket',
33
+ 'httpVersion',
34
+ ]);
35
+ function isValidFieldPath(path) {
36
+ // Allow any path starting with a valid prefix
37
+ for (const prefix of VALID_FIELD_PREFIXES) {
38
+ if (path === prefix || path.startsWith(prefix + '.'))
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ // Suggest similar field paths
44
+ function suggestFieldPath(path) {
45
+ const known = [
46
+ 'timings.wait', 'timings.blocked', 'timings.dns', 'timings.connect',
47
+ 'timings.ssl', 'timings.send', 'timings.receive', 'timings.total',
48
+ 'entry.request.url', 'entry.request.method', 'entry.request.httpVersion',
49
+ 'entry.response.status', 'entry.response.content.mimeType',
50
+ 'entry.response.content.size', 'entry.response.bodySize',
51
+ 'resourceType', 'transferSizeResolved', 'contentSize',
52
+ 'startTimeMs', 'totalDuration', 'isLongPoll', 'isWebSocket', 'httpVersion',
53
+ ];
54
+ // Simple Levenshtein-like: find paths sharing the most segments
55
+ const pathParts = path.split('.');
56
+ let bestMatch;
57
+ let bestScore = 0;
58
+ for (const candidate of known) {
59
+ const candidateParts = candidate.split('.');
60
+ let score = 0;
61
+ for (const part of pathParts) {
62
+ if (candidateParts.includes(part))
63
+ score++;
64
+ }
65
+ if (score > bestScore) {
66
+ bestScore = score;
67
+ bestMatch = candidate;
68
+ }
69
+ }
70
+ return bestScore > 0 ? bestMatch : undefined;
71
+ }
72
+ // ── Valid operators and values ───────────────────────────────────
73
+ const VALID_FIELD_OPERATORS = new Set([
74
+ 'field', 'field_fallback', 'equals', 'not_equals', 'in', 'not_in',
75
+ 'gt', 'gte', 'lt', 'lte', 'matches', 'not_matches',
76
+ ]);
77
+ const VALID_SEVERITIES = new Set(['info', 'warning', 'critical']);
78
+ const VALID_CATEGORIES = new Set([
79
+ 'server', 'network', 'client', 'optimization',
80
+ 'security', 'errors', 'informational', 'performance',
81
+ ]);
82
+ const VALID_RULE_FIELDS = new Set([
83
+ 'category', 'severity', 'severity_escalation', 'title', 'description',
84
+ 'recommendation', 'condition', 'min_count', 'type', 'aggregate_condition',
85
+ 'prerequisite', 'impact', 'root_cause_weight', 'inherits', 'exclude', 'overrides',
86
+ ]);
87
+ // ── Contradiction detection ─────────────────────────────────────
88
+ function detectContradictions(conditions) {
89
+ const contradictions = [];
90
+ // Group conditions by field path
91
+ const byField = new Map();
92
+ for (const c of conditions) {
93
+ const existing = byField.get(c.field) ?? [];
94
+ existing.push(c);
95
+ byField.set(c.field, existing);
96
+ }
97
+ for (const [field, conds] of byField) {
98
+ // Numeric range conflicts
99
+ const gts = [];
100
+ const lts = [];
101
+ const gtes = [];
102
+ const ltes = [];
103
+ for (const c of conds) {
104
+ if (c.gt !== undefined)
105
+ gts.push(c.gt);
106
+ if (c.gte !== undefined)
107
+ gtes.push(c.gte);
108
+ if (c.lt !== undefined)
109
+ lts.push(c.lt);
110
+ if (c.lte !== undefined)
111
+ ltes.push(c.lte);
112
+ }
113
+ // gt: X AND lt: Y where X >= Y is impossible
114
+ for (const g of gts) {
115
+ for (const l of lts) {
116
+ if (g >= l) {
117
+ contradictions.push(`Field "${field}": gt: ${g} AND lt: ${l} is impossible (no number is both > ${g} and < ${l})`);
118
+ }
119
+ }
120
+ for (const le of ltes) {
121
+ if (g >= le) {
122
+ contradictions.push(`Field "${field}": gt: ${g} AND lte: ${le} is impossible`);
123
+ }
124
+ }
125
+ }
126
+ for (const ge of gtes) {
127
+ for (const l of lts) {
128
+ if (ge >= l) {
129
+ contradictions.push(`Field "${field}": gte: ${ge} AND lt: ${l} is impossible`);
130
+ }
131
+ }
132
+ }
133
+ // equals + not_equals same value
134
+ const eqValues = conds.filter((c) => c.equals !== undefined).map((c) => c.equals);
135
+ const neqValues = conds.filter((c) => c.not_equals !== undefined).map((c) => c.not_equals);
136
+ for (const eq of eqValues) {
137
+ for (const neq of neqValues) {
138
+ if (eq === neq) {
139
+ contradictions.push(`Field "${field}": equals: ${JSON.stringify(eq)} AND not_equals: ${JSON.stringify(neq)} is impossible`);
140
+ }
141
+ }
142
+ }
143
+ // in/not_in overlap: if all items in `in` are also in `not_in`, impossible
144
+ const inValues = conds.filter((c) => c.in !== undefined).flatMap((c) => c.in);
145
+ const notInValues = new Set(conds.filter((c) => c.not_in !== undefined).flatMap((c) => c.not_in).map(String));
146
+ if (inValues.length > 0 && notInValues.size > 0) {
147
+ const allExcluded = inValues.every((v) => notInValues.has(String(v)));
148
+ if (allExcluded) {
149
+ contradictions.push(`Field "${field}": all values in "in" are excluded by "not_in" (no valid value remains)`);
150
+ }
151
+ }
152
+ }
153
+ return contradictions;
154
+ }
155
+ // ── Nesting depth check ─────────────────────────────────────────
156
+ function checkNestingDepth(node, depth, maxDepth) {
157
+ if (depth > maxDepth) {
158
+ return `Condition nesting exceeds ${maxDepth} levels. Simplify the rule or split into composed conditions.`;
159
+ }
160
+ if (isConditionGroup(node)) {
161
+ const children = node.match_all ?? node.match_any ?? [];
162
+ for (const child of children) {
163
+ const err = checkNestingDepth(child, depth + 1, maxDepth);
164
+ if (err)
165
+ return err;
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+ // ── Circular inheritance check ──────────────────────────────────
171
+ function checkCircularInheritance(ruleName, inherits, sharedConditions, visited, depth) {
172
+ if (depth > 10) {
173
+ return `Circular or deeply nested inheritance chain detected (depth > 10) starting from "${ruleName}"`;
174
+ }
175
+ for (const name of inherits) {
176
+ if (visited.has(name)) {
177
+ return `Circular inheritance: "${ruleName}" -> ... -> "${name}" -> "${ruleName}"`;
178
+ }
179
+ visited.add(name);
180
+ // In this system, shared conditions don't themselves inherit,
181
+ // but we check the reference exists
182
+ if (sharedConditions && !sharedConditions.conditions[name]) {
183
+ // Not a circular issue, just a missing reference (caught elsewhere)
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+ // ── Collect field conditions from a condition tree ───────────────
189
+ function collectFieldConditions(node, out) {
190
+ if (isFieldCondition(node)) {
191
+ out.push(node);
192
+ }
193
+ else if (isConditionGroup(node)) {
194
+ const children = node.match_all ?? node.match_any ?? [];
195
+ for (const child of children) {
196
+ collectFieldConditions(child, out);
197
+ }
198
+ }
199
+ }
200
+ // ── Validate a single condition node ────────────────────────────
201
+ function validateConditionNode(node, errors, warnings, depth) {
202
+ // Nesting check
203
+ if (depth > 5) {
204
+ errors.push({
205
+ code: RULE_ERRORS.RULE006,
206
+ message: 'Condition nesting exceeds 5 levels',
207
+ help: 'Simplify the condition tree or use shared conditions with inherits.',
208
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE006}.md`,
209
+ });
210
+ return;
211
+ }
212
+ if (isFieldCondition(node)) {
213
+ // Validate field path
214
+ if (!isValidFieldPath(node.field)) {
215
+ const suggestion = suggestFieldPath(node.field);
216
+ errors.push({
217
+ code: RULE_ERRORS.RULE002,
218
+ message: `Unknown field path: "${node.field}"`,
219
+ help: 'Check that the field path matches a property on NormalizedEntry.',
220
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE002}.md`,
221
+ suggestion: suggestion ? `Did you mean "${suggestion}"?` : undefined,
222
+ });
223
+ }
224
+ // Validate operators
225
+ for (const key of Object.keys(node)) {
226
+ if (!VALID_FIELD_OPERATORS.has(key)) {
227
+ errors.push({
228
+ code: RULE_ERRORS.RULE007,
229
+ message: `Unknown operator: "${key}" on field "${node.field}"`,
230
+ help: `Valid operators: ${[...VALID_FIELD_OPERATORS].filter((k) => k !== 'field' && k !== 'field_fallback').join(', ')}`,
231
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE007}.md`,
232
+ });
233
+ }
234
+ }
235
+ if (node.field_fallback && !isValidFieldPath(node.field_fallback)) {
236
+ const suggestion = suggestFieldPath(node.field_fallback);
237
+ warnings.push({
238
+ code: RULE_ERRORS.RULE002,
239
+ message: `Unknown fallback field path: "${node.field_fallback}"`,
240
+ help: 'Check that the fallback field path matches a property on NormalizedEntry.',
241
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE002}.md`,
242
+ suggestion: suggestion ? `Did you mean "${suggestion}"?` : undefined,
243
+ });
244
+ }
245
+ }
246
+ else if (isHeaderCondition(node)) {
247
+ // Header conditions are structurally validated by the parser
248
+ }
249
+ else if (isConditionGroup(node)) {
250
+ const children = node.match_all ?? node.match_any ?? [];
251
+ for (const child of children) {
252
+ validateConditionNode(child, errors, warnings, depth + 1);
253
+ }
254
+ }
255
+ }
256
+ // ── Main validate ───────────────────────────────────────────────
257
+ export function validate(yamlContent, sharedConditions) {
258
+ const errors = [];
259
+ const warnings = [];
260
+ // Level 1: YAML syntax
261
+ let parsed;
262
+ try {
263
+ parsed = yaml.load(yamlContent);
264
+ }
265
+ catch (e) {
266
+ const yamlError = e;
267
+ errors.push({
268
+ code: RULE_ERRORS.RULE001,
269
+ message: `Invalid YAML syntax: ${yamlError.message ?? 'parse error'}`,
270
+ line: yamlError.mark?.line !== undefined ? yamlError.mark.line + 1 : undefined,
271
+ column: yamlError.mark?.column,
272
+ help: 'Fix the YAML syntax error and try again.',
273
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE001}.md`,
274
+ });
275
+ return { valid: false, errors, warnings };
276
+ }
277
+ if (!parsed || typeof parsed !== 'object') {
278
+ errors.push({
279
+ code: RULE_ERRORS.RULE004,
280
+ message: 'YAML file is empty or not an object',
281
+ help: 'A rule file must have a top-level "rules:" key containing rule definitions.',
282
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE004}.md`,
283
+ });
284
+ return { valid: false, errors, warnings };
285
+ }
286
+ const file = parsed;
287
+ // Level 2: Schema conformance
288
+ if (!file.rules || typeof file.rules !== 'object') {
289
+ errors.push({
290
+ code: RULE_ERRORS.RULE004,
291
+ message: 'Missing required "rules:" key',
292
+ help: 'A rule file must have a top-level "rules:" key. Example:\nrules:\n my-rule:\n category: server\n severity: warning\n ...',
293
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE004}.md`,
294
+ });
295
+ return { valid: false, errors, warnings };
296
+ }
297
+ const rules = file.rules;
298
+ for (const [ruleId, ruleData] of Object.entries(rules)) {
299
+ if (!ruleData || typeof ruleData !== 'object') {
300
+ errors.push({
301
+ code: RULE_ERRORS.RULE004,
302
+ message: `Rule "${ruleId}": must be an object`,
303
+ help: 'Each rule must define at least: category, severity, title, description, recommendation.',
304
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE004}.md`,
305
+ });
306
+ continue;
307
+ }
308
+ const rule = ruleData;
309
+ // Required fields
310
+ for (const field of ['category', 'severity', 'title', 'description', 'recommendation']) {
311
+ if (!(field in rule)) {
312
+ errors.push({
313
+ code: RULE_ERRORS.RULE004,
314
+ message: `Rule "${ruleId}": missing required field "${field}"`,
315
+ help: `Add "${field}" to the rule definition.`,
316
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE004}.md`,
317
+ });
318
+ }
319
+ }
320
+ // Validate severity value
321
+ if (rule.severity && !VALID_SEVERITIES.has(rule.severity)) {
322
+ errors.push({
323
+ code: RULE_ERRORS.RULE008,
324
+ message: `Rule "${ruleId}": invalid severity "${rule.severity}"`,
325
+ help: `Valid severity values: ${[...VALID_SEVERITIES].join(', ')}`,
326
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE008}.md`,
327
+ });
328
+ }
329
+ // Validate category value
330
+ if (rule.category && !VALID_CATEGORIES.has(rule.category)) {
331
+ warnings.push({
332
+ code: RULE_ERRORS.RULE004,
333
+ message: `Rule "${ruleId}": unknown category "${rule.category}"`,
334
+ help: `Known categories: ${[...VALID_CATEGORIES].join(', ')}. Custom categories are allowed but may not display correctly.`,
335
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE004}.md`,
336
+ });
337
+ }
338
+ // Unknown fields
339
+ for (const key of Object.keys(rule)) {
340
+ if (!VALID_RULE_FIELDS.has(key)) {
341
+ warnings.push({
342
+ code: RULE_ERRORS.RULE004,
343
+ message: `Rule "${ruleId}": unknown field "${key}"`,
344
+ help: `Valid rule fields: ${[...VALID_RULE_FIELDS].join(', ')}`,
345
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE004}.md`,
346
+ });
347
+ }
348
+ }
349
+ // Level 3: Semantic validation
350
+ const typedRule = rule;
351
+ // Condition validation
352
+ if (typedRule.condition) {
353
+ validateConditionNode(typedRule.condition, errors, warnings, 0);
354
+ // Contradiction detection within match_all groups
355
+ const fieldConds = [];
356
+ collectFieldConditions(typedRule.condition, fieldConds);
357
+ const contradictions = detectContradictions(fieldConds);
358
+ for (const msg of contradictions) {
359
+ errors.push({
360
+ code: RULE_ERRORS.RULE005,
361
+ message: `Rule "${ruleId}": ${msg}`,
362
+ help: 'The conditions contradict each other and will never match any entry.',
363
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE005}.md`,
364
+ });
365
+ }
366
+ // Nesting depth
367
+ const nestingErr = checkNestingDepth(typedRule.condition, 0, 5);
368
+ if (nestingErr) {
369
+ errors.push({
370
+ code: RULE_ERRORS.RULE006,
371
+ message: `Rule "${ruleId}": ${nestingErr}`,
372
+ help: 'Use shared conditions with inherits to flatten deeply nested rules.',
373
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE006}.md`,
374
+ });
375
+ }
376
+ }
377
+ // Inheritance validation
378
+ if (typedRule.inherits) {
379
+ const visited = new Set();
380
+ const circularErr = checkCircularInheritance(ruleId, typedRule.inherits, sharedConditions, visited, 0);
381
+ if (circularErr) {
382
+ errors.push({
383
+ code: RULE_ERRORS.RULE003,
384
+ message: `Rule "${ruleId}": ${circularErr}`,
385
+ help: 'Remove the circular reference in the inherits chain.',
386
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE003}.md`,
387
+ });
388
+ }
389
+ // Check that inherited conditions exist
390
+ if (sharedConditions) {
391
+ for (const name of typedRule.inherits) {
392
+ if (!sharedConditions.conditions[name]) {
393
+ errors.push({
394
+ code: RULE_ERRORS.RULE002,
395
+ message: `Rule "${ruleId}": inherited condition "${name}" not found in shared conditions`,
396
+ help: 'Check the condition name in base-conditions.yaml.',
397
+ docsUrl: `${DOCS_BASE}/${RULE_ERRORS.RULE002}.md`,
398
+ });
399
+ }
400
+ }
401
+ }
402
+ }
403
+ }
404
+ return {
405
+ valid: errors.length === 0,
406
+ errors,
407
+ warnings,
408
+ };
409
+ }
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "har-o-scope",
3
+ "version": "0.1.0",
4
+ "description": "Zero-trust intelligent HAR file analyzer. Drop a HAR file, get instant diagnosis.",
5
+ "type": "module",
6
+ "license": "AGPL-3.0-only",
7
+ "author": "vegaPDX",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/vegaPDX/har-o-scope"
11
+ },
12
+ "keywords": [
13
+ "har",
14
+ "http-archive",
15
+ "network-analysis",
16
+ "performance",
17
+ "web-performance",
18
+ "devtools",
19
+ "diagnostics",
20
+ "waterfall",
21
+ "security",
22
+ "sanitizer"
23
+ ],
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/lib/index.d.ts",
30
+ "import": "./dist/lib/index.js"
31
+ },
32
+ "./analyze": {
33
+ "types": "./dist/lib/analyze.d.ts",
34
+ "import": "./dist/lib/analyze.js"
35
+ },
36
+ "./diff": {
37
+ "types": "./dist/lib/diff.d.ts",
38
+ "import": "./dist/lib/diff.js"
39
+ },
40
+ "./sanitize": {
41
+ "types": "./dist/lib/sanitizer.d.ts",
42
+ "import": "./dist/lib/sanitizer.js"
43
+ },
44
+ "./validate": {
45
+ "types": "./dist/lib/validator.d.ts",
46
+ "import": "./dist/lib/validator.js"
47
+ },
48
+ "./health-score": {
49
+ "types": "./dist/lib/health-score.d.ts",
50
+ "import": "./dist/lib/health-score.js"
51
+ }
52
+ },
53
+ "bin": {
54
+ "har-o-scope": "./dist/cli/index.js"
55
+ },
56
+ "files": [
57
+ "dist/lib",
58
+ "dist/cli",
59
+ "rules",
60
+ "completions"
61
+ ],
62
+ "scripts": {
63
+ "dev": "vite",
64
+ "build": "vite build && tsc -p tsconfig.cli.json",
65
+ "build:lib": "tsc -p tsconfig.lib.json",
66
+ "build:cli": "tsc -p tsconfig.cli.json",
67
+ "preview": "vite preview",
68
+ "test": "vitest run",
69
+ "test:watch": "vitest",
70
+ "test:coverage": "vitest run --coverage",
71
+ "lint": "tsc --noEmit",
72
+ "check": "tsc --noEmit && vitest run",
73
+ "generate:error-docs": "node scripts/generate-error-docs.mjs",
74
+ "prepublishOnly": "npm run lint && npm test && npm run build"
75
+ },
76
+ "dependencies": {
77
+ "commander": "^14.0.3",
78
+ "js-yaml": "^4.1.0",
79
+ "react-window": "^2.2.7"
80
+ },
81
+ "devDependencies": {
82
+ "@testing-library/jest-dom": "^6.6.3",
83
+ "@testing-library/react": "^16.3.0",
84
+ "@types/har-format": "^1.2.16",
85
+ "@types/js-yaml": "^4.0.9",
86
+ "@types/node": "^25.5.2",
87
+ "@types/react": "^19.1.2",
88
+ "@types/react-dom": "^19.1.2",
89
+ "@vitejs/plugin-react": "^4.4.1",
90
+ "jsdom": "^26.1.0",
91
+ "react": "^19.1.0",
92
+ "react-dom": "^19.1.0",
93
+ "tailwindcss": "^4.1.4",
94
+ "typescript": "^5.8.3",
95
+ "vite": "^6.3.2",
96
+ "vitest": "^3.1.2"
97
+ }
98
+ }