semlint-cli 0.1.5 → 0.1.6
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 +37 -9
- package/dist/adapters.js +27 -0
- package/dist/backend.js +103 -67
- package/dist/config.js +37 -27
- package/dist/diagnostics.js +23 -27
- package/dist/dispatch.js +94 -0
- package/dist/filter.js +13 -43
- package/dist/init.js +31 -4
- package/dist/main.js +35 -141
- package/dist/prompts.js +46 -0
- package/dist/reporter.js +12 -13
- package/dist/rules.js +11 -13
- package/dist/utils.js +18 -0
- package/package.json +2 -1
- package/prompts/batch.md +16 -0
- package/prompts/common-contract.md +20 -0
- package/prompts/retry-json.md +5 -0
- package/prompts/rule.md +16 -0
package/README.md
CHANGED
|
@@ -75,7 +75,6 @@ Unknown fields are ignored.
|
|
|
75
75
|
```json
|
|
76
76
|
{
|
|
77
77
|
"backend": "cursor-cli",
|
|
78
|
-
"model": "auto",
|
|
79
78
|
"budgets": {
|
|
80
79
|
"timeout_ms": 120000
|
|
81
80
|
},
|
|
@@ -93,7 +92,9 @@ Unknown fields are ignored.
|
|
|
93
92
|
},
|
|
94
93
|
"backends": {
|
|
95
94
|
"cursor-cli": {
|
|
96
|
-
"executable": "
|
|
95
|
+
"executable": "cursor",
|
|
96
|
+
"model": "auto",
|
|
97
|
+
"args": ["agent", "{prompt}", "--model", "{model}", "--print", "--mode", "ask", "--output-format", "text"]
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
}
|
|
@@ -138,16 +139,32 @@ Invalid rules cause runtime failure with exit code `2`.
|
|
|
138
139
|
|
|
139
140
|
## Backend contract
|
|
140
141
|
|
|
141
|
-
For
|
|
142
|
+
Semlint is fully config-driven at runtime. For the selected `backend`, it executes:
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
- `backends.<backend>.executable` as the binary
|
|
145
|
+
- `backends.<backend>.model` as the backend-specific model (unless `--model` is passed)
|
|
146
|
+
- `backends.<backend>.args` as the argument template
|
|
147
|
+
|
|
148
|
+
`args` supports placeholder tokens:
|
|
146
149
|
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
- `{prompt}`: replaced with the generated prompt
|
|
151
|
+
- `{model}`: replaced with the configured model
|
|
149
152
|
|
|
150
|
-
- `
|
|
153
|
+
Placeholders are exact-match substitutions on whole args. Backends must be fully configured in `semlint.json`; there are no runtime fallbacks.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"backends": {
|
|
160
|
+
"cursor-cli": {
|
|
161
|
+
"executable": "cursor",
|
|
162
|
+
"model": "auto",
|
|
163
|
+
"args": ["agent", "{prompt}", "--model", "{model}", "--print", "--mode", "ask", "--output-format", "text"]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
151
168
|
|
|
152
169
|
Backend stdout must be valid JSON with shape:
|
|
153
170
|
|
|
@@ -168,6 +185,17 @@ Backend stdout must be valid JSON with shape:
|
|
|
168
185
|
If parsing fails, Semlint retries once with appended instruction:
|
|
169
186
|
`Return valid JSON only.`
|
|
170
187
|
|
|
188
|
+
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.
|
|
189
|
+
|
|
190
|
+
## Prompt files
|
|
191
|
+
|
|
192
|
+
Core system prompts are externalized under `prompts/` so prompt behavior is easy to inspect and iterate:
|
|
193
|
+
|
|
194
|
+
- `prompts/common-contract.md`: shared output schema and base rules used by both modes
|
|
195
|
+
- `prompts/rule.md`: single-rule evaluation prompt
|
|
196
|
+
- `prompts/batch.md`: batch evaluation prompt
|
|
197
|
+
- `prompts/retry-json.md`: strict JSON retry instruction
|
|
198
|
+
|
|
171
199
|
## Batch mode
|
|
172
200
|
|
|
173
201
|
Use batch mode to reduce cost by evaluating all runnable rules in a single backend call:
|
package/dist/adapters.js
ADDED
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 [
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
|
10
|
+
const utils_1 = require("./utils");
|
|
11
11
|
const DEFAULTS = {
|
|
12
12
|
backend: "cursor-cli",
|
|
13
13
|
model: "auto",
|
|
@@ -20,9 +20,7 @@ const DEFAULTS = {
|
|
|
20
20
|
batchMode: false,
|
|
21
21
|
rulesDisable: [],
|
|
22
22
|
severityOverrides: {},
|
|
23
|
-
|
|
24
|
-
"cursor-cli": "cursor"
|
|
25
|
-
}
|
|
23
|
+
backendConfigs: {}
|
|
26
24
|
};
|
|
27
25
|
function readJsonIfExists(filePath) {
|
|
28
26
|
if (!node_fs_1.default.existsSync(filePath)) {
|
|
@@ -54,39 +52,51 @@ function sanitizeSeverityOverrides(value) {
|
|
|
54
52
|
if (!value) {
|
|
55
53
|
return {};
|
|
56
54
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
out[ruleId] = severity;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
55
|
+
return Object.fromEntries(Object.entries(value).flatMap(([ruleId, severity]) => typeof severity === "string" && utils_1.VALID_SEVERITIES.has(severity)
|
|
56
|
+
? [[ruleId, severity]]
|
|
57
|
+
: []));
|
|
64
58
|
}
|
|
65
|
-
function
|
|
66
|
-
const out = {
|
|
67
|
-
...DEFAULTS.backendExecutables
|
|
68
|
-
};
|
|
59
|
+
function sanitizeBackendConfigs(value) {
|
|
69
60
|
if (!value) {
|
|
70
|
-
return
|
|
61
|
+
return {};
|
|
71
62
|
}
|
|
72
|
-
|
|
73
|
-
if (typeof candidate
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
candidate.executable.trim()
|
|
78
|
-
|
|
63
|
+
return Object.fromEntries(Object.entries(value).flatMap(([name, candidate]) => {
|
|
64
|
+
if (typeof candidate !== "object" || candidate === null) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const executable = "executable" in candidate && typeof candidate.executable === "string"
|
|
68
|
+
? candidate.executable.trim()
|
|
69
|
+
: "";
|
|
70
|
+
const args = "args" in candidate ? candidate.args : undefined;
|
|
71
|
+
const model = "model" in candidate && typeof candidate.model === "string"
|
|
72
|
+
? candidate.model.trim()
|
|
73
|
+
: undefined;
|
|
74
|
+
if (!executable || !Array.isArray(args) || args.some((arg) => typeof arg !== "string")) {
|
|
75
|
+
return [];
|
|
79
76
|
}
|
|
77
|
+
const normalizedArgs = args;
|
|
78
|
+
if (!normalizedArgs.includes("{prompt}")) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return [[name, { executable, args: normalizedArgs, model: model && model !== "" ? model : undefined }]];
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
function ensureSelectedBackendIsConfigured(backend, backendConfigs) {
|
|
85
|
+
if (!(backend in backendConfigs)) {
|
|
86
|
+
throw new Error(`Backend "${backend}" is not configured. Add it under backends.${backend} with executable and args (including "{prompt}") in semlint.json.`);
|
|
80
87
|
}
|
|
81
|
-
return out;
|
|
82
88
|
}
|
|
83
89
|
function loadEffectiveConfig(options) {
|
|
84
90
|
const configPath = resolveConfigPath(options.configPath);
|
|
85
91
|
const parsed = configPath ? readJsonIfExists(configPath) : undefined;
|
|
86
92
|
const fileConfig = (parsed ?? {});
|
|
93
|
+
const backend = options.backend ?? fileConfig.backend ?? DEFAULTS.backend;
|
|
94
|
+
const backendConfigs = sanitizeBackendConfigs((fileConfig.backends ?? undefined));
|
|
95
|
+
ensureSelectedBackendIsConfigured(backend, backendConfigs);
|
|
96
|
+
const backendModel = backendConfigs[backend]?.model;
|
|
87
97
|
return {
|
|
88
|
-
backend
|
|
89
|
-
model: options.model ??
|
|
98
|
+
backend,
|
|
99
|
+
model: options.model ?? backendModel ?? DEFAULTS.model,
|
|
90
100
|
timeoutMs: typeof fileConfig.budgets?.timeout_ms === "number"
|
|
91
101
|
? fileConfig.budgets.timeout_ms
|
|
92
102
|
: DEFAULTS.timeoutMs,
|
|
@@ -103,6 +113,6 @@ function loadEffectiveConfig(options) {
|
|
|
103
113
|
? fileConfig.rules?.disable.filter((item) => typeof item === "string")
|
|
104
114
|
: DEFAULTS.rulesDisable,
|
|
105
115
|
severityOverrides: sanitizeSeverityOverrides((fileConfig.rules?.severity_overrides ?? undefined)),
|
|
106
|
-
|
|
116
|
+
backendConfigs
|
|
107
117
|
};
|
|
108
118
|
}
|
package/dist/diagnostics.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
return [];
|
|
48
44
|
}
|
|
49
|
-
|
|
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
|
-
|
|
49
|
+
return [];
|
|
55
50
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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,
|
package/dist/dispatch.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runBatchDispatch = runBatchDispatch;
|
|
4
|
+
exports.runParallelDispatch = runParallelDispatch;
|
|
5
|
+
const diagnostics_1 = require("./diagnostics");
|
|
6
|
+
const filter_1 = require("./filter");
|
|
7
|
+
const prompts_1 = require("./prompts");
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
function buildBatchPrompt(rules, diff) {
|
|
10
|
+
const ruleBlocks = rules
|
|
11
|
+
.map((rule) => [
|
|
12
|
+
`RULE_ID: ${rule.id}`,
|
|
13
|
+
`RULE_TITLE: ${rule.title}`,
|
|
14
|
+
`SEVERITY_DEFAULT: ${rule.effectiveSeverity}`,
|
|
15
|
+
"INSTRUCTIONS:",
|
|
16
|
+
rule.prompt
|
|
17
|
+
].join("\n"))
|
|
18
|
+
.join("\n\n---\n\n");
|
|
19
|
+
return (0, prompts_1.renderBatchPrompt)({ ruleBlocks, diff });
|
|
20
|
+
}
|
|
21
|
+
async function runBatchDispatch(input) {
|
|
22
|
+
const { rules, diff, changedFiles, backend, config, repoRoot } = input;
|
|
23
|
+
const diagnostics = [];
|
|
24
|
+
let backendErrors = 0;
|
|
25
|
+
(0, utils_1.debugLog)(config.debug, `Running ${rules.length} rule(s) in batch mode`);
|
|
26
|
+
const combinedDiff = rules
|
|
27
|
+
.map((rule) => (0, filter_1.buildScopedDiff)(rule, diff, changedFiles))
|
|
28
|
+
.filter((chunk) => chunk.trim() !== "")
|
|
29
|
+
.join("\n");
|
|
30
|
+
const batchPrompt = buildBatchPrompt(rules, combinedDiff || diff);
|
|
31
|
+
try {
|
|
32
|
+
const batchResult = await backend.runPrompt({
|
|
33
|
+
label: "Batch",
|
|
34
|
+
prompt: batchPrompt,
|
|
35
|
+
timeoutMs: config.timeoutMs
|
|
36
|
+
});
|
|
37
|
+
const groupedByRule = batchResult.diagnostics.reduce((acc, diagnostic) => {
|
|
38
|
+
if (typeof diagnostic === "object" &&
|
|
39
|
+
diagnostic !== null &&
|
|
40
|
+
!Array.isArray(diagnostic) &&
|
|
41
|
+
typeof diagnostic.rule_id === "string") {
|
|
42
|
+
const ruleId = diagnostic.rule_id;
|
|
43
|
+
acc.set(ruleId, [...(acc.get(ruleId) ?? []), diagnostic]);
|
|
44
|
+
return acc;
|
|
45
|
+
}
|
|
46
|
+
(0, utils_1.debugLog)(config.debug, "Batch: dropped diagnostic without valid rule_id");
|
|
47
|
+
return acc;
|
|
48
|
+
}, new Map());
|
|
49
|
+
const validRuleIds = new Set(rules.map((rule) => rule.id));
|
|
50
|
+
Array.from(groupedByRule.keys())
|
|
51
|
+
.filter((ruleId) => !validRuleIds.has(ruleId))
|
|
52
|
+
.forEach((ruleId) => {
|
|
53
|
+
(0, utils_1.debugLog)(config.debug, `Batch: dropped diagnostic for unknown rule_id ${ruleId}`);
|
|
54
|
+
});
|
|
55
|
+
const normalized = rules.flatMap((rule) => (0, diagnostics_1.normalizeDiagnostics)(rule.id, groupedByRule.get(rule.id) ?? [], config.debug, repoRoot));
|
|
56
|
+
diagnostics.push(...normalized);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
backendErrors += 1;
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
(0, utils_1.debugLog)(config.debug, `Batch backend error: ${message}`);
|
|
62
|
+
}
|
|
63
|
+
return { diagnostics, backendErrors };
|
|
64
|
+
}
|
|
65
|
+
async function runParallelDispatch(input) {
|
|
66
|
+
const { rules, diff, changedFiles, backend, config, repoRoot } = input;
|
|
67
|
+
(0, utils_1.debugLog)(config.debug, `Running ${rules.length} rule(s) in parallel`);
|
|
68
|
+
const runResults = await Promise.all(rules.map(async (rule) => {
|
|
69
|
+
const ruleStartedAt = Date.now();
|
|
70
|
+
(0, utils_1.debugLog)(config.debug, `Rule ${rule.id}: started`);
|
|
71
|
+
const scopedDiff = (0, filter_1.buildScopedDiff)(rule, diff, changedFiles);
|
|
72
|
+
const prompt = (0, filter_1.buildRulePrompt)(rule, scopedDiff);
|
|
73
|
+
try {
|
|
74
|
+
const result = await backend.runRule({
|
|
75
|
+
ruleId: rule.id,
|
|
76
|
+
prompt,
|
|
77
|
+
timeoutMs: config.timeoutMs
|
|
78
|
+
});
|
|
79
|
+
const normalized = (0, diagnostics_1.normalizeDiagnostics)(rule.id, result.diagnostics, config.debug, repoRoot);
|
|
80
|
+
(0, utils_1.debugLog)(config.debug, `Rule ${rule.id}: finished in ${Date.now() - ruleStartedAt}ms`);
|
|
81
|
+
return { backendError: false, normalized };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
(0, utils_1.debugLog)(config.debug, `Backend error for rule ${rule.id}: ${message}`);
|
|
86
|
+
(0, utils_1.debugLog)(config.debug, `Rule ${rule.id}: finished in ${Date.now() - ruleStartedAt}ms`);
|
|
87
|
+
return { backendError: true, normalized: [] };
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
return {
|
|
91
|
+
diagnostics: runResults.flatMap((result) => result.normalized),
|
|
92
|
+
backendErrors: runResults.filter((result) => result.backendError).length
|
|
93
|
+
};
|
|
94
|
+
}
|
package/dist/filter.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.shouldRunRule = shouldRunRule;
|
|
|
9
9
|
exports.buildScopedDiff = buildScopedDiff;
|
|
10
10
|
exports.buildRulePrompt = buildRulePrompt;
|
|
11
11
|
const picomatch_1 = __importDefault(require("picomatch"));
|
|
12
|
+
const prompts_1 = require("./prompts");
|
|
12
13
|
function unquoteDiffPath(raw) {
|
|
13
14
|
if (raw.startsWith("\"") && raw.endsWith("\"") && raw.length >= 2) {
|
|
14
15
|
return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
@@ -48,9 +49,6 @@ function extractChangedFilesFromDiff(diff) {
|
|
|
48
49
|
}
|
|
49
50
|
return Array.from(files);
|
|
50
51
|
}
|
|
51
|
-
function matchesAnyGlob(filePath, globs) {
|
|
52
|
-
return globs.some((glob) => (0, picomatch_1.default)(glob)(filePath));
|
|
53
|
-
}
|
|
54
52
|
function matchesAnyRegex(diff, regexes) {
|
|
55
53
|
for (const candidate of regexes) {
|
|
56
54
|
try {
|
|
@@ -67,14 +65,16 @@ function matchesAnyRegex(diff, regexes) {
|
|
|
67
65
|
}
|
|
68
66
|
function getRuleCandidateFiles(rule, changedFiles) {
|
|
69
67
|
let fileCandidates = changedFiles;
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
const includeMatcher = rule.include_globs && rule.include_globs.length > 0 ? (0, picomatch_1.default)(rule.include_globs) : null;
|
|
69
|
+
const excludeMatcher = rule.exclude_globs && rule.exclude_globs.length > 0 ? (0, picomatch_1.default)(rule.exclude_globs) : null;
|
|
70
|
+
if (includeMatcher) {
|
|
71
|
+
fileCandidates = changedFiles.filter((filePath) => includeMatcher(filePath));
|
|
72
72
|
if (fileCandidates.length === 0) {
|
|
73
73
|
return [];
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
if (
|
|
77
|
-
fileCandidates = fileCandidates.filter((filePath) => !
|
|
76
|
+
if (excludeMatcher) {
|
|
77
|
+
fileCandidates = fileCandidates.filter((filePath) => !excludeMatcher(filePath));
|
|
78
78
|
}
|
|
79
79
|
return fileCandidates;
|
|
80
80
|
}
|
|
@@ -130,41 +130,11 @@ function buildScopedDiff(rule, fullDiff, changedFiles) {
|
|
|
130
130
|
return scoped.trim() === "" ? fullDiff : scoped;
|
|
131
131
|
}
|
|
132
132
|
function buildRulePrompt(rule, diff) {
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"{",
|
|
139
|
-
" \"diagnostics\": [",
|
|
140
|
-
" {",
|
|
141
|
-
" \"rule_id\": string,",
|
|
142
|
-
" \"severity\": \"error\" | \"warn\" | \"info\",",
|
|
143
|
-
" \"message\": string,",
|
|
144
|
-
" \"file\": string,",
|
|
145
|
-
" \"line\": number,",
|
|
146
|
-
" \"column\"?: number,",
|
|
147
|
-
" \"end_line\"?: number,",
|
|
148
|
-
" \"end_column\"?: number,",
|
|
149
|
-
" \"evidence\"?: string,",
|
|
150
|
-
" \"confidence\"?: number",
|
|
151
|
-
" }",
|
|
152
|
-
" ]",
|
|
153
|
-
"}",
|
|
154
|
-
"Rules:",
|
|
155
|
-
"- If there are no findings, return {\"diagnostics\":[]}.",
|
|
156
|
-
"- Each diagnostic must reference a changed file from the DIFF.",
|
|
157
|
-
"- Use the provided RULE_ID exactly in every diagnostic.",
|
|
158
|
-
"- Keep messages concise and actionable.",
|
|
159
|
-
"",
|
|
160
|
-
`RULE_ID: ${rule.id}`,
|
|
161
|
-
`RULE_TITLE: ${rule.title}`,
|
|
162
|
-
`SEVERITY_DEFAULT: ${rule.effectiveSeverity}`,
|
|
163
|
-
"",
|
|
164
|
-
"INSTRUCTIONS:",
|
|
165
|
-
rule.prompt,
|
|
166
|
-
"",
|
|
167
|
-
"DIFF:",
|
|
133
|
+
return (0, prompts_1.renderRulePrompt)({
|
|
134
|
+
ruleId: rule.id,
|
|
135
|
+
ruleTitle: rule.title,
|
|
136
|
+
severityDefault: rule.effectiveSeverity,
|
|
137
|
+
instructions: rule.prompt,
|
|
168
138
|
diff
|
|
169
|
-
|
|
139
|
+
});
|
|
170
140
|
}
|
package/dist/init.js
CHANGED
|
@@ -8,6 +8,23 @@ const picocolors_1 = __importDefault(require("picocolors"));
|
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
10
|
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const SCAFFOLD_BACKENDS = {
|
|
12
|
+
"cursor-cli": {
|
|
13
|
+
executable: "cursor",
|
|
14
|
+
args: ["agent", "{prompt}", "--model", "{model}", "--print", "--mode", "ask", "--output-format", "text"],
|
|
15
|
+
model: "auto"
|
|
16
|
+
},
|
|
17
|
+
"claude-code": {
|
|
18
|
+
executable: "claude",
|
|
19
|
+
args: ["{prompt}", "--model", "{model}", "--output-format", "json"],
|
|
20
|
+
model: "auto"
|
|
21
|
+
},
|
|
22
|
+
"codex-cli": {
|
|
23
|
+
executable: "codex",
|
|
24
|
+
args: ["{prompt}", "--model", "{model}"],
|
|
25
|
+
model: "auto"
|
|
26
|
+
}
|
|
27
|
+
};
|
|
11
28
|
function commandExists(command) {
|
|
12
29
|
const result = (0, node_child_process_1.spawnSync)(command, ["--version"], {
|
|
13
30
|
stdio: "ignore"
|
|
@@ -34,12 +51,21 @@ function detectBackend() {
|
|
|
34
51
|
];
|
|
35
52
|
for (const candidate of candidates) {
|
|
36
53
|
if (commandExists(candidate.executable)) {
|
|
37
|
-
|
|
54
|
+
const scaffold = SCAFFOLD_BACKENDS[candidate.backend];
|
|
55
|
+
return {
|
|
56
|
+
backend: candidate.backend,
|
|
57
|
+
executable: scaffold.executable,
|
|
58
|
+
args: scaffold.args,
|
|
59
|
+
model: scaffold.model,
|
|
60
|
+
reason: candidate.reason
|
|
61
|
+
};
|
|
38
62
|
}
|
|
39
63
|
}
|
|
40
64
|
return {
|
|
41
65
|
backend: "cursor-cli",
|
|
42
|
-
executable: "cursor",
|
|
66
|
+
executable: SCAFFOLD_BACKENDS["cursor-cli"].executable,
|
|
67
|
+
args: SCAFFOLD_BACKENDS["cursor-cli"].args,
|
|
68
|
+
model: SCAFFOLD_BACKENDS["cursor-cli"].model,
|
|
43
69
|
reason: "no known agent CLI detected, using default Cursor setup"
|
|
44
70
|
};
|
|
45
71
|
}
|
|
@@ -52,7 +78,6 @@ function scaffoldConfig(force = false) {
|
|
|
52
78
|
const detected = detectBackend();
|
|
53
79
|
const scaffold = {
|
|
54
80
|
backend: detected.backend,
|
|
55
|
-
model: "auto",
|
|
56
81
|
budgets: {
|
|
57
82
|
timeout_ms: 120000
|
|
58
83
|
},
|
|
@@ -68,7 +93,9 @@ function scaffoldConfig(force = false) {
|
|
|
68
93
|
},
|
|
69
94
|
backends: {
|
|
70
95
|
[detected.backend]: {
|
|
71
|
-
executable: detected.executable
|
|
96
|
+
executable: detected.executable,
|
|
97
|
+
args: detected.args,
|
|
98
|
+
model: detected.model
|
|
72
99
|
}
|
|
73
100
|
}
|
|
74
101
|
};
|
package/dist/main.js
CHANGED
|
@@ -7,79 +7,28 @@ exports.runSemlint = runSemlint;
|
|
|
7
7
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
8
8
|
const nanospinner_1 = require("nanospinner");
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const package_json_1 = require("../package.json");
|
|
10
11
|
const backend_1 = require("./backend");
|
|
11
12
|
const config_1 = require("./config");
|
|
12
13
|
const diagnostics_1 = require("./diagnostics");
|
|
14
|
+
const dispatch_1 = require("./dispatch");
|
|
13
15
|
const filter_1 = require("./filter");
|
|
14
16
|
const git_1 = require("./git");
|
|
15
17
|
const reporter_1 = require("./reporter");
|
|
16
18
|
const rules_1 = require("./rules");
|
|
17
|
-
const
|
|
18
|
-
function debugLog(enabled, message) {
|
|
19
|
-
if (enabled) {
|
|
20
|
-
process.stderr.write(`${picocolors_1.default.gray(`[debug] ${message}`)}\n`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
19
|
+
const utils_1 = require("./utils");
|
|
23
20
|
function timed(enabled, label, action) {
|
|
24
21
|
const startedAt = Date.now();
|
|
25
22
|
const result = action();
|
|
26
|
-
debugLog(enabled, `${label} in ${Date.now() - startedAt}ms`);
|
|
23
|
+
(0, utils_1.debugLog)(enabled, `${label} in ${Date.now() - startedAt}ms`);
|
|
27
24
|
return result;
|
|
28
25
|
}
|
|
29
26
|
async function timedAsync(enabled, label, action) {
|
|
30
27
|
const startedAt = Date.now();
|
|
31
28
|
const result = await action();
|
|
32
|
-
debugLog(enabled, `${label} in ${Date.now() - startedAt}ms`);
|
|
29
|
+
(0, utils_1.debugLog)(enabled, `${label} in ${Date.now() - startedAt}ms`);
|
|
33
30
|
return result;
|
|
34
31
|
}
|
|
35
|
-
function buildBatchPrompt(rules, diff) {
|
|
36
|
-
const ruleBlocks = rules
|
|
37
|
-
.map((rule) => [
|
|
38
|
-
`RULE_ID: ${rule.id}`,
|
|
39
|
-
`RULE_TITLE: ${rule.title}`,
|
|
40
|
-
`SEVERITY_DEFAULT: ${rule.effectiveSeverity}`,
|
|
41
|
-
"INSTRUCTIONS:",
|
|
42
|
-
rule.prompt
|
|
43
|
-
].join("\n"))
|
|
44
|
-
.join("\n\n---\n\n");
|
|
45
|
-
return [
|
|
46
|
-
"You are Semlint, an expert semantic code reviewer.",
|
|
47
|
-
"BATCH_MODE: true",
|
|
48
|
-
"Evaluate all rules below against the DIFF in one pass.",
|
|
49
|
-
"Analyze ONLY the modified code present in the DIFF below.",
|
|
50
|
-
"Return JSON only (no markdown, no prose, no code fences).",
|
|
51
|
-
"Output schema:",
|
|
52
|
-
"{",
|
|
53
|
-
" \"diagnostics\": [",
|
|
54
|
-
" {",
|
|
55
|
-
" \"rule_id\": string,",
|
|
56
|
-
" \"severity\": \"error\" | \"warn\" | \"info\",",
|
|
57
|
-
" \"message\": string,",
|
|
58
|
-
" \"file\": string,",
|
|
59
|
-
" \"line\": number,",
|
|
60
|
-
" \"column\"?: number,",
|
|
61
|
-
" \"end_line\"?: number,",
|
|
62
|
-
" \"end_column\"?: number,",
|
|
63
|
-
" \"evidence\"?: string,",
|
|
64
|
-
" \"confidence\"?: number",
|
|
65
|
-
" }",
|
|
66
|
-
" ]",
|
|
67
|
-
"}",
|
|
68
|
-
"Rules:",
|
|
69
|
-
"- If there are no findings, return {\"diagnostics\":[]}.",
|
|
70
|
-
"- Each diagnostic must reference a changed file from the DIFF.",
|
|
71
|
-
"- rule_id must match one of the RULE_ID values listed below.",
|
|
72
|
-
"- Keep messages concise and actionable.",
|
|
73
|
-
"- Deduplicate semantically equivalent findings before returning output.",
|
|
74
|
-
"- When duplicates exist, keep a single diagnostic from the rule that semantically matches the issue best, even if that selected diagnostic has lower severity.",
|
|
75
|
-
"",
|
|
76
|
-
"RULES:",
|
|
77
|
-
ruleBlocks,
|
|
78
|
-
"",
|
|
79
|
-
"DIFF:",
|
|
80
|
-
diff
|
|
81
|
-
].join("\n");
|
|
82
|
-
}
|
|
83
32
|
async function runSemlint(options) {
|
|
84
33
|
const startedAt = Date.now();
|
|
85
34
|
let spinner = null;
|
|
@@ -87,28 +36,28 @@ async function runSemlint(options) {
|
|
|
87
36
|
const config = timed(options.debug, "Loaded effective config", () => (0, config_1.loadEffectiveConfig)(options));
|
|
88
37
|
const rulesDir = node_path_1.default.join(process.cwd(), ".semlint", "rules");
|
|
89
38
|
const rules = timed(config.debug, "Loaded and validated rules", () => (0, rules_1.loadRules)(rulesDir, config.rulesDisable, config.severityOverrides));
|
|
90
|
-
debugLog(config.debug, `Loaded ${rules.length} rule(s)`);
|
|
91
|
-
debugLog(config.debug, `Rule IDs: ${rules.map((rule) => rule.id).join(", ")}`);
|
|
39
|
+
(0, utils_1.debugLog)(config.debug, `Loaded ${rules.length} rule(s)`);
|
|
40
|
+
(0, utils_1.debugLog)(config.debug, `Rule IDs: ${rules.map((rule) => rule.id).join(", ")}`);
|
|
92
41
|
const useLocalBranchDiff = !options.base && !options.head;
|
|
93
42
|
const diff = await timedAsync(config.debug, "Computed git diff", () => useLocalBranchDiff ? (0, git_1.getLocalBranchDiff)() : (0, git_1.getGitDiff)(config.base, config.head));
|
|
94
43
|
const changedFiles = timed(config.debug, "Parsed changed files from diff", () => (0, filter_1.extractChangedFilesFromDiff)(diff));
|
|
95
|
-
debugLog(config.debug, useLocalBranchDiff
|
|
44
|
+
(0, utils_1.debugLog)(config.debug, useLocalBranchDiff
|
|
96
45
|
? "Using local branch diff (staged + unstaged + untracked only)"
|
|
97
46
|
: `Using explicit ref diff (${config.base}..${config.head})`);
|
|
98
|
-
debugLog(config.debug, `Detected ${changedFiles.length} changed file(s)`);
|
|
47
|
+
(0, utils_1.debugLog)(config.debug, `Detected ${changedFiles.length} changed file(s)`);
|
|
99
48
|
const repoRoot = await timedAsync(config.debug, "Resolved git repo root", () => (0, git_1.getRepoRoot)());
|
|
100
49
|
const backend = timed(config.debug, "Initialized backend runner", () => (0, backend_1.createBackendRunner)(config));
|
|
101
50
|
const runnableRules = rules.filter((rule) => {
|
|
102
51
|
const filterStartedAt = Date.now();
|
|
103
52
|
const shouldRun = (0, filter_1.shouldRunRule)(rule, changedFiles, diff);
|
|
104
|
-
debugLog(config.debug, `Rule ${rule.id}: filter check in ${Date.now() - filterStartedAt}ms`);
|
|
53
|
+
(0, utils_1.debugLog)(config.debug, `Rule ${rule.id}: filter check in ${Date.now() - filterStartedAt}ms`);
|
|
105
54
|
if (!shouldRun) {
|
|
106
|
-
debugLog(config.debug, `Skipping rule ${rule.id}: filters did not match`);
|
|
55
|
+
(0, utils_1.debugLog)(config.debug, `Skipping rule ${rule.id}: filters did not match`);
|
|
107
56
|
return false;
|
|
108
57
|
}
|
|
109
58
|
return true;
|
|
110
59
|
});
|
|
111
|
-
|
|
60
|
+
let diagnostics = [];
|
|
112
61
|
const rulesRun = runnableRules.length;
|
|
113
62
|
let backendErrors = 0;
|
|
114
63
|
if (config.format !== "json" && rulesRun > 0) {
|
|
@@ -122,82 +71,27 @@ async function runSemlint(options) {
|
|
|
122
71
|
config.format !== "json" && rulesRun > 0
|
|
123
72
|
? (0, nanospinner_1.createSpinner)(`Analyzing ${changedFiles.length} file${changedFiles.length === 1 ? "" : "s"} with ${config.backend} in ${config.batchMode ? "batch" : "parallel"} mode...`).start()
|
|
124
73
|
: null;
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
74
|
+
if (runnableRules.length > 0) {
|
|
75
|
+
const dispatchLabel = config.batchMode ? "batch" : "parallel";
|
|
76
|
+
const result = await timedAsync(config.debug, `Dispatch (${dispatchLabel})`, () => config.batchMode
|
|
77
|
+
? (0, dispatch_1.runBatchDispatch)({
|
|
78
|
+
rules: runnableRules,
|
|
79
|
+
diff,
|
|
80
|
+
changedFiles,
|
|
81
|
+
backend,
|
|
82
|
+
config,
|
|
83
|
+
repoRoot
|
|
84
|
+
})
|
|
85
|
+
: (0, dispatch_1.runParallelDispatch)({
|
|
86
|
+
rules: runnableRules,
|
|
87
|
+
diff,
|
|
88
|
+
changedFiles,
|
|
89
|
+
backend,
|
|
90
|
+
config,
|
|
91
|
+
repoRoot
|
|
137
92
|
}));
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (typeof diagnostic === "object" &&
|
|
141
|
-
diagnostic !== null &&
|
|
142
|
-
!Array.isArray(diagnostic) &&
|
|
143
|
-
typeof diagnostic.rule_id === "string") {
|
|
144
|
-
const ruleId = diagnostic.rule_id;
|
|
145
|
-
const current = groupedByRule.get(ruleId) ?? [];
|
|
146
|
-
current.push(diagnostic);
|
|
147
|
-
groupedByRule.set(ruleId, current);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
debugLog(config.debug, "Batch: dropped diagnostic without valid rule_id");
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
const validRuleIds = new Set(runnableRules.map((rule) => rule.id));
|
|
154
|
-
for (const [ruleId] of groupedByRule) {
|
|
155
|
-
if (!validRuleIds.has(ruleId)) {
|
|
156
|
-
debugLog(config.debug, `Batch: dropped diagnostic for unknown rule_id ${ruleId}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
for (const rule of runnableRules) {
|
|
160
|
-
const normalized = timed(config.debug, `Batch: diagnostics normalization for ${rule.id}`, () => (0, diagnostics_1.normalizeDiagnostics)(rule.id, groupedByRule.get(rule.id) ?? [], config.debug, repoRoot));
|
|
161
|
-
diagnostics.push(...normalized);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
backendErrors += 1;
|
|
166
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
167
|
-
debugLog(config.debug, `Batch backend error: ${message}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
debugLog(config.debug, `Running ${runnableRules.length} rule(s) in parallel`);
|
|
172
|
-
const runResults = await Promise.all(runnableRules.map(async (rule) => {
|
|
173
|
-
let backendError = false;
|
|
174
|
-
let normalized = [];
|
|
175
|
-
const ruleStartedAt = Date.now();
|
|
176
|
-
debugLog(config.debug, `Rule ${rule.id}: started`);
|
|
177
|
-
const scopedDiff = timed(config.debug, `Rule ${rule.id}: scoped diff build`, () => (0, filter_1.buildScopedDiff)(rule, diff, changedFiles));
|
|
178
|
-
const prompt = timed(config.debug, `Rule ${rule.id}: prompt build`, () => (0, filter_1.buildRulePrompt)(rule, scopedDiff));
|
|
179
|
-
try {
|
|
180
|
-
const result = await timedAsync(config.debug, `Rule ${rule.id}: backend run`, () => backend.runRule({
|
|
181
|
-
ruleId: rule.id,
|
|
182
|
-
prompt,
|
|
183
|
-
timeoutMs: config.timeoutMs
|
|
184
|
-
}));
|
|
185
|
-
normalized = timed(config.debug, `Rule ${rule.id}: diagnostics normalization`, () => (0, diagnostics_1.normalizeDiagnostics)(rule.id, result.diagnostics, config.debug, repoRoot));
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
backendError = true;
|
|
189
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
-
debugLog(config.debug, `Backend error for rule ${rule.id}: ${message}`);
|
|
191
|
-
}
|
|
192
|
-
debugLog(config.debug, `Rule ${rule.id}: finished in ${Date.now() - ruleStartedAt}ms`);
|
|
193
|
-
return { backendError, normalized };
|
|
194
|
-
}));
|
|
195
|
-
for (const result of runResults) {
|
|
196
|
-
if (result.backendError) {
|
|
197
|
-
backendErrors += 1;
|
|
198
|
-
}
|
|
199
|
-
diagnostics.push(...result.normalized);
|
|
200
|
-
}
|
|
93
|
+
diagnostics = result.diagnostics;
|
|
94
|
+
backendErrors = result.backendErrors;
|
|
201
95
|
}
|
|
202
96
|
const sorted = timed(config.debug, "Sorted diagnostics", () => (0, diagnostics_1.sortDiagnostics)(diagnostics));
|
|
203
97
|
const durationMs = Date.now() - startedAt;
|
|
@@ -211,13 +105,13 @@ async function runSemlint(options) {
|
|
|
211
105
|
}
|
|
212
106
|
const outputStartedAt = Date.now();
|
|
213
107
|
if (config.format === "json") {
|
|
214
|
-
process.stdout.write(`${(0, reporter_1.formatJsonOutput)(
|
|
108
|
+
process.stdout.write(`${(0, reporter_1.formatJsonOutput)(package_json_1.version, sorted, { rulesRun, durationMs, backendErrors })}\n`);
|
|
215
109
|
}
|
|
216
110
|
else {
|
|
217
111
|
process.stdout.write(`${(0, reporter_1.formatTextOutput)(sorted, { rulesRun, durationMs, backendErrors })}\n`);
|
|
218
112
|
}
|
|
219
|
-
debugLog(config.debug, `Rendered output in ${Date.now() - outputStartedAt}ms`);
|
|
220
|
-
debugLog(config.debug, `Total run duration ${durationMs}ms`);
|
|
113
|
+
(0, utils_1.debugLog)(config.debug, `Rendered output in ${Date.now() - outputStartedAt}ms`);
|
|
114
|
+
(0, utils_1.debugLog)(config.debug, `Total run duration ${durationMs}ms`);
|
|
221
115
|
if (backendErrors > 0) {
|
|
222
116
|
return 2;
|
|
223
117
|
}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getStrictJsonRetryInstruction = getStrictJsonRetryInstruction;
|
|
7
|
+
exports.renderRulePrompt = renderRulePrompt;
|
|
8
|
+
exports.renderBatchPrompt = renderBatchPrompt;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const promptCache = new Map();
|
|
12
|
+
function readPromptFile(fileName) {
|
|
13
|
+
const cached = promptCache.get(fileName);
|
|
14
|
+
if (cached !== undefined) {
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
|
+
const promptPath = node_path_1.default.resolve(__dirname, "..", "prompts", fileName);
|
|
18
|
+
const content = node_fs_1.default.readFileSync(promptPath, "utf8").trim();
|
|
19
|
+
promptCache.set(fileName, content);
|
|
20
|
+
return content;
|
|
21
|
+
}
|
|
22
|
+
function renderTemplate(template, values) {
|
|
23
|
+
return Object.entries(values).reduce((current, [key, value]) => current.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value), template);
|
|
24
|
+
}
|
|
25
|
+
function getStrictJsonRetryInstruction() {
|
|
26
|
+
return readPromptFile("retry-json.md").replace(/\s+/g, " ").trim();
|
|
27
|
+
}
|
|
28
|
+
function renderRulePrompt(values) {
|
|
29
|
+
const commonContract = readPromptFile("common-contract.md");
|
|
30
|
+
return renderTemplate(readPromptFile("rule.md"), {
|
|
31
|
+
COMMON_CONTRACT: commonContract,
|
|
32
|
+
RULE_ID: values.ruleId,
|
|
33
|
+
RULE_TITLE: values.ruleTitle,
|
|
34
|
+
SEVERITY_DEFAULT: values.severityDefault,
|
|
35
|
+
INSTRUCTIONS: values.instructions,
|
|
36
|
+
DIFF: values.diff
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function renderBatchPrompt(values) {
|
|
40
|
+
const commonContract = readPromptFile("common-contract.md");
|
|
41
|
+
return renderTemplate(readPromptFile("batch.md"), {
|
|
42
|
+
COMMON_CONTRACT: commonContract,
|
|
43
|
+
RULE_BLOCKS: values.ruleBlocks,
|
|
44
|
+
DIFF: values.diff
|
|
45
|
+
});
|
|
46
|
+
}
|
package/dist/reporter.js
CHANGED
|
@@ -31,19 +31,18 @@ function formatJsonOutput(version, diagnostics, stats) {
|
|
|
31
31
|
return JSON.stringify(payload, null, 2);
|
|
32
32
|
}
|
|
33
33
|
function formatTextOutput(diagnostics, stats) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
34
|
+
const groupedByFile = diagnostics.reduce((acc, diagnostic) => {
|
|
35
|
+
acc.set(diagnostic.file, [...(acc.get(diagnostic.file) ?? []), diagnostic]);
|
|
36
|
+
return acc;
|
|
37
|
+
}, new Map());
|
|
38
|
+
const lines = Array.from(groupedByFile.entries()).flatMap(([file, fileDiagnostics], fileIndex) => [
|
|
39
|
+
...(fileIndex > 0 ? [""] : []),
|
|
40
|
+
picocolors_1.default.underline(file),
|
|
41
|
+
...fileDiagnostics.map((diagnostic) => {
|
|
42
|
+
const column = diagnostic.column ?? 1;
|
|
43
|
+
return ` ${picocolors_1.default.dim(`${diagnostic.line}:${column}`)} ${formatSeverity(diagnostic.severity)} ${picocolors_1.default.gray(diagnostic.rule_id)} ${diagnostic.message}`;
|
|
44
|
+
})
|
|
45
|
+
]);
|
|
47
46
|
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
48
47
|
const warnings = diagnostics.filter((d) => d.severity === "warn").length;
|
|
49
48
|
const problems = diagnostics.length;
|
package/dist/rules.js
CHANGED
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.loadRules = loadRules;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
10
|
function assertNonEmptyString(value, fieldName, filePath) {
|
|
11
11
|
if (typeof value !== "string" || value.trim() === "") {
|
|
12
12
|
throw new Error(`Invalid rule in ${filePath}: "${fieldName}" must be a non-empty string`);
|
|
@@ -28,7 +28,7 @@ function validateRuleObject(raw, filePath) {
|
|
|
28
28
|
}
|
|
29
29
|
const obj = raw;
|
|
30
30
|
const severity = assertNonEmptyString(obj.severity_default, "severity_default", filePath);
|
|
31
|
-
if (!VALID_SEVERITIES.has(severity)) {
|
|
31
|
+
if (!utils_1.VALID_SEVERITIES.has(severity)) {
|
|
32
32
|
throw new Error(`Invalid rule in ${filePath}: "severity_default" must be one of error|warn|info`);
|
|
33
33
|
}
|
|
34
34
|
return {
|
|
@@ -52,10 +52,10 @@ function loadRules(rulesDir, disabledRuleIds, severityOverrides) {
|
|
|
52
52
|
.readdirSync(rulesDir)
|
|
53
53
|
.filter((name) => name.endsWith(".json"))
|
|
54
54
|
.sort((a, b) => a.localeCompare(b));
|
|
55
|
-
const seenIds = new Set();
|
|
56
55
|
const disabled = new Set(disabledRuleIds);
|
|
57
|
-
const
|
|
58
|
-
|
|
56
|
+
const seenIds = new Set();
|
|
57
|
+
return entries
|
|
58
|
+
.map((fileName) => {
|
|
59
59
|
const filePath = node_path_1.default.join(rulesDir, fileName);
|
|
60
60
|
let parsed;
|
|
61
61
|
try {
|
|
@@ -70,19 +70,17 @@ function loadRules(rulesDir, disabledRuleIds, severityOverrides) {
|
|
|
70
70
|
throw new Error(`Duplicate rule id detected: ${validated.id}`);
|
|
71
71
|
}
|
|
72
72
|
seenIds.add(validated.id);
|
|
73
|
-
if (disabled.has(validated.id)) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
73
|
const overrideSeverity = severityOverrides[validated.id];
|
|
77
74
|
const effectiveSeverity = overrideSeverity ?? validated.severity_default;
|
|
78
|
-
if (!VALID_SEVERITIES.has(effectiveSeverity)) {
|
|
75
|
+
if (!utils_1.VALID_SEVERITIES.has(effectiveSeverity)) {
|
|
79
76
|
throw new Error(`Invalid severity override for rule ${validated.id}`);
|
|
80
77
|
}
|
|
81
|
-
|
|
78
|
+
return {
|
|
82
79
|
...validated,
|
|
83
80
|
sourcePath: filePath,
|
|
84
81
|
effectiveSeverity
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
82
|
+
};
|
|
83
|
+
})
|
|
84
|
+
.filter((rule) => !disabled.has(rule.id))
|
|
85
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
88
86
|
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.VALID_SEVERITIES = void 0;
|
|
7
|
+
exports.isPositiveInteger = isPositiveInteger;
|
|
8
|
+
exports.debugLog = debugLog;
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
exports.VALID_SEVERITIES = new Set(["error", "warn", "info"]);
|
|
11
|
+
function isPositiveInteger(value) {
|
|
12
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1;
|
|
13
|
+
}
|
|
14
|
+
function debugLog(enabled, message) {
|
|
15
|
+
if (enabled) {
|
|
16
|
+
process.stderr.write(`${picocolors_1.default.gray(`[debug] ${message}`)}\n`);
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semlint-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Semantic lint CLI — runs LLM-backed rules on your git diff and returns CI-friendly exit codes",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
+
"prompts",
|
|
12
13
|
"rules",
|
|
13
14
|
"README.md"
|
|
14
15
|
],
|
package/prompts/batch.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
You are Semlint, an expert semantic code reviewer.
|
|
2
|
+
BATCH_MODE: true
|
|
3
|
+
Evaluate all rules below against the DIFF in one pass.
|
|
4
|
+
Start from the modified/added code in the DIFF. You may inspect adjacent files/functions referenced by the changed code when necessary to verify patterns or behavior, but keep findings anchored to changed files and lines.
|
|
5
|
+
Return JSON only (no markdown, no prose, no code fences).
|
|
6
|
+
{{COMMON_CONTRACT}}
|
|
7
|
+
- rule_id must match one of the RULE_ID values listed below.
|
|
8
|
+
- Keep messages concise and actionable.
|
|
9
|
+
- Deduplicate semantically equivalent findings before returning output.
|
|
10
|
+
- When duplicates exist, keep a single diagnostic from the rule that semantically matches the issue best, even if that selected diagnostic has lower severity.
|
|
11
|
+
|
|
12
|
+
RULES:
|
|
13
|
+
{{RULE_BLOCKS}}
|
|
14
|
+
|
|
15
|
+
DIFF:
|
|
16
|
+
{{DIFF}}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Output schema:
|
|
2
|
+
{
|
|
3
|
+
"diagnostics": [
|
|
4
|
+
{
|
|
5
|
+
"rule_id": string,
|
|
6
|
+
"severity": "error" | "warn" | "info",
|
|
7
|
+
"message": string,
|
|
8
|
+
"file": string,
|
|
9
|
+
"line": number,
|
|
10
|
+
"column"?: number,
|
|
11
|
+
"end_line"?: number,
|
|
12
|
+
"end_column"?: number,
|
|
13
|
+
"evidence"?: string,
|
|
14
|
+
"confidence"?: number
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
Rules:
|
|
19
|
+
- If there are no findings, return {"diagnostics":[]}.
|
|
20
|
+
- Each diagnostic must reference a changed file from the DIFF.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Return valid JSON only.
|
|
2
|
+
Do not include markdown fences.
|
|
3
|
+
Do not include commentary, headings, or any text before/after JSON.
|
|
4
|
+
The first character of your response must be '{' and the last must be '}'.
|
|
5
|
+
Output must match: {"diagnostics":[{"rule_id":"<id>","severity":"error|warn|info","message":"<text>","file":"<path>","line":1}]}
|
package/prompts/rule.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
You are Semlint, an expert semantic code reviewer.
|
|
2
|
+
Analyze ONLY the modified code present in the DIFF below.
|
|
3
|
+
Return JSON only (no markdown, no prose, no code fences).
|
|
4
|
+
{{COMMON_CONTRACT}}
|
|
5
|
+
- Use the provided RULE_ID exactly in every diagnostic.
|
|
6
|
+
- Keep messages concise and actionable.
|
|
7
|
+
|
|
8
|
+
RULE_ID: {{RULE_ID}}
|
|
9
|
+
RULE_TITLE: {{RULE_TITLE}}
|
|
10
|
+
SEVERITY_DEFAULT: {{SEVERITY_DEFAULT}}
|
|
11
|
+
|
|
12
|
+
INSTRUCTIONS:
|
|
13
|
+
{{INSTRUCTIONS}}
|
|
14
|
+
|
|
15
|
+
DIFF:
|
|
16
|
+
{{DIFF}}
|