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/dist/cli.js CHANGED
@@ -1,24 +1,30 @@
1
1
  import {
2
- scan
3
- } from "./chunk-YYX4R25B.js";
2
+ executeScan,
3
+ isFailOn,
4
+ isRulePolicySeverity,
5
+ isSeverity,
6
+ toRulePolicyEntries
7
+ } from "./chunk-I4RZLVE2.js";
4
8
 
5
9
  // src/cli.ts
10
+ import { existsSync, readFileSync } from "fs";
11
+ import { basename, relative, resolve } from "path";
6
12
  import { parseArgs } from "util";
7
- import { relative } from "path";
8
13
  import pc from "picocolors";
9
- function isSeverity(value) {
10
- return value === "info" || value === "warning" || value === "error";
11
- }
12
14
  async function main(argv) {
13
15
  const rawArgs = argv.slice(2);
14
16
  const userArgs = rawArgs[0] === "scan" ? rawArgs.slice(1) : rawArgs;
15
- const { values, positionals: paths } = parseArgs({
17
+ const { values, positionals: cliPaths } = parseArgs({
16
18
  args: userArgs,
17
19
  options: {
18
20
  strict: { type: "boolean", default: false },
19
21
  filter: { type: "string" },
20
22
  format: { type: "string", default: "grouped" },
21
23
  severity: { type: "string", multiple: true, default: [] },
24
+ ignore: { type: "string", multiple: true, default: [] },
25
+ rule: { type: "string", multiple: true, default: [] },
26
+ config: { type: "string" },
27
+ "fail-on": { type: "string" },
22
28
  help: { type: "boolean", short: "h", default: false }
23
29
  },
24
30
  allowPositionals: true
@@ -27,25 +33,57 @@ async function main(argv) {
27
33
  printHelp();
28
34
  return 0;
29
35
  }
30
- const result = await scan({
36
+ let config = null;
37
+ try {
38
+ const configPath = findConfigPath(values.config);
39
+ config = configPath ? loadConfig(configPath) : null;
40
+ } catch (err) {
41
+ const message = err instanceof Error ? err.message : String(err);
42
+ console.error(pc.red(message));
43
+ return 1;
44
+ }
45
+ const rulePolicyResult = parseRulePolicyArgs(values.rule);
46
+ if (!rulePolicyResult.ok) {
47
+ console.error(pc.red(rulePolicyResult.message));
48
+ return 1;
49
+ }
50
+ const severityFiltersResult = parseSeverityFilters(values.severity);
51
+ if (!severityFiltersResult.ok) {
52
+ console.error(pc.red(severityFiltersResult.message));
53
+ return 1;
54
+ }
55
+ const failOnResult = resolveFailOn(values["fail-on"], config?.failOn);
56
+ if (!failOnResult.ok) {
57
+ console.error(pc.red(failOnResult.message));
58
+ return 1;
59
+ }
60
+ const paths = cliPaths.length > 0 ? cliPaths : config?.paths ?? [];
61
+ const ignore = [...config?.ignore ?? [], ...values.ignore];
62
+ const rulePolicy = [
63
+ ...toRulePolicyEntries(config?.rules),
64
+ ...rulePolicyResult.value
65
+ ];
66
+ const execution = await executeScan({
31
67
  paths,
32
68
  strict: values.strict,
33
- rules: values.filter ? [values.filter] : void 0
69
+ rules: values.filter ? [values.filter] : void 0,
70
+ ignore: ignore.length > 0 ? ignore : void 0,
71
+ rulePolicy: rulePolicy.length > 0 ? rulePolicy : void 0,
72
+ showSeverities: severityFiltersResult.value.length > 0 ? severityFiltersResult.value : void 0,
73
+ failOn: failOnResult.value
34
74
  });
35
- const severitySet = values.severity.length > 0 ? new Set(values.severity.filter(isSeverity)) : null;
36
- const filtered = severitySet ? result.diagnostics.filter((d) => severitySet.has(d.severity)) : result.diagnostics;
37
- if (filtered.length === 0) {
38
- console.log(pc.green(`No issues found in ${result.fileCount} files.`));
39
- return 0;
75
+ const diagnostics = execution.visibleDiagnostics;
76
+ if (diagnostics.length === 0) {
77
+ console.log(pc.green(`No issues found in ${execution.fileCount} files.`));
78
+ return execution.exitCode;
40
79
  }
41
80
  if (values.format === "flat") {
42
- printDiagnosticsFlat(filtered);
81
+ printDiagnosticsFlat(diagnostics);
43
82
  } else {
44
- printDiagnostics(filtered);
83
+ printDiagnostics(diagnostics);
45
84
  }
46
- printSummary(filtered, result.fileCount);
47
- const hasError = filtered.some((d) => d.severity === "error");
48
- return hasError ? 2 : 1;
85
+ printSummary(diagnostics, execution.fileCount);
86
+ return execution.exitCode;
49
87
  }
50
88
  function printHelp() {
51
89
  console.log(`unguard: data-shape static analyzer
@@ -55,46 +93,50 @@ Usage:
55
93
  unguard [paths...] [options]
56
94
 
57
95
  Options:
58
- --strict Treat all warnings as errors
96
+ --config <path> Path to unguard config file (defaults to ./unguard.config.json)
97
+ --strict Treat all diagnostics as errors
59
98
  --filter <rule> Run only the specified rule
60
- --severity <level> Filter by severity (error, warning, info). Repeatable.
99
+ --rule <sel=sev> Override rule severity (supports id, *, category:<name>, tag:<name>)
100
+ --ignore <glob> Ignore path glob. Repeatable.
101
+ --severity <levels> Filter output by severity (comma-separated and/or repeatable)
102
+ --fail-on <level> Exit threshold: none, error, warning, info (default: info)
61
103
  --format <mode> Output format: grouped (default) or flat
62
104
  -h, --help Show this help
63
105
 
64
106
  Exit codes:
65
- 0 No issues
66
- 1 Warnings or info only
67
- 2 At least one error
107
+ 0 Clean, or does not meet --fail-on threshold
108
+ 1 Meets threshold without errors
109
+ 2 Meets threshold with at least one error
68
110
 
69
111
  Examples:
70
112
  unguard scan src
71
- unguard scan src --strict
72
- unguard scan src --filter no-empty-catch
73
- unguard scan src --severity=error
74
- unguard scan src --severity=error --severity=warning
75
- unguard scan src --format=flat | grep error`);
113
+ unguard scan src --rule duplicate-*=warning
114
+ unguard scan src --rule category:cross-file=warning
115
+ unguard scan src --rule tag:safety=error
116
+ unguard scan src --severity=error,warning
117
+ unguard scan src --fail-on=error`);
76
118
  }
77
119
  function printDiagnostics(diagnostics) {
78
120
  const byFile = /* @__PURE__ */ new Map();
79
- for (const d of diagnostics) {
80
- let list = byFile.get(d.file);
121
+ for (const diagnostic of diagnostics) {
122
+ let list = byFile.get(diagnostic.file);
81
123
  if (list === void 0) {
82
124
  list = [];
83
- byFile.set(d.file, list);
125
+ byFile.set(diagnostic.file, list);
84
126
  }
85
- list.push(d);
127
+ list.push(diagnostic);
86
128
  }
87
- const cwd = process.cwd() + "/";
129
+ const cwd = `${process.cwd()}/`;
88
130
  const cwdBase = process.cwd();
89
- for (const [file, diags] of byFile) {
131
+ for (const [file, fileDiagnostics] of byFile) {
90
132
  const rel = relative(cwdBase, file);
91
133
  console.log(pc.underline(rel));
92
- for (const d of diags) {
93
- const loc = `${d.line}:${d.column}`;
94
- const sev = d.severity === "error" ? pc.red("error") : d.severity === "warning" ? pc.yellow("warning") : pc.blue("info");
95
- const msg = d.message.replaceAll(cwd, "");
96
- const rule = pc.dim(d.ruleId);
97
- const annotation = d.annotation !== void 0 ? ` ${pc.cyan(d.annotation)}` : "";
134
+ for (const diagnostic of fileDiagnostics) {
135
+ const loc = `${diagnostic.line}:${diagnostic.column}`;
136
+ const sev = diagnostic.severity === "error" ? pc.red("error") : diagnostic.severity === "warning" ? pc.yellow("warning") : pc.blue("info");
137
+ const msg = diagnostic.message.replaceAll(cwd, "");
138
+ const rule = pc.dim(diagnostic.ruleId);
139
+ const annotation = diagnostic.annotation !== void 0 ? ` ${pc.cyan(diagnostic.annotation)}` : "";
98
140
  console.log(` ${pc.dim(loc.padEnd(10))} ${sev} ${msg} ${rule}${annotation}`);
99
141
  }
100
142
  console.log();
@@ -102,21 +144,21 @@ function printDiagnostics(diagnostics) {
102
144
  }
103
145
  function printDiagnosticsFlat(diagnostics) {
104
146
  const cwdBase = process.cwd();
105
- const cwd = cwdBase + "/";
106
- for (const d of diagnostics) {
107
- const rel = relative(cwdBase, d.file);
108
- const msg = d.message.replaceAll(cwd, "");
109
- console.log(`${rel}:${d.line}:${d.column} ${d.severity} [${d.ruleId}] ${msg}`);
147
+ const cwd = `${cwdBase}/`;
148
+ for (const diagnostic of diagnostics) {
149
+ const rel = relative(cwdBase, diagnostic.file);
150
+ const msg = diagnostic.message.replaceAll(cwd, "");
151
+ console.log(`${rel}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity} [${diagnostic.ruleId}] ${msg}`);
110
152
  }
111
153
  }
112
154
  function printSummary(diagnostics, fileCount) {
113
155
  const counts = /* @__PURE__ */ new Map();
114
- for (const d of diagnostics) {
115
- const prev = counts.get(d.ruleId);
116
- counts.set(d.ruleId, (prev === void 0 ? 0 : prev) + 1);
156
+ for (const diagnostic of diagnostics) {
157
+ const prev = counts.get(diagnostic.ruleId);
158
+ counts.set(diagnostic.ruleId, (prev === void 0 ? 0 : prev) + 1);
117
159
  }
118
160
  const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
119
- const maxRule = Math.max(...sorted.map(([r]) => r.length));
161
+ const maxRule = Math.max(...sorted.map(([rule]) => rule.length));
120
162
  console.log(pc.bold("Summary"));
121
163
  for (const [rule, count] of sorted) {
122
164
  console.log(` ${rule.padEnd(maxRule)} ${pc.bold(String(count))}`);
@@ -126,6 +168,112 @@ function printSummary(diagnostics, fileCount) {
126
168
  ${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? "" : "s"} in ${fileCount} files.`
127
169
  );
128
170
  }
171
+ function parseSeverityFilters(values) {
172
+ const levels = splitCsv(values);
173
+ const parsed = [];
174
+ for (const value of levels) {
175
+ if (!isSeverity(value)) {
176
+ return { ok: false, message: `Invalid --severity value "${value}". Use error, warning, info.` };
177
+ }
178
+ parsed.push(value);
179
+ }
180
+ return { ok: true, value: parsed };
181
+ }
182
+ function parseRulePolicyArgs(values) {
183
+ const entries = [];
184
+ for (const arg of values) {
185
+ const separator = resolveRuleSeparator(arg);
186
+ if (separator === null) {
187
+ return { ok: false, message: `Invalid --rule value "${arg}". Use <selector>=<off|info|warning|error>.` };
188
+ }
189
+ const selector = arg.slice(0, separator).trim();
190
+ const severity = arg.slice(separator + 1).trim();
191
+ if (!selector) {
192
+ return { ok: false, message: `Invalid --rule value "${arg}". Rule selector is required.` };
193
+ }
194
+ if (!isRulePolicySeverity(severity)) {
195
+ return { ok: false, message: `Invalid --rule value "${arg}". Severity must be off, info, warning, or error.` };
196
+ }
197
+ entries.push({ selector, severity });
198
+ }
199
+ return { ok: true, value: entries };
200
+ }
201
+ function resolveRuleSeparator(value) {
202
+ const equalIndex = value.indexOf("=");
203
+ if (equalIndex >= 0) return equalIndex;
204
+ const colonIndex = value.lastIndexOf(":");
205
+ if (colonIndex <= 0) return null;
206
+ return colonIndex;
207
+ }
208
+ function splitCsv(values) {
209
+ return values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
210
+ }
211
+ function findConfigPath(cliValue) {
212
+ if (cliValue) {
213
+ const explicitPath = resolve(process.cwd(), cliValue);
214
+ if (!existsSync(explicitPath)) throw new Error(`Config file not found: ${cliValue}`);
215
+ return explicitPath;
216
+ }
217
+ const candidates = ["unguard.config.json", ".unguardrc.json"];
218
+ for (const candidate of candidates) {
219
+ const candidatePath = resolve(process.cwd(), candidate);
220
+ if (existsSync(candidatePath)) return candidatePath;
221
+ }
222
+ return null;
223
+ }
224
+ function loadConfig(path) {
225
+ const text = readFileSync(path, "utf8");
226
+ let parsed;
227
+ try {
228
+ parsed = JSON.parse(text);
229
+ } catch (err) {
230
+ const message = err instanceof Error ? err.message : String(err);
231
+ throw new Error(`Invalid JSON in ${basename(path)}: ${message}`);
232
+ }
233
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
234
+ throw new Error(`Invalid config in ${basename(path)}: expected a JSON object.`);
235
+ }
236
+ const raw = parsed;
237
+ const config = {};
238
+ if (raw.paths !== void 0) {
239
+ if (!isStringArray(raw.paths)) throw new Error(`Invalid config in ${basename(path)}: "paths" must be a string array.`);
240
+ config.paths = raw.paths;
241
+ }
242
+ if (raw.ignore !== void 0) {
243
+ if (!isStringArray(raw.ignore)) throw new Error(`Invalid config in ${basename(path)}: "ignore" must be a string array.`);
244
+ config.ignore = raw.ignore;
245
+ }
246
+ if (raw.failOn !== void 0) {
247
+ if (typeof raw.failOn !== "string" || !isFailOn(raw.failOn)) {
248
+ throw new Error(`Invalid config in ${basename(path)}: "failOn" must be one of none, error, warning, info.`);
249
+ }
250
+ config.failOn = raw.failOn;
251
+ }
252
+ if (raw.rules !== void 0) {
253
+ if (typeof raw.rules !== "object" || raw.rules === null || Array.isArray(raw.rules)) {
254
+ throw new Error(`Invalid config in ${basename(path)}: "rules" must be an object.`);
255
+ }
256
+ const rules = {};
257
+ for (const [selector, severity] of Object.entries(raw.rules)) {
258
+ if (typeof severity !== "string" || !isRulePolicySeverity(severity)) {
259
+ throw new Error(`Invalid config in ${basename(path)}: rule "${selector}" must be off, info, warning, or error.`);
260
+ }
261
+ rules[selector] = severity;
262
+ }
263
+ config.rules = rules;
264
+ }
265
+ return config;
266
+ }
267
+ function isStringArray(value) {
268
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
269
+ }
270
+ function resolveFailOn(cliValue, configValue) {
271
+ const selected = cliValue ?? configValue ?? "info";
272
+ if (!isFailOn(selected)) {
273
+ return { ok: false, message: `Invalid --fail-on value "${selected}". Use none, error, warning, or info.` };
274
+ }
275
+ return { ok: true, value: selected };
276
+ }
129
277
  export {
130
278
  main
131
279
  };
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { parseArgs } from \"node:util\";\nimport { relative } from \"node:path\";\nimport pc from \"picocolors\";\nimport { scan } from \"./engine.ts\";\nimport type { Diagnostic } from \"./rules/types.ts\";\n\ntype Severity = \"info\" | \"warning\" | \"error\";\nfunction isSeverity(value: string): value is Severity {\n return value === \"info\" || value === \"warning\" || value === \"error\";\n}\n\nexport async function main(argv: string[]): Promise<number> {\n const rawArgs = argv.slice(2);\n const userArgs = rawArgs[0] === \"scan\" ? rawArgs.slice(1) : rawArgs;\n\n const { values, positionals: paths } = parseArgs({\n args: userArgs,\n options: {\n strict: { type: \"boolean\", default: false },\n filter: { type: \"string\" },\n format: { type: \"string\", default: \"grouped\" },\n severity: { type: \"string\", multiple: true, default: [] },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n allowPositionals: true,\n });\n\n if (values.help) {\n printHelp();\n return 0;\n }\n\n const result = await scan({\n paths,\n strict: values.strict,\n rules: values.filter ? [values.filter] : undefined,\n });\n\n const severitySet = values.severity.length > 0\n ? new Set(values.severity.filter(isSeverity))\n : null;\n const filtered = severitySet\n ? result.diagnostics.filter((d) => severitySet.has(d.severity))\n : result.diagnostics;\n\n if (filtered.length === 0) {\n console.log(pc.green(`No issues found in ${result.fileCount} files.`));\n return 0;\n }\n\n if (values.format === \"flat\") {\n printDiagnosticsFlat(filtered);\n } else {\n printDiagnostics(filtered);\n }\n printSummary(filtered, result.fileCount);\n\n const hasError = filtered.some((d) => d.severity === \"error\");\n return hasError ? 2 : 1;\n}\n\nfunction printHelp() {\n console.log(`unguard: data-shape static analyzer\n\nUsage:\n unguard scan [paths...] [options]\n unguard [paths...] [options]\n\nOptions:\n --strict Treat all warnings as errors\n --filter <rule> Run only the specified rule\n --severity <level> Filter by severity (error, warning, info). Repeatable.\n --format <mode> Output format: grouped (default) or flat\n -h, --help Show this help\n\nExit codes:\n 0 No issues\n 1 Warnings or info only\n 2 At least one error\n\nExamples:\n unguard scan src\n unguard scan src --strict\n unguard scan src --filter no-empty-catch\n unguard scan src --severity=error\n unguard scan src --severity=error --severity=warning\n unguard scan src --format=flat | grep error`);\n}\n\n\nfunction printDiagnostics(diagnostics: Diagnostic[]) {\n const byFile = new Map<string, Diagnostic[]>();\n for (const d of diagnostics) {\n let list = byFile.get(d.file);\n if (list === undefined) {\n list = [];\n byFile.set(d.file, list);\n }\n list.push(d);\n }\n\n const cwd = process.cwd() + \"/\";\n const cwdBase = process.cwd();\n\n for (const [file, diags] of byFile) {\n const rel = relative(cwdBase, file);\n console.log(pc.underline(rel));\n\n for (const d of diags) {\n const loc = `${d.line}:${d.column}`;\n const sev = d.severity === \"error\" ? pc.red(\"error\") : d.severity === \"warning\" ? pc.yellow(\"warning\") : pc.blue(\"info\");\n const msg = d.message.replaceAll(cwd, \"\");\n const rule = pc.dim(d.ruleId);\n const annotation = d.annotation !== undefined ? ` ${pc.cyan(d.annotation)}` : \"\";\n console.log(` ${pc.dim(loc.padEnd(10))} ${sev} ${msg} ${rule}${annotation}`);\n }\n\n console.log();\n }\n}\n\nfunction printDiagnosticsFlat(diagnostics: Diagnostic[]) {\n const cwdBase = process.cwd();\n const cwd = cwdBase + \"/\";\n\n for (const d of diagnostics) {\n const rel = relative(cwdBase, d.file);\n const msg = d.message.replaceAll(cwd, \"\");\n console.log(`${rel}:${d.line}:${d.column} ${d.severity} [${d.ruleId}] ${msg}`);\n }\n}\n\nfunction printSummary(diagnostics: Diagnostic[], fileCount: number) {\n const counts = new Map<string, number>();\n for (const d of diagnostics) {\n const prev = counts.get(d.ruleId);\n counts.set(d.ruleId, (prev === undefined ? 0 : prev) + 1);\n }\n\n const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);\n const maxRule = Math.max(...sorted.map(([r]) => r.length));\n\n console.log(pc.bold(\"Summary\"));\n for (const [rule, count] of sorted) {\n console.log(` ${rule.padEnd(maxRule)} ${pc.bold(String(count))}`);\n }\n\n console.log(\n `\\n${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? \"\" : \"s\"} in ${fileCount} files.`,\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { basename, relative, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport pc from \"picocolors\";\nimport { executeScan, type FailOn, type RulePolicyEntry, type RulePolicySeverity, type Severity } from \"./engine.ts\";\nimport { isFailOn, isRulePolicySeverity, isSeverity, toRulePolicyEntries } from \"./scan/config.ts\";\nimport type { RulePolicy } from \"./scan/types.ts\";\nimport type { Diagnostic } from \"./rules/types.ts\";\n\ninterface UnguardConfig {\n paths?: string[];\n ignore?: string[];\n rules?: RulePolicy;\n failOn?: FailOn;\n}\n\nexport async function main(argv: string[]): Promise<number> {\n const rawArgs = argv.slice(2);\n const userArgs = rawArgs[0] === \"scan\" ? rawArgs.slice(1) : rawArgs;\n\n const { values, positionals: cliPaths } = parseArgs({\n args: userArgs,\n options: {\n strict: { type: \"boolean\", default: false },\n filter: { type: \"string\" },\n format: { type: \"string\", default: \"grouped\" },\n severity: { type: \"string\", multiple: true, default: [] },\n ignore: { type: \"string\", multiple: true, default: [] },\n rule: { type: \"string\", multiple: true, default: [] },\n config: { type: \"string\" },\n \"fail-on\": { type: \"string\" },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n allowPositionals: true,\n });\n\n if (values.help) {\n printHelp();\n return 0;\n }\n\n let config: UnguardConfig | null = null;\n try {\n const configPath = findConfigPath(values.config);\n config = configPath ? loadConfig(configPath) : null;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(pc.red(message));\n return 1;\n }\n\n const rulePolicyResult = parseRulePolicyArgs(values.rule);\n if (!rulePolicyResult.ok) {\n console.error(pc.red(rulePolicyResult.message));\n return 1;\n }\n\n const severityFiltersResult = parseSeverityFilters(values.severity);\n if (!severityFiltersResult.ok) {\n console.error(pc.red(severityFiltersResult.message));\n return 1;\n }\n\n const failOnResult = resolveFailOn(values[\"fail-on\"], config?.failOn);\n if (!failOnResult.ok) {\n console.error(pc.red(failOnResult.message));\n return 1;\n }\n\n const paths = cliPaths.length > 0 ? cliPaths : config?.paths ?? [];\n const ignore = [...(config?.ignore ?? []), ...values.ignore];\n const rulePolicy = [\n ...toRulePolicyEntries(config?.rules),\n ...rulePolicyResult.value,\n ];\n\n const execution = await executeScan({\n paths,\n strict: values.strict,\n rules: values.filter ? [values.filter] : undefined,\n ignore: ignore.length > 0 ? ignore : undefined,\n rulePolicy: rulePolicy.length > 0 ? rulePolicy : undefined,\n showSeverities: severityFiltersResult.value.length > 0 ? severityFiltersResult.value : undefined,\n failOn: failOnResult.value,\n });\n\n const diagnostics = execution.visibleDiagnostics;\n if (diagnostics.length === 0) {\n console.log(pc.green(`No issues found in ${execution.fileCount} files.`));\n return execution.exitCode;\n }\n\n if (values.format === \"flat\") {\n printDiagnosticsFlat(diagnostics);\n } else {\n printDiagnostics(diagnostics);\n }\n printSummary(diagnostics, execution.fileCount);\n\n return execution.exitCode;\n}\n\nfunction printHelp() {\n console.log(`unguard: data-shape static analyzer\n\nUsage:\n unguard scan [paths...] [options]\n unguard [paths...] [options]\n\nOptions:\n --config <path> Path to unguard config file (defaults to ./unguard.config.json)\n --strict Treat all diagnostics as errors\n --filter <rule> Run only the specified rule\n --rule <sel=sev> Override rule severity (supports id, *, category:<name>, tag:<name>)\n --ignore <glob> Ignore path glob. Repeatable.\n --severity <levels> Filter output by severity (comma-separated and/or repeatable)\n --fail-on <level> Exit threshold: none, error, warning, info (default: info)\n --format <mode> Output format: grouped (default) or flat\n -h, --help Show this help\n\nExit codes:\n 0 Clean, or does not meet --fail-on threshold\n 1 Meets threshold without errors\n 2 Meets threshold with at least one error\n\nExamples:\n unguard scan src\n unguard scan src --rule duplicate-*=warning\n unguard scan src --rule category:cross-file=warning\n unguard scan src --rule tag:safety=error\n unguard scan src --severity=error,warning\n unguard scan src --fail-on=error`);\n}\n\nfunction printDiagnostics(diagnostics: Diagnostic[]) {\n const byFile = new Map<string, Diagnostic[]>();\n for (const diagnostic of diagnostics) {\n let list = byFile.get(diagnostic.file);\n if (list === undefined) {\n list = [];\n byFile.set(diagnostic.file, list);\n }\n list.push(diagnostic);\n }\n\n const cwd = `${process.cwd()}/`;\n const cwdBase = process.cwd();\n\n for (const [file, fileDiagnostics] of byFile) {\n const rel = relative(cwdBase, file);\n console.log(pc.underline(rel));\n\n for (const diagnostic of fileDiagnostics) {\n const loc = `${diagnostic.line}:${diagnostic.column}`;\n const sev = diagnostic.severity === \"error\"\n ? pc.red(\"error\")\n : diagnostic.severity === \"warning\"\n ? pc.yellow(\"warning\")\n : pc.blue(\"info\");\n const msg = diagnostic.message.replaceAll(cwd, \"\");\n const rule = pc.dim(diagnostic.ruleId);\n const annotation = diagnostic.annotation !== undefined ? ` ${pc.cyan(diagnostic.annotation)}` : \"\";\n console.log(` ${pc.dim(loc.padEnd(10))} ${sev} ${msg} ${rule}${annotation}`);\n }\n\n console.log();\n }\n}\n\nfunction printDiagnosticsFlat(diagnostics: Diagnostic[]) {\n const cwdBase = process.cwd();\n const cwd = `${cwdBase}/`;\n\n for (const diagnostic of diagnostics) {\n const rel = relative(cwdBase, diagnostic.file);\n const msg = diagnostic.message.replaceAll(cwd, \"\");\n console.log(`${rel}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity} [${diagnostic.ruleId}] ${msg}`);\n }\n}\n\nfunction printSummary(diagnostics: Diagnostic[], fileCount: number) {\n const counts = new Map<string, number>();\n for (const diagnostic of diagnostics) {\n const prev = counts.get(diagnostic.ruleId);\n counts.set(diagnostic.ruleId, (prev === undefined ? 0 : prev) + 1);\n }\n\n const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);\n const maxRule = Math.max(...sorted.map(([rule]) => rule.length));\n\n console.log(pc.bold(\"Summary\"));\n for (const [rule, count] of sorted) {\n console.log(` ${rule.padEnd(maxRule)} ${pc.bold(String(count))}`);\n }\n\n console.log(\n `\\n${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? \"\" : \"s\"} in ${fileCount} files.`,\n );\n}\n\nfunction parseSeverityFilters(values: string[]): { ok: true; value: Severity[] } | { ok: false; message: string } {\n const levels = splitCsv(values);\n const parsed: Severity[] = [];\n for (const value of levels) {\n if (!isSeverity(value)) {\n return { ok: false, message: `Invalid --severity value \"${value}\". Use error, warning, info.` };\n }\n parsed.push(value);\n }\n return { ok: true, value: parsed };\n}\n\nfunction parseRulePolicyArgs(values: string[]): { ok: true; value: RulePolicyEntry[] } | { ok: false; message: string } {\n const entries: RulePolicyEntry[] = [];\n\n for (const arg of values) {\n const separator = resolveRuleSeparator(arg);\n if (separator === null) {\n return { ok: false, message: `Invalid --rule value \"${arg}\". Use <selector>=<off|info|warning|error>.` };\n }\n\n const selector = arg.slice(0, separator).trim();\n const severity = arg.slice(separator + 1).trim();\n if (!selector) {\n return { ok: false, message: `Invalid --rule value \"${arg}\". Rule selector is required.` };\n }\n if (!isRulePolicySeverity(severity)) {\n return { ok: false, message: `Invalid --rule value \"${arg}\". Severity must be off, info, warning, or error.` };\n }\n\n entries.push({ selector, severity });\n }\n\n return { ok: true, value: entries };\n}\n\nfunction resolveRuleSeparator(value: string): number | null {\n const equalIndex = value.indexOf(\"=\");\n if (equalIndex >= 0) return equalIndex;\n const colonIndex = value.lastIndexOf(\":\");\n if (colonIndex <= 0) return null;\n return colonIndex;\n}\n\nfunction splitCsv(values: string[]): string[] {\n return values\n .flatMap((value) => value.split(\",\"))\n .map((value) => value.trim())\n .filter((value) => value.length > 0);\n}\n\nfunction findConfigPath(cliValue: string | undefined): string | null {\n if (cliValue) {\n const explicitPath = resolve(process.cwd(), cliValue);\n if (!existsSync(explicitPath)) throw new Error(`Config file not found: ${cliValue}`);\n return explicitPath;\n }\n\n const candidates = [\"unguard.config.json\", \".unguardrc.json\"];\n for (const candidate of candidates) {\n const candidatePath = resolve(process.cwd(), candidate);\n if (existsSync(candidatePath)) return candidatePath;\n }\n return null;\n}\n\nfunction loadConfig(path: string): UnguardConfig {\n const text = readFileSync(path, \"utf8\");\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(`Invalid JSON in ${basename(path)}: ${message}`);\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(`Invalid config in ${basename(path)}: expected a JSON object.`);\n }\n\n const raw = parsed as Record<string, unknown>;\n const config: UnguardConfig = {};\n\n if (raw.paths !== undefined) {\n if (!isStringArray(raw.paths)) throw new Error(`Invalid config in ${basename(path)}: \"paths\" must be a string array.`);\n config.paths = raw.paths;\n }\n\n if (raw.ignore !== undefined) {\n if (!isStringArray(raw.ignore)) throw new Error(`Invalid config in ${basename(path)}: \"ignore\" must be a string array.`);\n config.ignore = raw.ignore;\n }\n\n if (raw.failOn !== undefined) {\n if (typeof raw.failOn !== \"string\" || !isFailOn(raw.failOn)) {\n throw new Error(`Invalid config in ${basename(path)}: \"failOn\" must be one of none, error, warning, info.`);\n }\n config.failOn = raw.failOn;\n }\n\n if (raw.rules !== undefined) {\n if (typeof raw.rules !== \"object\" || raw.rules === null || Array.isArray(raw.rules)) {\n throw new Error(`Invalid config in ${basename(path)}: \"rules\" must be an object.`);\n }\n\n const rules: Record<string, RulePolicySeverity> = {};\n for (const [selector, severity] of Object.entries(raw.rules as Record<string, unknown>)) {\n if (typeof severity !== \"string\" || !isRulePolicySeverity(severity)) {\n throw new Error(`Invalid config in ${basename(path)}: rule \"${selector}\" must be off, info, warning, or error.`);\n }\n rules[selector] = severity;\n }\n config.rules = rules;\n }\n\n return config;\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((entry) => typeof entry === \"string\");\n}\n\nfunction resolveFailOn(\n cliValue: string | undefined,\n configValue: FailOn | undefined,\n): { ok: true; value: FailOn } | { ok: false; message: string } {\n const selected = cliValue ?? configValue ?? \"info\";\n if (!isFailOn(selected)) {\n return { ok: false, message: `Invalid --fail-on value \"${selected}\". Use none, error, warning, or info.` };\n }\n return { ok: true, value: selected };\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -110,17 +110,42 @@ interface CrossFileRule {
110
110
  }
111
111
  type Rule = CrossFileRule | TSRule;
112
112
 
113
+ type RuleCategory = "type-evasion" | "defensive-code" | "error-handling" | "interface-design" | "cross-file" | "imports";
114
+ interface RuleMetadata {
115
+ category: RuleCategory;
116
+ tags: string[];
117
+ }
118
+ declare const allRules: Rule[];
119
+ declare function getRuleMetadata(ruleId: string): RuleMetadata;
120
+
121
+ type Severity = Diagnostic["severity"];
122
+ type RulePolicySeverity = Severity | "off";
123
+ type FailOn = "none" | Severity;
124
+ interface RulePolicyEntry {
125
+ selector: string;
126
+ severity: RulePolicySeverity;
127
+ }
128
+ type RulePolicy = Record<string, RulePolicySeverity> | RulePolicyEntry[];
113
129
  interface ScanOptions {
114
130
  paths: string[];
115
131
  strict?: boolean;
116
132
  rules?: string[];
133
+ ignore?: string[];
134
+ rulePolicy?: RulePolicy;
135
+ showSeverities?: Severity[];
136
+ failOn?: FailOn;
137
+ useGitIgnore?: boolean;
117
138
  }
118
139
  interface ScanResult {
119
140
  diagnostics: Diagnostic[];
120
141
  fileCount: number;
121
142
  }
122
- declare function scan(options: ScanOptions): Promise<ScanResult>;
143
+ interface ScanExecutionResult extends ScanResult {
144
+ visibleDiagnostics: Diagnostic[];
145
+ exitCode: number;
146
+ }
123
147
 
124
- declare const allRules: Rule[];
148
+ declare function executeScan(options: ScanOptions): Promise<ScanExecutionResult>;
149
+ declare function scan(options: ScanOptions): Promise<ScanResult>;
125
150
 
126
- export { type CrossFileRule, type Diagnostic, type Rule, type ScanOptions, type ScanResult, type TSRule, type TSVisitContext, allRules, scan };
151
+ export { type CrossFileRule, type Diagnostic, type Rule, type RuleCategory, type RuleMetadata, type ScanExecutionResult, type ScanOptions, type ScanResult, type TSRule, type TSVisitContext, allRules, executeScan, getRuleMetadata, scan };
package/dist/index.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import {
2
2
  allRules,
3
+ executeScan,
4
+ getRuleMetadata,
3
5
  scan
4
- } from "./chunk-YYX4R25B.js";
6
+ } from "./chunk-I4RZLVE2.js";
5
7
  export {
6
8
  allRules,
9
+ executeScan,
10
+ getRuleMetadata,
7
11
  scan
8
12
  };
9
13
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unguard",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Detect overdefensive AI-generated code",
5
5
  "author": "Seva Maltsev <me@seva.dev> (https://seva.dev)",
6
6
  "license": "MIT",
@@ -36,10 +36,12 @@
36
36
  "scripts": {
37
37
  "build": "tsup",
38
38
  "typecheck": "tsc",
39
+ "lint:biome": "biome lint src tests/**/*.test.ts tests/harness.ts",
40
+ "lint": "npm run lint:biome",
39
41
  "test": "vitest run",
40
42
  "test:watch": "vitest",
41
- "scan": "node ./bin/unguard.mjs scan .",
42
- "scan:strict": "node ./bin/unguard.mjs scan . --strict",
43
+ "scan": "node ./bin/unguard.mjs scan src \"tests/**/*.test.ts\" tests/harness.ts --fail-on=error",
44
+ "scan:strict": "node ./bin/unguard.mjs scan src \"tests/**/*.test.ts\" tests/harness.ts --strict --fail-on=error",
43
45
  "prepublishOnly": "npm run build && npm run test"
44
46
  },
45
47
  "engines": {
@@ -47,10 +49,12 @@
47
49
  },
48
50
  "dependencies": {
49
51
  "fast-glob": "^3.3.3",
52
+ "ignore": "^7.0.5",
50
53
  "picocolors": "^1.1.1",
51
54
  "typescript": "^5.9.3"
52
55
  },
53
56
  "devDependencies": {
57
+ "@biomejs/biome": "^1.9.4",
54
58
  "@types/node": "^25.3.0",
55
59
  "tsup": "^8.5.1",
56
60
  "vitest": "^4.0.18"