unguard 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -25,31 +25,71 @@ npx unguard
25
25
  ## Usage
26
26
 
27
27
  ```bash
28
- unguard src
29
- unguard src --strict # treat warnings as errors (CI)
30
- unguard src --filter no-any-cast # run a single rule
31
- unguard src --severity=error # only show errors
32
- unguard src --severity=error,warning # errors and warnings only
33
- unguard src --format=flat # one-line-per-diagnostic, grepable
28
+ unguard src # scan files/directories
29
+ unguard src --config ./unguard.config.json # load config
30
+ unguard src --ignore '**/*.gen.ts' # add ignore globs
31
+ unguard src --filter no-any-cast # run a single rule
32
+ unguard src --rule duplicate-*=warning # override rule severity/policy
33
+ unguard src --rule category:cross-file=warning
34
+ unguard src --rule tag:safety=error
35
+ unguard src --severity=error,warning # show errors+warnings
36
+ unguard src --fail-on=error # fail only on errors
37
+ unguard src --format=flat # one-line-per-diagnostic, grepable
34
38
  unguard src --format=flat | grep error
35
39
  ```
36
40
 
37
41
  Add `unguard` to your lint check, especially if code is written by AI.
38
42
 
43
+ ### Config
44
+
45
+ `unguard` automatically loads `./unguard.config.json` (or `./.unguardrc.json`). Use `--config <path>` to specify another file.
46
+
47
+ ```json
48
+ {
49
+ "paths": ["src", "apps/web/src"],
50
+ "ignore": ["**/*.gen.ts", "**/routeTree.gen.ts"],
51
+ "rules": {
52
+ "duplicate-*": "warning",
53
+ "category:cross-file": "warning",
54
+ "tag:safety": "error",
55
+ "no-ts-ignore": "error",
56
+ "prefer-*": "off"
57
+ },
58
+ "failOn": "error"
59
+ }
60
+ ```
61
+
62
+ `rules` values can be `off`, `info`, `warning`, or `error`.
63
+ Selectors support:
64
+ - exact rule id: `no-ts-ignore`
65
+ - wildcard: `duplicate-*`
66
+ - category: `category:cross-file`
67
+ - tag: `tag:safety`
68
+
69
+ ### Ignore behavior
70
+
71
+ `unguard` ignores:
72
+ - built-in: `node_modules`, `dist`, `.git`, declaration files (`*.d.ts`, `*.d.cts`, `*.d.mts`)
73
+ - generated files: `*.gen.*`, `*.generated.*`
74
+ - project `.gitignore`
75
+ - anything passed via `--ignore` or config `ignore`
76
+
39
77
  ### Exit codes
40
78
 
41
79
  | Code | Meaning |
42
80
  | ---- | ------- |
43
- | 0 | No issues |
44
- | 1 | Warnings or info only |
45
- | 2 | At least one error |
81
+ | 0 | No issues, or issues below `--fail-on` threshold |
82
+ | 1 | Failing diagnostics without errors |
83
+ | 2 | Failing diagnostics with at least one error |
46
84
 
47
- Use `--severity=error` in CI to only fail on errors:
85
+ Use `--fail-on=error` in CI to fail only on errors while still showing all diagnostics:
48
86
 
49
87
  ```bash
50
- unguard src --severity=error || exit 1
88
+ unguard src --fail-on=error
51
89
  ```
52
90
 
91
+ `--severity` filters display only. `--fail-on` evaluates all diagnostics after rule policy.
92
+
53
93
  ### Output formats
54
94
 
55
95
  **Grouped** (default) -- diagnostics grouped by file:
@@ -112,10 +152,10 @@ These rules use the TypeScript type checker. Non-nullable types suppress the dia
112
152
 
113
153
  | Rule | Severity | What it catches |
114
154
  | ---- | -------- | --------------- |
115
- | `duplicate-type-declaration` | error | Same type shape in multiple files |
116
- | `duplicate-type-name` | error | Same exported type name, different shapes |
117
- | `duplicate-function-declaration` | error | Same function body in multiple files |
118
- | `duplicate-function-name` | error | Same exported function name, different bodies |
155
+ | `duplicate-type-declaration` | warning | Same type shape in multiple files |
156
+ | `duplicate-type-name` | warning | Same exported type name, different shapes |
157
+ | `duplicate-function-declaration` | warning | Same function body in multiple files |
158
+ | `duplicate-function-name` | warning | Same exported function name, different bodies |
119
159
  | `optional-arg-always-used` | warning | Optional param provided at every call site |
120
160
  | `explicit-null-arg` | warning | `fn(null)` / `fn(undefined)` to project functions |
121
161
 
@@ -141,10 +181,24 @@ file.ts:2:31 error Explicit `any` annotation ... (intentional escape hatch for u
141
181
  ## API
142
182
 
143
183
  ```typescript
144
- import { scan } from "unguard";
145
-
146
- const result = await scan({ paths: ["src/"] });
147
- for (const d of result.diagnostics) {
184
+ import { executeScan, scan } from "unguard";
185
+
186
+ const result = await scan({ paths: ["src/"] }); // raw diagnostics
187
+ const execution = await executeScan({
188
+ paths: ["src"],
189
+ ignore: ["**/*.gen.ts"],
190
+ rulePolicy: {
191
+ "duplicate-*": "warning",
192
+ "category:cross-file": "warning",
193
+ "tag:safety": "error",
194
+ "prefer-*": "off",
195
+ },
196
+ showSeverities: ["error", "warning"],
197
+ failOn: "error",
198
+ });
199
+
200
+ console.log(execution.exitCode);
201
+ for (const d of execution.visibleDiagnostics) {
148
202
  console.log(`${d.file}:${d.line} [${d.ruleId}] ${d.message}`);
149
203
  }
150
204
  ```
@@ -630,7 +630,7 @@ var preferRequiredParamWithGuard = {
630
630
  // src/rules/cross-file/duplicate-type-declaration.ts
631
631
  var duplicateTypeDeclaration = {
632
632
  id: "duplicate-type-declaration",
633
- severity: "error",
633
+ severity: "warning",
634
634
  message: "Identical type shape declared in multiple files; consolidate to a single definition",
635
635
  analyze(project) {
636
636
  const diagnostics = [];
@@ -658,7 +658,7 @@ var duplicateTypeDeclaration = {
658
658
  import * as ts19 from "typescript";
659
659
  var duplicateFunctionDeclaration = {
660
660
  id: "duplicate-function-declaration",
661
- severity: "error",
661
+ severity: "warning",
662
662
  message: "Identical function body declared in multiple files; consolidate to a single definition",
663
663
  analyze(project) {
664
664
  const diagnostics = [];
@@ -862,7 +862,7 @@ import { dirname, resolve } from "path";
862
862
  var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
863
863
  var duplicateFunctionName = {
864
864
  id: "duplicate-function-name",
865
- severity: "error",
865
+ severity: "warning",
866
866
  message: "Same function name exported from multiple files; consolidate or rename to avoid ambiguity",
867
867
  analyze(project) {
868
868
  const diagnostics = [];
@@ -911,7 +911,7 @@ function resolveCandidates(fromFile, specifier) {
911
911
  const candidates = [base];
912
912
  for (const ext of EXTENSIONS) {
913
913
  candidates.push(base + ext);
914
- candidates.push(resolve(base, "index" + ext));
914
+ candidates.push(resolve(base, `index${ext}`));
915
915
  }
916
916
  return candidates;
917
917
  }
@@ -920,7 +920,7 @@ function resolveCandidates(fromFile, specifier) {
920
920
  import * as ts23 from "typescript";
921
921
  var duplicateTypeName = {
922
922
  id: "duplicate-type-name",
923
- severity: "error",
923
+ severity: "warning",
924
924
  message: "Same type name exported from multiple files; consolidate or rename to avoid ambiguity",
925
925
  analyze(project) {
926
926
  const diagnostics = [];
@@ -991,15 +991,93 @@ var allRules = [
991
991
  duplicateTypeName,
992
992
  noDynamicImport
993
993
  ];
994
+ var ruleMetadata = {
995
+ "no-any-cast": { category: "type-evasion", tags: ["safety"] },
996
+ "no-explicit-any-annotation": { category: "type-evasion", tags: ["safety"] },
997
+ "no-type-assertion": { category: "type-evasion", tags: ["safety"] },
998
+ "no-ts-ignore": { category: "type-evasion", tags: ["safety"] },
999
+ "no-optional-property-access": { category: "defensive-code", tags: ["type-aware"] },
1000
+ "no-optional-element-access": { category: "defensive-code", tags: ["type-aware"] },
1001
+ "no-optional-call": { category: "defensive-code", tags: ["type-aware"] },
1002
+ "no-nullish-coalescing": { category: "defensive-code", tags: ["type-aware"] },
1003
+ "no-logical-or-fallback": { category: "defensive-code", tags: ["type-aware"] },
1004
+ "no-null-ternary-normalization": { category: "defensive-code", tags: ["type-aware"] },
1005
+ "no-non-null-assertion": { category: "defensive-code", tags: ["type-aware"] },
1006
+ "no-double-negation-coercion": { category: "defensive-code", tags: ["readability"] },
1007
+ "no-redundant-existence-guard": { category: "defensive-code", tags: ["type-aware"] },
1008
+ "no-empty-catch": { category: "error-handling", tags: ["safety"] },
1009
+ "no-catch-return": { category: "error-handling", tags: ["safety"] },
1010
+ "no-error-rewrap": { category: "error-handling", tags: ["safety"] },
1011
+ "no-inline-type-in-params": { category: "interface-design", tags: ["api"] },
1012
+ "prefer-default-param-value": { category: "interface-design", tags: ["api"] },
1013
+ "prefer-required-param-with-guard": { category: "interface-design", tags: ["api"] },
1014
+ "duplicate-type-declaration": { category: "cross-file", tags: ["duplicate"] },
1015
+ "duplicate-type-name": { category: "cross-file", tags: ["duplicate"] },
1016
+ "duplicate-function-declaration": { category: "cross-file", tags: ["duplicate"] },
1017
+ "duplicate-function-name": { category: "cross-file", tags: ["duplicate"] },
1018
+ "optional-arg-always-used": { category: "cross-file", tags: ["api"] },
1019
+ "explicit-null-arg": { category: "cross-file", tags: ["api"] },
1020
+ "no-dynamic-import": { category: "imports", tags: ["safety"] }
1021
+ };
1022
+ function getRuleMetadata(ruleId) {
1023
+ const metadata = ruleMetadata[ruleId];
1024
+ if (metadata !== void 0) return metadata;
1025
+ return { category: "cross-file", tags: [] };
1026
+ }
994
1027
 
995
- // src/engine.ts
996
- import fg from "fast-glob";
997
- import { readFileSync as readFileSync2 } from "fs";
998
-
999
- // src/rules/types.ts
1000
- function isTSRule(r) {
1001
- return "kind" in r && r.kind === "ts";
1028
+ // src/scan/config.ts
1029
+ var BUILTIN_IGNORE = [
1030
+ "**/node_modules/**",
1031
+ "**/dist/**",
1032
+ "**/.git/**",
1033
+ "**/*.d.ts",
1034
+ "**/*.d.cts",
1035
+ "**/*.d.mts"
1036
+ ];
1037
+ var GENERATED_IGNORE = [
1038
+ "**/*.gen.*",
1039
+ "**/*.generated.*"
1040
+ ];
1041
+ var DEFAULT_FAIL_ON = "info";
1042
+ function resolveScanConfig(options) {
1043
+ const paths = options.paths.length > 0 ? options.paths : ["."];
1044
+ const ignore2 = [...BUILTIN_IGNORE, ...GENERATED_IGNORE, ...options.ignore ?? []];
1045
+ return {
1046
+ paths,
1047
+ strict: options.strict ?? false,
1048
+ rules: options.rules ? [...options.rules] : null,
1049
+ ignore: ignore2,
1050
+ rulePolicy: toRulePolicyEntries(options.rulePolicy),
1051
+ showSeverities: normalizeSeveritySet(options.showSeverities),
1052
+ failOn: options.failOn ?? DEFAULT_FAIL_ON,
1053
+ useGitIgnore: options.useGitIgnore ?? true
1054
+ };
1055
+ }
1056
+ function toRulePolicyEntries(policy) {
1057
+ if (policy === void 0) return [];
1058
+ if (Array.isArray(policy)) return [...policy];
1059
+ const entries = [];
1060
+ for (const [selector, severity] of Object.entries(policy)) {
1061
+ entries.push({ selector, severity });
1062
+ }
1063
+ return entries;
1002
1064
  }
1065
+ function normalizeSeveritySet(levels) {
1066
+ if (levels === void 0 || levels.length === 0) return null;
1067
+ return new Set(levels);
1068
+ }
1069
+ function isRulePolicySeverity(value) {
1070
+ return value === "off" || value === "info" || value === "warning" || value === "error";
1071
+ }
1072
+ function isSeverity(value) {
1073
+ return value === "info" || value === "warning" || value === "error";
1074
+ }
1075
+ function isFailOn(value) {
1076
+ return value === "none" || isSeverity(value);
1077
+ }
1078
+
1079
+ // src/scan/analyze.ts
1080
+ import { readFileSync as readFileSync2 } from "fs";
1003
1081
 
1004
1082
  // src/collect/index.ts
1005
1083
  import * as ts26 from "typescript";
@@ -1299,6 +1377,11 @@ function collectAllComments(sourceFile) {
1299
1377
  return comments;
1300
1378
  }
1301
1379
 
1380
+ // src/rules/types.ts
1381
+ function isTSRule(r) {
1382
+ return "kind" in r && r.kind === "ts";
1383
+ }
1384
+
1302
1385
  // src/typecheck/program.ts
1303
1386
  import * as ts27 from "typescript";
1304
1387
  import { dirname as dirname2 } from "path";
@@ -1388,86 +1471,67 @@ function buildContext(rule, sourceFile, checker, source, filename, diagnostics)
1388
1471
  };
1389
1472
  }
1390
1473
 
1391
- // src/engine.ts
1392
- async function scan(options) {
1393
- const patterns = options.paths.length > 0 ? options.paths : ["."];
1394
- const globs = patterns.map((p) => {
1395
- if (p === ".") return `./**/*.{ts,cts,mts,tsx}`;
1396
- if (p.endsWith("/")) return `${p}**/*.{ts,cts,mts,tsx}`;
1397
- if (!p.includes("*") && !p.endsWith(".ts") && !p.endsWith(".tsx") && !p.endsWith(".cts") && !p.endsWith(".mts")) {
1398
- return `${p}/**/*.{ts,cts,mts,tsx}`;
1399
- }
1400
- return p;
1401
- });
1402
- const files = await fg(globs, {
1403
- ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/*.d.ts", "**/*.d.cts", "**/*.d.mts"],
1404
- absolute: true
1405
- });
1406
- const activeRules = allRules.filter((r) => {
1407
- if (options.rules && !options.rules.includes(r.id)) return false;
1408
- return true;
1409
- });
1410
- const tsRules = activeRules.filter(isTSRule);
1411
- const crossFileRules = activeRules.filter((r) => !isTSRule(r));
1474
+ // src/scan/analyze.ts
1475
+ function analyzeFiles(files, rules) {
1476
+ const tsRules = rules.filter(isTSRule);
1477
+ const crossFileRules = rules.filter((r) => !isTSRule(r));
1412
1478
  const diagnostics = [];
1413
1479
  const program = files.length > 0 ? createProgramFromFiles(files) : null;
1414
- if (tsRules.length > 0 && program) {
1480
+ if (!program) return diagnostics;
1481
+ if (tsRules.length > 0) {
1415
1482
  const checker = program.getTypeChecker();
1416
1483
  for (const file of files) {
1417
1484
  const source = readFileSync2(file, "utf8");
1418
1485
  const sourceFile = program.getSourceFile(file);
1419
1486
  if (sourceFile) {
1420
- const tsDiags = runTSRules(tsRules, sourceFile, checker, source, file);
1421
- diagnostics.push(...tsDiags);
1487
+ diagnostics.push(...runTSRules(tsRules, sourceFile, checker, source, file));
1422
1488
  }
1423
1489
  }
1424
1490
  }
1425
- if (crossFileRules.length > 0 && program) {
1491
+ if (crossFileRules.length > 0) {
1426
1492
  const projectIndex = collectProject(program);
1427
1493
  for (const rule of crossFileRules) {
1428
- const crossDiags = rule.analyze(projectIndex);
1429
- for (const d of crossDiags) {
1430
- const fileData = projectIndex.files.get(d.file);
1431
- if (fileData !== void 0) annotate([d], fileData.comments, fileData.source);
1494
+ const crossDiagnostics = rule.analyze(projectIndex);
1495
+ for (const diagnostic of crossDiagnostics) {
1496
+ const fileData = projectIndex.files.get(diagnostic.file);
1497
+ if (fileData !== void 0) annotate([diagnostic], fileData.comments, fileData.source);
1432
1498
  }
1433
- diagnostics.push(...crossDiags);
1499
+ diagnostics.push(...crossDiagnostics);
1434
1500
  }
1435
1501
  }
1502
+ return dedupeDiagnostics(diagnostics);
1503
+ }
1504
+ function dedupeDiagnostics(diagnostics) {
1436
1505
  const seen = /* @__PURE__ */ new Set();
1437
1506
  const deduped = [];
1438
- for (const d of diagnostics) {
1439
- const key = `${d.file}:${d.line}:${d.ruleId}`;
1507
+ for (const diagnostic of diagnostics) {
1508
+ const key = `${diagnostic.file}:${diagnostic.line}:${diagnostic.ruleId}`;
1440
1509
  if (seen.has(key)) continue;
1441
1510
  seen.add(key);
1442
- deduped.push(d);
1443
- }
1444
- if (options.strict) {
1445
- for (const d of deduped) {
1446
- d.severity = "error";
1447
- }
1511
+ deduped.push(diagnostic);
1448
1512
  }
1449
- return { diagnostics: deduped, fileCount: files.length };
1513
+ return deduped;
1450
1514
  }
1451
1515
  function annotate(diagnostics, comments, source) {
1452
1516
  if (comments.length === 0 || diagnostics.length === 0) return;
1453
1517
  const byEndLine = /* @__PURE__ */ new Map();
1454
- for (const c of comments) {
1455
- const endLine = lineAt(source, c.end);
1518
+ for (const comment of comments) {
1519
+ const endLine = lineAt(source, comment.end);
1456
1520
  let list = byEndLine.get(endLine);
1457
1521
  if (list === void 0) {
1458
1522
  list = [];
1459
1523
  byEndLine.set(endLine, list);
1460
1524
  }
1461
- list.push(c);
1525
+ list.push(comment);
1462
1526
  }
1463
- for (const d of diagnostics) {
1464
- const inline = findInlineComment(d.line, byEndLine);
1527
+ for (const diagnostic of diagnostics) {
1528
+ const inline = findInlineComment(diagnostic.line, byEndLine);
1465
1529
  if (inline !== null) {
1466
- d.annotation = inline;
1530
+ diagnostic.annotation = inline;
1467
1531
  continue;
1468
1532
  }
1469
- const above = collectAnnotation(d.line - 1, byEndLine, source);
1470
- if (above !== null) d.annotation = above;
1533
+ const above = collectAnnotation(diagnostic.line - 1, byEndLine, source);
1534
+ if (above !== null) diagnostic.annotation = above;
1471
1535
  }
1472
1536
  }
1473
1537
  function findInlineComment(diagLine, byEndLine) {
@@ -1485,9 +1549,7 @@ function collectAnnotation(commentEndLine, byEndLine, source) {
1485
1549
  if (commentsOnLine === void 0 || commentsOnLine.length === 0) return null;
1486
1550
  const comment = commentsOnLine.at(-1);
1487
1551
  if (comment === void 0) return null;
1488
- if (comment.type === "Block") {
1489
- return cleanBlockComment(comment.value);
1490
- }
1552
+ if (comment.type === "Block") return cleanBlockComment(comment.value);
1491
1553
  const lines = [comment.value.trim()];
1492
1554
  let prevLine = commentEndLine - 1;
1493
1555
  for (; ; ) {
@@ -1512,8 +1574,145 @@ function lineAt(source, offset) {
1512
1574
  return line;
1513
1575
  }
1514
1576
 
1577
+ // src/scan/discover.ts
1578
+ import fg from "fast-glob";
1579
+ import ignore from "ignore";
1580
+ import { existsSync, readFileSync as readFileSync3 } from "fs";
1581
+ import { relative, resolve as resolve2, sep } from "path";
1582
+ async function discoverFiles(config) {
1583
+ const globs = expandGlobs(config.paths);
1584
+ const discoveredFiles = await fg(globs, {
1585
+ ignore: config.ignore,
1586
+ absolute: true
1587
+ });
1588
+ if (!config.useGitIgnore) return discoveredFiles;
1589
+ return applyGitIgnore(discoveredFiles);
1590
+ }
1591
+ function expandGlobs(paths) {
1592
+ return paths.map((p) => {
1593
+ if (p === ".") return "./**/*.{ts,cts,mts,tsx}";
1594
+ if (p.endsWith("/")) return `${p}**/*.{ts,cts,mts,tsx}`;
1595
+ if (!p.includes("*") && !p.endsWith(".ts") && !p.endsWith(".tsx") && !p.endsWith(".cts") && !p.endsWith(".mts")) {
1596
+ return `${p}/**/*.{ts,cts,mts,tsx}`;
1597
+ }
1598
+ return p;
1599
+ });
1600
+ }
1601
+ function applyGitIgnore(files) {
1602
+ const gitIgnorePath = resolve2(process.cwd(), ".gitignore");
1603
+ if (!existsSync(gitIgnorePath)) return files;
1604
+ const matcher = ignore().add(readFileSync3(gitIgnorePath, "utf8"));
1605
+ return files.filter((file) => {
1606
+ const rel = relative(process.cwd(), file);
1607
+ if (rel.startsWith("..")) return true;
1608
+ const normalized = rel.split(sep).join("/");
1609
+ return !matcher.ignores(normalized);
1610
+ });
1611
+ }
1612
+
1613
+ // src/scan/policy.ts
1614
+ function buildRuleDescriptors(rules) {
1615
+ return rules.map((rule) => {
1616
+ const metadata = getRuleMetadata(rule.id);
1617
+ return {
1618
+ rule,
1619
+ category: metadata.category,
1620
+ tags: metadata.tags
1621
+ };
1622
+ });
1623
+ }
1624
+ function resolveActiveRules(descriptors, config) {
1625
+ const selectedRules = config.rules ? new Set(config.rules) : null;
1626
+ const active = [];
1627
+ for (const descriptor of descriptors) {
1628
+ if (selectedRules && !selectedRules.has(descriptor.rule.id)) continue;
1629
+ const resolvedSeverity = resolveRuleSeverity(descriptor, config);
1630
+ if (resolvedSeverity === "off") continue;
1631
+ if (descriptor.rule.severity === resolvedSeverity) {
1632
+ active.push(descriptor.rule);
1633
+ continue;
1634
+ }
1635
+ active.push({ ...descriptor.rule, severity: resolvedSeverity });
1636
+ }
1637
+ return active;
1638
+ }
1639
+ function resolveRuleSeverity(descriptor, config) {
1640
+ let severity = descriptor.rule.severity;
1641
+ for (const entry of config.rulePolicy) {
1642
+ if (!matchesSelector(descriptor, entry.selector)) continue;
1643
+ severity = entry.severity;
1644
+ }
1645
+ if (severity === "off") return "off";
1646
+ if (config.strict) return "error";
1647
+ return severity;
1648
+ }
1649
+ function matchesSelector(descriptor, selector) {
1650
+ if (selector.startsWith("category:")) {
1651
+ return descriptor.category === selector.slice("category:".length);
1652
+ }
1653
+ if (selector.startsWith("tag:")) {
1654
+ return descriptor.tags.includes(selector.slice("tag:".length));
1655
+ }
1656
+ if (selector === descriptor.rule.id) return true;
1657
+ if (!selector.includes("*")) return false;
1658
+ const regex = new RegExp(`^${escapeRegex(selector).replaceAll("\\*", ".*")}$`);
1659
+ return regex.test(descriptor.rule.id);
1660
+ }
1661
+ function escapeRegex(value) {
1662
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1663
+ }
1664
+ function finalizeScanResult(diagnostics, fileCount, config) {
1665
+ const visibleDiagnostics = config.showSeverities ? diagnostics.filter((d) => config.showSeverities?.has(d.severity)) : diagnostics;
1666
+ return {
1667
+ diagnostics,
1668
+ visibleDiagnostics,
1669
+ fileCount,
1670
+ exitCode: computeExitCode(diagnostics, config.failOn)
1671
+ };
1672
+ }
1673
+ function toScanResult(execution) {
1674
+ return {
1675
+ diagnostics: execution.diagnostics,
1676
+ fileCount: execution.fileCount
1677
+ };
1678
+ }
1679
+ function computeExitCode(diagnostics, failOn) {
1680
+ if (failOn === "none") return 0;
1681
+ const hasError = diagnostics.some((d) => d.severity === "error");
1682
+ const hasWarning = diagnostics.some((d) => d.severity === "warning");
1683
+ const hasInfo = diagnostics.some((d) => d.severity === "info");
1684
+ if (failOn === "error") return hasError ? 2 : 0;
1685
+ if (failOn === "warning") {
1686
+ if (hasError) return 2;
1687
+ return hasWarning ? 1 : 0;
1688
+ }
1689
+ if (hasError) return 2;
1690
+ if (hasWarning || hasInfo) return 1;
1691
+ return 0;
1692
+ }
1693
+
1694
+ // src/engine.ts
1695
+ async function executeScan(options) {
1696
+ const config = resolveScanConfig(options);
1697
+ const files = await discoverFiles(config);
1698
+ const descriptors = buildRuleDescriptors(allRules);
1699
+ const activeRules = resolveActiveRules(descriptors, config);
1700
+ const diagnostics = analyzeFiles(files, activeRules);
1701
+ return finalizeScanResult(diagnostics, files.length, config);
1702
+ }
1703
+ async function scan(options) {
1704
+ const execution = await executeScan(options);
1705
+ return toScanResult(execution);
1706
+ }
1707
+
1515
1708
  export {
1516
1709
  allRules,
1710
+ getRuleMetadata,
1711
+ toRulePolicyEntries,
1712
+ isRulePolicySeverity,
1713
+ isSeverity,
1714
+ isFailOn,
1715
+ executeScan,
1517
1716
  scan
1518
1717
  };
1519
- //# sourceMappingURL=chunk-YYX4R25B.js.map
1718
+ //# sourceMappingURL=chunk-I4RZLVE2.js.map