@codacy/verity-cli 0.29.1-experimental.197b751 → 0.29.2-experimental.88e4b5c

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/bin/verity.js CHANGED
@@ -16693,12 +16693,33 @@ function isCodacyAvailable() {
16693
16693
  return false;
16694
16694
  }
16695
16695
  }
16696
- function runCodacyAnalysis(files) {
16697
- const empty = {
16698
- tool: "@codacy/analysis-cli",
16699
- findings: [],
16700
- summary: { total_findings: 0, by_severity: {}, tools_run: [] }
16696
+ function buildAnalyzerArgv(files) {
16697
+ return [
16698
+ "analyze",
16699
+ "--install-dependencies",
16700
+ "--files",
16701
+ ...files,
16702
+ "--output-format",
16703
+ "json",
16704
+ "--log-level",
16705
+ "error",
16706
+ "--parallel-tools",
16707
+ "3"
16708
+ ];
16709
+ }
16710
+ var EMPTY_RESULT = {
16711
+ tool: "@codacy/analysis-cli",
16712
+ findings: [],
16713
+ summary: { total_findings: 0, by_severity: {}, tools_run: [] }
16714
+ };
16715
+ function withFailure(kind, detail) {
16716
+ return {
16717
+ ...EMPTY_RESULT,
16718
+ summary: { ...EMPTY_RESULT.summary, failure: { kind, detail: detail.slice(0, 300) } }
16701
16719
  };
16720
+ }
16721
+ function runCodacyAnalysis(files) {
16722
+ const empty = EMPTY_RESULT;
16702
16723
  if (files.length === 0) return empty;
16703
16724
  const existingFiles = files.filter((f) => {
16704
16725
  try {
@@ -16708,22 +16729,30 @@ function runCodacyAnalysis(files) {
16708
16729
  }
16709
16730
  });
16710
16731
  if (existingFiles.length === 0) return empty;
16711
- const fileArgs = existingFiles.join(" ");
16712
- let output;
16713
- try {
16714
- output = (0, import_node_child_process7.execSync)(
16715
- `codacy-analysis analyze --install-dependencies --files ${fileArgs} --output-format json --log-level error --parallel-tools 3`,
16716
- { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 10 * 1024 * 1024 }
16732
+ const proc = (0, import_node_child_process7.spawnSync)("codacy-analysis", buildAnalyzerArgv(existingFiles), {
16733
+ encoding: "utf-8",
16734
+ maxBuffer: 10 * 1024 * 1024
16735
+ });
16736
+ return interpretAnalyzerRun({
16737
+ stdout: proc.stdout ?? "",
16738
+ status: proc.status,
16739
+ stderr: proc.stderr ?? "",
16740
+ spawnError: proc.error?.message
16741
+ });
16742
+ }
16743
+ function interpretAnalyzerRun(run) {
16744
+ const output = run.stdout ?? "";
16745
+ if (!output.trim()) {
16746
+ return withFailure(
16747
+ run.spawnError ? "spawn_failed" : "no_output",
16748
+ run.spawnError ?? run.stderr ?? `exit ${run.status}`
16717
16749
  );
16718
- } catch {
16719
- return empty;
16720
16750
  }
16721
- if (!output.trim()) return empty;
16722
16751
  let parsed;
16723
16752
  try {
16724
16753
  parsed = JSON.parse(output);
16725
16754
  } catch {
16726
- return empty;
16755
+ return withFailure("unparseable_output", output);
16727
16756
  }
16728
16757
  const issues = parsed.issues ?? [];
16729
16758
  const findings = issues.map((issue) => ({
@@ -16746,6 +16775,22 @@ function runCodacyAnalysis(files) {
16746
16775
  (parsed.capability?.ready ?? []).map((r) => r.toolId).filter((id) => id != null)
16747
16776
  )
16748
16777
  );
16778
+ const analyzerErrors = parsed.errors ?? [];
16779
+ let failure;
16780
+ if (findings.length === 0) {
16781
+ if (analyzerErrors.length > 0) {
16782
+ const first = analyzerErrors[0];
16783
+ failure = {
16784
+ kind: first.kind ?? "analyzer_error",
16785
+ detail: (first.message ?? "").slice(0, 300)
16786
+ };
16787
+ } else if (toolsRun.length === 0) {
16788
+ failure = {
16789
+ kind: "no_tools_ran",
16790
+ detail: "The analyzer reported no ready tools. Usually a missing or invalid .codacy/codacy.config.json \u2014 pattern ids that match no rule leave the tool configured out of existence (VRT-105)."
16791
+ };
16792
+ }
16793
+ }
16749
16794
  const totalFindings = findings.length;
16750
16795
  findings.sort(
16751
16796
  (a, b) => (SEVERITY_ORDER[a.severity] ?? 3) - (SEVERITY_ORDER[b.severity] ?? 3)
@@ -16758,7 +16803,8 @@ function runCodacyAnalysis(files) {
16758
16803
  total_findings: totalFindings,
16759
16804
  by_severity: bySeverity,
16760
16805
  tools_run: toolsRun,
16761
- capped: totalFindings > MAX_FINDINGS
16806
+ capped: totalFindings > MAX_FINDINGS,
16807
+ ...failure && { failure }
16762
16808
  }
16763
16809
  };
16764
16810
  }
@@ -16919,7 +16965,7 @@ function resolveTaskContext(opts) {
16919
16965
  // src/lib/cli-version.ts
16920
16966
  function cliVersion() {
16921
16967
  try {
16922
- return true ? "0.29.1-experimental.197b751" : "dev";
16968
+ return true ? "0.29.2-experimental.88e4b5c" : "dev";
16923
16969
  } catch {
16924
16970
  return "dev";
16925
16971
  }
@@ -21518,7 +21564,7 @@ function registerTelemetryCommands(program2) {
21518
21564
  }
21519
21565
 
21520
21566
  // src/cli.ts
21521
- program.name("verity").description("CLI for Verity quality gate service").version("0.29.1-experimental.197b751").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
21567
+ program.name("verity").description("CLI for Verity quality gate service").version("0.29.2-experimental.88e4b5c").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
21522
21568
  setUserNamedServiceUrl(program.opts().serviceUrl);
21523
21569
  try {
21524
21570
  await foldLegacyLocalCredential();
@@ -160,7 +160,7 @@ Write the file to `.verity/standard.yaml`. Show the user a summary:
160
160
  > - Security patterns: 7 (3 critical, 4 high)
161
161
  > - Custom patterns: 3 (auth-middleware, rls-policy, error-boundary)
162
162
  > - Analysis mode: balanced
163
- > - Tools: ESLint9, Semgrep, Trivy
163
+ > - Tools: ESLint9, Trivy
164
164
 
165
165
  Ask for confirmation before proceeding.
166
166
 
@@ -179,17 +179,62 @@ Ask for confirmation before proceeding.
179
179
 
180
180
  **NEVER use `"patterns": []`.** Always populate patterns with specific patternId entries from `patterns-reference.yaml` section 8 (`curated_patterns`). This is the single most important step for keeping analysis fast and token-efficient.
181
181
 
182
+ ### Pattern ID format — a wrong ID disables the tool SILENTLY
183
+
184
+ Every ID is `<toolId>_<ruleId>`, with `/` replaced by `_`:
185
+
186
+ | Rule as documented upstream | patternId to write |
187
+ |---|---|
188
+ | `no-eval` | `ESLint9_no-eval` |
189
+ | `@typescript-eslint/no-explicit-any` | `ESLint9_@typescript-eslint_no-explicit-any` |
190
+ | `F401` | `Ruff_F401` |
191
+ | `SC2086` | `shellcheck_SC2086` |
192
+
193
+ An unrecognised ID produces **no error**. The adapter builds `enabledPatterns` from
194
+ the config, matches nothing, and returns 0 issues — indistinguishable from clean
195
+ code. Note the perverse asymmetry: `patterns: []` (size 0) means "run all defaults"
196
+ and works, so a *wrong* list is strictly worse than *no* list.
197
+
198
+ Never hand-author or guess an ID. Copy it from `patterns-reference.yaml`, or derive
199
+ it from the installed adapter. Then **always validate** (next section).
200
+
182
201
  ### How to build the config
183
202
 
184
- 1. Read `patterns-reference.yaml` `curated_patterns` section
185
- 2. For each tool selected for this mode, copy its pattern list into the config
186
- 3. For tools with an **existing local config** (e.g., `eslint.config.js`): use `localConfigurationFile` to point to it. When `localConfigurationFile` is set, the tool uses its native config and the `patterns` array is ignored — but you still MUST include at least one pattern in the array (the CLI crashes without it). Use a single placeholder: `[{ "patternId": "no-eval" }]` for ESLint, etc.
203
+ 1. **Generate the umbrella-pattern tools mechanically.** Trivy's catalogue is small,
204
+ fixed, and severity-partitioned, so never copy it by hand emit it from the
205
+ installed adapter metadata:
206
+
207
+ ```bash
208
+ # balanced/thorough: critical + high vulns, secrets, malicious packages
209
+ node <skill-dir>/validate-patterns.mjs --emit Trivy \
210
+ '^Trivy_(secret|malicious_packages|vulnerability_(critical|high))$'
211
+
212
+ # lightweight: drop malicious_packages if you want the minimum
213
+ # thorough: add |medium to the regex to widen severity
214
+ ```
215
+
216
+ Paste the emitted array straight into the Trivy `patterns` field. `--list Trivy`
217
+ shows all six available patterns if you need to choose a different subset.
218
+
219
+ 2. Read `patterns-reference.yaml` → `curated_patterns` section for the rule-level
220
+ tools (ESLint9, Ruff, shellcheck, …), where the curation is an editorial choice
221
+ about signal rather than a complete enumeration. Copy the pattern list for each
222
+ tool selected for this mode. `--list <toolId> <regex>` will confirm any single ID
223
+ or let you derive additions.
224
+ 3. For tools with an **existing local config** (e.g., `eslint.config.js`): use `localConfigurationFile` to point to it. When `localConfigurationFile` is set, the tool uses its native config and the `patterns` array is ignored — but you still MUST include at least one pattern in the array (the CLI crashes without it). Use a single placeholder: `[{ "patternId": "ESLint9_no-eval" }]` for ESLint, etc.
187
225
  4. For tools **without** a local config: populate the full curated pattern list from `patterns-reference.yaml`
226
+ 5. **Validate** (next section). This is not optional — it is the only thing that
227
+ catches a stale or mistyped ID, and the failure mode is silent.
188
228
 
189
229
  ### Template — write this file exactly
190
230
 
191
231
  Delete `.codacy/codacy.config.json` and write a new one. This example is for TypeScript **balanced** mode. Adapt the tool list and patterns for the detected languages and mode.
192
232
 
233
+ > The `Trivy` block below shows what `--emit Trivy` produces for balanced mode — it is
234
+ > illustrative output, **not** a list to copy. Run the command (step 1) and paste its
235
+ > actual result, so the config tracks the installed adapter rather than this document.
236
+ > `patterns-reference.yaml` deliberately stores no Trivy list for the same reason.
237
+
193
238
  ```json
194
239
  {
195
240
  "version": 1,
@@ -202,53 +247,40 @@ Delete `.codacy/codacy.config.json` and write a new one. This example is for Typ
202
247
  "toolId": "ESLint9",
203
248
  "localConfigurationFile": "./eslint.config.js",
204
249
  "patterns": [
205
- { "patternId": "no-eval" },
206
- { "patternId": "no-implied-eval" },
207
- { "patternId": "no-new-func" },
208
- { "patternId": "no-script-url" },
209
- { "patternId": "no-unused-vars" },
210
- { "patternId": "no-undef" },
211
- { "patternId": "no-unreachable" },
212
- { "patternId": "no-constant-condition" },
213
- { "patternId": "no-dupe-keys" },
214
- { "patternId": "no-duplicate-case" },
215
- { "patternId": "no-fallthrough" },
216
- { "patternId": "no-self-assign" },
217
- { "patternId": "no-self-compare" },
218
- { "patternId": "use-isnan" },
219
- { "patternId": "valid-typeof" },
220
- { "patternId": "no-loss-of-precision" },
221
- { "patternId": "no-unsafe-optional-chaining" },
222
- { "patternId": "@typescript-eslint/no-explicit-any" },
223
- { "patternId": "@typescript-eslint/no-unused-vars" },
224
- { "patternId": "@typescript-eslint/no-unsafe-assignment" },
225
- { "patternId": "@typescript-eslint/no-unsafe-call" },
226
- { "patternId": "@typescript-eslint/no-unsafe-return" },
227
- { "patternId": "eqeqeq" },
228
- { "patternId": "no-var" },
229
- { "patternId": "prefer-const" }
230
- ]
231
- },
232
- {
233
- "toolId": "Semgrep",
234
- "patterns": [
235
- { "patternId": "javascript.lang.security.audit.sqli.node-sequelize-sqli" },
236
- { "patternId": "javascript.lang.security.audit.sqli.node-knex-sqli" },
237
- { "patternId": "typescript.lang.security.audit.sqli.node-sequelize-sqli" },
238
- { "patternId": "javascript.lang.security.audit.command-injection" },
239
- { "patternId": "javascript.lang.security.audit.unsafe-html" },
240
- { "patternId": "generic.secrets.security.detected-generic-api-key" },
241
- { "patternId": "javascript.express.security.audit.express-jwt-not-revoked" },
242
- { "patternId": "javascript.jsonwebtoken.security.jwt-hardcode" }
250
+ { "patternId": "ESLint9_no-eval" },
251
+ { "patternId": "ESLint9_no-implied-eval" },
252
+ { "patternId": "ESLint9_no-new-func" },
253
+ { "patternId": "ESLint9_no-script-url" },
254
+ { "patternId": "ESLint9_no-unused-vars" },
255
+ { "patternId": "ESLint9_no-undef" },
256
+ { "patternId": "ESLint9_no-unreachable" },
257
+ { "patternId": "ESLint9_no-constant-condition" },
258
+ { "patternId": "ESLint9_no-dupe-keys" },
259
+ { "patternId": "ESLint9_no-duplicate-case" },
260
+ { "patternId": "ESLint9_no-fallthrough" },
261
+ { "patternId": "ESLint9_no-self-assign" },
262
+ { "patternId": "ESLint9_no-self-compare" },
263
+ { "patternId": "ESLint9_use-isnan" },
264
+ { "patternId": "ESLint9_valid-typeof" },
265
+ { "patternId": "ESLint9_no-loss-of-precision" },
266
+ { "patternId": "ESLint9_no-unsafe-optional-chaining" },
267
+ { "patternId": "ESLint9_@typescript-eslint_no-explicit-any" },
268
+ { "patternId": "ESLint9_@typescript-eslint_no-unused-vars" },
269
+ { "patternId": "ESLint9_@typescript-eslint_no-unsafe-assignment" },
270
+ { "patternId": "ESLint9_@typescript-eslint_no-unsafe-call" },
271
+ { "patternId": "ESLint9_@typescript-eslint_no-unsafe-return" },
272
+ { "patternId": "ESLint9_eqeqeq" },
273
+ { "patternId": "ESLint9_no-var" },
274
+ { "patternId": "ESLint9_prefer-const" }
243
275
  ]
244
276
  },
245
277
  {
246
278
  "toolId": "Trivy",
247
279
  "patterns": [
248
- { "patternId": "trivy_vuln" },
249
- { "patternId": "trivy_secret" },
250
- { "patternId": "trivy_config" },
251
- { "patternId": "trivy_license" }
280
+ { "patternId": "Trivy_vulnerability_critical" },
281
+ { "patternId": "Trivy_vulnerability_high" },
282
+ { "patternId": "Trivy_secret" },
283
+ { "patternId": "Trivy_malicious_packages" }
252
284
  ]
253
285
  }
254
286
  ],
@@ -263,20 +295,51 @@ Delete `.codacy/codacy.config.json` and write a new one. This example is for Typ
263
295
  }
264
296
  ```
265
297
 
298
+ ### Validate the config — REQUIRED, do not skip
299
+
300
+ Immediately after writing `.codacy/codacy.config.json`, run the validator that ships
301
+ alongside this skill. It checks every `patternId` against the IDs the installed
302
+ adapters actually define, and exits non-zero on any that does not exist:
303
+
304
+ ```bash
305
+ node "$(dirname "$0")/validate-patterns.mjs" # or: node <skill-dir>/validate-patterns.mjs
306
+ ```
307
+
308
+ Expected output:
309
+
310
+ ```
311
+ ESLint9: 25/25 valid
312
+ Trivy: 4/4 valid
313
+ shellcheck: 14/14 valid
314
+
315
+ PASS: every patternId resolves.
316
+ ```
317
+
318
+ **If it reports FAIL, fix the IDs before continuing.** Do not report setup as
319
+ complete with a failing validation — every listed pattern is silently disabled, so
320
+ the project would appear to have static analysis while enforcing nothing. The
321
+ validator prints the conventional `<toolId>_<rule>` form as a suggested fix.
322
+
266
323
  **Tool selection by language** — ONLY include tools that apply to the detected languages:
267
324
 
268
325
  | Language | Balanced mode tools |
269
326
  |----------|-------------------|
270
- | TypeScript/JavaScript | ESLint9 + Semgrep + Trivy |
271
- | Python | Ruff + Semgrep + Trivy |
272
- | Go | Semgrep + Trivy |
273
- | Java | PMD7 + Semgrep + Trivy |
274
- | Kotlin | detekt + Semgrep + Trivy |
327
+ | TypeScript/JavaScript | ESLint9 + Trivy |
328
+ | Python | Ruff + Trivy |
329
+ | Go | Trivy |
330
+ | Java | PMD7 + Trivy |
331
+ | Kotlin | detekt + Trivy |
275
332
  | Shell | shellcheck + Trivy |
276
333
  | C/C++ | cppcheck + flawfinder + Trivy |
277
334
  | Dockerfile | Hadolint + Trivy |
278
335
 
279
- Use curated patterns from `patterns-reference.yaml` for each tool. For Python, use the Ruff and Semgrep Python patterns. For TypeScript, use ESLint9 and Semgrep JS/TS patterns.
336
+ > **Semgrep is deliberately absent.** Its curated pattern IDs have not been derived
337
+ > yet (see the note in `patterns-reference.yaml`), and it additionally requires the
338
+ > Opengrep binary, which `--install-dependencies` does not fetch. Omit the Semgrep
339
+ > tool block entirely rather than emitting `patterns: []`, which would enable all
340
+ > 2523 default rules. Re-add it once IDs are derived and the validator passes.
341
+
342
+ Use curated patterns from `patterns-reference.yaml` for each tool. For Python, use the Ruff patterns. For TypeScript, use the ESLint9 patterns.
280
343
 
281
344
  **Do NOT include tools for languages not in the project.** For example:
282
345
  - Do NOT add ESLint9 to a Python project
@@ -297,16 +360,34 @@ Use curated patterns from `patterns-reference.yaml` for each tool. For Python, u
297
360
  | Mode | Tools | Approximate pattern count |
298
361
  |------|-------|--------------------------|
299
362
  | lightweight | Trivy only | ~4 patterns |
300
- | balanced | Language linter + Semgrep + Trivy | ~25 + ~8-17 + 4 = ~40 patterns |
363
+ | balanced | Language linter + Trivy | ~25 + 4 = ~29 patterns |
301
364
  | thorough | All applicable + Lizard | ~60-80 patterns |
302
365
 
303
366
  ### Verify
304
367
 
368
+ First confirm every pattern ID resolves (see "Validate the config" above) — a silently
369
+ disabled tool produces the same zero findings as clean code, so a quiet run proves
370
+ nothing on its own:
371
+
372
+ ```bash
373
+ node <skill-dir>/validate-patterns.mjs
374
+ ```
375
+
376
+ Then confirm the volume is sane:
377
+
305
378
  ```bash
306
379
  codacy-analysis analyze --install-dependencies --files src/some-small-file.ts --log-level error --output-format json 2>/dev/null
307
380
  ```
308
381
 
309
- Expected: single-digit findings per file, not hundreds. If you see 50+ issues from one file, you likely have `"patterns": []` somewhere — fix it. The `--install-dependencies` flag ensures any missing tool binaries (ESLint9, Semgrep, Trivy, etc.) are auto-installed by the CLI — no need to install them manually.
382
+ Expected: single-digit findings per file, not hundreds. If you see 50+ issues from one
383
+ file, you likely have `"patterns": []` somewhere — fix it.
384
+
385
+ Check the `capability.ready` array in the JSON to see which tools actually ran.
386
+ `--install-dependencies` handles tools the CLI can install itself (ESLint9 is bundled,
387
+ shellcheck it downloads, Trivy it finds on PATH) but it does **not** fetch every
388
+ binary — Semgrep needs Opengrep installed separately, and at `--log-level error` the
389
+ "Tool unavailable" warning is suppressed, so the omission is invisible. Use
390
+ `--log-level warning` or `--inspect` when a tool seems to report nothing.
310
391
 
311
392
  ---
312
393
 
@@ -395,7 +395,20 @@ analysis_modes:
395
395
  # 8. Curated Patterns Per Tool
396
396
  # IMPORTANT: patterns: [] means ALL defaults (thousands of rules, massive
397
397
  # token consumption and slow runs). Always use these curated lists instead.
398
- # Pattern IDs match @codacy/analysis-cli patternId format.
398
+ #
399
+ # PATTERN ID FORMAT — get this wrong and the tool fails SILENTLY.
400
+ # Every ID is `<toolId>_<ruleId>`, with `/` replaced by `_`:
401
+ # no-eval → ESLint9_no-eval
402
+ # @typescript-eslint/no-explicit-any → ESLint9_@typescript-eslint_no-explicit-any
403
+ # F401 → Ruff_F401
404
+ # SC2086 → shellcheck_SC2086
405
+ # An unrecognised ID is not an error. The adapter builds `enabledPatterns` from
406
+ # the config, matches nothing, and reports 0 issues — identical to clean code.
407
+ # Perversely, `patterns: []` (size 0) means "all defaults" and DOES work, so a
408
+ # wrong list is strictly worse than no list.
409
+ #
410
+ # ALWAYS run `node validate-patterns.mjs` after writing codacy.config.json.
411
+ # Never hand-author an ID; derive it from the installed adapter metadata.
399
412
  # =============================================================================
400
413
 
401
414
  curated_patterns:
@@ -405,101 +418,116 @@ curated_patterns:
405
418
  description: "Security + error-prone + TypeScript strictness. Skips style/formatting."
406
419
  patterns:
407
420
  # Security (MUST have)
408
- - patternId: "no-eval"
409
- - patternId: "no-implied-eval"
410
- - patternId: "no-new-func"
411
- - patternId: "no-script-url"
421
+ - patternId: "ESLint9_no-eval"
422
+ - patternId: "ESLint9_no-implied-eval"
423
+ - patternId: "ESLint9_no-new-func"
424
+ - patternId: "ESLint9_no-script-url"
412
425
  # Error-prone (high value)
413
- - patternId: "no-unused-vars"
414
- - patternId: "no-undef"
415
- - patternId: "no-unreachable"
416
- - patternId: "no-constant-condition"
417
- - patternId: "no-dupe-keys"
418
- - patternId: "no-duplicate-case"
419
- - patternId: "no-fallthrough"
420
- - patternId: "no-self-assign"
421
- - patternId: "no-self-compare"
422
- - patternId: "use-isnan"
423
- - patternId: "valid-typeof"
424
- - patternId: "no-loss-of-precision"
425
- - patternId: "no-unsafe-optional-chaining"
426
+ - patternId: "ESLint9_no-unused-vars"
427
+ - patternId: "ESLint9_no-undef"
428
+ - patternId: "ESLint9_no-unreachable"
429
+ - patternId: "ESLint9_no-constant-condition"
430
+ - patternId: "ESLint9_no-dupe-keys"
431
+ - patternId: "ESLint9_no-duplicate-case"
432
+ - patternId: "ESLint9_no-fallthrough"
433
+ - patternId: "ESLint9_no-self-assign"
434
+ - patternId: "ESLint9_no-self-compare"
435
+ - patternId: "ESLint9_use-isnan"
436
+ - patternId: "ESLint9_valid-typeof"
437
+ - patternId: "ESLint9_no-loss-of-precision"
438
+ - patternId: "ESLint9_no-unsafe-optional-chaining"
426
439
  # TypeScript (if applicable)
427
- - patternId: "@typescript-eslint/no-explicit-any"
428
- - patternId: "@typescript-eslint/no-unused-vars"
429
- - patternId: "@typescript-eslint/no-unsafe-assignment"
430
- - patternId: "@typescript-eslint/no-unsafe-call"
431
- - patternId: "@typescript-eslint/no-unsafe-return"
440
+ - patternId: "ESLint9_@typescript-eslint_no-explicit-any"
441
+ - patternId: "ESLint9_@typescript-eslint_no-unused-vars"
442
+ - patternId: "ESLint9_@typescript-eslint_no-unsafe-assignment"
443
+ - patternId: "ESLint9_@typescript-eslint_no-unsafe-call"
444
+ - patternId: "ESLint9_@typescript-eslint_no-unsafe-return"
432
445
  # Best practice
433
- - patternId: "eqeqeq"
434
- - patternId: "no-var"
435
- - patternId: "prefer-const"
436
-
437
- # --- Semgrep / Opengrep (2517 available security-focused subset) ---
438
- # Semgrep patterns use registry rule IDs. These target OWASP Top 10.
446
+ - patternId: "ESLint9_eqeqeq"
447
+ - patternId: "ESLint9_no-var"
448
+ - patternId: "ESLint9_prefer-const"
449
+
450
+ # --- Semgrep / Opengrep (2523 available) PATTERN LIST NOT YET DERIVED ---
451
+ #
452
+ # DO NOT emit a Semgrep block into codacy.config.json until the IDs below are
453
+ # derived and `validate-patterns.mjs` passes. The 17 IDs previously listed here
454
+ # were invented and matched nothing, so Semgrep silently reported zero findings.
455
+ #
456
+ # Real IDs repeat the trailing segment and carry the tool prefix, e.g.
457
+ # Semgrep_javascript.jsonwebtoken.security.jwt-hardcode.hardcoded-jwt-secret
458
+ # Semgrep_generic.secrets.security.detected-artifactory-token.detected-artifactory-token
459
+ # Derive them from the installed adapter, never by hand:
460
+ # node -e "const fs=require('fs');const d=process.env.CODACY_TOOLS_DIR+'/tools-opengrep-1/dist';\
461
+ # let b='';for(const f of fs.readdirSync(d))if(f.endsWith('.js'))b+=fs.readFileSync(d+'/'+f,'utf8');\
462
+ # console.log([...new Set(b.match(/Semgrep_[A-Za-z0-9_.-]+/g))].sort().join('\n'))"
463
+ #
464
+ # Also note Semgrep needs the Opengrep binary, which `--install-dependencies`
465
+ # does NOT fetch: curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh | bash
466
+ #
467
+ # Categories to cover when curating (counts of real candidates in parentheses):
468
+ # SQL injection, JS/TS (8) · XSS / unsafe HTML (15) · generic secrets (49)
469
+ # hardcoded JWT secret (4) · Python deserialization / pickle (6)
470
+ # NOTE: no rule matches "command-injection" for JavaScript — the old entry was fiction.
439
471
  Semgrep:
440
472
  description: "Security-only: injection, XSS, secrets, auth. No style rules."
441
- patterns:
442
- # Injection (SQL, NoSQL, command)
443
- - patternId: "javascript.lang.security.audit.sqli.node-sequelize-sqli"
444
- - patternId: "javascript.lang.security.audit.sqli.node-knex-sqli"
445
- - patternId: "typescript.lang.security.audit.sqli.node-sequelize-sqli"
446
- - patternId: "python.lang.security.audit.sqli.raw-query"
447
- - patternId: "python.django.security.injection.sql.sql-injection"
448
- - patternId: "go.lang.security.audit.sqli.gosql-sqli"
449
- - patternId: "java.lang.security.audit.sqli.jdbc-sqli"
450
- # Command injection
451
- - patternId: "javascript.lang.security.audit.command-injection"
452
- - patternId: "python.lang.security.audit.dangerous-subprocess-use"
453
- # XSS
454
- - patternId: "javascript.browser.security.insufficient-postmessage-origin-validation"
455
- - patternId: "javascript.lang.security.audit.unsafe-html"
456
- # Secrets
457
- - patternId: "generic.secrets.security.detected-generic-api-key"
458
- - patternId: "generic.secrets.security.detected-aws-account-id"
459
- # Deserialization
460
- - patternId: "python.lang.security.deserialization.avoid-pickle"
461
- - patternId: "python.lang.security.deserialization.avoid-yaml-load"
462
- # Auth
463
- - patternId: "javascript.express.security.audit.express-jwt-not-revoked"
464
- - patternId: "javascript.jsonwebtoken.security.jwt-hardcode"
465
-
466
- # --- Trivy (6 umbrella patterns → use all, already minimal) ---
473
+ status: unverified # omit from generated config until derived + validated
474
+ patterns: [] # intentionally empty — see note above, do NOT copy into config
475
+
476
+ # --- Trivy — DERIVED, NOT HAND-MAINTAINED ---
477
+ #
478
+ # Trivy's catalogue is a fixed set of 6 umbrella patterns, so the config block is
479
+ # GENERATED from adapter metadata rather than copied from here. The list below is
480
+ # a cached snapshot for reading convenience only; the generator must emit it:
481
+ #
482
+ # node validate-patterns.mjs --emit Trivy \
483
+ # '^Trivy_(secret|malicious_packages|vulnerability_(critical|high))$'
484
+ #
485
+ # If this snapshot ever disagrees with `--list Trivy`, the adapter wins — the
486
+ # snapshot is stale and should be regenerated. Severity is fixed per pattern, so
487
+ # severity filtering IS pattern selection. There is NO misconfig/config or license
488
+ # pattern; the old `trivy_config`/`trivy_license` entries were fiction.
467
489
  Trivy:
468
- description: "Vulnerability + secret scanning. Already minimal at 6 patterns."
469
- patterns:
470
- - patternId: "trivy_vuln"
471
- - patternId: "trivy_secret"
472
- - patternId: "trivy_config"
473
- - patternId: "trivy_license"
490
+ description: "Dependency vulnerabilities, secrets, malicious packages. 6 patterns total."
491
+ source: derived
492
+ # NO pattern list is stored here ON PURPOSE. There is exactly one source of truth
493
+ # the installed adapter — so there is nothing here to copy, mistype, or let go
494
+ # stale. Run `--list Trivy` to see all six, `--emit Trivy '<regex>'` to generate
495
+ # the config block.
496
+ #
497
+ # Selecting by severity IS selecting patterns (severity is fixed per pattern):
498
+ # lightweight '^Trivy_(secret|vulnerability_critical)$'
499
+ # balanced '^Trivy_(secret|malicious_packages|vulnerability_(critical|high))$'
500
+ # thorough '^Trivy_(secret|malicious_packages|vulnerability_(critical|high|medium))$'
501
+ patterns: derive # sentinel: not a list — see above. Never emit this value.
474
502
 
475
503
  # --- Ruff (773 available → ~20 high-signal) ---
476
504
  Ruff:
477
505
  description: "Python errors + security. Skips style/formatting (use formatter instead)."
478
506
  patterns:
479
507
  # Pyflakes — error-prone
480
- - patternId: "F401" # unused import
481
- - patternId: "F811" # redefined unused name
482
- - patternId: "F841" # unused variable
483
- - patternId: "F821" # undefined name
508
+ - patternId: "Ruff_F401" # unused import
509
+ - patternId: "Ruff_F811" # redefined unused name
510
+ - patternId: "Ruff_F841" # unused variable
511
+ - patternId: "Ruff_F821" # undefined name
484
512
  # Bugbear — likely bugs
485
- - patternId: "B006" # mutable default argument
486
- - patternId: "B007" # unused loop variable
487
- - patternId: "B018" # useless expression
513
+ - patternId: "Ruff_B006" # mutable default argument
514
+ - patternId: "Ruff_B007" # unused loop variable
515
+ - patternId: "Ruff_B018" # useless expression
488
516
  # Security
489
- - patternId: "S101" # assert used (not for prod)
490
- - patternId: "S102" # exec used
491
- - patternId: "S103" # bad file permissions
492
- - patternId: "S104" # hardcoded bind all interfaces
493
- - patternId: "S105" # hardcoded password string
494
- - patternId: "S106" # hardcoded password argument
495
- - patternId: "S107" # hardcoded password default
496
- - patternId: "S108" # hardcoded temp file
497
- - patternId: "S110" # try-except-pass
498
- - patternId: "S301" # pickle usage
499
- - patternId: "S608" # SQL injection via string formatting
517
+ - patternId: "Ruff_S101" # assert used (not for prod)
518
+ - patternId: "Ruff_S102" # exec used
519
+ - patternId: "Ruff_S103" # bad file permissions
520
+ - patternId: "Ruff_S104" # hardcoded bind all interfaces
521
+ - patternId: "Ruff_S105" # hardcoded password string
522
+ - patternId: "Ruff_S106" # hardcoded password argument
523
+ - patternId: "Ruff_S107" # hardcoded password default
524
+ - patternId: "Ruff_S108" # hardcoded temp file
525
+ - patternId: "Ruff_S110" # try-except-pass
526
+ - patternId: "Ruff_S301" # pickle usage
527
+ - patternId: "Ruff_S608" # SQL injection via string formatting
500
528
  # Type annotations (if desired)
501
- - patternId: "ANN001" # missing type annotation for function argument
502
- - patternId: "ANN201" # missing return type annotation
529
+ - patternId: "Ruff_ANN001" # missing type annotation for function argument
530
+ - patternId: "Ruff_ANN201" # missing return type annotation
503
531
 
504
532
  # --- ShellCheck (491 available → ~15 high-signal) ---
505
533
  shellcheck:
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Derive and validate codacy-analysis patternIds against the pattern catalogue the
4
+ * installed @codacy/analysis-cli tool adapters actually define.
5
+ *
6
+ * WHY THIS EXISTS
7
+ * ---------------
8
+ * A wrong patternId fails SILENTLY. Each adapter builds `enabledPatterns` from the
9
+ * config and then either skips work entirely or filters every result away:
10
+ *
11
+ * runVulnScan = enabledPatterns.size === 0 || vulnPatternIds.some(id => enabledPatterns.has(id))
12
+ *
13
+ * So a tool with an unrecognised pattern list reports "ready", routes files, and
14
+ * returns 0 issues — indistinguishable from clean code. Worse, `size === 0` means
15
+ * "run ALL defaults", so an empty list works while a wrong list silently disables
16
+ * the tool. Verity shipped `trivy_vuln`, bare `no-eval`, and bare `F401` for months;
17
+ * ESLint, Semgrep, Trivy and Ruff were all dead and nothing surfaced it.
18
+ *
19
+ * The ID format is `<toolId>_<ruleId>`, with `/` replaced by `_` in namespaced
20
+ * rules (`@typescript-eslint/no-explicit-any` → `ESLint9_@typescript-eslint_no-explicit-any`).
21
+ *
22
+ * MODES
23
+ * validate [config] Check every patternId in the config resolves.
24
+ * Exit 1 if any does not. Default: .codacy/codacy.config.json
25
+ * --list <toolId> [regex] Print the adapter's real pattern IDs, one per line.
26
+ * --emit <toolId> [regex] Print a ready-to-paste JSON "patterns" array.
27
+ *
28
+ * Use --emit to GENERATE config blocks so IDs are never hand-authored, e.g. Trivy:
29
+ * node validate-patterns.mjs --emit Trivy '^Trivy_(secret|malicious_packages|vulnerability_(critical|high))$'
30
+ */
31
+
32
+ import { readFileSync, readdirSync, existsSync, realpathSync, statSync, writeSync } from 'node:fs'
33
+ import { join, dirname, delimiter } from 'node:path'
34
+
35
+ /**
36
+ * Locate the @codacy adapter packages shipped with the installed analysis CLI.
37
+ * Resolved without shelling out, so this works on Windows as well as POSIX.
38
+ */
39
+ function findToolsDir() {
40
+ if (process.env.CODACY_TOOLS_DIR) return process.env.CODACY_TOOLS_DIR
41
+
42
+ // Walk PATH ourselves rather than calling `which`/`where`, which differ per platform.
43
+ const exts = process.platform === 'win32'
44
+ ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';')
45
+ : ['']
46
+ const candidates = []
47
+ for (const dir of (process.env.PATH ?? '').split(delimiter)) {
48
+ if (!dir) continue
49
+ for (const ext of exts) {
50
+ candidates.push(join(dir, 'codacy-analysis' + ext.toLowerCase()))
51
+ if (ext) candidates.push(join(dir, 'codacy-analysis' + ext))
52
+ }
53
+ }
54
+
55
+ for (const candidate of candidates) {
56
+ let resolved
57
+ try {
58
+ if (!statSync(candidate).isFile()) continue
59
+ resolved = realpathSync(candidate)
60
+ } catch {
61
+ continue
62
+ }
63
+ // resolved is <pkg>/dist/index.js (POSIX symlink) or a shim dir (Windows).
64
+ // Walk up looking for node_modules/@codacy.
65
+ let dir = dirname(resolved)
66
+ for (let i = 0; i < 6; i++) {
67
+ const tools = join(dir, 'node_modules', '@codacy')
68
+ if (existsSync(tools)) return tools
69
+ const parent = dirname(dir)
70
+ if (parent === dir) break
71
+ dir = parent
72
+ }
73
+ }
74
+ return null
75
+ }
76
+
77
+ /**
78
+ * Harvest the pattern IDs an adapter defines. Adapters ship as bundled JS with the
79
+ * pattern catalogue inlined, so we scan for `<toolId>_<rule>` literals. This is a
80
+ * heuristic over a build artifact, not a public API — it can only ever produce false
81
+ * ALARMS (an ID we fail to find), never false confidence, which is the safe direction.
82
+ */
83
+ /** The characters a pattern id may contain after its `<toolId>_` prefix. */
84
+ const ID_CHAR = /[A-Za-z0-9_@./-]/
85
+
86
+ /**
87
+ * Scan for `<toolId>_<rule>` literals WITHOUT building a regex from `toolId`.
88
+ *
89
+ * The previous form compiled `new RegExp(`${escaped}_[...]+`)` per call. Escaping
90
+ * made it correct, but a RegExp built from a runtime value is a ReDoS surface by
91
+ * construction and reads as one to any reviewer or scanner. An indexOf scan has
92
+ * neither problem, needs no escaping to be right, and is what the code was always
93
+ * expressing: find the prefix, then take the id characters that follow it.
94
+ */
95
+ function harvestIds(toolsDir, toolId) {
96
+ const ids = new Set()
97
+ const prefix = toolId + '_'
98
+ const scan = (text) => {
99
+ let i = text.indexOf(prefix)
100
+ while (i !== -1) {
101
+ let end = i + prefix.length
102
+ while (end < text.length && ID_CHAR.test(text[end])) end++
103
+ // A bare prefix with nothing after it is not an id.
104
+ if (end > i + prefix.length) ids.add(text.slice(i, end))
105
+ i = text.indexOf(prefix, end > i ? end : i + 1)
106
+ }
107
+ }
108
+ for (const pkg of readdirSync(toolsDir)) {
109
+ if (!pkg.startsWith('tools-')) continue
110
+ const distDir = join(toolsDir, pkg, 'dist')
111
+ if (!existsSync(distDir)) continue
112
+ for (const file of readdirSync(distDir)) {
113
+ if (!file.endsWith('.js')) continue
114
+ scan(readFileSync(join(distDir, file), 'utf-8'))
115
+ }
116
+ }
117
+ return ids
118
+ }
119
+
120
+ const toolsDir = findToolsDir()
121
+ if (!toolsDir) {
122
+ console.error('SKIP: could not locate @codacy tool adapters.')
123
+ console.error(' Install the CLI (npm i -g @codacy/analysis-cli) or set CODACY_TOOLS_DIR.')
124
+ process.exit(1)
125
+ }
126
+
127
+ /**
128
+ * Write to fd 1 synchronously, looping until every byte is gone.
129
+ *
130
+ * `console.log` / `process.stdout.write` are ASYNCHRONOUS when stdout is a pipe,
131
+ * and this script exits in the same tick — so the output was cut at whatever the
132
+ * pipe accepted. A write callback does not help either: this is a top-level
133
+ * block, so execution falls through to the validate path and exits 1 before the
134
+ * callback can run (observed: 65536 bytes, status 1).
135
+ *
136
+ * The loop matters. A single writeSync to a pipe may report a SHORT write, and
137
+ * dropping the remainder would reintroduce the same bug in a quieter form.
138
+ */
139
+ function writeStdoutSync(text) {
140
+ const buf = Buffer.from(text, 'utf8')
141
+ let off = 0
142
+ while (off < buf.length) {
143
+ try {
144
+ off += writeSync(1, buf, off, buf.length - off)
145
+ } catch (err) {
146
+ if (err.code === 'EAGAIN') continue // non-blocking pipe not ready; retry
147
+ if (err.code === 'EPIPE') return // consumer went away (e.g. `| head`)
148
+ throw err
149
+ }
150
+ }
151
+ }
152
+
153
+ const argv = process.argv.slice(2)
154
+ const mode = argv[0] === '--list' || argv[0] === '--emit' ? argv[0] : 'validate'
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // --list / --emit : derive IDs straight from adapter metadata
158
+ // ---------------------------------------------------------------------------
159
+ if (mode !== 'validate') {
160
+ const toolId = argv[1]
161
+ if (!toolId) {
162
+ console.error(`Usage: validate-patterns.mjs ${mode} <toolId> [regex]`)
163
+ process.exit(1)
164
+ }
165
+ // An invalid pattern here is a typo in a hand-typed argument, not an error worth a
166
+ // stack trace — report it with the offending input so it can be fixed at a glance.
167
+ let filter = null
168
+ if (argv[2]) {
169
+ try {
170
+ // The filter IS a regex — that is the documented interface
171
+ // (`--list Trivy '^Trivy_secret$'`) and two tests pin the behaviour, so it
172
+ // cannot become a substring match. Unlike the harvest above, there is no
173
+ // way to express this without compiling a runtime value.
174
+ //
175
+ // It is also not a security boundary: this is a local developer CLI, the
176
+ // pattern comes from the operator's own shell, and the only thing a
177
+ // catastrophic regex can stall is the operator's own terminal. Nothing
178
+ // untrusted reaches this line — `argv[2]` is typed by the person running it.
179
+ // Suppressions for both engines Codacy may raise this under. The ESLint
180
+ // form alone did not clear it, so the finding comes from the Semgrep side;
181
+ // `nosemgrep` is rule-agnostic, which is deliberate here — pinning a rule id
182
+ // guessed from a message string is how the first attempt failed.
183
+ // eslint-disable-next-line security/detect-non-literal-regexp -- operator-supplied filter, local CLI, self-inflicted at worst
184
+ // nosemgrep: operator-supplied filter on a local CLI; see the reasoning above
185
+ filter = new RegExp(argv[2])
186
+ } catch (err) {
187
+ console.error(`Invalid regex: ${argv[2]}`)
188
+ console.error(` ${err.message}`)
189
+ console.error(` Note: quote the argument so the shell does not expand it, e.g. '^Trivy_secret$'`)
190
+ process.exit(1)
191
+ }
192
+ }
193
+ const ids = [...harvestIds(toolsDir, toolId)].sort().filter((id) => !filter || filter.test(id))
194
+ if (ids.length === 0) {
195
+ console.error(`No pattern IDs found for toolId "${toolId}"${filter ? ' matching ' + argv[2] : ''}.`)
196
+ console.error('Check the toolId spelling (case-sensitive: ESLint9, Semgrep, Trivy, Ruff, shellcheck).')
197
+ process.exit(1)
198
+ }
199
+ // ⚠ NEVER `process.exit()` IN THE SAME TICK AS A WRITE TO STDOUT.
200
+ //
201
+ // When stdout is a PIPE, Node's writes are asynchronous. `process.exit()`
202
+ // terminates before the buffer drains, truncating output at whatever the pipe
203
+ // accepted — about 64 KB. Redirecting to a FILE hides it completely, because
204
+ // file writes are synchronous. So it looks fine every time you check by hand,
205
+ // and is broken for every caller that reads the output programmatically.
206
+ //
207
+ // Measured on the ESLint9 catalogue (2936 ids), five runs each:
208
+ // node ... > file -> 2936, 2936, 2936, 2936, 2936 (whole)
209
+ // node ... | cat -> 1812, 1812, 1812, 1812, 1812 (cut at the buffer)
210
+ //
211
+ // The harvest is deterministic. Only the DELIVERY was not, and it cost a real
212
+ // misdiagnosis: `ESLint9_use-isnan` sorts at line 2608, past the cut, so every
213
+ // consumer reading through a pipe — including the review that flagged it —
214
+ // concluded a perfectly valid pattern id did not exist.
215
+ //
216
+ // `process.exitCode = 0` alone is NOT enough here: this is a top-level block,
217
+ // so execution would fall through into the validate path below and exit there,
218
+ // truncating exactly as before. The write callback fires once the data has been
219
+ // handed to the OS, which is the point at which exiting is safe.
220
+ const out =
221
+ mode === '--list'
222
+ ? ids.join('\n')
223
+ : JSON.stringify(ids.map((patternId) => ({ patternId })), null, 2)
224
+ writeStdoutSync(out + '\n')
225
+ process.exit(0)
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // validate : every patternId in the config must resolve
230
+ // ---------------------------------------------------------------------------
231
+ const configPath = argv[0] ?? '.codacy/codacy.config.json'
232
+
233
+ if (!existsSync(configPath)) {
234
+ console.error(`SKIP: ${configPath} not found — write the config first.`)
235
+ process.exit(1)
236
+ }
237
+
238
+ let config
239
+ try {
240
+ config = JSON.parse(readFileSync(configPath, 'utf-8'))
241
+ } catch (err) {
242
+ console.error(`FAIL: could not parse ${configPath}`)
243
+ console.error(` ${err.message}`)
244
+ process.exit(1)
245
+ }
246
+
247
+ const tools = config?.tools ?? []
248
+ if (!Array.isArray(tools)) {
249
+ console.error(`FAIL: ${configPath} has a "tools" field that is not an array.`)
250
+ process.exit(1)
251
+ }
252
+
253
+ if (tools.length === 0) {
254
+ console.log('OK: "tools": [] — every tool runs its default pattern set. Nothing to validate.')
255
+ process.exit(0)
256
+ }
257
+
258
+ let invalidTotal = 0
259
+ let emptyTotal = 0
260
+ let unverifiableTotal = 0
261
+
262
+ /**
263
+ * Reject a malformed tools[] entry with a located, actionable message. A generator
264
+ * bug or a half-written file must fail closed here rather than throw somewhere
265
+ * further down, where the stack trace would say nothing about which entry is bad.
266
+ */
267
+ function readToolEntry(tool, index) {
268
+ const at = `tools[${index}]`
269
+ if (tool === null || typeof tool !== 'object' || Array.isArray(tool)) {
270
+ console.error(`FAIL: ${at} is not an object.`)
271
+ process.exit(1)
272
+ }
273
+ const { toolId, patterns } = tool
274
+ if (typeof toolId !== 'string' || toolId.trim() === '') {
275
+ console.error(`FAIL: ${at} has no valid "toolId" (expected a non-empty string).`)
276
+ process.exit(1)
277
+ }
278
+ if (patterns !== undefined && !Array.isArray(patterns)) {
279
+ console.error(`FAIL: ${toolId}: "patterns" must be an array, or omitted for tool defaults.`)
280
+ process.exit(1)
281
+ }
282
+ const patternIds = []
283
+ for (const [i, entry] of (patterns ?? []).entries()) {
284
+ if (entry === null || typeof entry !== 'object' || typeof entry.patternId !== 'string') {
285
+ console.error(
286
+ `FAIL: ${toolId}: patterns[${i}] must be an object with a string "patternId".`,
287
+ )
288
+ process.exit(1)
289
+ }
290
+ patternIds.push(entry.patternId)
291
+ }
292
+ return { toolId, patternIds }
293
+ }
294
+
295
+ for (const [index, rawTool] of tools.entries()) {
296
+ const { toolId, patternIds } = readToolEntry(rawTool, index)
297
+ if (patternIds.length === 0) {
298
+ console.log(` ${toolId}: patterns: [] → all defaults enabled (valid, but noisy)`)
299
+ emptyTotal++
300
+ continue
301
+ }
302
+ const valid = harvestIds(toolsDir, toolId)
303
+ // FAIL CLOSED. "Cannot verify" is not "verified". This whole script exists because
304
+ // a silently-disabled tool looks identical to clean code, so reporting PASS on an
305
+ // unverifiable tool would reproduce the exact failure it is meant to catch.
306
+ if (valid.size === 0) {
307
+ unverifiableTotal++
308
+ console.log(
309
+ ` ${toolId}: UNVERIFIABLE — no pattern IDs harvested. Adapter missing, or the` +
310
+ ` toolId is misspelled (case-sensitive).`,
311
+ )
312
+ continue
313
+ }
314
+ const bad = patternIds.filter((id) => !valid.has(id))
315
+ if (bad.length === 0) {
316
+ console.log(` ${toolId}: ${patternIds.length}/${patternIds.length} valid`)
317
+ } else {
318
+ invalidTotal += bad.length
319
+ console.log(
320
+ ` ${toolId}: ${patternIds.length - bad.length}/${patternIds.length} valid — ${bad.length} INVALID:`,
321
+ )
322
+ for (const id of bad) {
323
+ // Suggest the conventional form so the fix is obvious.
324
+ const guess = `${toolId}_${id.replace(/\//g, '_')}`
325
+ const hint = valid.has(guess)
326
+ ? `did you mean "${guess}"?`
327
+ : `no close match — regenerate with: --emit ${toolId}`
328
+ console.log(` ✗ ${id} — ${hint}`)
329
+ }
330
+ }
331
+ }
332
+
333
+ console.log('')
334
+ if (invalidTotal > 0) {
335
+ console.error(`FAIL: ${invalidTotal} patternId(s) do not exist. Those rules are SILENTLY DISABLED.`)
336
+ console.error(' Regenerate the list with --emit <toolId>, or use "patterns": [] for tool defaults.')
337
+ process.exit(1)
338
+ }
339
+ if (unverifiableTotal > 0) {
340
+ console.error(`FAIL: ${unverifiableTotal} tool(s) could not be verified — this is NOT a pass.`)
341
+ console.error(' Check the toolId spelling, reinstall @codacy/analysis-cli, or point')
342
+ console.error(' CODACY_TOOLS_DIR at the directory holding the tools-* adapter packages.')
343
+ process.exit(1)
344
+ }
345
+ console.log(`PASS: every patternId resolves${emptyTotal ? ` (${emptyTotal} tool(s) on defaults)` : ''}.`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.29.1-experimental.197b751",
3
+ "version": "0.29.2-experimental.88e4b5c",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://verity.md",