@salesforce/afv-skills 1.17.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/skills/analyzing-test-failures/SKILL.md +159 -0
  3. package/skills/building-sf-integrations/SKILL.md +1 -1
  4. package/skills/checking-devops-prerequisites/SKILL.md +141 -0
  5. package/skills/configuring-code-analyzer/SKILL.md +482 -0
  6. package/skills/configuring-code-analyzer/examples/apex-project-config.yml +41 -0
  7. package/skills/configuring-code-analyzer/examples/ci-github-actions.yml +96 -0
  8. package/skills/configuring-code-analyzer/examples/fullstack-project-config.yml +46 -0
  9. package/skills/configuring-code-analyzer/examples/lwc-project-config.yml +26 -0
  10. package/skills/configuring-code-analyzer/references/ci-cd-templates.md +648 -0
  11. package/skills/configuring-code-analyzer/references/config-schema.md +257 -0
  12. package/skills/configuring-code-analyzer/references/diagnostic-flow.md +70 -0
  13. package/skills/configuring-code-analyzer/references/engine-prerequisites.md +276 -0
  14. package/skills/configuring-code-analyzer/references/rule-name-resolution.md +67 -0
  15. package/skills/configuring-code-analyzer/references/troubleshooting.md +298 -0
  16. package/skills/configuring-code-analyzer/scripts/check-prerequisites.sh +189 -0
  17. package/skills/configuring-code-analyzer/scripts/generate-config.sh +143 -0
  18. package/skills/configuring-code-analyzer/scripts/validate-config.sh +153 -0
  19. package/skills/configuring-quality-gate/SKILL.md +120 -0
  20. package/skills/configuring-test-provider/SKILL.md +113 -0
  21. package/skills/creating-fix-work-item/SKILL.md +66 -0
  22. package/skills/managing-cdc-enablement/SKILL.md +164 -0
  23. package/skills/managing-cdc-enablement/assets/PlatformEventChannel-template.xml +5 -0
  24. package/skills/managing-cdc-enablement/assets/PlatformEventChannelMember-template.xml +11 -0
  25. package/skills/managing-cdc-enablement/references/deploy-troubleshooting.md +73 -0
  26. package/skills/managing-cdc-enablement/references/filter-expressions.md +93 -0
  27. package/skills/managing-suite-assignments/SKILL.md +161 -0
  28. package/skills/polling-test-results/SKILL.md +72 -0
  29. package/skills/recommending-devops-tests/SKILL.md +137 -0
  30. package/skills/running-code-analyzer/SKILL.md +264 -267
  31. package/skills/running-code-analyzer/references/post-scan-workflows.md +286 -0
  32. package/skills/running-code-analyzer/scripts/describe-rule.js +382 -0
  33. package/skills/running-code-analyzer/scripts/list-rules.js +260 -0
  34. package/skills/running-code-analyzer/scripts/query-results.js +230 -0
  35. package/skills/running-devops-test-suite/SKILL.md +144 -0
  36. package/skills/syncing-test-providers/SKILL.md +108 -0
  37. package/skills/using-salesforce-archive/SKILL.md +121 -0
  38. package/skills/using-salesforce-archive/examples/monitor-failed-jobs.md +47 -0
  39. package/skills/using-salesforce-archive/references/archive-activity-entity.md +59 -0
  40. package/skills/using-salesforce-archive/references/connect-api-operations.md +157 -0
@@ -0,0 +1,286 @@
1
+ # Post-Scan Workflows
2
+
3
+ After presenting initial scan results (Step 5), the user may ask follow-up questions to explore results or understand violations. This reference covers three post-scan workflows:
4
+
5
+ 1. **Result Querying** — filter/drill into existing results without re-scanning
6
+ 2. **Rule Description** — explain what a rule does and how to fix violations
7
+ 3. **Rule Listing** — browse available rules without running a scan
8
+
9
+ ---
10
+
11
+ ## Result Querying (Step 7)
12
+
13
+ ### When to Use
14
+
15
+ Trigger this workflow when the user asks to explore existing results:
16
+ - "Show me just the security violations"
17
+ - "What's in AccountService.cls?"
18
+ - "Show only PMD issues"
19
+ - "Filter to severity 1 and 2"
20
+ - "What ESLint rules fired?"
21
+ - "Show violations in the lwc folder"
22
+ - "Top 20 by file"
23
+
24
+ ### How It Works
25
+
26
+ The `query-results.js` script re-filters the SAME results JSON file (from Step 4) with different criteria. No re-scan is needed — it is instant.
27
+
28
+ ### Script Reference
29
+
30
+ ```bash
31
+ node "<skill_dir>/scripts/query-results.js" "<results-file.json>" [options]
32
+ ```
33
+
34
+ **Filter options (combine any):**
35
+
36
+ | Option | Description | Example |
37
+ |--------|-------------|---------|
38
+ | `--engine <name>` | Filter by engine | `--engine pmd` |
39
+ | `--severity <n>` | Filter by severity (comma-separated) | `--severity 1,2` |
40
+ | `--category <tag>` | Filter by category/tag | `--category Security` |
41
+ | `--rule <name>` | Filter by exact rule name | `--rule ApexCRUDViolation` |
42
+ | `--file <substring>` | Filter by file path substring | `--file AccountService` |
43
+ | `--top <n>` | Return top N results (default: 10) | `--top 20` |
44
+ | `--sort <field>` | Sort by: severity, rule, engine, file | `--sort file` |
45
+ | `--sort-dir <dir>` | Sort direction: asc, desc | `--sort-dir desc` |
46
+ | `--summary` | Show only counts (no individual violations) | `--summary` |
47
+
48
+ **Options can be combined freely:**
49
+ ```bash
50
+ # Security violations in PMD, top 5
51
+ node "<skill_dir>/scripts/query-results.js" "./results.json" --engine pmd --category Security --top 5
52
+
53
+ # All Critical+High in a specific file
54
+ node "<skill_dir>/scripts/query-results.js" "./results.json" --severity 1,2 --file AccountService.cls
55
+
56
+ # Summary of ESLint issues only
57
+ node "<skill_dir>/scripts/query-results.js" "./results.json" --engine eslint --summary
58
+ ```
59
+
60
+ ### Output Format
61
+
62
+ The script outputs JSON with this structure:
63
+
64
+ ```json
65
+ {
66
+ "query": { "engine": "pmd", "severity": [1,2], ... },
67
+ "totalViolations": 500,
68
+ "totalMatches": 23,
69
+ "severityCounts": { "1": 5, "2": 18, "3": 0, "4": 0, "5": 0 },
70
+ "topRules": [{ "rule": "ApexCRUDViolation", "engine": "pmd", "count": 12 }, ...],
71
+ "topFiles": [{ "file": "AccountService.cls", "count": 8 }, ...],
72
+ "violations": [
73
+ { "rule": "...", "engine": "...", "severity": 1, "message": "...", "file": "...", "startLine": 42, "tags": [...] },
74
+ ...
75
+ ]
76
+ }
77
+ ```
78
+
79
+ When `--summary` is used, the `violations` array is omitted.
80
+
81
+ ### Presentation Rules
82
+
83
+ Present query results using the same format as Step 5, but with a header indicating the active filter:
84
+
85
+ ```
86
+ ## Filtered Results: [description of filter]
87
+
88
+ **X matches** out of Y total violations.
89
+
90
+ | Severity | Count |
91
+ |----------|-------|
92
+ | Critical (1) | X |
93
+ | High (2) | X |
94
+ | ... |
95
+
96
+ ### Matching Violations
97
+ | # | Rule | Engine | Sev | File | Line |
98
+ |---|------|--------|-----|------|------|
99
+ | 1 | ... | ... | ... | ... | ... |
100
+
101
+ ### Top Rules (within filter)
102
+ | Rule | Engine | Count |
103
+ |------|--------|-------|
104
+ | ... | ... | ... |
105
+
106
+ Full results: `<original-results-file>`
107
+ ```
108
+
109
+ ### Follow-Up Offers
110
+
111
+ After presenting filtered results, offer:
112
+ - "Want me to narrow further?" (add more filters)
113
+ - "Want me to explain any of these rules?" (→ Step 8)
114
+ - "Want me to apply fixes for these?" (→ Step 6, scoped to matched rules)
115
+
116
+ ---
117
+
118
+ ## Rule Description (Step 8)
119
+
120
+ ### When to Use
121
+
122
+ Trigger this workflow when the user asks about a specific rule:
123
+ - "What is ApexCRUDViolation?"
124
+ - "Explain this rule"
125
+ - "What does no-var mean?"
126
+ - "How do I fix OperationWithLimitsInLoop?"
127
+ - "Tell me about the ApexSOQLInjection rule"
128
+ - "Why is this flagged?"
129
+
130
+ ### How It Works
131
+
132
+ The `describe-rule.js` script calls `sf code-analyzer rules` with a targeted selector to extract rule metadata including description and documentation links.
133
+
134
+ ### Script Reference
135
+
136
+ ```bash
137
+ node "<skill_dir>/scripts/describe-rule.js" "<rule-name>" [--engine <engine>]
138
+ ```
139
+
140
+ **Arguments:**
141
+
142
+ | Argument | Description | Example |
143
+ |----------|-------------|---------|
144
+ | `<rule-name>` | The rule name to look up | `ApexCRUDViolation` |
145
+ | `--engine <engine>` | Narrow to a specific engine (optional) | `--engine pmd` |
146
+
147
+ **Examples:**
148
+ ```bash
149
+ node "<skill_dir>/scripts/describe-rule.js" "ApexCRUDViolation" --engine pmd
150
+ node "<skill_dir>/scripts/describe-rule.js" "no-var" --engine eslint
151
+ node "<skill_dir>/scripts/describe-rule.js" "OperationWithLimitsInLoop"
152
+ ```
153
+
154
+ ### Output Format
155
+
156
+ **Success — single rule found:**
157
+ ```json
158
+ {
159
+ "status": "success",
160
+ "rule": {
161
+ "name": "ApexCRUDViolation",
162
+ "engine": "pmd",
163
+ "severity": "2 (High)",
164
+ "tags": ["Security", "Recommended", "Apex"],
165
+ "description": "Validates that CRUD and FLS checks are performed before DML operations...",
166
+ "resources": ["https://pmd.github.io/latest/pmd_rules_apex_security.html#apexcrudviolation"]
167
+ }
168
+ }
169
+ ```
170
+
171
+ **Multiple matches (partial name):**
172
+ ```json
173
+ {
174
+ "status": "multiple_matches",
175
+ "message": "Rule \"CRUD\" not found as exact match. Found 3 potential matches:",
176
+ "candidates": [
177
+ { "name": "ApexCRUDViolation", "engine": "pmd", "severity": "2", "tags": "Security, Recommended" },
178
+ ...
179
+ ]
180
+ }
181
+ ```
182
+
183
+ **Not found:**
184
+ ```json
185
+ {
186
+ "status": "not_found",
187
+ "message": "Rule \"FakeRule\" not found. Verify the rule name with: sf code-analyzer rules ..."
188
+ }
189
+ ```
190
+
191
+ ### Presentation Rules
192
+
193
+ **For a successful lookup**, present:
194
+
195
+ ```
196
+ ## Rule: ApexCRUDViolation
197
+
198
+ | Property | Value |
199
+ |----------|-------|
200
+ | Engine | pmd |
201
+ | Severity | 2 (High) |
202
+ | Tags | Security, Recommended, Apex |
203
+
204
+ ### Description
205
+ Validates that CRUD and FLS checks are performed before DML operations. Without these
206
+ checks, data may be accessed or modified without proper user permissions, violating
207
+ the Salesforce security model.
208
+
209
+ ### How to Fix
210
+ [Provide actionable fix guidance based on the description. If the description mentions
211
+ a fix pattern, elaborate. If resources are available, include the link.]
212
+
213
+ ### Resources
214
+ - [PMD Documentation](https://pmd.github.io/...)
215
+
216
+ ---
217
+ Want me to show all violations of this rule in your scan results?
218
+ ```
219
+
220
+ **For multiple matches**, present:
221
+
222
+ ```
223
+ I found multiple rules matching "CRUD":
224
+
225
+ | # | Rule | Engine | Severity |
226
+ |---|------|--------|----------|
227
+ | 1 | ApexCRUDViolation | pmd | 2 (High) |
228
+ | 2 | ... | ... | ... |
229
+
230
+ Which rule would you like details on?
231
+ ```
232
+
233
+ **For not found**, present:
234
+
235
+ ```
236
+ I couldn't find a rule named "FakeRule". Would you like me to:
237
+ - Search for similar rules? (I'll grep the full rule list)
238
+ - List all rules for a specific engine or category?
239
+ ```
240
+
241
+ ### After Describing a Rule
242
+
243
+ Offer next steps:
244
+ - "Want me to show all violations of this rule in your results?" (→ Step 7 with `--rule`)
245
+ - "Want me to apply the engine fix for this rule?" (→ Step 6)
246
+ - "Want me to explain another rule?"
247
+
248
+ ---
249
+
250
+ ## Rule Listing (Step 9)
251
+
252
+ ### Presentation Rules
253
+
254
+ Present available rules in this format:
255
+
256
+ ```
257
+ ## Available Rules: Security
258
+
259
+ **Found X rules** across Y engines.
260
+
261
+ | Engine | Count |
262
+ |--------|-------|
263
+ | pmd | 12 |
264
+ | eslint | 6 |
265
+
266
+ | Severity | Count |
267
+ |----------|-------|
268
+ | Critical (1) | 3 |
269
+ | High (2) | 15 |
270
+
271
+ ### Rules (top 25)
272
+ | # | Rule | Engine | Severity | Tags |
273
+ |---|------|--------|----------|------|
274
+ | 1 | ApexCRUDViolation | pmd | 2 (High) | Security, Recommended |
275
+ | 2 | ApexSOQLInjection | pmd | 1 (Critical) | Security, Recommended |
276
+ | ... |
277
+
278
+ Want me to explain any of these rules? Or run a scan with this selector?
279
+ ```
280
+
281
+ ### Follow-Up Offers
282
+
283
+ After listing rules:
284
+ - "Want me to explain any of these?" (→ Step 8)
285
+ - "Want me to scan with this selector?" (→ Steps 1-5 with the same selector)
286
+ - "Narrow to just high severity?" (re-run with `--severity 1,2`)
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ // Version: v1.1 | SHA256: placeholder
3
+ // Get detailed description and documentation for a Code Analyzer rule
4
+ // Usage: node describe-rule.js <rule-name> [--engine <engine>]
5
+ //
6
+ // This script runs `sf code-analyzer rules --view detail` with a targeted
7
+ // selector and parses the output to extract rule details including description,
8
+ // severity, tags, and documentation resources.
9
+ //
10
+ // The CLI output format is:
11
+ // === 1. RuleName
12
+ // severity: 2 (High)
13
+ // engine: pmd
14
+ // tags: Recommended, Security, Apex
15
+ // resource: https://...
16
+ // description: Some description text
17
+
18
+ const { execSync } = require("child_process");
19
+
20
+ function printUsage() {
21
+ console.error(`Usage: node describe-rule.js <rule-name> [--engine <engine>]
22
+
23
+ Arguments:
24
+ <rule-name> The rule name to look up (case-insensitive partial match)
25
+
26
+ Options:
27
+ --engine <engine> Narrow lookup to a specific engine (pmd, eslint, cpd, etc.)
28
+
29
+ Examples:
30
+ node describe-rule.js ApexCRUDViolation
31
+ node describe-rule.js ApexCRUDViolation --engine pmd
32
+ node describe-rule.js no-var --engine eslint
33
+ node describe-rule.js OperationWithLimitsInLoop`);
34
+ process.exit(1);
35
+ }
36
+
37
+ // Parse CLI arguments
38
+ const args = process.argv.slice(2);
39
+ if (args.length < 1 || args[0] === "--help" || args[0] === "-h") {
40
+ printUsage();
41
+ }
42
+
43
+ const ruleName = args[0];
44
+ let engine = null;
45
+
46
+ for (let i = 1; i < args.length; i++) {
47
+ if (args[i] === "--engine" && args[i + 1]) {
48
+ engine = args[++i].toLowerCase();
49
+ }
50
+ }
51
+
52
+ // Build the rule selector for the lookup
53
+ const selector = engine ? `${engine}:${ruleName}` : ruleName;
54
+
55
+ // Run `sf code-analyzer rules` with --view detail to get full rule info
56
+ let rawOutput;
57
+ try {
58
+ const cmd = `sf code-analyzer rules --rule-selector "${selector}" --view detail 2>&1`;
59
+ rawOutput = execSync(cmd, {
60
+ encoding: "utf8",
61
+ timeout: 60000,
62
+ maxBuffer: 2 * 1024 * 1024,
63
+ });
64
+ } catch (err) {
65
+ // execSync throws on non-zero exit, but we still want the output
66
+ rawOutput = err.stdout || err.stderr || (err.output && err.output.join("")) || "";
67
+ if (!rawOutput) {
68
+ console.log(JSON.stringify({
69
+ status: "error",
70
+ message: `Failed to run sf code-analyzer rules: ${err.message}`,
71
+ }));
72
+ process.exit(0);
73
+ }
74
+ }
75
+
76
+ // Parse the detail view output
77
+ // Format: === N. RuleName\n key: value\n key: value\n
78
+ const rules = parseDetailOutput(rawOutput);
79
+
80
+ if (rules.length === 0) {
81
+ // Try grep fallback for partial/substring match
82
+ const grepResult = tryGrepFallback(ruleName, engine);
83
+ if (grepResult) {
84
+ console.log(JSON.stringify(grepResult));
85
+ process.exit(0);
86
+ }
87
+
88
+ // Try fuzzy match as final fallback (catches typos like "Violtion" → "Violation")
89
+ const fuzzyResult = tryFuzzyFallback(ruleName, engine);
90
+ if (fuzzyResult) {
91
+ console.log(JSON.stringify(fuzzyResult));
92
+ process.exit(0);
93
+ }
94
+
95
+ console.log(JSON.stringify({
96
+ status: "not_found",
97
+ message: `Rule "${ruleName}" not found${engine ? ` in engine "${engine}"` : ""}. Verify the rule name with: sf code-analyzer rules --rule-selector ${engine || "all"} 2>&1 | grep -i "${ruleName}"`,
98
+ }));
99
+ process.exit(0);
100
+ }
101
+
102
+ // Find exact match (case-insensitive)
103
+ const exactMatch = rules.find(
104
+ (r) => r.name.toLowerCase() === ruleName.toLowerCase()
105
+ );
106
+
107
+ if (exactMatch) {
108
+ console.log(JSON.stringify({
109
+ status: "success",
110
+ rule: exactMatch,
111
+ }));
112
+ } else if (rules.length === 1) {
113
+ // Single result, use it
114
+ console.log(JSON.stringify({
115
+ status: "success",
116
+ rule: rules[0],
117
+ }));
118
+ } else {
119
+ // Multiple matches
120
+ console.log(JSON.stringify({
121
+ status: "multiple_matches",
122
+ message: `Found ${rules.length} rules matching "${ruleName}":`,
123
+ candidates: rules.map((r) => ({
124
+ name: r.name,
125
+ engine: r.engine,
126
+ severity: r.severity,
127
+ tags: r.tags.join(", "),
128
+ })),
129
+ }));
130
+ }
131
+
132
+ /**
133
+ * Parse the `sf code-analyzer rules --view detail` output format.
134
+ *
135
+ * Expected format:
136
+ * === 1. RuleName
137
+ * severity: 2 (High)
138
+ * engine: pmd
139
+ * tags: Recommended, Security, Apex
140
+ * resource: https://pmd.github.io/...
141
+ * description: Validates that CRUD permissions...
142
+ */
143
+ function parseDetailOutput(output) {
144
+ const rules = [];
145
+ const lines = output.split("\n");
146
+
147
+ let currentRule = null;
148
+
149
+ for (let i = 0; i < lines.length; i++) {
150
+ const line = lines[i];
151
+
152
+ // Match rule header: === N. RuleName (must have a number prefix)
153
+ // Skip "=== Summary" which is the footer section
154
+ const headerMatch = line.match(/^===\s+(\d+)\.\s+(.+)$/);
155
+ if (headerMatch) {
156
+ if (currentRule && currentRule.name) {
157
+ rules.push(currentRule);
158
+ }
159
+ currentRule = {
160
+ name: headerMatch[2].trim(),
161
+ engine: "unknown",
162
+ severity: "unknown",
163
+ tags: [],
164
+ description: "",
165
+ resources: [],
166
+ };
167
+ continue;
168
+ }
169
+
170
+ // Skip lines if no current rule context
171
+ if (!currentRule) continue;
172
+
173
+ // Match key-value pairs (indented with spaces):
174
+ // severity: 3 (Moderate)
175
+ // engine: eslint
176
+ // tags: Recommended, BestPractices, JavaScript
177
+ // resource: https://...
178
+ // description: Some text here
179
+ const kvMatch = line.match(/^\s{2,}(\w+):\s+(.+)$/);
180
+ if (kvMatch) {
181
+ const key = kvMatch[1].toLowerCase();
182
+ const value = kvMatch[2].trim();
183
+
184
+ switch (key) {
185
+ case "severity":
186
+ currentRule.severity = value;
187
+ break;
188
+ case "engine":
189
+ currentRule.engine = value;
190
+ break;
191
+ case "tags":
192
+ currentRule.tags = value.split(",").map((t) => t.trim()).filter(Boolean);
193
+ break;
194
+ case "resource":
195
+ currentRule.resources.push(value);
196
+ break;
197
+ case "description":
198
+ currentRule.description = value;
199
+ // Description may continue on next lines (indented further)
200
+ while (
201
+ i + 1 < lines.length &&
202
+ lines[i + 1].match(/^\s{14,}/) &&
203
+ !lines[i + 1].match(/^\s{2,}\w+:/)
204
+ ) {
205
+ i++;
206
+ currentRule.description += " " + lines[i].trim();
207
+ }
208
+ break;
209
+ }
210
+ }
211
+ }
212
+
213
+ // Push the last rule
214
+ if (currentRule && currentRule.name) {
215
+ rules.push(currentRule);
216
+ }
217
+
218
+ return rules;
219
+ }
220
+
221
+ /**
222
+ * Fallback: grep the full rule list for partial matches
223
+ */
224
+ function tryGrepFallback(ruleName, engine) {
225
+ try {
226
+ const sel = engine || "Recommended";
227
+ const cmd = `sf code-analyzer rules --rule-selector "${sel}" 2>&1 | grep -i "${ruleName}"`;
228
+ const grepOutput = execSync(cmd, {
229
+ encoding: "utf8",
230
+ timeout: 60000,
231
+ maxBuffer: 2 * 1024 * 1024,
232
+ });
233
+
234
+ if (!grepOutput.trim()) return null;
235
+
236
+ // Parse table output lines
237
+ // Format: index name engine severity tags
238
+ const candidates = grepOutput
239
+ .trim()
240
+ .split("\n")
241
+ .filter((line) => line.trim() && !line.startsWith("─") && !line.startsWith("="))
242
+ .slice(0, 10)
243
+ .map((line) => {
244
+ const parts = line.trim().split(/\s{2,}/);
245
+ // Try to identify which part is the rule name (usually index 1 after the row number)
246
+ if (parts.length >= 4) {
247
+ return {
248
+ name: parts[1] || parts[0],
249
+ engine: parts[2] || "unknown",
250
+ severity: parts[3] || "unknown",
251
+ tags: parts[4] || "",
252
+ };
253
+ }
254
+ return { name: line.trim(), engine: "unknown", severity: "unknown", tags: "" };
255
+ });
256
+
257
+ if (candidates.length === 0) return null;
258
+
259
+ return {
260
+ status: "multiple_matches",
261
+ message: `Rule "${ruleName}" not found as exact match. Found ${candidates.length} potential matches:`,
262
+ candidates,
263
+ };
264
+ } catch (err) {
265
+ return null;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Fuzzy fallback: get all rule names and find closest matches by edit distance.
271
+ * Catches typos like "ApexCRUDVioltion" → "ApexCRUDViolation"
272
+ */
273
+ function tryFuzzyFallback(ruleName, engine) {
274
+ try {
275
+ const sel = engine || "Recommended";
276
+ const cmd = `sf code-analyzer rules --rule-selector "${sel}" 2>&1`;
277
+ const output = execSync(cmd, {
278
+ encoding: "utf8",
279
+ timeout: 60000,
280
+ maxBuffer: 2 * 1024 * 1024,
281
+ });
282
+
283
+ // Extract rule names from table output
284
+ // Lines with rule data have: index name engine severity tags
285
+ const ruleNames = [];
286
+ const ruleInfo = {};
287
+ output.split("\n").forEach((line) => {
288
+ const parts = line.trim().split(/\s{2,}/);
289
+ if (parts.length >= 4 && /^\d+$/.test(parts[0])) {
290
+ const name = parts[1];
291
+ ruleNames.push(name);
292
+ ruleInfo[name] = {
293
+ name: name,
294
+ engine: parts[2] || "unknown",
295
+ severity: parts[3] || "unknown",
296
+ tags: parts[4] || "",
297
+ };
298
+ }
299
+ });
300
+
301
+ if (ruleNames.length === 0) return null;
302
+
303
+ // Score each rule by edit distance to the query
304
+ const queryLower = ruleName.toLowerCase();
305
+ const scored = ruleNames
306
+ .map((name) => ({
307
+ name,
308
+ distance: levenshtein(queryLower, name.toLowerCase()),
309
+ // Also check if query is a subsequence (handles missing chars)
310
+ containsSubseq: isSubsequence(queryLower, name.toLowerCase()),
311
+ }))
312
+ .filter((r) => {
313
+ // Only include if distance is reasonable (within 30% of query length)
314
+ const maxDistance = Math.max(3, Math.floor(ruleName.length * 0.3));
315
+ return r.distance <= maxDistance || r.containsSubseq;
316
+ })
317
+ .sort((a, b) => a.distance - b.distance)
318
+ .slice(0, 5);
319
+
320
+ if (scored.length === 0) return null;
321
+
322
+ const candidates = scored.map((s) => ({
323
+ ...ruleInfo[s.name],
324
+ distance: s.distance,
325
+ }));
326
+
327
+ // If the best match is very close (distance <= 2), mark as likely match
328
+ const best = scored[0];
329
+ if (best.distance <= 2) {
330
+ return {
331
+ status: "multiple_matches",
332
+ message: `Rule "${ruleName}" not found. Did you mean "${best.name}"? (${best.distance} character${best.distance === 1 ? "" : "s"} different)`,
333
+ candidates,
334
+ };
335
+ }
336
+
337
+ return {
338
+ status: "multiple_matches",
339
+ message: `Rule "${ruleName}" not found. Closest matches by name similarity:`,
340
+ candidates,
341
+ };
342
+ } catch (err) {
343
+ return null;
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Levenshtein edit distance between two strings
349
+ */
350
+ function levenshtein(a, b) {
351
+ const m = a.length;
352
+ const n = b.length;
353
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
354
+
355
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
356
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
357
+
358
+ for (let i = 1; i <= m; i++) {
359
+ for (let j = 1; j <= n; j++) {
360
+ if (a[i - 1] === b[j - 1]) {
361
+ dp[i][j] = dp[i - 1][j - 1];
362
+ } else {
363
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
364
+ }
365
+ }
366
+ }
367
+
368
+ return dp[m][n];
369
+ }
370
+
371
+ /**
372
+ * Check if 'query' is a subsequence of 'target' (handles missing chars)
373
+ * e.g., "CRUDVioltion" is a subsequence of "CRUDViolation"
374
+ */
375
+ function isSubsequence(query, target) {
376
+ let qi = 0;
377
+ for (let ti = 0; ti < target.length && qi < query.length; ti++) {
378
+ if (query[qi] === target[ti]) qi++;
379
+ }
380
+ // Consider it a match if at least 80% of query chars appear in order
381
+ return qi >= query.length * 0.8;
382
+ }