semlint-cli 0.1.5 → 0.1.7

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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "SEMLINT_NAMING_001",
3
+ "title": "Ambient naming convention consistency",
4
+ "severity_default": "warn",
5
+ "include_globs": ["src/**/*.ts"],
6
+ "exclude_globs": ["**/*.test.ts", "**/*.spec.ts"],
7
+ "diff_regex": [
8
+ "^[+-].*\\b(const|let|var|function|class|interface|type|enum)\\b",
9
+ "^[+-].*\\b[A-Za-z_][A-Za-z0-9_]*\\b"
10
+ ],
11
+ "prompt": "Verify naming is consistent with the ambient naming conventions already used in surrounding code. Both in term of semantic and casing."
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "SEMLINT_PATTERN_002",
3
+ "title": "Ambient pattern is respected",
4
+ "severity_default": "warn",
5
+ "include_globs": ["src/**/*.ts"],
6
+ "exclude_globs": ["**/*.test.ts", "**/*.spec.ts"],
7
+ "diff_regex": [
8
+ "^[+-].*\\b(async|await|Promise|try|catch|throw|switch|map|filter|reduce|forEach)\\b",
9
+ "^[+-].*\\b(import|export|class|interface|type|function|return)\\b"
10
+ ],
11
+ "prompt": "Check whether ambient implementation patterns are respected. Compare new or changed code against nearby established patterns. Flag clear regressions where the proposed change deviates from consistent local patterns without obvious justification."
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "SEMLINT_SWE_003",
3
+ "title": "Obvious SWE mistakes",
4
+ "severity_default": "warn",
5
+ "include_globs": ["src/**/*.ts"],
6
+ "exclude_globs": ["**/*.test.ts", "**/*.spec.ts"],
7
+ "diff_regex": [
8
+ "^[+-].*\\b(any|as\\s+any|TODO|FIXME|console\\.log|@ts-ignore|throw\\s+new\\s+Error|catch\\s*\\()\\b",
9
+ "^[+-].*\\b(if|else|switch|return|await|Promise|map|forEach|reduce)\\b"
10
+ ],
11
+ "prompt": "Find obvious software-engineering mistakes in the proposed change that are likely unintended. Do not nitpick style or architecture unless the issue is clearly harmful. Report only high-signal findings."
12
+ }
package/README.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Semlint CLI MVP
2
2
 
3
+ ## Motivation
4
+
5
+ Upstream instruction files (`AGENTS.md`, `CURSOR.md`, etc.) are the standard way to guide coding agents — but a [recent study (Gloaguen et al., 2026)](https://arxiv.org/abs/2602.11988) found that such context files tend to *reduce* task success rates compared to providing no context at all, while increasing inference cost by over 20%. The root cause: agents respect the instructions, but unnecessary or over-specified requirements make tasks harder, with no feedback mechanism to catch when rules are ignored or misapplied.
6
+
7
+ Semlint takes a different approach. Instead of providing guidance upfront and hoping for the best, rules are enforced *after the fact* as a lint pass on the diff — giving agents a deterministic red/green signal and closing the feedback loop.
8
+
9
+ ---
10
+
3
11
  Semlint is a deterministic semantic lint CLI that:
4
12
 
5
13
  - reads a git diff,
@@ -75,7 +83,6 @@ Unknown fields are ignored.
75
83
  ```json
76
84
  {
77
85
  "backend": "cursor-cli",
78
- "model": "auto",
79
86
  "budgets": {
80
87
  "timeout_ms": 120000
81
88
  },
@@ -85,15 +92,23 @@ Unknown fields are ignored.
85
92
  "execution": {
86
93
  "batch": false
87
94
  },
95
+ "security": {
96
+ "secret_guard": true,
97
+ "allow_patterns": [],
98
+ "allow_files": [],
99
+ "ignore_files": [".gitignore", ".cursorignore", ".semlintignore", ".cursoringore"]
100
+ },
88
101
  "rules": {
89
- "disable": ["SEMLINT_EXAMPLE_001"],
102
+ "disable": [],
90
103
  "severity_overrides": {
91
104
  "SEMLINT_API_001": "error"
92
105
  }
93
106
  },
94
107
  "backends": {
95
108
  "cursor-cli": {
96
- "executable": "agent"
109
+ "executable": "cursor",
110
+ "model": "auto",
111
+ "args": ["agent", "{prompt}", "--model", "{model}", "--print", "--mode", "ask", "--output-format", "text"]
97
112
  }
98
113
  }
99
114
  }
@@ -115,11 +130,11 @@ This creates `./semlint.json` and auto-detects installed coding agent CLIs in th
115
130
 
116
131
  If no known CLI is detected, Semlint falls back to `cursor-cli` + executable `cursor`.
117
132
 
118
- Use `semlint init --force` to overwrite an existing config file. Init also creates `.semlint/rules/` and a starter rule `SEMLINT_EXAMPLE_001.json` (with a placeholder title and prompt) if they do not exist.
133
+ Use `semlint init --force` to overwrite an existing config file. Init also creates `.semlint/rules/` and copies the bundled Semlint rule files into it.
119
134
 
120
135
  ## Rule files
121
136
 
122
- Rule JSON files are loaded from `.semlint/rules/`. Run `semlint init` to create this folder and an example rule you can edit.
137
+ Rule JSON files are loaded from `.semlint/rules/`. Run `semlint init` to create this folder and copy bundled rules into it.
123
138
 
124
139
  Required fields:
125
140
 
@@ -138,16 +153,32 @@ Invalid rules cause runtime failure with exit code `2`.
138
153
 
139
154
  ## Backend contract
140
155
 
141
- For backend `cursor-cli`, Semlint executes:
156
+ Semlint is fully config-driven at runtime. For the selected `backend`, it executes:
142
157
 
143
- ```bash
144
- cursor agent "<prompt>" --model <model> --print --output-format text
145
- ```
158
+ - `backends.<backend>.executable` as the binary
159
+ - `backends.<backend>.model` as the backend-specific model (unless `--model` is passed)
160
+ - `backends.<backend>.args` as the argument template
161
+
162
+ `args` supports placeholder tokens:
163
+
164
+ - `{prompt}`: replaced with the generated prompt
165
+ - `{model}`: replaced with the configured model
146
166
 
147
- For `cursor-cli`, Semlint always uses `cursor agent` directly.
148
- Other backend names still resolve executables from config:
167
+ Placeholders are exact-match substitutions on whole args. Backends must be fully configured in `semlint.json`; there are no runtime fallbacks.
149
168
 
150
- - `backends.<backend>.executable` if provided
169
+ Example:
170
+
171
+ ```json
172
+ {
173
+ "backends": {
174
+ "cursor-cli": {
175
+ "executable": "cursor",
176
+ "model": "auto",
177
+ "args": ["agent", "{prompt}", "--model", "{model}", "--print", "--mode", "ask", "--output-format", "text"]
178
+ }
179
+ }
180
+ }
181
+ ```
151
182
 
152
183
  Backend stdout must be valid JSON with shape:
153
184
 
@@ -168,6 +199,44 @@ Backend stdout must be valid JSON with shape:
168
199
  If parsing fails, Semlint retries once with appended instruction:
169
200
  `Return valid JSON only.`
170
201
 
202
+ If backend execution still fails and Semlint is running in an interactive terminal (TTY), it automatically performs one interactive passthrough run so you can satisfy backend setup prompts (for example auth/workspace trust), then retries machine parsing once.
203
+
204
+ ## Secret guard
205
+
206
+ Semlint uses a fail-closed secret guard before any backend call:
207
+
208
+ - Filters diff chunks using path ignore rules from `.gitignore`, `.cursorignore`, `.semlintignore`
209
+ - Applies additional built-in sensitive path deny patterns (`.env*`, key files, secrets/credentials folders)
210
+ - Scans added diff lines for high-signal secret keywords and token prefixes (password/token/api key/private key/JWT/provider key prefixes)
211
+ - If any potential secrets are found, Semlint exits with code `2` and sends nothing to the backend
212
+
213
+ Config:
214
+
215
+ ```json
216
+ {
217
+ "security": {
218
+ "secret_guard": true,
219
+ "allow_patterns": [],
220
+ "allow_files": [],
221
+ "ignore_files": [".gitignore", ".cursorignore", ".semlintignore", ".cursoringore"]
222
+ }
223
+ }
224
+ ```
225
+
226
+ - `secret_guard`: enable/disable secret blocking (default `true`)
227
+ - `allow_patterns`: regex list to suppress known-safe fixtures from triggering the guard
228
+ - `allow_files`: file glob allowlist to skip secret scanning for known-safe files (example: `["src/test-fixtures/**"]`)
229
+ - `ignore_files`: ignore files Semlint reads for path-level filtering (default: `.gitignore`, `.cursorignore`, `.semlintignore`, `.cursoringore`)
230
+
231
+ ## Prompt files
232
+
233
+ Core system prompts are externalized under `prompts/` so prompt behavior is easy to inspect and iterate:
234
+
235
+ - `prompts/common-contract.md`: shared output schema and base rules used by both modes
236
+ - `prompts/rule.md`: single-rule evaluation prompt
237
+ - `prompts/batch.md`: batch evaluation prompt
238
+ - `prompts/retry-json.md`: strict JSON retry instruction
239
+
171
240
  ## Batch mode
172
241
 
173
242
  Use batch mode to reduce cost by evaluating all runnable rules in a single backend call:
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BUILTIN_ADAPTERS = void 0;
4
+ exports.BUILTIN_ADAPTERS = {
5
+ "cursor-cli": {
6
+ defaultExecutable: "cursor",
7
+ buildArgs: (prompt, model) => [
8
+ "agent",
9
+ prompt,
10
+ "--model",
11
+ model,
12
+ "--print",
13
+ "--mode",
14
+ "ask",
15
+ "--output-format",
16
+ "text"
17
+ ]
18
+ },
19
+ "claude-code": {
20
+ defaultExecutable: "claude",
21
+ buildArgs: (prompt, model) => [prompt, "--model", model, "--output-format", "json"]
22
+ },
23
+ "codex-cli": {
24
+ defaultExecutable: "codex",
25
+ buildArgs: (prompt, model) => [prompt, "--model", model]
26
+ }
27
+ };
package/dist/backend.js CHANGED
@@ -2,34 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createBackendRunner = createBackendRunner;
4
4
  const node_child_process_1 = require("node:child_process");
5
- const STRICT_JSON_RETRY_INSTRUCTION = [
6
- "Return valid JSON only.",
7
- "Do not include markdown fences.",
8
- "Do not include commentary, headings, or any text before/after JSON.",
9
- "The first character of your response must be '{' and the last must be '}'.",
10
- 'Output must match: {"diagnostics":[{"rule_id":"<id>","severity":"error|warn|info","message":"<text>","file":"<path>","line":1}]}'
11
- ].join(" ");
5
+ const prompts_1 = require("./prompts");
6
+ const utils_1 = require("./utils");
7
+ const STRICT_JSON_RETRY_INSTRUCTION = (0, prompts_1.getStrictJsonRetryInstruction)();
12
8
  function isEnoentError(error) {
13
9
  return (typeof error === "object" &&
14
10
  error !== null &&
15
11
  "code" in error &&
16
12
  error.code === "ENOENT");
17
13
  }
18
- function debugLog(enabled, message) {
19
- if (enabled) {
20
- process.stderr.write(`[debug] ${message}\n`);
21
- }
14
+ function logBackendCommand(label, executable, args, prompt) {
15
+ const printableParts = [executable, ...args.map((arg) => (arg === prompt ? "<prompt-redacted>" : arg))];
16
+ process.stderr.write(`[semlint] ${label}: ${printableParts.join(" ")}\n`);
22
17
  }
23
18
  function resolveCommandSpecs(config) {
24
- if (config.backend === "cursor-cli") {
25
- // Always use `cursor agent` directly for cursor-cli.
26
- return [{ executable: "cursor", argsPrefix: ["agent"] }];
27
- }
28
- const configuredExecutable = config.backendExecutables[config.backend];
29
- if (!configuredExecutable) {
30
- throw new Error(`No executable configured for backend "${config.backend}". Configure it under backends.${config.backend}.executable`);
19
+ const configuredBackend = config.backendConfigs[config.backend];
20
+ if (!configuredBackend) {
21
+ throw new Error(`Backend "${config.backend}" is not configured. Add it under backends.${config.backend} in semlint.json (run "semlint init" to scaffold a complete config).`);
31
22
  }
32
- return [{ executable: configuredExecutable, argsPrefix: [] }];
23
+ return [
24
+ {
25
+ executable: configuredBackend.executable,
26
+ args: configuredBackend.args
27
+ }
28
+ ];
29
+ }
30
+ function interpolateArgs(args, prompt, model) {
31
+ return args.map((arg) => {
32
+ if (arg === "{prompt}")
33
+ return prompt;
34
+ if (arg === "{model}")
35
+ return model;
36
+ return arg;
37
+ });
33
38
  }
34
39
  function executeBackendCommand(executable, args, timeoutMs) {
35
40
  return new Promise((resolve, reject) => {
@@ -68,6 +73,26 @@ function executeBackendCommand(executable, args, timeoutMs) {
68
73
  });
69
74
  });
70
75
  }
76
+ function executeBackendCommandInteractive(executable, args) {
77
+ return new Promise((resolve, reject) => {
78
+ const child = (0, node_child_process_1.spawn)(executable, args, {
79
+ stdio: "inherit"
80
+ });
81
+ child.on("error", (error) => {
82
+ reject(error);
83
+ });
84
+ child.on("close", (code) => {
85
+ if (code !== 0) {
86
+ reject(new Error(`Interactive backend command failed with code ${code}`));
87
+ return;
88
+ }
89
+ resolve();
90
+ });
91
+ });
92
+ }
93
+ function canUseInteractiveRecovery() {
94
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY);
95
+ }
71
96
  function extractFirstJsonObject(raw) {
72
97
  let start = -1;
73
98
  let depth = 0;
@@ -137,62 +162,73 @@ function parseBackendResult(raw) {
137
162
  }
138
163
  function createBackendRunner(config) {
139
164
  const commandSpecs = resolveCommandSpecs(config);
165
+ async function runWithRetry(spec, input) {
166
+ const commandName = spec.executable;
167
+ const firstPrompt = input.prompt;
168
+ const firstArgs = interpolateArgs(spec.args, firstPrompt, config.model);
169
+ let interactiveRecoveryAttempted = false;
170
+ try {
171
+ if (config.debug) {
172
+ logBackendCommand(`${input.label} attempt 1`, spec.executable, firstArgs, firstPrompt);
173
+ }
174
+ (0, utils_1.debugLog)(config.debug, `${input.label}: backend attempt 1 via "${commandName}" (timeout ${input.timeoutMs}ms)`);
175
+ const first = await executeBackendCommand(spec.executable, firstArgs, input.timeoutMs);
176
+ (0, utils_1.debugLog)(config.debug, `${input.label}: backend attempt 1 completed in ${first.elapsedMs}ms`);
177
+ return parseBackendResult(first.stdout.trim());
178
+ }
179
+ catch (firstError) {
180
+ (0, utils_1.debugLog)(config.debug, `${input.label}: backend attempt 1 failed (${firstError instanceof Error ? firstError.message : String(firstError)})`);
181
+ const retryPrompt = `${input.prompt}\n\n${STRICT_JSON_RETRY_INSTRUCTION}`;
182
+ const retryArgs = interpolateArgs(spec.args, retryPrompt, config.model);
183
+ try {
184
+ if (config.debug) {
185
+ logBackendCommand(`${input.label} attempt 2`, spec.executable, retryArgs, retryPrompt);
186
+ }
187
+ (0, utils_1.debugLog)(config.debug, `${input.label}: backend attempt 2 via "${commandName}" (timeout ${input.timeoutMs}ms)`);
188
+ const second = await executeBackendCommand(spec.executable, retryArgs, input.timeoutMs);
189
+ (0, utils_1.debugLog)(config.debug, `${input.label}: backend attempt 2 completed in ${second.elapsedMs}ms`);
190
+ return parseBackendResult(second.stdout.trim());
191
+ }
192
+ catch (secondError) {
193
+ (0, utils_1.debugLog)(config.debug, `${input.label}: backend attempt 2 failed (${secondError instanceof Error ? secondError.message : String(secondError)})`);
194
+ if (!interactiveRecoveryAttempted &&
195
+ !isEnoentError(firstError) &&
196
+ !isEnoentError(secondError) &&
197
+ canUseInteractiveRecovery()) {
198
+ interactiveRecoveryAttempted = true;
199
+ process.stderr.write("[semlint] Backend requires interactive setup. Switching to interactive passthrough once...\n");
200
+ await executeBackendCommandInteractive(spec.executable, firstArgs);
201
+ (0, utils_1.debugLog)(config.debug, `${input.label}: interactive setup completed; retrying backend in machine mode`);
202
+ const recovered = await executeBackendCommand(spec.executable, firstArgs, input.timeoutMs);
203
+ return parseBackendResult(recovered.stdout.trim());
204
+ }
205
+ if (isEnoentError(secondError) || isEnoentError(firstError)) {
206
+ return null;
207
+ }
208
+ throw firstError;
209
+ }
210
+ }
211
+ }
140
212
  return {
141
213
  async runPrompt(input) {
142
214
  let lastError;
143
215
  for (const spec of commandSpecs) {
144
- const commandName = [spec.executable, ...spec.argsPrefix].join(" ");
145
- const baseArgs = [
146
- ...spec.argsPrefix,
147
- input.prompt,
148
- "--model",
149
- config.model,
150
- "--print",
151
- "--mode",
152
- "ask",
153
- "--output-format",
154
- "text"
155
- ];
156
216
  try {
157
- debugLog(config.debug, `${input.label}: backend attempt 1 via "${commandName}" (timeout ${input.timeoutMs}ms)`);
158
- const first = await executeBackendCommand(spec.executable, baseArgs, input.timeoutMs);
159
- debugLog(config.debug, `${input.label}: backend attempt 1 completed in ${first.elapsedMs}ms`);
160
- return parseBackendResult(first.stdout.trim());
161
- }
162
- catch (firstError) {
163
- debugLog(config.debug, `${input.label}: backend attempt 1 failed (${firstError instanceof Error ? firstError.message : String(firstError)})`);
164
- const retryPrompt = `${input.prompt}\n\n${STRICT_JSON_RETRY_INSTRUCTION}`;
165
- const retryArgs = [
166
- ...spec.argsPrefix,
167
- retryPrompt,
168
- "--model",
169
- config.model,
170
- "--print",
171
- "--mode",
172
- "ask",
173
- "--output-format",
174
- "text"
175
- ];
176
- try {
177
- debugLog(config.debug, `${input.label}: backend attempt 2 via "${commandName}" (timeout ${input.timeoutMs}ms)`);
178
- const second = await executeBackendCommand(spec.executable, retryArgs, input.timeoutMs);
179
- debugLog(config.debug, `${input.label}: backend attempt 2 completed in ${second.elapsedMs}ms`);
180
- return parseBackendResult(second.stdout.trim());
181
- }
182
- catch (secondError) {
183
- debugLog(config.debug, `${input.label}: backend attempt 2 failed (${secondError instanceof Error ? secondError.message : String(secondError)})`);
184
- lastError = secondError;
185
- if (isEnoentError(secondError)) {
186
- continue;
187
- }
188
- if (isEnoentError(firstError)) {
189
- continue;
190
- }
191
- throw firstError;
217
+ const result = await runWithRetry(spec, input);
218
+ if (result) {
219
+ return result;
192
220
  }
221
+ continue;
193
222
  }
223
+ catch (error) {
224
+ lastError = error;
225
+ throw error;
226
+ }
227
+ }
228
+ if (lastError) {
229
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
194
230
  }
195
- throw lastError instanceof Error ? lastError : new Error(String(lastError));
231
+ throw new Error(`No backend command could be executed for "${config.backend}"`);
196
232
  },
197
233
  async runRule(input) {
198
234
  return this.runPrompt({
package/dist/config.js CHANGED
@@ -7,7 +7,7 @@ exports.resolveConfigPath = resolveConfigPath;
7
7
  exports.loadEffectiveConfig = loadEffectiveConfig;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
- const VALID_SEVERITIES = new Set(["error", "warn", "info"]);
10
+ const utils_1 = require("./utils");
11
11
  const DEFAULTS = {
12
12
  backend: "cursor-cli",
13
13
  model: "auto",
@@ -20,8 +20,12 @@ const DEFAULTS = {
20
20
  batchMode: false,
21
21
  rulesDisable: [],
22
22
  severityOverrides: {},
23
- backendExecutables: {
24
- "cursor-cli": "cursor"
23
+ backendConfigs: {},
24
+ security: {
25
+ secretGuard: true,
26
+ allowPatterns: [],
27
+ ignoreFiles: [".gitignore", ".cursorignore", ".semlintignore"],
28
+ allowFiles: []
25
29
  }
26
30
  };
27
31
  function readJsonIfExists(filePath) {
@@ -54,39 +58,96 @@ function sanitizeSeverityOverrides(value) {
54
58
  if (!value) {
55
59
  return {};
56
60
  }
57
- const out = {};
58
- for (const [ruleId, severity] of Object.entries(value)) {
59
- if (typeof severity === "string" && VALID_SEVERITIES.has(severity)) {
60
- out[ruleId] = severity;
61
+ return Object.fromEntries(Object.entries(value).flatMap(([ruleId, severity]) => typeof severity === "string" && utils_1.VALID_SEVERITIES.has(severity)
62
+ ? [[ruleId, severity]]
63
+ : []));
64
+ }
65
+ function sanitizeBackendConfigs(value) {
66
+ if (!value) {
67
+ return {};
68
+ }
69
+ return Object.fromEntries(Object.entries(value).flatMap(([name, candidate]) => {
70
+ if (typeof candidate !== "object" || candidate === null) {
71
+ return [];
72
+ }
73
+ const executable = "executable" in candidate && typeof candidate.executable === "string"
74
+ ? candidate.executable.trim()
75
+ : "";
76
+ const args = "args" in candidate ? candidate.args : undefined;
77
+ const model = "model" in candidate && typeof candidate.model === "string"
78
+ ? candidate.model.trim()
79
+ : undefined;
80
+ if (!executable || !Array.isArray(args) || args.some((arg) => typeof arg !== "string")) {
81
+ return [];
82
+ }
83
+ const normalizedArgs = args;
84
+ if (!normalizedArgs.includes("{prompt}")) {
85
+ return [];
86
+ }
87
+ return [[name, { executable, args: normalizedArgs, model: model && model !== "" ? model : undefined }]];
88
+ }));
89
+ }
90
+ function sanitizeAllowPatterns(value) {
91
+ if (!Array.isArray(value)) {
92
+ return [];
93
+ }
94
+ return value.flatMap((candidate) => {
95
+ if (typeof candidate !== "string" || candidate.trim() === "") {
96
+ return [];
61
97
  }
98
+ try {
99
+ new RegExp(candidate);
100
+ return [candidate];
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ });
106
+ }
107
+ function sanitizeIgnoreFiles(value) {
108
+ if (!Array.isArray(value)) {
109
+ return [...DEFAULTS.security.ignoreFiles];
62
110
  }
63
- return out;
111
+ const normalized = value.flatMap((candidate) => {
112
+ if (typeof candidate !== "string") {
113
+ return [];
114
+ }
115
+ const trimmed = candidate.trim();
116
+ if (trimmed === "") {
117
+ return [];
118
+ }
119
+ return [trimmed];
120
+ });
121
+ return normalized.length > 0 ? normalized : [...DEFAULTS.security.ignoreFiles];
64
122
  }
65
- function sanitizeBackendExecutables(value) {
66
- const out = {
67
- ...DEFAULTS.backendExecutables
68
- };
69
- if (!value) {
70
- return out;
123
+ function sanitizeAllowFiles(value) {
124
+ if (!Array.isArray(value)) {
125
+ return [];
71
126
  }
72
- for (const [name, candidate] of Object.entries(value)) {
73
- if (typeof candidate === "object" &&
74
- candidate !== null &&
75
- "executable" in candidate &&
76
- typeof candidate.executable === "string" &&
77
- candidate.executable.trim() !== "") {
78
- out[name] = candidate.executable.trim();
127
+ return value.flatMap((candidate) => {
128
+ if (typeof candidate !== "string") {
129
+ return [];
79
130
  }
131
+ const trimmed = candidate.trim();
132
+ return trimmed === "" ? [] : [trimmed];
133
+ });
134
+ }
135
+ function ensureSelectedBackendIsConfigured(backend, backendConfigs) {
136
+ if (!(backend in backendConfigs)) {
137
+ throw new Error(`Backend "${backend}" is not configured. Add it under backends.${backend} with executable and args (including "{prompt}") in semlint.json.`);
80
138
  }
81
- return out;
82
139
  }
83
140
  function loadEffectiveConfig(options) {
84
141
  const configPath = resolveConfigPath(options.configPath);
85
142
  const parsed = configPath ? readJsonIfExists(configPath) : undefined;
86
143
  const fileConfig = (parsed ?? {});
144
+ const backend = options.backend ?? fileConfig.backend ?? DEFAULTS.backend;
145
+ const backendConfigs = sanitizeBackendConfigs((fileConfig.backends ?? undefined));
146
+ ensureSelectedBackendIsConfigured(backend, backendConfigs);
147
+ const backendModel = backendConfigs[backend]?.model;
87
148
  return {
88
- backend: options.backend ?? fileConfig.backend ?? DEFAULTS.backend,
89
- model: options.model ?? fileConfig.model ?? DEFAULTS.model,
149
+ backend,
150
+ model: options.model ?? backendModel ?? DEFAULTS.model,
90
151
  timeoutMs: typeof fileConfig.budgets?.timeout_ms === "number"
91
152
  ? fileConfig.budgets.timeout_ms
92
153
  : DEFAULTS.timeoutMs,
@@ -103,6 +164,14 @@ function loadEffectiveConfig(options) {
103
164
  ? fileConfig.rules?.disable.filter((item) => typeof item === "string")
104
165
  : DEFAULTS.rulesDisable,
105
166
  severityOverrides: sanitizeSeverityOverrides((fileConfig.rules?.severity_overrides ?? undefined)),
106
- backendExecutables: sanitizeBackendExecutables((fileConfig.backends ?? undefined))
167
+ backendConfigs,
168
+ security: {
169
+ secretGuard: typeof fileConfig.security?.secret_guard === "boolean"
170
+ ? fileConfig.security.secret_guard
171
+ : DEFAULTS.security.secretGuard,
172
+ allowPatterns: sanitizeAllowPatterns(fileConfig.security?.allow_patterns),
173
+ ignoreFiles: sanitizeIgnoreFiles(fileConfig.security?.ignore_files),
174
+ allowFiles: sanitizeAllowFiles(fileConfig.security?.allow_files)
175
+ }
107
176
  };
108
177
  }
@@ -8,23 +8,19 @@ exports.sortDiagnostics = sortDiagnostics;
8
8
  exports.hasBlockingDiagnostic = hasBlockingDiagnostic;
9
9
  const node_fs_1 = __importDefault(require("node:fs"));
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
- const VALID_SEVERITIES = new Set(["error", "warn", "info"]);
12
- function isPositiveInteger(value) {
13
- return typeof value === "number" && Number.isInteger(value) && value >= 1;
14
- }
11
+ const utils_1 = require("./utils");
15
12
  /**
16
13
  * @param resolveRoot - Directory to resolve diagnostic file paths against (e.g. git repo root).
17
14
  * Git diff paths are repo-relative; resolving from repo root ensures paths exist when running from subdirs.
18
15
  */
19
16
  function normalizeDiagnostics(ruleId, diagnostics, debug, resolveRoot) {
20
- const out = [];
21
17
  const baseDir = resolveRoot && resolveRoot.length > 0 ? resolveRoot : process.cwd();
22
- for (const raw of diagnostics) {
18
+ return diagnostics.flatMap((raw) => {
23
19
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
24
20
  if (debug) {
25
21
  process.stderr.write(`[debug] Dropped diagnostic for ${ruleId}: not an object\n`);
26
22
  }
27
- continue;
23
+ return [];
28
24
  }
29
25
  const candidate = raw;
30
26
  const severity = candidate.severity;
@@ -39,34 +35,34 @@ function normalizeDiagnostics(ruleId, diagnostics, debug, resolveRoot) {
39
35
  typeof message !== "string" ||
40
36
  message.trim() === "" ||
41
37
  typeof severity !== "string" ||
42
- !VALID_SEVERITIES.has(severity) ||
43
- !isPositiveInteger(line)) {
38
+ !utils_1.VALID_SEVERITIES.has(severity) ||
39
+ !(0, utils_1.isPositiveInteger)(line)) {
44
40
  if (debug) {
45
41
  process.stderr.write(`[debug] Dropped diagnostic for ${ruleId}: failed required field validation\n`);
46
42
  }
47
- continue;
43
+ return [];
48
44
  }
49
- const absolute = node_path_1.default.resolve(baseDir, file);
50
- if (!node_fs_1.default.existsSync(absolute)) {
45
+ if (!node_fs_1.default.existsSync(node_path_1.default.resolve(baseDir, file))) {
51
46
  if (debug) {
52
47
  process.stderr.write(`[debug] Dropped diagnostic for ${ruleId}: file does not exist (${file})\n`);
53
48
  }
54
- continue;
49
+ return [];
55
50
  }
56
- out.push({
57
- rule_id: candidateRuleId,
58
- severity: severity,
59
- message,
60
- file,
61
- line,
62
- column: isPositiveInteger(candidate.column) ? candidate.column : undefined,
63
- end_line: isPositiveInteger(candidate.end_line) ? candidate.end_line : undefined,
64
- end_column: isPositiveInteger(candidate.end_column) ? candidate.end_column : undefined,
65
- evidence: typeof candidate.evidence === "string" ? candidate.evidence : undefined,
66
- confidence: typeof candidate.confidence === "number" ? candidate.confidence : undefined
67
- });
68
- }
69
- return out;
51
+ return [
52
+ {
53
+ rule_id: candidateRuleId,
54
+ severity: severity,
55
+ message,
56
+ file,
57
+ line,
58
+ column: (0, utils_1.isPositiveInteger)(candidate.column) ? candidate.column : undefined,
59
+ end_line: (0, utils_1.isPositiveInteger)(candidate.end_line) ? candidate.end_line : undefined,
60
+ end_column: (0, utils_1.isPositiveInteger)(candidate.end_column) ? candidate.end_column : undefined,
61
+ evidence: typeof candidate.evidence === "string" ? candidate.evidence : undefined,
62
+ confidence: typeof candidate.confidence === "number" ? candidate.confidence : undefined
63
+ }
64
+ ];
65
+ });
70
66
  }
71
67
  const SEVERITY_ORDER = {
72
68
  error: 3,