@rainy-updates/cli 0.5.0-rc.1 → 0.5.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/CHANGELOG.md CHANGED
@@ -2,6 +2,74 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.5.0] - 2026-02-27
6
+
7
+ ### Changed
8
+
9
+ - Promoted `0.5.0-rc.4` to General Availability.
10
+ - Stabilized deterministic CI artifact behavior for JSON, SARIF, and GitHub outputs.
11
+ - Finalized fix-PR summary metadata contract defaults for automation pipelines.
12
+
13
+ ### Added
14
+
15
+ - GA release gate includes `perf:smoke` CI check for regression protection.
16
+
17
+ ## [0.5.0-rc.4] - 2026-02-27
18
+
19
+ ### Changed
20
+
21
+ - Hardened deterministic CI artifacts:
22
+ - stable key ordering for JSON and SARIF files,
23
+ - deterministic sorting for updates, warnings, and errors in output pipelines.
24
+ - Improved fail-reason classification consistency for registry/runtime failures across commands.
25
+ - Fix-PR metadata in summary now has stable defaults (`fixPrApplied`, `fixBranchName`, `fixCommitSha`) to reduce contract drift.
26
+ - Fix-PR staging now includes only updated manifests plus explicit report files, with deterministic file ordering.
27
+ - Added warning when Bun runtime falls back from SQLite cache backend to file cache backend.
28
+
29
+ ### Added
30
+
31
+ - Added `perf:smoke` script and CI gate to enforce a basic performance regression threshold.
32
+ - Added deterministic output and summary regression tests.
33
+
34
+ ## [0.5.0-rc.2] - 2026-02-27
35
+
36
+ ### Added
37
+
38
+ - New fix-PR automation flags for CI branch workflows:
39
+ - `--fix-pr`
40
+ - `--fix-branch <name>`
41
+ - `--fix-commit-message <text>`
42
+ - `--fix-dry-run`
43
+ - `--no-pr-report`
44
+ - New summary metadata for fix-PR execution:
45
+ - `fixPrApplied`
46
+ - `fixBranchName`
47
+ - `fixCommitSha`
48
+ - New GitHub output values for fix-PR state:
49
+ - `fix_pr_applied`
50
+ - `fix_pr_branch`
51
+ - `fix_pr_commit`
52
+ - Added command-specific help output for `check --help`.
53
+
54
+ ### Changed
55
+
56
+ - `check --fix-pr` now executes update application flow to support branch+commit automation without requiring `upgrade`.
57
+ - Default PR report path is auto-assigned when `--fix-pr` is enabled: `.artifacts/deps-report.md`.
58
+ - CLI path-like options are resolved against the final effective `--cwd` value (stable behavior when option order varies).
59
+ - Workspace discovery now supports recursive patterns (`**`) and negated patterns (`!pattern`) with safer directory traversal defaults.
60
+ - Registry resolution now loads `.npmrc` scope mappings (`@scope:registry=...`) from user and project config.
61
+
62
+ ### Fixed
63
+
64
+ - Prevented stale output contracts by writing fix-PR metadata into JSON/GitHub/SARIF artifact flow after git automation is resolved.
65
+
66
+ ### Tests
67
+
68
+ - Added parser tests for fix-PR flags and final-cwd path resolution.
69
+ - Added workspace discovery coverage for recursive and negated patterns.
70
+ - Added fix-PR dry-run workflow test in temporary git repos.
71
+ - Extended GitHub output tests for new fix-PR keys.
72
+
5
73
  ## [0.5.0-rc.1] - 2026-02-27
6
74
 
7
75
  ### Added
package/README.md CHANGED
@@ -39,6 +39,9 @@ npx @rainy-updates/cli check --workspace --ci --format json --json-file .artifac
39
39
  # 3) Apply upgrades with workspace sync
40
40
  npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
41
41
 
42
+ # 3b) Generate a fix branch + commit for CI automation
43
+ npx @rainy-updates/cli check --workspace --fix-pr --fix-branch chore/rainy-updates
44
+
42
45
  # 4) Warm cache for deterministic offline checks
43
46
  npx @rainy-updates/cli warm-cache --workspace --concurrency 32
44
47
  npx @rainy-updates/cli check --workspace --offline --ci
@@ -154,6 +157,11 @@ Schedule:
154
157
  - `--github-output <path>`
155
158
  - `--sarif-file <path>`
156
159
  - `--pr-report-file <path>`
160
+ - `--fix-pr`
161
+ - `--fix-branch <name>`
162
+ - `--fix-commit-message <text>`
163
+ - `--fix-dry-run`
164
+ - `--no-pr-report`
157
165
  - `--ci`
158
166
 
159
167
  ### Upgrade-only
@@ -189,6 +197,7 @@ rainy-updates --version
189
197
  - Node.js 20+ runtime.
190
198
  - Works with npm and pnpm workflows.
191
199
  - Uses optional `undici` pool path for high-throughput HTTP.
200
+ - Reads `.npmrc` default and scoped registries for private package environments.
192
201
  - Cache-first architecture for speed and resilience.
193
202
 
194
203
  ## CI/CD included
@@ -196,6 +205,7 @@ rainy-updates --version
196
205
  This package ships with production CI/CD pipelines in the repository:
197
206
 
198
207
  - Continuous integration pipeline for typecheck, tests, build, and production smoke checks.
208
+ - Performance smoke gate (`perf:smoke`) to catch startup/runtime regressions in CI.
199
209
  - Tag-driven release pipeline for npm publishing with provenance.
200
210
  - Release preflight validation for npm auth/scope checks before publishing.
201
211
 
package/dist/bin/cli.js CHANGED
@@ -9,10 +9,14 @@ import { upgrade } from "../core/upgrade.js";
9
9
  import { warmCache } from "../core/warm-cache.js";
10
10
  import { initCiWorkflow } from "../core/init-ci.js";
11
11
  import { diffBaseline, saveBaseline } from "../core/baseline.js";
12
+ import { applyFixPr } from "../core/fix-pr.js";
12
13
  import { renderResult } from "../output/format.js";
13
14
  import { writeGitHubOutput } from "../output/github.js";
14
15
  import { createSarifReport } from "../output/sarif.js";
15
16
  import { renderPrReport } from "../output/pr-report.js";
17
+ import { writeFileAtomic } from "../utils/io.js";
18
+ import { resolveFailReason } from "../core/summary.js";
19
+ import { stableStringify } from "../utils/stable-json.js";
16
20
  async function main() {
17
21
  try {
18
22
  const argv = process.argv.slice(2);
@@ -60,31 +64,39 @@ async function main() {
60
64
  process.exitCode = 1;
61
65
  return;
62
66
  }
63
- const result = parsed.command === "upgrade"
64
- ? await upgrade(parsed.options)
65
- : parsed.command === "warm-cache"
66
- ? await warmCache(parsed.options)
67
- : await check(parsed.options);
68
- const rendered = renderResult(result, parsed.options.format);
69
- process.stdout.write(rendered + "\n");
70
- if (parsed.options.jsonFile) {
71
- await fs.mkdir(path.dirname(parsed.options.jsonFile), { recursive: true });
72
- await fs.writeFile(parsed.options.jsonFile, JSON.stringify(result, null, 2) + "\n", "utf8");
73
- }
67
+ const result = await runCommand(parsed);
74
68
  if (parsed.options.prReportFile) {
75
69
  const markdown = renderPrReport(result);
76
- await fs.mkdir(path.dirname(parsed.options.prReportFile), { recursive: true });
77
- await fs.writeFile(parsed.options.prReportFile, markdown + "\n", "utf8");
70
+ await writeFileAtomic(parsed.options.prReportFile, markdown + "\n");
71
+ }
72
+ if (parsed.options.fixPr && (parsed.command === "check" || parsed.command === "upgrade")) {
73
+ result.summary.fixPrApplied = false;
74
+ result.summary.fixBranchName = parsed.options.fixBranch ?? "chore/rainy-updates";
75
+ result.summary.fixCommitSha = "";
76
+ const fixResult = await applyFixPr(parsed.options, result, parsed.options.prReportFile ? [parsed.options.prReportFile] : []);
77
+ result.summary.fixPrApplied = fixResult.applied;
78
+ result.summary.fixBranchName = fixResult.branchName ?? "";
79
+ result.summary.fixCommitSha = fixResult.commitSha ?? "";
80
+ }
81
+ result.summary.failReason = resolveFailReason(result.updates, result.errors, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
82
+ const renderStartedAt = Date.now();
83
+ let rendered = renderResult(result, parsed.options.format);
84
+ result.summary.durationMs.render = Math.max(0, Date.now() - renderStartedAt);
85
+ if (parsed.options.format === "json" || parsed.options.format === "metrics") {
86
+ rendered = renderResult(result, parsed.options.format);
87
+ }
88
+ if (parsed.options.jsonFile) {
89
+ await writeFileAtomic(parsed.options.jsonFile, stableStringify(result, 2) + "\n");
78
90
  }
79
91
  if (parsed.options.githubOutputFile) {
80
92
  await writeGitHubOutput(parsed.options.githubOutputFile, result);
81
93
  }
82
94
  if (parsed.options.sarifFile) {
83
95
  const sarif = createSarifReport(result);
84
- await fs.mkdir(path.dirname(parsed.options.sarifFile), { recursive: true });
85
- await fs.writeFile(parsed.options.sarifFile, JSON.stringify(sarif, null, 2) + "\n", "utf8");
96
+ await writeFileAtomic(parsed.options.sarifFile, stableStringify(sarif, 2) + "\n");
86
97
  }
87
- process.exitCode = resolveExitCode(result, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
98
+ process.stdout.write(rendered + "\n");
99
+ process.exitCode = resolveExitCode(result, result.summary.failReason);
88
100
  }
89
101
  catch (error) {
90
102
  process.stderr.write(`rainy-updates: ${String(error)}\n`);
@@ -94,6 +106,36 @@ async function main() {
94
106
  void main();
95
107
  function renderHelp(command) {
96
108
  const isCommand = command && !command.startsWith("-");
109
+ if (isCommand && command === "check") {
110
+ return `rainy-updates check [options]
111
+
112
+ Detect available dependency updates.
113
+
114
+ Options:
115
+ --workspace
116
+ --target patch|minor|major|latest
117
+ --filter <pattern>
118
+ --reject <pattern>
119
+ --dep-kinds deps,dev,optional,peer
120
+ --concurrency <n>
121
+ --cache-ttl <seconds>
122
+ --policy-file <path>
123
+ --offline
124
+ --fix-pr
125
+ --fix-branch <name>
126
+ --fix-commit-message <text>
127
+ --fix-dry-run
128
+ --fix-pr-no-checkout
129
+ --no-pr-report
130
+ --json-file <path>
131
+ --github-output <path>
132
+ --sarif-file <path>
133
+ --pr-report-file <path>
134
+ --fail-on none|patch|minor|major|any
135
+ --max-updates <n>
136
+ --log-level error|warn|info|debug
137
+ --ci`;
138
+ }
97
139
  if (isCommand && command === "warm-cache") {
98
140
  return `rainy-updates warm-cache [options]
99
141
 
@@ -126,6 +168,12 @@ Options:
126
168
  --target patch|minor|major|latest
127
169
  --policy-file <path>
128
170
  --concurrency <n>
171
+ --fix-pr
172
+ --fix-branch <name>
173
+ --fix-commit-message <text>
174
+ --fix-dry-run
175
+ --fix-pr-no-checkout
176
+ --no-pr-report
129
177
  --json-file <path>
130
178
  --pr-report-file <path>`;
131
179
  }
@@ -166,7 +214,7 @@ Global options:
166
214
  --cwd <path>
167
215
  --workspace
168
216
  --target patch|minor|major|latest
169
- --format table|json|minimal|github
217
+ --format table|json|minimal|github|metrics
170
218
  --json-file <path>
171
219
  --github-output <path>
172
220
  --sarif-file <path>
@@ -174,6 +222,13 @@ Global options:
174
222
  --policy-file <path>
175
223
  --fail-on none|patch|minor|major|any
176
224
  --max-updates <n>
225
+ --fix-pr
226
+ --fix-branch <name>
227
+ --fix-commit-message <text>
228
+ --fix-dry-run
229
+ --fix-pr-no-checkout
230
+ --no-pr-report
231
+ --log-level error|warn|info|debug
177
232
  --concurrency <n>
178
233
  --cache-ttl <seconds>
179
234
  --offline
@@ -181,6 +236,24 @@ Global options:
181
236
  --help, -h
182
237
  --version, -v`;
183
238
  }
239
+ async function runCommand(parsed) {
240
+ if (parsed.command === "upgrade") {
241
+ return await upgrade(parsed.options);
242
+ }
243
+ if (parsed.command === "warm-cache") {
244
+ return await warmCache(parsed.options);
245
+ }
246
+ if (parsed.options.fixPr) {
247
+ const upgradeOptions = {
248
+ ...parsed.options,
249
+ install: false,
250
+ packageManager: "auto",
251
+ sync: false,
252
+ };
253
+ return await upgrade(upgradeOptions);
254
+ }
255
+ return await check(parsed.options);
256
+ }
184
257
  async function readPackageVersion() {
185
258
  const currentFile = fileURLToPath(import.meta.url);
186
259
  const packageJsonPath = path.resolve(path.dirname(currentFile), "../../package.json");
@@ -188,22 +261,10 @@ async function readPackageVersion() {
188
261
  const parsed = JSON.parse(content);
189
262
  return parsed.version ?? "0.0.0";
190
263
  }
191
- function resolveExitCode(result, failOn, maxUpdates, ciMode) {
264
+ function resolveExitCode(result, failReason) {
192
265
  if (result.errors.length > 0)
193
266
  return 2;
194
- if (typeof maxUpdates === "number" && result.updates.length > maxUpdates)
267
+ if (failReason !== "none")
195
268
  return 1;
196
- const effectiveFailOn = failOn && failOn !== "none" ? failOn : ciMode ? "any" : "none";
197
- if (!shouldFailForUpdates(result.updates, effectiveFailOn))
198
- return 0;
199
- return 1;
200
- }
201
- function shouldFailForUpdates(updates, failOn) {
202
- if (failOn === "none")
203
- return false;
204
- if (failOn === "any" || failOn === "patch")
205
- return updates.length > 0;
206
- if (failOn === "minor")
207
- return updates.some((update) => update.diffType === "minor" || update.diffType === "major");
208
- return updates.some((update) => update.diffType === "major");
269
+ return 0;
209
270
  }
@@ -1,6 +1,8 @@
1
1
  import type { CachedVersion, TargetLevel } from "../types/index.js";
2
2
  export declare class VersionCache {
3
3
  private readonly store;
4
+ readonly backend: "sqlite" | "file";
5
+ readonly degraded: boolean;
4
6
  private constructor();
5
7
  static create(customPath?: string): Promise<VersionCache>;
6
8
  getValid(packageName: string, target: TargetLevel): Promise<CachedVersion | null>;
@@ -106,17 +106,22 @@ class SqliteCacheStore {
106
106
  }
107
107
  export class VersionCache {
108
108
  store;
109
- constructor(store) {
109
+ backend;
110
+ degraded;
111
+ constructor(store, backend, degraded) {
110
112
  this.store = store;
113
+ this.backend = backend;
114
+ this.degraded = degraded;
111
115
  }
112
116
  static async create(customPath) {
113
117
  const basePath = customPath ?? path.join(os.homedir(), ".cache", "rainy-updates");
114
118
  const sqlitePath = path.join(basePath, "cache.db");
115
119
  const sqliteStore = await tryCreateSqliteStore(sqlitePath);
116
120
  if (sqliteStore)
117
- return new VersionCache(sqliteStore);
121
+ return new VersionCache(sqliteStore, "sqlite", false);
118
122
  const jsonPath = path.join(basePath, "cache.json");
119
- return new VersionCache(new FileCacheStore(jsonPath));
123
+ const degraded = typeof Bun !== "undefined";
124
+ return new VersionCache(new FileCacheStore(jsonPath), "file", degraded);
120
125
  }
121
126
  async getValid(packageName, target) {
122
127
  const entry = await this.store.get(packageName, target);
@@ -1,4 +1,4 @@
1
- import type { DependencyKind, FailOnLevel, OutputFormat, TargetLevel } from "../types/index.js";
1
+ import type { DependencyKind, FailOnLevel, LogLevel, OutputFormat, TargetLevel } from "../types/index.js";
2
2
  export interface FileConfig {
3
3
  target?: TargetLevel;
4
4
  filter?: string;
@@ -17,6 +17,13 @@ export interface FileConfig {
17
17
  prReportFile?: string;
18
18
  failOn?: FailOnLevel;
19
19
  maxUpdates?: number;
20
+ fixPr?: boolean;
21
+ fixBranch?: string;
22
+ fixCommitMessage?: string;
23
+ fixDryRun?: boolean;
24
+ fixPrNoCheckout?: boolean;
25
+ noPrReport?: boolean;
26
+ logLevel?: LogLevel;
20
27
  install?: boolean;
21
28
  packageManager?: "auto" | "npm" | "pnpm";
22
29
  sync?: boolean;
@@ -2,15 +2,26 @@ import type { TargetLevel } from "../types/index.js";
2
2
  export interface PolicyConfig {
3
3
  ignore?: string[];
4
4
  packageRules?: Record<string, {
5
+ match?: string;
5
6
  maxTarget?: TargetLevel;
6
7
  ignore?: boolean;
8
+ maxUpdatesPerRun?: number;
9
+ cooldownDays?: number;
10
+ allowPrerelease?: boolean;
7
11
  }>;
8
12
  }
13
+ export interface PolicyRule {
14
+ match?: string;
15
+ maxTarget?: TargetLevel;
16
+ ignore: boolean;
17
+ maxUpdatesPerRun?: number;
18
+ cooldownDays?: number;
19
+ allowPrerelease?: boolean;
20
+ }
9
21
  export interface ResolvedPolicy {
10
22
  ignorePatterns: string[];
11
- packageRules: Map<string, {
12
- maxTarget?: TargetLevel;
13
- ignore: boolean;
14
- }>;
23
+ packageRules: Map<string, PolicyRule>;
24
+ matchRules: PolicyRule[];
15
25
  }
16
26
  export declare function loadPolicy(cwd: string, policyFile?: string): Promise<ResolvedPolicy>;
27
+ export declare function resolvePolicyRule(packageName: string, policy: ResolvedPolicy): PolicyRule | undefined;
@@ -12,13 +12,10 @@ export async function loadPolicy(cwd, policyFile) {
12
12
  const parsed = JSON.parse(content);
13
13
  return {
14
14
  ignorePatterns: parsed.ignore ?? [],
15
- packageRules: new Map(Object.entries(parsed.packageRules ?? {}).map(([pkg, rule]) => [
16
- pkg,
17
- {
18
- maxTarget: rule.maxTarget,
19
- ignore: rule.ignore === true,
20
- },
21
- ])),
15
+ packageRules: new Map(Object.entries(parsed.packageRules ?? {}).map(([pkg, rule]) => [pkg, normalizeRule(rule)])),
16
+ matchRules: Object.values(parsed.packageRules ?? {})
17
+ .map((rule) => normalizeRule(rule))
18
+ .filter((rule) => typeof rule.match === "string" && rule.match.length > 0),
22
19
  };
23
20
  }
24
21
  catch {
@@ -28,5 +25,36 @@ export async function loadPolicy(cwd, policyFile) {
28
25
  return {
29
26
  ignorePatterns: [],
30
27
  packageRules: new Map(),
28
+ matchRules: [],
31
29
  };
32
30
  }
31
+ export function resolvePolicyRule(packageName, policy) {
32
+ const exact = policy.packageRules.get(packageName);
33
+ if (exact)
34
+ return exact;
35
+ return policy.matchRules.find((rule) => matchesPattern(packageName, rule.match));
36
+ }
37
+ function normalizeRule(rule) {
38
+ return {
39
+ match: typeof rule.match === "string" ? rule.match : undefined,
40
+ maxTarget: rule.maxTarget,
41
+ ignore: rule.ignore === true,
42
+ maxUpdatesPerRun: asNonNegativeInt(rule.maxUpdatesPerRun),
43
+ cooldownDays: asNonNegativeInt(rule.cooldownDays),
44
+ allowPrerelease: rule.allowPrerelease === true,
45
+ };
46
+ }
47
+ function asNonNegativeInt(value) {
48
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0)
49
+ return undefined;
50
+ return value;
51
+ }
52
+ function matchesPattern(value, pattern) {
53
+ if (!pattern || pattern.length === 0)
54
+ return false;
55
+ if (pattern === "*")
56
+ return true;
57
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
58
+ const regex = new RegExp(`^${escaped}$`);
59
+ return regex.test(value);
60
+ }
@@ -6,16 +6,26 @@ import { VersionCache } from "../cache/cache.js";
6
6
  import { NpmRegistryClient } from "../registry/npm.js";
7
7
  import { detectPackageManager } from "../pm/detect.js";
8
8
  import { discoverPackageDirs } from "../workspace/discover.js";
9
- import { loadPolicy } from "../config/policy.js";
9
+ import { loadPolicy, resolvePolicyRule } from "../config/policy.js";
10
+ import { createSummary, finalizeSummary } from "./summary.js";
10
11
  export async function check(options) {
12
+ const startedAt = Date.now();
13
+ let discoveryMs = 0;
14
+ let cacheMs = 0;
15
+ let registryMs = 0;
16
+ const discoveryStartedAt = Date.now();
11
17
  const packageManager = await detectPackageManager(options.cwd);
12
18
  const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
19
+ discoveryMs += Date.now() - discoveryStartedAt;
13
20
  const cache = await VersionCache.create();
14
- const registryClient = new NpmRegistryClient();
21
+ const registryClient = new NpmRegistryClient(options.cwd);
15
22
  const policy = await loadPolicy(options.cwd, options.policyFile);
16
23
  const updates = [];
17
24
  const errors = [];
18
25
  const warnings = [];
26
+ if (cache.degraded) {
27
+ warnings.push("SQLite cache backend unavailable in Bun runtime. Falling back to file cache backend.");
28
+ }
19
29
  let totalDependencies = 0;
20
30
  const tasks = [];
21
31
  let skipped = 0;
@@ -35,7 +45,7 @@ export async function check(options) {
35
45
  continue;
36
46
  if (options.reject && matchesPattern(dep.name, options.reject))
37
47
  continue;
38
- const rule = policy.packageRules.get(dep.name);
48
+ const rule = resolvePolicyRule(dep.name, policy);
39
49
  if (rule?.ignore === true) {
40
50
  skipped += 1;
41
51
  continue;
@@ -47,9 +57,10 @@ export async function check(options) {
47
57
  tasks.push({ packageDir, dependency: dep });
48
58
  }
49
59
  }
50
- const uniquePackageNames = Array.from(new Set(tasks.map((task) => task.dependency.name)));
60
+ const uniquePackageNames = Array.from(new Set(tasks.map((task) => task.dependency.name))).sort((a, b) => a.localeCompare(b));
51
61
  const resolvedVersions = new Map();
52
62
  const unresolvedPackages = [];
63
+ const cacheLookupStartedAt = Date.now();
53
64
  for (const packageName of uniquePackageNames) {
54
65
  const cached = await cache.getValid(packageName, options.target);
55
66
  if (cached) {
@@ -62,8 +73,10 @@ export async function check(options) {
62
73
  unresolvedPackages.push(packageName);
63
74
  }
64
75
  }
76
+ cacheMs += Date.now() - cacheLookupStartedAt;
65
77
  if (unresolvedPackages.length > 0) {
66
78
  if (options.offline) {
79
+ const cacheFallbackStartedAt = Date.now();
67
80
  for (const packageName of unresolvedPackages) {
68
81
  const stale = await cache.getAny(packageName, options.target);
69
82
  if (stale) {
@@ -77,11 +90,15 @@ export async function check(options) {
77
90
  errors.push(`Offline cache miss for ${packageName}. Run once without --offline to warm cache.`);
78
91
  }
79
92
  }
93
+ cacheMs += Date.now() - cacheFallbackStartedAt;
80
94
  }
81
95
  else {
96
+ const registryStartedAt = Date.now();
82
97
  const fetched = await registryClient.resolveManyPackageMetadata(unresolvedPackages, {
83
98
  concurrency: options.concurrency,
84
99
  });
100
+ registryMs += Date.now() - registryStartedAt;
101
+ const cacheWriteStartedAt = Date.now();
85
102
  for (const [packageName, metadata] of fetched.metadata) {
86
103
  resolvedVersions.set(packageName, {
87
104
  latestVersion: metadata.latestVersion,
@@ -91,6 +108,8 @@ export async function check(options) {
91
108
  await cache.set(packageName, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
92
109
  }
93
110
  }
111
+ cacheMs += Date.now() - cacheWriteStartedAt;
112
+ const cacheStaleStartedAt = Date.now();
94
113
  for (const [packageName, error] of fetched.errors) {
95
114
  const stale = await cache.getAny(packageName, options.target);
96
115
  if (stale) {
@@ -104,13 +123,14 @@ export async function check(options) {
104
123
  errors.push(`Unable to resolve ${packageName}: ${error}`);
105
124
  }
106
125
  }
126
+ cacheMs += Date.now() - cacheStaleStartedAt;
107
127
  }
108
128
  }
109
129
  for (const task of tasks) {
110
130
  const metadata = resolvedVersions.get(task.dependency.name);
111
131
  if (!metadata?.latestVersion)
112
132
  continue;
113
- const rule = policy.packageRules.get(task.dependency.name);
133
+ const rule = resolvePolicyRule(task.dependency.name, policy);
114
134
  const effectiveTarget = clampTarget(options.target, rule?.maxTarget);
115
135
  const picked = pickTargetVersionFromAvailable(task.dependency.range, metadata.availableVersions, metadata.latestVersion, effectiveTarget);
116
136
  if (!picked)
@@ -130,15 +150,26 @@ export async function check(options) {
130
150
  reason: rule?.maxTarget ? `policy maxTarget=${rule.maxTarget}` : undefined,
131
151
  });
132
152
  }
133
- const summary = {
153
+ const limitedUpdates = sortUpdates(applyRuleUpdateCaps(updates, policy));
154
+ const sortedErrors = [...errors].sort((a, b) => a.localeCompare(b));
155
+ const sortedWarnings = [...warnings].sort((a, b) => a.localeCompare(b));
156
+ const summary = finalizeSummary(createSummary({
134
157
  scannedPackages: packageDirs.length,
135
158
  totalDependencies,
136
159
  checkedDependencies: tasks.length,
137
- updatesFound: updates.length,
160
+ updatesFound: limitedUpdates.length,
138
161
  upgraded: 0,
139
162
  skipped,
140
163
  warmedPackages: 0,
141
- };
164
+ errors: sortedErrors,
165
+ warnings: sortedWarnings,
166
+ durations: {
167
+ totalMs: Date.now() - startedAt,
168
+ discoveryMs,
169
+ registryMs,
170
+ cacheMs,
171
+ },
172
+ }));
142
173
  return {
143
174
  projectPath: options.cwd,
144
175
  packagePaths: packageDirs,
@@ -146,8 +177,44 @@ export async function check(options) {
146
177
  target: options.target,
147
178
  timestamp: new Date().toISOString(),
148
179
  summary,
149
- updates,
150
- errors,
151
- warnings,
180
+ updates: limitedUpdates,
181
+ errors: sortedErrors,
182
+ warnings: sortedWarnings,
152
183
  };
153
184
  }
185
+ function applyRuleUpdateCaps(updates, policy) {
186
+ const limited = [];
187
+ const seenPerPackage = new Map();
188
+ for (const update of updates) {
189
+ const rule = resolvePolicyRule(update.name, policy);
190
+ const cap = rule?.maxUpdatesPerRun;
191
+ if (typeof cap !== "number") {
192
+ limited.push(update);
193
+ continue;
194
+ }
195
+ const seen = seenPerPackage.get(update.name) ?? 0;
196
+ if (seen >= cap) {
197
+ continue;
198
+ }
199
+ seenPerPackage.set(update.name, seen + 1);
200
+ limited.push(update);
201
+ }
202
+ return limited;
203
+ }
204
+ function sortUpdates(updates) {
205
+ return [...updates].sort((left, right) => {
206
+ const byPath = left.packagePath.localeCompare(right.packagePath);
207
+ if (byPath !== 0)
208
+ return byPath;
209
+ const byName = left.name.localeCompare(right.name);
210
+ if (byName !== 0)
211
+ return byName;
212
+ const byKind = left.kind.localeCompare(right.kind);
213
+ if (byKind !== 0)
214
+ return byKind;
215
+ const byFrom = left.fromRange.localeCompare(right.fromRange);
216
+ if (byFrom !== 0)
217
+ return byFrom;
218
+ return left.toRange.localeCompare(right.toRange);
219
+ });
220
+ }
@@ -0,0 +1,7 @@
1
+ import type { CheckResult, RunOptions } from "../types/index.js";
2
+ export interface FixPrResult {
3
+ applied: boolean;
4
+ branchName?: string;
5
+ commitSha?: string;
6
+ }
7
+ export declare function applyFixPr(options: RunOptions, result: CheckResult, extraFiles: string[]): Promise<FixPrResult>;