renovate-mcp 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,6 +31,7 @@ Restart your client and try the prompt: *"List the namespaces available under `r
31
31
  ## What you can do
32
32
 
33
33
  - **Read and explain configs** — locate the active `renovate.json*`, expand every `extends` preset offline, and trace which preset set each field.
34
+ - **Discover presets from intent** — describe what you want ("automerge patches, group dev deps") and get ranked built-in (and your own local-repo) presets, plus a draft config skeleton when nothing fits.
34
35
  - **Preview custom managers** before running Renovate — regex and JSONata, with file/line hits and extracted dep info.
35
36
  - **Validate and lint** — schema validation plus a semantic lint pass for Renovate-specific footguns (unwrapped regexes, unknown manager names, deprecated keys).
36
37
  - **Dry-run** against a local checkout or a remote GitHub/GitLab — see exactly which PRs Renovate would open.
@@ -38,21 +39,24 @@ Restart your client and try the prompt: *"List the namespaces available under `r
38
39
 
39
40
  ## Tools & resources
40
41
 
41
- Thirteen tools and three resource templates. Each tool name below links to its full reference in [`docs/tools.md`](docs/tools.md).
42
+ Sixteen tools and three resource templates. Each tool name below links to its full reference in [`docs/tools.md`](docs/tools.md).
42
43
 
43
44
  | Tool | Purpose |
44
45
  | --- | --- |
45
46
  | [`check_setup`](docs/tools.md#check_setup) | Report Renovate CLI + validator availability, versions, install hints, and a `platformContext` block for env diagnosis. Also runs at startup. Pass an optional `repoPath` to add a `repoContext` block diagnosing the repo's git origin, config endpoint/platform, endpoint reachability, and token coverage. |
46
47
  | [`get_version`](docs/tools.md#get_version) | Report the renovate-mcp server version and whether it's a released or local/dev build. |
47
48
  | [`read_config`](docs/tools.md#read_config) | Locate and parse a repo's Renovate config in Renovate's own discovery order. |
49
+ | [`suggest_presets`](docs/tools.md#suggest_presets) | Search built-in (and optional local-repo) presets by natural-language intent; ranks matches and sketches an unvalidated draft config skeleton for multi-facet or weakly-covered intents. Offline. |
48
50
  | [`resolve_config`](docs/tools.md#resolve_config) | Expand every `extends` preset offline. Opt in to fetching `github>` / `gitlab>` presets over HTTPS. |
49
51
  | [`explain_config`](docs/tools.md#explain_config) | Inverse of `resolve_config`: annotate every leaf field with the chain of presets that set it. |
50
52
  | [`resolve_config_diff`](docs/tools.md#resolve_config_diff) | Offline structural diff of two fully-resolved configs — changed fields plus an order-insensitive set diff of array keys (`packageRules`, `customManagers`, …). The refactor-friendly counterpart to `dry_run_diff`. |
53
+ | [`test_package_rules`](docs/tools.md#test_package_rules) | Offline what-if: which `packageRules` match a hypothetical dependency, which matcher decided each, and what each contributes. Faithful (Renovate's real matchers in a worker); fields you don't supply are reported as unevaluatable, not non-matches. |
51
54
  | [`preview_custom_manager`](docs/tools.md#preview_custom_manager) | Preview a `customManagers` entry (regex or JSONata) against a local repo. Offline. |
52
55
  | [`validate_config`](docs/tools.md#validate_config) | Run `renovate-config-validator` against a file or inline object. |
53
56
  | [`lint_config`](docs/tools.md#lint_config) | Semantic lint pass for Renovate-specific footguns the schema validator declares valid. Offline. |
54
57
  | [`dry_run`](docs/tools.md#dry_run) | Run Renovate with `--dry-run` and return the structured JSON report. Local-by-default; remote with `platform` + `endpoint` + `token` + `repository`. No PRs, no pushes. |
55
58
  | [`dry_run_diff`](docs/tools.md#dry_run_diff) | Stateless semantic diff between two `dry_run` reports — added / removed / changed updates. |
59
+ | [`annotate_dry_run`](docs/tools.md#annotate_dry_run) | Attribute each proposed update in a `dry_run` report to the `packageRules` that caused it; flags rules that never matched and report fields the matchers needed but the report lacked. Stateless + offline. |
56
60
  | [`migrate_config`](docs/tools.md#migrate_config) | Apply Renovate's built-in migrations and return the migrated config plus a unified diff. Does not write. |
57
61
  | [`write_config`](docs/tools.md#write_config) | Validate, then atomically write a config to disk. Preserves comments/key order on existing JSON-with-comments files. |
58
62
  | [`renovate://presets`](docs/tools.md#renovatepresets) (resource) | Markdown index of all built-in presets grouped by namespace. |
@@ -64,7 +68,7 @@ Thirteen tools and three resource templates. Each tool name below links to its f
64
68
  - **Linux or macOS.** Windows is not supported — `package.json` declares `"os": ["darwin", "linux"]`, so `npm i` surfaces an `EBADPLATFORM` warning on Windows and the server exits with a clear stderr message at startup. Use WSL2 or a Linux/macOS host instead.
65
69
  - **Node.js ≥ 24** (aligns with Renovate's own engine requirement).
66
70
 
67
- Renovate ships bundled — the `renovate` package is a runtime dependency, so `validate_config`, `dry_run`, and `write_config` work out of the box with no separate install. The offline tools (`read_config`, `resolve_config`, `explain_config`, `resolve_config_diff`, `preview_custom_manager`, `lint_config`) never spawn Renovate at all.
71
+ Renovate ships bundled — the `renovate` package is a runtime dependency, so `validate_config`, `dry_run`, and `write_config` work out of the box with no separate install. The offline tools (`read_config`, `suggest_presets`, `resolve_config`, `explain_config`, `resolve_config_diff`, `preview_custom_manager`, `lint_config`) never spawn Renovate at all.
68
72
 
69
73
  **Optional env vars:**
70
74
 
@@ -125,11 +129,12 @@ Once the server is wired up, try prompts like these. Written for Claude but work
125
129
  - "Resolve my config and list anything that landed in `presetsUnresolved`, with the reason for each."
126
130
  - "Why is my `prCreation` set to `not-pending`? Use `explain_config` to trace which preset set it."
127
131
 
128
- **Browsing presets**
132
+ **Discovering & browsing presets**
129
133
 
134
+ - "I want to automerge patch and minor updates and group my dev dependencies — suggest presets for that, search my `../renovate-presets` repo too, and draft a config I can validate."
130
135
  - "List the presets in the `config` namespace." (uses the `renovate://presets/config` sub-resource — cheaper than pulling the whole index)
131
136
  - "What does `config:recommended` actually enable? Show me its expanded JSON."
132
- - "Find a built-in preset that pins GitHub Actions digests."
137
+ - "Find a built-in preset that pins GitHub Actions digests." (or use `suggest_presets` with that intent)
133
138
 
134
139
  **Self-hosted GitLab / GitHub Enterprise** (env set per [Platform setup](#platform-setup))
135
140
 
@@ -1,4 +1,4 @@
1
- export declare const RENOVATE_VERSION = "43.233.4";
1
+ export declare const RENOVATE_VERSION = "43.245.0";
2
2
  /**
3
3
  * All manager names accepted by Renovate's `matchManagers` /
4
4
  * `excludeManagers` fields. Includes both regular managers and custom
@@ -1,6 +1,6 @@
1
- // AUTO-GENERATED by scripts/generate-managers.mjs from renovate@43.233.4.
1
+ // AUTO-GENERATED by scripts/generate-managers.mjs from renovate@43.245.0.
2
2
  // Do not edit by hand. Run `npm run generate:managers` to refresh.
3
- export const RENOVATE_VERSION = "43.233.4";
3
+ export const RENOVATE_VERSION = "43.245.0";
4
4
  /**
5
5
  * All manager names accepted by Renovate's `matchManagers` /
6
6
  * `excludeManagers` fields. Includes both regular managers and custom
@@ -1,4 +1,4 @@
1
- export declare const RENOVATE_VERSION = "43.233.4";
1
+ export declare const RENOVATE_VERSION = "43.245.0";
2
2
  export interface DeprecatedKeyEntry {
3
3
  /** Key as it appeared in old configs. */
4
4
  readonly oldKey: string;
@@ -1,6 +1,6 @@
1
- // AUTO-GENERATED by scripts/generate-migrations.mjs from renovate@43.233.4.
1
+ // AUTO-GENERATED by scripts/generate-migrations.mjs from renovate@43.245.0.
2
2
  // Do not edit by hand. Run `npm run generate:migrations` to refresh.
3
- export const RENOVATE_VERSION = "43.233.4";
3
+ export const RENOVATE_VERSION = "43.245.0";
4
4
  /**
5
5
  * Deprecated config keys with their current replacement. Sourced from
6
6
  * Renovate's `MigrationsService.renamedProperties` — the "RenamePropertyMigration"
@@ -3,6 +3,6 @@ export interface GeneratedPreset {
3
3
  description: string | null;
4
4
  body: Record<string, unknown>;
5
5
  }
6
- export declare const RENOVATE_VERSION = "43.233.4";
6
+ export declare const RENOVATE_VERSION = "43.245.0";
7
7
  export declare const PRESETS: Record<string, GeneratedPreset>;
8
8
  export declare const PRESET_NAMES: readonly string[];
@@ -1,6 +1,6 @@
1
- // AUTO-GENERATED by scripts/generate-presets.mjs from renovate@43.233.4.
1
+ // AUTO-GENERATED by scripts/generate-presets.mjs from renovate@43.245.0.
2
2
  // Do not edit by hand. Run `npm run generate:presets` to refresh.
3
- export const RENOVATE_VERSION = "43.233.4";
3
+ export const RENOVATE_VERSION = "43.245.0";
4
4
  export const PRESETS = {
5
5
  "abandonments:recommended": {
6
6
  "namespace": "abandonments",
@@ -17861,6 +17861,7 @@ export const PRESETS = {
17861
17861
  "replacements:actions-attest-build-provenance-to-actions-attest",
17862
17862
  "replacements:airbnb-prop-types-to-prop-types-tools",
17863
17863
  "replacements:apollo-server-to-scoped",
17864
+ "replacements:aws-sdk-smithy-client-to-smithy",
17864
17865
  "replacements:babel-eslint-to-eslint-parser",
17865
17866
  "replacements:base-ui-components-to-base-ui",
17866
17867
  "replacements:containerbase",
@@ -18011,6 +18012,24 @@ export const PRESETS = {
18011
18012
  ]
18012
18013
  }
18013
18014
  },
18015
+ "replacements:aws-sdk-smithy-client-to-smithy": {
18016
+ "namespace": "replacements",
18017
+ "description": "`@aws-sdk/smithy-client` was moved to `@smithy/smithy-client`.",
18018
+ "body": {
18019
+ "packageRules": [
18020
+ {
18021
+ "matchDatasources": [
18022
+ "npm"
18023
+ ],
18024
+ "matchPackageNames": [
18025
+ "@aws-sdk/smithy-client"
18026
+ ],
18027
+ "replacementName": "@smithy/smithy-client",
18028
+ "replacementVersion": "1.0.5"
18029
+ }
18030
+ ]
18031
+ }
18032
+ },
18014
18033
  "replacements:babel-eslint-to-eslint-parser": {
18015
18034
  "namespace": "replacements",
18016
18035
  "description": "`babel-eslint` was renamed under the `@babel` scope.",
package/dist/index.js CHANGED
@@ -8,9 +8,12 @@ import { registerLintConfig } from "./tools/lintConfig.js";
8
8
  import { registerResolveConfig } from "./tools/resolveConfig.js";
9
9
  import { registerExplainConfig } from "./tools/explainConfig.js";
10
10
  import { registerResolveConfigDiff } from "./tools/resolveConfigDiff.js";
11
+ import { registerSuggestPresets } from "./tools/suggestPresets.js";
11
12
  import { registerPreviewCustomManager } from "./tools/previewCustomManager.js";
12
13
  import { registerDryRun } from "./tools/dryRun.js";
13
14
  import { registerDryRunDiff } from "./tools/dryRunDiff.js";
15
+ import { registerTestPackageRules } from "./tools/testPackageRules.js";
16
+ import { registerAnnotateDryRun } from "./tools/annotateDryRun.js";
14
17
  import { registerWriteConfig } from "./tools/writeConfig.js";
15
18
  import { registerMigrateConfig } from "./tools/migrateConfig.js";
16
19
  import { registerGetVersion } from "./tools/getVersion.js";
@@ -32,16 +35,19 @@ const BASE_INSTRUCTIONS = [
32
35
  "",
33
36
  "Workflow:",
34
37
  " 1. read_config — inspect the current config in a repo",
35
- " 2. resolve_config — expand built-in presets to see what a config actually becomes (offline)",
36
- " 3. explain_config — inverse of resolve_config: trace which preset set each field (offline)",
37
- " 4. resolve_config_diff — offline structural diff of two resolved configs (changed fields + array/packageRules set diffs); use this over dry_run_diff for config refactors, especially when registries are unreachable",
38
- " 5. preview_custom_manager — iterate on a regex-based customManagers entry; shows file/line hits and extracted deps",
39
- " 6. validate_config — check a proposed config against Renovate's schema",
40
- " 7. lint_config — semantic lint pass: catches Renovate-specific footguns schema validation misses (e.g. malformed /…/ regex patterns)",
41
- " 8. dry_run — preview what Renovate would actually do (no PRs)",
42
- " 9. dry_run_diff — semantic diff between two dry_run reports (added/removed/changed updates)",
43
- " 10. migrate_config — apply Renovate's built-in migrations (deprecated keys → current schema) and return the migrated config",
44
- " 11. write_config — save the agreed-upon config (validates first)",
38
+ " 2. suggest_presets — go from a natural-language intent to candidate built-in/local presets + an unvalidated draft skeleton (offline); then validate_config + lint_config",
39
+ " 3. resolve_config — expand built-in presets to see what a config actually becomes (offline)",
40
+ " 4. explain_config — inverse of resolve_config: trace which preset set each field (offline)",
41
+ " 5. resolve_config_diff — offline structural diff of two resolved configs (changed fields + array/packageRules set diffs); use this over dry_run_diff for config refactors, especially when registries are unreachable",
42
+ " 6. test_package_rules — offline what-if: which packageRules match a hypothetical dependency, which matcher decided each, and what each contributes (debug \"why didn't my rule match?\")",
43
+ " 7. preview_custom_manager — iterate on a regex-based customManagers entry; shows file/line hits and extracted deps",
44
+ " 8. validate_config — check a proposed config against Renovate's schema",
45
+ " 9. lint_config — semantic lint pass: catches Renovate-specific footguns schema validation misses (e.g. malformed /…/ regex patterns)",
46
+ " 10. dry_run — preview what Renovate would actually do (no PRs)",
47
+ " 11. dry_run_diff — semantic diff between two dry_run reports (added/removed/changed updates)",
48
+ " 12. annotate_dry_run — attribute each proposed update in a dry_run report to the packageRules that caused it (and flag rules that never matched)",
49
+ " 13. migrate_config — apply Renovate's built-in migrations (deprecated keys → current schema) and return the migrated config",
50
+ " 14. write_config — save the agreed-upon config (validates first)",
45
51
  "",
46
52
  "Before the first repo-touching tool call in a session (read_config, resolve_config, dry_run, write_config, …), call check_setup with the same repoPath. It surfaces token / endpoint / connectivity problems up front (e.g. \"set GITHUB_TOKEN or github-actions deps will be skipped\") instead of waiting for dry_run to fail. Skip if the user has already confirmed setup or is doing offline-only work that doesn't depend on git origin or registries.",
47
53
  "If any tool fails unexpectedly, call check_setup to diagnose CLI availability.",
@@ -64,11 +70,14 @@ registerReadConfig(server);
64
70
  registerResolveConfig(server);
65
71
  registerExplainConfig(server);
66
72
  registerResolveConfigDiff(server);
73
+ registerSuggestPresets(server);
67
74
  registerPreviewCustomManager(server);
68
75
  registerValidateConfig(server);
69
76
  registerLintConfig(server);
70
77
  registerDryRun(server);
71
78
  registerDryRunDiff(server);
79
+ registerTestPackageRules(server);
80
+ registerAnnotateDryRun(server);
72
81
  registerMigrateConfig(server);
73
82
  registerWriteConfig(server);
74
83
  registerGetVersion(server);
@@ -61,5 +61,10 @@ export interface DryRunDiff {
61
61
  * would actually become a PR — which is what the diff is about.
62
62
  */
63
63
  export declare function collectProposedUpdates(report: unknown): ProposedUpdate[];
64
+ export declare function identityKey(u: {
65
+ manager: string;
66
+ packageFile: string;
67
+ depName: string;
68
+ }): string;
64
69
  export declare function diffDryRunReports(beforeInput: unknown, afterInput: unknown): DryRunDiff;
65
70
  export {};
Binary file
@@ -4,6 +4,7 @@ export declare const TOKEN_MAX_BYTES = 1024;
4
4
  export declare const ENDPOINT_MAX_BYTES = 2048;
5
5
  export declare const REPOSITORY_MAX_BYTES = 256;
6
6
  export declare const FILENAME_MAX_BYTES = 512;
7
+ export declare const QUERY_MAX_BYTES = 2048;
7
8
  export declare const CONFIG_JSON_MAX_BYTES = 1000000;
8
9
  export declare const REPORT_JSON_MAX_BYTES = 10000000;
9
10
  export declare const HOST_RULES_MAX_ITEMS = 256;
@@ -13,6 +14,7 @@ export declare const tokenString: (description: string) => z.ZodString;
13
14
  export declare const endpointString: (description: string) => z.ZodString;
14
15
  export declare const repositoryString: (description: string) => z.ZodString;
15
16
  export declare const filenameString: (description: string) => z.ZodString;
17
+ export declare const queryString: (description: string) => z.ZodString;
16
18
  export declare const configRecord: (description: string) => z.ZodRecord<z.ZodString, z.ZodUnknown>;
17
19
  export declare const reportRecord: (description: string) => z.ZodRecord<z.ZodString, z.ZodUnknown>;
18
20
  export declare const hostRuleRecord: (description: string) => z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -9,6 +9,7 @@ export const TOKEN_MAX_BYTES = 1024;
9
9
  export const ENDPOINT_MAX_BYTES = 2048;
10
10
  export const REPOSITORY_MAX_BYTES = 256;
11
11
  export const FILENAME_MAX_BYTES = 512;
12
+ export const QUERY_MAX_BYTES = 2048;
12
13
  export const CONFIG_JSON_MAX_BYTES = 1_000_000;
13
14
  export const REPORT_JSON_MAX_BYTES = 10_000_000;
14
15
  export const HOST_RULES_MAX_ITEMS = 256;
@@ -18,6 +19,7 @@ export const tokenString = (description) => z.string().max(TOKEN_MAX_BYTES).desc
18
19
  export const endpointString = (description) => z.string().max(ENDPOINT_MAX_BYTES).describe(description);
19
20
  export const repositoryString = (description) => z.string().max(REPOSITORY_MAX_BYTES).describe(description);
20
21
  export const filenameString = (description) => z.string().max(FILENAME_MAX_BYTES).describe(description);
22
+ export const queryString = (description) => z.string().min(1).max(QUERY_MAX_BYTES).describe(description);
21
23
  const refineJsonSize = (limit) => (value) => {
22
24
  try {
23
25
  return JSON.stringify(value).length <= limit;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Shared analysis layer for `test_package_rules` and `annotate_dry_run`. Wraps
3
+ * the worker-isolated faithful matcher (see ADR-0006) and turns its raw
4
+ * per-matcher verdict vector into a human-meaningful classification:
5
+ * - matched — the matcher returned true
6
+ * - real non-match — the matcher returned false AND the caller supplied
7
+ * the field(s) it reads (so the verdict is trustworthy)
8
+ * - unevaluatable — the matcher returned false but a field it needs was
9
+ * not supplied, OR it threw (matchConfidence needs the
10
+ * merge-confidence API), OR it is a JSONata expression
11
+ * whose result may depend on unsupplied computed fields
12
+ *
13
+ * The crucial subtlety (verified in Renovate's matchers): a matcher returns
14
+ * `false` identically for "supplied-but-no-match" and "field-absent". Only this
15
+ * layer — which knows what the caller actually supplied — can tell them apart.
16
+ *
17
+ * If the worker is unavailable, falls back to a thin in-process glob-only matcher
18
+ * over the common static keys and reports `matchQuality: "preview"`, mirroring
19
+ * resolve_config / explain_config.
20
+ */
21
+ export type MatchQuality = "faithful" | "preview";
22
+ type MatcherKind = "static" | "computed" | "mc-api" | "jsonata";
23
+ interface MatcherMeta {
24
+ /** Friendly `match*` key as written in a packageRule. */
25
+ key: string;
26
+ /** Context fields the matcher reads; evaluatable iff ≥1 is supplied. */
27
+ requiredAnyOf: string[];
28
+ kind: MatcherKind;
29
+ }
30
+ /**
31
+ * Class-name → metadata for every matcher in Renovate's registry. The keys are
32
+ * tied to the matcher-registry drift sentinel (see packageRulesWorker.test.ts):
33
+ * a Renovate bump that renames/adds/removes a matcher breaks both this map's
34
+ * coverage assertion and the sentinel, forcing a deliberate update with ADR-0006.
35
+ */
36
+ export declare const MATCHER_META: Record<string, MatcherMeta>;
37
+ export type UnevaluatableReason = "missing-input-field" | "needs-merge-confidence-api" | "jsonata-may-reference-computed-fields" | "matcher-error";
38
+ export interface MatcherFinding {
39
+ /** Friendly `match*` key. */
40
+ matcher: string;
41
+ /** Context fields the matcher reads (when relevant). */
42
+ requiredFields: string[];
43
+ reason: UnevaluatableReason;
44
+ /** Present for matcher-error / needs-merge-confidence-api. */
45
+ error?: string;
46
+ }
47
+ export interface RuleAnalysis {
48
+ index: number;
49
+ rule: Record<string, unknown>;
50
+ /** True iff the rule cleanly matched (no matcher returned false, none threw). */
51
+ matched: boolean;
52
+ /** Friendly keys that returned true. */
53
+ matchedBy: string[];
54
+ /** Friendly keys that returned a trustworthy false (their fields were supplied). */
55
+ decidedBy: string[];
56
+ /** Matchers we could not evaluate (missing field, mc-api, jsonata advisory, error). */
57
+ unevaluatable: MatcherFinding[];
58
+ /** The rule's contribution (match/exclude stripped, overrides applied). Present only when matched. */
59
+ contributedConfig?: Record<string, unknown>;
60
+ }
61
+ export interface ContextAnalysis {
62
+ /** Faithful merged config (worker path) or approximate (preview path). */
63
+ mergedConfig: Record<string, unknown>;
64
+ rules: RuleAnalysis[];
65
+ }
66
+ export interface PackageRulesAnalysis {
67
+ matchQuality: MatchQuality;
68
+ contexts: ContextAnalysis[];
69
+ warnings: string[];
70
+ }
71
+ /** Field names the caller actually supplied (present and not undefined). */
72
+ export declare function suppliedFieldsOf(context: Record<string, unknown>): Set<string>;
73
+ /**
74
+ * Detect deprecated matcher keys. Renovate migrates these to matchPackageNames /
75
+ * matchFileNames internally before applying rules; the real matchers we evaluate
76
+ * only understand the migrated keys, so a legacy rule would faithfully match
77
+ * nothing. We warn and point at migrate_config (consistent with
78
+ * lint_config → migrate_config) rather than auto-migrating.
79
+ */
80
+ export declare function detectLegacyKeys(packageRules: Record<string, unknown>[]): string[];
81
+ /**
82
+ * Evaluate `packageRules` against each context and classify the results. The
83
+ * caller passes the same `contexts` it would hand the worker; `suppliedFieldsOf`
84
+ * is applied per context so false-vs-missing-field can be distinguished.
85
+ */
86
+ export declare function analyzePackageRules(packageRules: Record<string, unknown>[], contexts: Record<string, unknown>[], options?: {
87
+ timeoutMs?: number;
88
+ }): Promise<PackageRulesAnalysis>;
89
+ export {};
@@ -0,0 +1,252 @@
1
+ import { runApplyPackageRules, } from "./packageRulesWorker.js";
2
+ /**
3
+ * Class-name → metadata for every matcher in Renovate's registry. The keys are
4
+ * tied to the matcher-registry drift sentinel (see packageRulesWorker.test.ts):
5
+ * a Renovate bump that renames/adds/removes a matcher breaks both this map's
6
+ * coverage assertion and the sentinel, forcing a deliberate update with ADR-0006.
7
+ */
8
+ export const MATCHER_META = {
9
+ MergeConfidenceMatcher: { key: "matchConfidence", requiredAnyOf: ["mergeConfidenceLevel"], kind: "mc-api" },
10
+ RepositoriesMatcher: { key: "matchRepositories", requiredAnyOf: ["repository"], kind: "static" },
11
+ BaseBranchesMatcher: { key: "matchBaseBranches", requiredAnyOf: ["baseBranch"], kind: "static" },
12
+ CategoriesMatcher: { key: "matchCategories", requiredAnyOf: ["categories"], kind: "static" },
13
+ ManagersMatcher: { key: "matchManagers", requiredAnyOf: ["manager"], kind: "static" },
14
+ FileNamesMatcher: { key: "matchFileNames", requiredAnyOf: ["packageFile", "lockFiles"], kind: "static" },
15
+ DatasourcesMatcher: { key: "matchDatasources", requiredAnyOf: ["datasource"], kind: "static" },
16
+ PackageNameMatcher: { key: "matchPackageNames", requiredAnyOf: ["packageName", "depName"], kind: "static" },
17
+ DepNameMatcher: { key: "matchDepNames", requiredAnyOf: ["depName"], kind: "static" },
18
+ DepTypesMatcher: { key: "matchDepTypes", requiredAnyOf: ["depType", "depTypes"], kind: "static" },
19
+ CurrentValueMatcher: { key: "matchCurrentValue", requiredAnyOf: ["currentValue"], kind: "static" },
20
+ CurrentVersionMatcher: { key: "matchCurrentVersion", requiredAnyOf: ["currentVersion", "lockedVersion"], kind: "computed" },
21
+ UpdateTypesMatcher: { key: "matchUpdateTypes", requiredAnyOf: ["updateType", "isBump"], kind: "computed" },
22
+ SourceUrlsMatcher: { key: "matchSourceUrls", requiredAnyOf: ["sourceUrl"], kind: "static" },
23
+ RegistryUrlsMatcher: { key: "matchRegistryUrls", requiredAnyOf: ["registryUrls"], kind: "static" },
24
+ NewValueMatcher: { key: "matchNewValue", requiredAnyOf: ["newValue"], kind: "computed" },
25
+ CurrentAgeMatcher: { key: "matchCurrentAge", requiredAnyOf: ["currentVersionTimestamp"], kind: "computed" },
26
+ JsonataMatcher: { key: "matchJsonata", requiredAnyOf: [], kind: "jsonata" },
27
+ };
28
+ const LEGACY_MATCHER_KEYS = [
29
+ "matchPackagePatterns",
30
+ "matchPackagePrefixes",
31
+ "excludePackagePatterns",
32
+ "excludePackagePrefixes",
33
+ "matchPaths",
34
+ "paths",
35
+ ];
36
+ /** Field names the caller actually supplied (present and not undefined). */
37
+ export function suppliedFieldsOf(context) {
38
+ const set = new Set();
39
+ for (const [k, v] of Object.entries(context)) {
40
+ if (v !== undefined)
41
+ set.add(k);
42
+ }
43
+ return set;
44
+ }
45
+ /**
46
+ * Detect deprecated matcher keys. Renovate migrates these to matchPackageNames /
47
+ * matchFileNames internally before applying rules; the real matchers we evaluate
48
+ * only understand the migrated keys, so a legacy rule would faithfully match
49
+ * nothing. We warn and point at migrate_config (consistent with
50
+ * lint_config → migrate_config) rather than auto-migrating.
51
+ */
52
+ export function detectLegacyKeys(packageRules) {
53
+ const found = new Set();
54
+ for (const rule of packageRules) {
55
+ for (const key of LEGACY_MATCHER_KEYS) {
56
+ if (key in rule)
57
+ found.add(key);
58
+ }
59
+ }
60
+ if (found.size === 0)
61
+ return [];
62
+ return [
63
+ `Config uses deprecated matcher key(s) ${[...found]
64
+ .map((k) => `\`${k}\``)
65
+ .join(", ")}. Renovate migrates these before applying rules, but this tool ` +
66
+ `evaluates the real (post-migration) matchers, so those rules will appear to ` +
67
+ `match nothing. Run migrate_config first, then re-run this tool.`,
68
+ ];
69
+ }
70
+ /**
71
+ * Evaluate `packageRules` against each context and classify the results. The
72
+ * caller passes the same `contexts` it would hand the worker; `suppliedFieldsOf`
73
+ * is applied per context so false-vs-missing-field can be distinguished.
74
+ */
75
+ export async function analyzePackageRules(packageRules, contexts, options = {}) {
76
+ const warnings = detectLegacyKeys(packageRules);
77
+ let workerResults;
78
+ try {
79
+ workerResults = await runApplyPackageRules(packageRules, contexts, options);
80
+ }
81
+ catch (err) {
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ warnings.push(`Faithful packageRules worker unavailable (${message}); used an approximate ` +
84
+ `glob-only preview that only evaluates matchPackageNames / matchDepNames / ` +
85
+ `matchManagers / matchDatasources / matchFileNames / matchCategories. Run ` +
86
+ `dry_run for authoritative output.`);
87
+ return {
88
+ matchQuality: "preview",
89
+ warnings,
90
+ contexts: contexts.map((ctx) => previewContext(packageRules, ctx)),
91
+ };
92
+ }
93
+ const analysed = workerResults.map((res, i) => {
94
+ const supplied = suppliedFieldsOf(contexts[i] ?? {});
95
+ return {
96
+ mergedConfig: res.mergedConfig,
97
+ rules: res.rules.map((ruleResult, index) => classifyRule(ruleResult, packageRules[index] ?? {}, index, supplied)),
98
+ };
99
+ });
100
+ return { matchQuality: "faithful", warnings, contexts: analysed };
101
+ }
102
+ function classifyRule(ruleResult, rule, index, supplied) {
103
+ const matchedBy = [];
104
+ const decidedBy = [];
105
+ const unevaluatable = [];
106
+ for (const m of ruleResult.matchers) {
107
+ const meta = MATCHER_META[m.name];
108
+ const key = meta?.key ?? m.name;
109
+ const requiredFields = meta?.requiredAnyOf ?? [];
110
+ if (m.result === "null")
111
+ continue; // matcher not used by this rule
112
+ if (m.result === "threw") {
113
+ unevaluatable.push({
114
+ matcher: key,
115
+ requiredFields,
116
+ reason: meta?.kind === "mc-api" ? "needs-merge-confidence-api" : "matcher-error",
117
+ ...(m.error ? { error: m.error } : {}),
118
+ });
119
+ continue;
120
+ }
121
+ if (meta?.kind === "jsonata") {
122
+ // JSONata can read any field; its verdict may depend on unsupplied
123
+ // computed context. Surface as advisory regardless of true/false.
124
+ unevaluatable.push({
125
+ matcher: key,
126
+ requiredFields: [],
127
+ reason: "jsonata-may-reference-computed-fields",
128
+ });
129
+ if (m.result === "true")
130
+ matchedBy.push(key);
131
+ continue;
132
+ }
133
+ if (m.result === "true") {
134
+ matchedBy.push(key);
135
+ continue;
136
+ }
137
+ // result === "false": trustworthy only if a field it reads was supplied.
138
+ const evaluatable = requiredFields.some((f) => supplied.has(f));
139
+ if (evaluatable) {
140
+ decidedBy.push(key);
141
+ }
142
+ else {
143
+ // The only way to reach here is a missing input field. mc-api matchers
144
+ // (matchConfidence) reach `unevaluatable` via the "threw" branch above —
145
+ // they throw offline rather than returning false — so the
146
+ // "needs-merge-confidence-api" reason belongs there, never here.
147
+ unevaluatable.push({ matcher: key, requiredFields, reason: "missing-input-field" });
148
+ }
149
+ }
150
+ return {
151
+ index,
152
+ rule,
153
+ matched: ruleResult.matched,
154
+ matchedBy,
155
+ decidedBy,
156
+ unevaluatable,
157
+ ...(ruleResult.contributedConfig
158
+ ? { contributedConfig: ruleResult.contributedConfig }
159
+ : {}),
160
+ };
161
+ }
162
+ // ---------------------------------------------------------------------------
163
+ // Preview fallback: thin in-process glob-only matcher over the common static
164
+ // keys. Deliberately approximate — only reached when the worker is unavailable.
165
+ // ---------------------------------------------------------------------------
166
+ const PREVIEW_SUPPORTED = {
167
+ matchPackageNames: { field: "packageName" },
168
+ matchDepNames: { field: "depName" },
169
+ matchManagers: { field: "manager" },
170
+ matchDatasources: { field: "datasource" },
171
+ matchFileNames: { field: "packageFile" },
172
+ matchCategories: { field: "categories", array: true },
173
+ };
174
+ function escapeRegex(s) {
175
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
176
+ }
177
+ function globEquals(pattern, value) {
178
+ if (!pattern.includes("*"))
179
+ return pattern === value;
180
+ const re = new RegExp(`^${pattern.split("*").map(escapeRegex).join(".*")}$`);
181
+ return re.test(value);
182
+ }
183
+ function globListMatches(patterns, value) {
184
+ if (!Array.isArray(patterns) || typeof value !== "string")
185
+ return false;
186
+ return patterns.some((p) => typeof p === "string" && globEquals(p, value));
187
+ }
188
+ function previewContext(packageRules, context) {
189
+ const supplied = suppliedFieldsOf(context);
190
+ const rules = packageRules.map((rule, index) => {
191
+ const matchedBy = [];
192
+ const decidedBy = [];
193
+ const unevaluatable = [];
194
+ let unmatched = false;
195
+ for (const key of Object.keys(rule)) {
196
+ if (!key.startsWith("match") && !key.startsWith("exclude"))
197
+ continue;
198
+ const supported = PREVIEW_SUPPORTED[key];
199
+ if (!supported) {
200
+ unevaluatable.push({
201
+ matcher: key,
202
+ requiredFields: [],
203
+ reason: "matcher-error",
204
+ error: "not evaluated in preview mode (worker unavailable)",
205
+ });
206
+ continue;
207
+ }
208
+ const value = context[supported.field];
209
+ const ok = supported.array
210
+ ? Array.isArray(value) &&
211
+ value.some((v) => globListMatches(rule[key], v))
212
+ : globListMatches(rule[key], value);
213
+ if (ok) {
214
+ matchedBy.push(key);
215
+ }
216
+ else if (supplied.has(supported.field)) {
217
+ // Field supplied but didn't match → a trustworthy non-match.
218
+ decidedBy.push(key);
219
+ unmatched = true;
220
+ }
221
+ else {
222
+ // Field absent → can't decide (same distinction the faithful path makes).
223
+ unevaluatable.push({
224
+ matcher: key,
225
+ requiredFields: [supported.field],
226
+ reason: "missing-input-field",
227
+ });
228
+ }
229
+ }
230
+ // Match only if no supported matcher rejected AND nothing was unevaluatable.
231
+ const matched = !unmatched && unevaluatable.length === 0;
232
+ return {
233
+ index,
234
+ rule,
235
+ matched,
236
+ matchedBy,
237
+ decidedBy,
238
+ unevaluatable,
239
+ ...(matched ? { contributedConfig: stripMatchers(rule) } : {}),
240
+ };
241
+ });
242
+ return { mergedConfig: { ...context }, rules };
243
+ }
244
+ function stripMatchers(rule) {
245
+ const out = {};
246
+ for (const [k, v] of Object.entries(rule)) {
247
+ if (k.startsWith("match") || k.startsWith("exclude"))
248
+ continue;
249
+ out[k] = v;
250
+ }
251
+ return out;
252
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Evaluates Renovate's real `packageRules` matchers against one or more
3
+ * dependency contexts in a worker thread, isolating the main-process cost of
4
+ * importing `renovate`'s matcher registry (which transitively pulls in the
5
+ * versioning subsystem, JSONata, etc.). The whole batch is evaluated in ONE
6
+ * worker round-trip, so `annotate_dry_run` with N updates pays at most one cold
7
+ * start.
8
+ *
9
+ * Same worker-isolation invariant as `migrate_config` / the merge worker: the
10
+ * main process never imports `renovate`. See ADR-0006.
11
+ *
12
+ * The worker REPLICATES `applyPackageRules`' loop (rather than calling it as a
13
+ * black box) so it can return per-rule / per-matcher provenance Renovate exposes
14
+ * nowhere; see `packageRulesWorkerImpl.ts`.
15
+ */
16
+ export type MatcherResultValue = "null" | "true" | "false" | "threw";
17
+ export interface MatcherResult {
18
+ /** The matcher's class name, e.g. `DepNameMatcher`. */
19
+ name: string;
20
+ result: MatcherResultValue;
21
+ /** Present only when `result === "threw"`. */
22
+ error?: string;
23
+ }
24
+ export interface PerRuleResult {
25
+ /** True iff no applicable matcher returned false and none threw. */
26
+ matched: boolean;
27
+ matchers: MatcherResult[];
28
+ /** The rule's contribution (match/exclude keys stripped, overrides applied). Present only when matched. */
29
+ contributedConfig?: Record<string, unknown>;
30
+ }
31
+ export interface PerContextResult {
32
+ /** Faithful merged config — bit-identical to applyPackageRules for matchConfidence-free configs. */
33
+ mergedConfig: Record<string, unknown>;
34
+ rules: PerRuleResult[];
35
+ }
36
+ export declare class PackageRulesTimeoutError extends Error {
37
+ readonly timeoutMs: number;
38
+ constructor(timeoutMs: number);
39
+ }
40
+ export interface RunPackageRulesOptions {
41
+ timeoutMs?: number;
42
+ }
43
+ /**
44
+ * Evaluate `packageRules` against each of `contexts`, returning one result per
45
+ * context (faithful merged config + per-rule provenance). Throws
46
+ * `PackageRulesTimeoutError` on timeout or a plain Error on worker failure — the
47
+ * caller (packageRulesAnalysis) catches and degrades to a preview fallback.
48
+ */
49
+ export declare function runApplyPackageRules(packageRules: Record<string, unknown>[], contexts: Record<string, unknown>[], options?: RunPackageRulesOptions): Promise<PerContextResult[]>;