@fuzdev/fuz_gitops 0.78.0 → 0.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +91 -17
  2. package/dist/ModulesDetail.svelte +22 -27
  3. package/dist/ModulesDetail.svelte.d.ts.map +1 -1
  4. package/dist/ModulesNav.svelte +8 -5
  5. package/dist/ModulesNav.svelte.d.ts.map +1 -1
  6. package/dist/ModulesPage.svelte +3 -5
  7. package/dist/ModulesPage.svelte.d.ts.map +1 -1
  8. package/dist/PageFooter.svelte +2 -2
  9. package/dist/PageHeader.svelte +6 -6
  10. package/dist/PageHeader.svelte.d.ts +0 -3
  11. package/dist/PageHeader.svelte.d.ts.map +1 -1
  12. package/dist/PullRequestsDetail.svelte +29 -20
  13. package/dist/PullRequestsDetail.svelte.d.ts.map +1 -1
  14. package/dist/PullRequestsPage.svelte +4 -6
  15. package/dist/PullRequestsPage.svelte.d.ts.map +1 -1
  16. package/dist/ReposTable.svelte +43 -55
  17. package/dist/ReposTable.svelte.d.ts.map +1 -1
  18. package/dist/ReposTree.svelte +9 -21
  19. package/dist/ReposTree.svelte.d.ts.map +1 -1
  20. package/dist/ReposTreeNav.svelte +9 -12
  21. package/dist/ReposTreeNav.svelte.d.ts.map +1 -1
  22. package/dist/TablePage.svelte +3 -5
  23. package/dist/TablePage.svelte.d.ts.map +1 -1
  24. package/dist/TreeItemPage.svelte +3 -5
  25. package/dist/TreeItemPage.svelte.d.ts.map +1 -1
  26. package/dist/TreePage.svelte +3 -5
  27. package/dist/TreePage.svelte.d.ts.map +1 -1
  28. package/dist/changeset_generator.d.ts.map +1 -1
  29. package/dist/changeset_generator.js +2 -2
  30. package/dist/changeset_reader.d.ts.map +1 -1
  31. package/dist/changeset_reader.js +3 -3
  32. package/dist/ci_reconcile.d.ts +6 -10
  33. package/dist/ci_reconcile.d.ts.map +1 -1
  34. package/dist/ci_reconcile.js +6 -8
  35. package/dist/dependency_graph.d.ts +26 -49
  36. package/dist/dependency_graph.d.ts.map +1 -1
  37. package/dist/dependency_graph.js +40 -83
  38. package/dist/dependency_updater.d.ts +1 -1
  39. package/dist/dependency_updater.d.ts.map +1 -1
  40. package/dist/dependency_updater.js +14 -9
  41. package/dist/fetch_repo_data.d.ts +34 -8
  42. package/dist/fetch_repo_data.d.ts.map +1 -1
  43. package/dist/fetch_repo_data.js +44 -22
  44. package/dist/fs_fetch_value_cache.d.ts +1 -1
  45. package/dist/fs_fetch_value_cache.d.ts.map +1 -1
  46. package/dist/fs_fetch_value_cache.js +3 -3
  47. package/dist/git_operations.d.ts +6 -49
  48. package/dist/git_operations.d.ts.map +1 -1
  49. package/dist/git_operations.js +13 -126
  50. package/dist/github.d.ts +17 -2
  51. package/dist/github.d.ts.map +1 -1
  52. package/dist/github.js +17 -8
  53. package/dist/github_helpers.d.ts.map +1 -1
  54. package/dist/github_helpers.js +3 -11
  55. package/dist/gitops_analyze.task.d.ts +24 -2
  56. package/dist/gitops_analyze.task.d.ts.map +1 -1
  57. package/dist/gitops_analyze.task.js +47 -34
  58. package/dist/gitops_config.d.ts +34 -79
  59. package/dist/gitops_config.d.ts.map +1 -1
  60. package/dist/gitops_config.js +76 -53
  61. package/dist/gitops_constants.d.ts +6 -4
  62. package/dist/gitops_constants.d.ts.map +1 -1
  63. package/dist/gitops_constants.js +6 -4
  64. package/dist/gitops_plan.task.d.ts +29 -3
  65. package/dist/gitops_plan.task.d.ts.map +1 -1
  66. package/dist/gitops_plan.task.js +83 -61
  67. package/dist/gitops_publish.task.d.ts +62 -2
  68. package/dist/gitops_publish.task.d.ts.map +1 -1
  69. package/dist/gitops_publish.task.js +193 -105
  70. package/dist/gitops_run.task.d.ts +74 -0
  71. package/dist/gitops_run.task.d.ts.map +1 -1
  72. package/dist/gitops_run.task.js +176 -162
  73. package/dist/gitops_sync.task.d.ts +65 -3
  74. package/dist/gitops_sync.task.d.ts.map +1 -1
  75. package/dist/gitops_sync.task.js +163 -37
  76. package/dist/gitops_task_helpers.d.ts +86 -59
  77. package/dist/gitops_task_helpers.d.ts.map +1 -1
  78. package/dist/gitops_task_helpers.js +120 -79
  79. package/dist/gitops_validate.task.d.ts +1 -2
  80. package/dist/gitops_validate.task.d.ts.map +1 -1
  81. package/dist/gitops_validate.task.js +25 -32
  82. package/dist/graph_validation.d.ts +16 -27
  83. package/dist/graph_validation.d.ts.map +1 -1
  84. package/dist/graph_validation.js +24 -65
  85. package/dist/local_repo.d.ts +50 -62
  86. package/dist/local_repo.d.ts.map +1 -1
  87. package/dist/local_repo.js +83 -196
  88. package/dist/log_helpers.d.ts +8 -8
  89. package/dist/log_helpers.d.ts.map +1 -1
  90. package/dist/multi_repo_publisher.d.ts +10 -0
  91. package/dist/multi_repo_publisher.d.ts.map +1 -1
  92. package/dist/multi_repo_publisher.js +125 -61
  93. package/dist/npm_registry.d.ts +23 -19
  94. package/dist/npm_registry.d.ts.map +1 -1
  95. package/dist/npm_registry.js +21 -48
  96. package/dist/operations.d.ts +75 -180
  97. package/dist/operations.d.ts.map +1 -1
  98. package/dist/operations.js +15 -7
  99. package/dist/operations_defaults.d.ts +17 -2
  100. package/dist/operations_defaults.d.ts.map +1 -1
  101. package/dist/operations_defaults.js +111 -128
  102. package/dist/output_helpers.d.ts +36 -0
  103. package/dist/output_helpers.d.ts.map +1 -1
  104. package/dist/output_helpers.js +37 -3
  105. package/dist/preflight_checks.d.ts +20 -27
  106. package/dist/preflight_checks.d.ts.map +1 -1
  107. package/dist/preflight_checks.js +37 -126
  108. package/dist/publish_gate.d.ts.map +1 -1
  109. package/dist/publish_steps.d.ts +2 -4
  110. package/dist/publish_steps.d.ts.map +1 -1
  111. package/dist/publish_steps.js +4 -8
  112. package/dist/publishing_event.d.ts +2 -8
  113. package/dist/publishing_event.d.ts.map +1 -1
  114. package/dist/publishing_event.js +20 -17
  115. package/dist/publishing_event_handler.d.ts +10 -8
  116. package/dist/publishing_event_handler.d.ts.map +1 -1
  117. package/dist/publishing_event_handler.js +19 -17
  118. package/dist/publishing_plan.d.ts +16 -1
  119. package/dist/publishing_plan.d.ts.map +1 -1
  120. package/dist/publishing_plan.js +332 -254
  121. package/dist/publishing_plan_helpers.d.ts.map +1 -1
  122. package/dist/publishing_plan_helpers.js +3 -3
  123. package/dist/publishing_plan_logging.d.ts +1 -1
  124. package/dist/publishing_plan_logging.d.ts.map +1 -1
  125. package/dist/publishing_plan_logging.js +22 -15
  126. package/dist/repo.svelte.d.ts +7 -0
  127. package/dist/repo.svelte.d.ts.map +1 -1
  128. package/dist/repo.svelte.js +3 -0
  129. package/dist/repo_readiness.d.ts +210 -0
  130. package/dist/repo_readiness.d.ts.map +1 -0
  131. package/dist/repo_readiness.js +516 -0
  132. package/dist/repos_status.d.ts +3340 -0
  133. package/dist/repos_status.d.ts.map +1 -0
  134. package/dist/repos_status.js +555 -0
  135. package/dist/repos_status_load.d.ts +65 -0
  136. package/dist/repos_status_load.d.ts.map +1 -0
  137. package/dist/repos_status_load.js +131 -0
  138. package/dist/version_utils.js +1 -1
  139. package/package.json +15 -34
  140. package/src/lib/cargo_toml.ts +3 -3
  141. package/src/lib/changeset_generator.ts +19 -19
  142. package/src/lib/changeset_reader.ts +18 -18
  143. package/src/lib/ci_reconcile.ts +12 -16
  144. package/src/lib/dependency_graph.ts +61 -113
  145. package/src/lib/dependency_updater.ts +36 -30
  146. package/src/lib/fetch_repo_data.ts +71 -34
  147. package/src/lib/fs_fetch_value_cache.ts +15 -11
  148. package/src/lib/git_operations.ts +22 -194
  149. package/src/lib/github.ts +33 -21
  150. package/src/lib/github_helpers.ts +9 -16
  151. package/src/lib/gitops_analyze.task.ts +104 -66
  152. package/src/lib/gitops_config.ts +90 -139
  153. package/src/lib/gitops_constants.ts +6 -4
  154. package/src/lib/gitops_plan.task.ts +128 -76
  155. package/src/lib/gitops_publish.task.ts +280 -144
  156. package/src/lib/gitops_run.task.ts +265 -193
  157. package/src/lib/gitops_sync.task.ts +236 -52
  158. package/src/lib/gitops_task_helpers.ts +170 -114
  159. package/src/lib/gitops_validate.task.ts +44 -54
  160. package/src/lib/graph_validation.ts +28 -89
  161. package/src/lib/local_repo.ts +128 -335
  162. package/src/lib/log_helpers.ts +18 -28
  163. package/src/lib/multi_repo_publisher.ts +192 -110
  164. package/src/lib/npm_registry.ts +42 -67
  165. package/src/lib/operations.ts +99 -160
  166. package/src/lib/operations_defaults.ts +157 -201
  167. package/src/lib/output_helpers.ts +58 -6
  168. package/src/lib/preflight_checks.ts +52 -188
  169. package/src/lib/publish_gate.ts +7 -9
  170. package/src/lib/publish_steps.ts +27 -25
  171. package/src/lib/publishing_event.ts +26 -23
  172. package/src/lib/publishing_event_handler.ts +24 -22
  173. package/src/lib/publishing_plan.ts +474 -310
  174. package/src/lib/publishing_plan_helpers.ts +16 -12
  175. package/src/lib/publishing_plan_logging.ts +53 -44
  176. package/src/lib/repo.svelte.ts +16 -8
  177. package/src/lib/repo_readiness.ts +647 -0
  178. package/src/lib/repos_status.ts +671 -0
  179. package/src/lib/repos_status_load.ts +173 -0
  180. package/src/lib/version_utils.ts +3 -3
  181. package/dist/config_reconcile.d.ts +0 -46
  182. package/dist/config_reconcile.d.ts.map +0 -1
  183. package/dist/config_reconcile.js +0 -60
  184. package/dist/paths.d.ts +0 -7
  185. package/dist/paths.d.ts.map +0 -1
  186. package/dist/paths.js +0 -6
  187. package/dist/repo_ops.d.ts +0 -59
  188. package/dist/repo_ops.d.ts.map +0 -1
  189. package/dist/repo_ops.js +0 -170
  190. package/dist/resolved_gitops_config.d.ts +0 -9
  191. package/dist/resolved_gitops_config.d.ts.map +0 -1
  192. package/dist/resolved_gitops_config.js +0 -12
  193. package/src/lib/config_reconcile.ts +0 -94
  194. package/src/lib/paths.ts +0 -6
  195. package/src/lib/repo_ops.ts +0 -216
  196. package/src/lib/resolved_gitops_config.ts +0 -27
@@ -1,26 +1,23 @@
1
- import type {Logger} from '@fuzdev/fuz_util/log.ts';
2
- import {styleText as st} from 'node:util';
1
+ import type { Logger } from '@fuzdev/fuz_util/log.ts';
2
+ import { styleText as st } from 'node:util';
3
3
 
4
- import {repo_is_npm, type LocalRepo} from './local_repo.ts';
5
- import {validate_dependency_graph} from './graph_validation.ts';
4
+ import { repo_is_npm, type LocalRepo } from './local_repo.ts';
5
+ import { validate_dependency_graph } from './graph_validation.ts';
6
6
  import {
7
7
  type BumpType,
8
8
  is_breaking_change,
9
9
  compare_bump_types,
10
- calculate_next_version,
10
+ calculate_next_version
11
11
  } from './version_utils.ts';
12
- import type {ChangesetOperations} from './operations.ts';
13
- import {default_changeset_operations} from './operations_defaults.ts';
14
- import {GITOPS_MAX_ITERATIONS_DEFAULT} from './gitops_constants.ts';
15
- import type {DependencyGraph} from './dependency_graph.ts';
12
+ import type { ChangesetOperations } from './operations.ts';
13
+ import { default_changeset_operations } from './operations_defaults.ts';
14
+ import { GITOPS_MAX_ITERATIONS_DEFAULT } from './gitops_constants.ts';
15
+ import type { DependencyGraph } from './dependency_graph.ts';
16
16
  import {
17
17
  calculate_dependency_updates,
18
- get_required_bump_for_dependencies,
18
+ get_required_bump_for_dependencies
19
19
  } from './publishing_plan_helpers.ts';
20
20
 
21
- // Re-export logging functions
22
- export {log_publishing_plan, type LogPlanOptions} from './publishing_plan_logging.ts';
23
-
24
21
  export interface VersionChange {
25
22
  package_name: string;
26
23
  from: string;
@@ -34,6 +31,22 @@ export interface VersionChange {
34
31
  required_bump?: BumpType; // The required bump type from dependencies
35
32
  }
36
33
 
34
+ /**
35
+ * How a version change arises in the plan: from the package's own changesets
36
+ * (`explicit`), from its changesets with the bump raised to what a breaking
37
+ * dependency requires (`escalation`), or from a changeset the executor
38
+ * generates for a dependency update (`auto`).
39
+ */
40
+ export type VersionChangeKind = 'explicit' | 'escalation' | 'auto';
41
+
42
+ /**
43
+ * Classifies a version change — the one classification the plan's logger,
44
+ * markdown, and side-effect preview share. A change without changesets of its
45
+ * own is `auto` even if its bump was raised.
46
+ */
47
+ export const version_change_kind = (change: VersionChange): VersionChangeKind =>
48
+ !change.has_changesets ? 'auto' : change.needs_bump_escalation ? 'escalation' : 'explicit';
49
+
37
50
  export interface DependencyUpdate {
38
51
  dependent_package: string;
39
52
  updated_dependency: string;
@@ -45,7 +58,7 @@ export interface DependencyUpdate {
45
58
  // Verbose data types for diagnostic output
46
59
  export interface VerboseChangesetDetail {
47
60
  package_name: string;
48
- files: Array<{filename: string; bump_type: BumpType; summary: string}>;
61
+ files: Array<{ filename: string; bump_type: BumpType; summary: string }>;
49
62
  }
50
63
 
51
64
  export interface VerboseIterationPackage {
@@ -67,14 +80,14 @@ export interface VerboseIteration {
67
80
 
68
81
  export interface VerbosePropagationChain {
69
82
  source: string;
70
- chain: Array<{pkg: string; dep_type: 'prod' | 'peer'; action: string}>;
83
+ chain: Array<{ pkg: string; dep_type: 'prod' | 'peer'; action: string }>;
71
84
  }
72
85
 
73
86
  export interface VerboseGraphSummary {
74
87
  package_count: number;
75
88
  internal_dep_count: number;
76
- prod_peer_edges: Array<{from: string; to: string; type: 'prod' | 'peer'}>;
77
- dev_edges: Array<{from: string; to: string}>;
89
+ prod_peer_edges: Array<{ from: string; to: string; type: 'prod' | 'peer' }>;
90
+ dev_edges: Array<{ from: string; to: string }>;
78
91
  prod_cycle_count: number;
79
92
  dev_cycle_count: number;
80
93
  }
@@ -93,7 +106,10 @@ export interface PublishingPlan {
93
106
  dependency_updates: Array<DependencyUpdate>;
94
107
  breaking_cascades: Map<string, Array<string>>;
95
108
  warnings: Array<string>;
96
- info: Array<string>; // Informational status (not warnings)
109
+ /** Informational sentences, not warnings: excluded non-npm repos, dev dependency cycles. */
110
+ info: Array<string>;
111
+ /** Package names with no changesets and no version change — nothing to publish. */
112
+ no_changes: Array<string>;
97
113
  errors: Array<string>;
98
114
  verbose_data?: VerboseData;
99
115
  }
@@ -104,6 +120,18 @@ export interface GeneratePlanOptions {
104
120
  verbose?: boolean;
105
121
  }
106
122
 
123
+ type ChangesetPresence = Awaited<ReturnType<ChangesetOperations['has_changesets']>>;
124
+
125
+ /**
126
+ * The plan's working state, shared by the initial pass and the fixed-point
127
+ * iteration that extends it.
128
+ */
129
+ interface CascadeState {
130
+ version_changes: Array<VersionChange>;
131
+ predicted_versions: Map<string, string>;
132
+ breaking_packages: Set<string>;
133
+ }
134
+
107
135
  /**
108
136
  * Generates a publishing plan showing what would happen during publishing.
109
137
  * Shows version changes, dependency updates, and breaking change cascades.
@@ -111,13 +139,13 @@ export interface GeneratePlanOptions {
111
139
  */
112
140
  export const generate_publishing_plan = async (
113
141
  all_repos: Array<LocalRepo>,
114
- options: GeneratePlanOptions = {},
142
+ options: GeneratePlanOptions = {}
115
143
  ): Promise<PublishingPlan> => {
116
- const {log, ops = default_changeset_operations, verbose} = options;
144
+ const { log, ops = default_changeset_operations, verbose = false } = options;
117
145
  log?.info(st('cyan', 'Generating publishing plan...'));
118
146
 
119
147
  const warnings: Array<string> = [];
120
- const info: Array<string> = []; // Informational status (not warnings)
148
+ const info: Array<string> = [];
121
149
  const errors: Array<string> = [];
122
150
 
123
151
  // Publishing concerns only npm packages. Non-npm repos (e.g. Rust cargo repos) are
@@ -127,27 +155,19 @@ export const generate_publishing_plan = async (
127
155
  if (non_npm_repos.length > 0) {
128
156
  info.push(
129
157
  `Excluded ${non_npm_repos.length} non-npm repo(s) from publishing: ` +
130
- non_npm_repos.map((r) => r.library.name).join(', '),
158
+ non_npm_repos.map((r) => r.library.name).join(', ')
131
159
  );
132
160
  }
133
161
  const repos = all_repos.filter(repo_is_npm);
134
162
 
135
- // Verbose data collection
136
- const verbose_changeset_details: Array<VerboseChangesetDetail> = [];
137
- const verbose_iterations: Array<VerboseIteration> = [];
138
-
139
163
  // Build dependency graph and validate
140
164
  let publishing_order: Array<string>;
141
165
  let production_cycles: Array<Array<string>>;
142
166
  let dev_cycles: Array<Array<string>>;
143
- let graph: DependencyGraph | null = null;
167
+ let graph: DependencyGraph;
144
168
 
145
169
  try {
146
- const validation = validate_dependency_graph(repos, {
147
- throw_on_prod_cycles: false, // Collect errors instead of throwing
148
- log_cycles: false, // We'll handle our own error collection
149
- log_order: false, // Plan generation doesn't need to log order
150
- });
170
+ const validation = validate_dependency_graph(repos);
151
171
  publishing_order = validation.publishing_order;
152
172
  production_cycles = validation.production_cycles;
153
173
  dev_cycles = validation.dev_cycles;
@@ -166,111 +186,281 @@ export const generate_publishing_plan = async (
166
186
  breaking_cascades: new Map(),
167
187
  warnings,
168
188
  info,
169
- errors,
189
+ no_changes: [],
190
+ errors
170
191
  };
171
192
  }
172
193
 
173
- // Collect cycle errors
174
- if (production_cycles.length > 0) {
175
- for (const cycle of production_cycles) {
176
- errors.push(`Production dependency cycle: ${cycle.join(' → ')}`);
177
- }
194
+ for (const cycle of production_cycles) {
195
+ errors.push(`Production dependency cycle: ${cycle.join(' → ')}`);
178
196
  }
179
197
 
180
198
  // Dev cycles are shown in gitops_analyze, not repeated here
181
199
  if (dev_cycles.length > 0) {
182
200
  info.push(
183
- `${dev_cycles.length} dev dependency cycle(s) detected (normal, shown in gitops_analyze)`,
201
+ `${dev_cycles.length} dev dependency cycle(s) detected (normal, shown in gitops_analyze)`
184
202
  );
185
203
  }
186
204
 
187
- // Initial pass: get all packages with explicit changesets
188
- const predicted_versions: Map<string, string> = new Map();
189
- const breaking_packages: Set<string> = new Set();
190
- const version_changes: Array<VersionChange> = [];
205
+ // Checked once per repo; the initial pass and the no-changes list both read it.
206
+ const changeset_presence: Map<string, ChangesetPresence> = new Map(
207
+ await Promise.all(
208
+ repos.map(async (repo) => [repo.library.name, await ops.has_changesets({ repo })] as const)
209
+ )
210
+ );
211
+
212
+ const state: CascadeState = {
213
+ version_changes: [],
214
+ predicted_versions: new Map(),
215
+ breaking_packages: new Set()
216
+ };
217
+
218
+ const { changeset_details, repos_without_bump } = await plan_initial_pass({
219
+ repos,
220
+ publishing_order,
221
+ changeset_presence,
222
+ state,
223
+ ops,
224
+ log,
225
+ verbose,
226
+ warnings,
227
+ errors
228
+ });
229
+
230
+ const cascade = plan_resolve_cascades({ repos, state, changeset_details, verbose });
231
+
232
+ // Worded after the cascades, which may give such a repo an auto-changeset
233
+ for (const repo of repos_without_bump) {
234
+ const publishes = state.version_changes.some((vc) => vc.package_name === repo.library.name);
235
+ warnings.push(await describe_changesets_without_bump(repo, publishes, ops));
236
+ }
237
+
238
+ if (!cascade.converged) {
239
+ const convergence_warning = plan_convergence_warning(repos, state);
240
+ if (convergence_warning) warnings.push(convergence_warning);
241
+ }
242
+
243
+ // Final dependency updates calculation after convergence
244
+ const { dependency_updates, breaking_cascades } = calculate_dependency_updates(
245
+ repos,
246
+ state.predicted_versions,
247
+ state.breaking_packages
248
+ );
249
+
250
+ // Packages with nothing to publish: no version change and no changesets
251
+ const no_changes: Array<string> = [];
252
+ for (const repo of repos) {
253
+ const name = repo.library.name;
254
+ if (state.version_changes.some((vc) => vc.package_name === name)) continue;
255
+ const presence = changeset_presence.get(name);
256
+ if (presence?.ok && !presence.value) {
257
+ no_changes.push(name);
258
+ }
259
+ }
260
+
261
+ const verbose_data = verbose
262
+ ? plan_verbose_data({
263
+ graph,
264
+ version_changes: state.version_changes,
265
+ dependency_updates,
266
+ breaking_cascades,
267
+ changeset_details,
268
+ iterations: cascade.verbose_iterations,
269
+ total_iterations: cascade.iterations,
270
+ prod_cycle_count: production_cycles.length,
271
+ dev_cycle_count: dev_cycles.length
272
+ })
273
+ : undefined;
274
+
275
+ return {
276
+ publishing_order,
277
+ version_changes: state.version_changes,
278
+ dependency_updates,
279
+ breaking_cascades,
280
+ warnings,
281
+ info,
282
+ no_changes,
283
+ errors,
284
+ verbose_data
285
+ };
286
+ };
287
+
288
+ interface PlanInitialPassOptions {
289
+ repos: Array<LocalRepo>;
290
+ publishing_order: Array<string>;
291
+ changeset_presence: Map<string, ChangesetPresence>;
292
+ state: CascadeState;
293
+ ops: ChangesetOperations;
294
+ log: Logger | undefined;
295
+ verbose: boolean;
296
+ warnings: Array<string>;
297
+ errors: Array<string>;
298
+ }
299
+
300
+ interface PlanInitialPassResult {
301
+ /** The changeset details for verbose output, empty unless `verbose`. */
302
+ changeset_details: Array<VerboseChangesetDetail>;
303
+ /**
304
+ * Repos whose changeset files yield no bump for their package, warned on
305
+ * once the cascades settle whether each publishes.
306
+ */
307
+ repos_without_bump: Array<LocalRepo>;
308
+ }
309
+
310
+ /**
311
+ * The initial pass: a version change for each package with explicit changesets,
312
+ * in publishing order.
313
+ *
314
+ * @mutates options.state - adds the explicit version changes, their predicted versions, and the breaking ones
315
+ * @mutates options.warnings - private packages carrying changesets
316
+ * @mutates options.errors - failed changeset checks and predictions
317
+ */
318
+ const plan_initial_pass = async (
319
+ options: PlanInitialPassOptions
320
+ ): Promise<PlanInitialPassResult> => {
321
+ const { repos, publishing_order, changeset_presence, state, ops, log, verbose } = options;
322
+ const { warnings, errors } = options;
323
+ const changeset_details: Array<VerboseChangesetDetail> = [];
324
+ const repos_without_bump: Array<LocalRepo> = [];
191
325
 
192
326
  for (const pkg_name of publishing_order) {
193
327
  const repo = repos.find((r) => r.library.name === pkg_name);
194
328
  if (!repo) continue;
329
+ const has_result = changeset_presence.get(pkg_name);
330
+ if (!has_result) continue;
195
331
 
196
332
  // Private packages never publish — exclude them from version changes entirely (no
197
333
  // publish step, npm-wait, bump escalation, or auto-changeset). They keep their slot in
198
334
  // the topological order. Flag a private package that carries a changeset, since that
199
335
  // changeset can't be published.
200
336
  if (repo.package_json.private) {
201
- const private_has_changesets = await ops.has_changesets({repo});
202
- if (private_has_changesets.ok && private_has_changesets.value) {
337
+ if (has_result.ok && has_result.value) {
203
338
  warnings.push(`${pkg_name} is private — its changeset(s) will not be published`);
204
339
  }
205
340
  continue;
206
341
  }
207
342
 
208
- // Check for changesets
209
- const has_result = await ops.has_changesets({repo});
210
-
211
343
  if (!has_result.ok) {
212
344
  errors.push(`Failed to check changesets for ${pkg_name}: ${has_result.message}`);
213
345
  continue;
214
346
  }
215
347
 
216
- if (has_result.value) {
217
- // Predict version from changesets
218
- const prediction = await ops.predict_next_version({repo, log});
348
+ if (!has_result.value) continue;
219
349
 
220
- if (!prediction) {
221
- // No changesets found - this shouldn't happen since has_changesets returned true
222
- continue;
223
- }
350
+ // Predict version from changesets
351
+ const prediction = await ops.predict_next_version({ repo, log });
224
352
 
225
- if (!prediction.ok) {
226
- errors.push(`Failed to predict version for ${pkg_name}: ${prediction.message}`);
227
- continue;
228
- }
353
+ if (!prediction) {
354
+ // Changeset files exist, but none yields a bump for this package
355
+ repos_without_bump.push(repo);
356
+ continue;
357
+ }
229
358
 
230
- // Capture changeset details for verbose output
231
- if (verbose) {
232
- const changesets_result = await ops.read_changesets({repo, log});
233
- if (changesets_result.ok) {
234
- const files = changesets_result.value
235
- .filter((cs) => cs.packages.some((p) => p.name === pkg_name))
236
- .map((cs) => {
237
- const pkg_entry = cs.packages.find((p) => p.name === pkg_name);
238
- return {
239
- filename: cs.filename,
240
- bump_type: pkg_entry?.bump_type || prediction.bump_type,
241
- summary: cs.summary,
242
- };
243
- });
244
- if (files.length > 0) {
245
- verbose_changeset_details.push({package_name: pkg_name, files});
246
- }
359
+ if (!prediction.ok) {
360
+ errors.push(`Failed to predict version for ${pkg_name}: ${prediction.message}`);
361
+ continue;
362
+ }
363
+
364
+ // Capture changeset details for verbose output
365
+ if (verbose) {
366
+ const changesets_result = await ops.read_changesets({ repo, log });
367
+ if (changesets_result.ok) {
368
+ const files = changesets_result.value
369
+ .filter((cs) => cs.packages.some((p) => p.name === pkg_name))
370
+ .map((cs) => {
371
+ const pkg_entry = cs.packages.find((p) => p.name === pkg_name);
372
+ return {
373
+ filename: cs.filename,
374
+ bump_type: pkg_entry?.bump_type || prediction.bump_type,
375
+ summary: cs.summary
376
+ };
377
+ });
378
+ if (files.length > 0) {
379
+ changeset_details.push({ package_name: pkg_name, files });
247
380
  }
248
381
  }
382
+ }
249
383
 
250
- {
251
- const old_version = repo.package_json.version || '0.0.0';
252
- const is_breaking = is_breaking_change(old_version, prediction.bump_type);
384
+ const old_version = repo.package_json.version || '0.0.0';
385
+ const is_breaking = is_breaking_change(old_version, prediction.bump_type);
253
386
 
254
- predicted_versions.set(pkg_name, prediction.version);
387
+ state.predicted_versions.set(pkg_name, prediction.version);
255
388
 
256
- if (is_breaking) {
257
- breaking_packages.add(pkg_name);
258
- }
259
-
260
- version_changes.push({
261
- package_name: pkg_name,
262
- from: old_version,
263
- to: prediction.version,
264
- bump_type: prediction.bump_type,
265
- breaking: is_breaking,
266
- has_changesets: true,
267
- });
268
- }
389
+ if (is_breaking) {
390
+ state.breaking_packages.add(pkg_name);
269
391
  }
392
+
393
+ state.version_changes.push({
394
+ package_name: pkg_name,
395
+ from: old_version,
396
+ to: prediction.version,
397
+ bump_type: prediction.bump_type,
398
+ breaking: is_breaking,
399
+ has_changesets: true
400
+ });
401
+ }
402
+
403
+ return { changeset_details, repos_without_bump };
404
+ };
405
+
406
+ /**
407
+ * Words the warning for a repo whose changeset files yield no bump for its
408
+ * package. It takes no bump from them: it publishes only as the auto-generated
409
+ * change a dependency update requires, and otherwise not at all.
410
+ *
411
+ * @param publishes - whether the settled plan has a version change for the repo
412
+ */
413
+ const describe_changesets_without_bump = async (
414
+ repo: LocalRepo,
415
+ publishes: boolean,
416
+ ops: ChangesetOperations
417
+ ): Promise<string> => {
418
+ const pkg_name = repo.library.name;
419
+ const consequence = publishes
420
+ ? 'it takes no bump from them and publishes only as the auto-generated change a dependency update requires'
421
+ : 'it will not publish';
422
+ // no `log`: `predict_next_version` already logged any invalid files
423
+ const read = await ops.read_changesets({ repo });
424
+ if (!read.ok) {
425
+ return `${pkg_name} has changeset files that could not be read (${read.message}) — ${consequence}`;
426
+ }
427
+ if (read.value.length === 0) {
428
+ return `${pkg_name} has changeset files, but none parses — ${consequence}; check their frontmatter`;
270
429
  }
430
+ return (
431
+ `${pkg_name} has changeset files, but none that parses names ${pkg_name} — ${consequence}; ` +
432
+ `check the package names in their frontmatter`
433
+ );
434
+ };
435
+
436
+ interface PlanResolveCascadesOptions {
437
+ repos: Array<LocalRepo>;
438
+ state: CascadeState;
439
+ changeset_details: Array<VerboseChangesetDetail>;
440
+ verbose: boolean;
441
+ }
442
+
443
+ interface PlanCascadeResult {
444
+ /** Iterations run, at most `GITOPS_MAX_ITERATIONS_DEFAULT`. */
445
+ iterations: number;
446
+ /** False when the last iteration still found changes. */
447
+ converged: boolean;
448
+ /** Per-iteration details, empty unless `verbose`. */
449
+ verbose_iterations: Array<VerboseIteration>;
450
+ }
451
+
452
+ /**
453
+ * Fixed-point iteration over the dependency updates the current predictions
454
+ * imply: escalates explicit changes a breaking dependency outgrows, and adds or
455
+ * raises auto-changeset changes for dependents, until a pass finds nothing new.
456
+ *
457
+ * @mutates options.state - escalates and adds version changes, with their predicted versions and breaking set
458
+ */
459
+ const plan_resolve_cascades = (options: PlanResolveCascadesOptions): PlanCascadeResult => {
460
+ const { repos, state, changeset_details, verbose } = options;
461
+ const { version_changes, predicted_versions, breaking_packages } = state;
462
+ const verbose_iterations: Array<VerboseIteration> = [];
271
463
 
272
- // Fixed-point iteration to resolve transitive cascades
273
- // Loop until no new version changes are discovered
274
464
  let iteration = 0;
275
465
  let changed = true;
276
466
 
@@ -284,10 +474,10 @@ export const generate_publishing_plan = async (
284
474
 
285
475
  // Recalculate dependency updates based on current predicted versions
286
476
  // (breaking_cascades not needed during iteration, only calculated at the end)
287
- const {dependency_updates} = calculate_dependency_updates(
477
+ const { dependency_updates } = calculate_dependency_updates(
288
478
  repos,
289
479
  predicted_versions,
290
- breaking_packages,
480
+ breaking_packages
291
481
  );
292
482
 
293
483
  // Process packages to check for bump escalation and auto-generated changesets
@@ -302,113 +492,103 @@ export const generate_publishing_plan = async (
302
492
  const required_bump = get_required_bump_for_dependencies(
303
493
  repo,
304
494
  dependency_updates,
305
- breaking_packages,
495
+ breaking_packages
306
496
  );
497
+ if (!required_bump) continue;
307
498
 
308
499
  // Find triggering dependency for verbose output
309
500
  let triggering_dep: string | null = null;
310
- if (required_bump) {
311
- const relevant_updates = dependency_updates.filter(
312
- (u) =>
313
- u.dependent_package === pkg_name &&
314
- (u.type === 'dependencies' || u.type === 'peerDependencies') &&
315
- breaking_packages.has(u.updated_dependency),
316
- );
317
- if (relevant_updates.length > 0) {
318
- triggering_dep = `${relevant_updates[0]!.updated_dependency} BREAKING`;
319
- }
501
+ const relevant_updates = dependency_updates.filter(
502
+ (u) =>
503
+ u.dependent_package === pkg_name &&
504
+ (u.type === 'dependencies' || u.type === 'peerDependencies') &&
505
+ breaking_packages.has(u.updated_dependency)
506
+ );
507
+ if (relevant_updates.length > 0) {
508
+ triggering_dep = `${relevant_updates[0]!.updated_dependency} BREAKING`;
320
509
  }
321
510
 
322
- // Check if already in version_changes (has changesets)
511
+ const old_version = repo.package_json.version || '0.0.0';
512
+ const new_version = calculate_next_version(old_version, required_bump);
513
+ const is_breaking = is_breaking_change(old_version, required_bump);
514
+
323
515
  const existing_entry = version_changes.find((vc) => vc.package_name === pkg_name);
324
516
 
325
517
  if (existing_entry) {
326
- // Package has changesets - check if it needs bump escalation
327
- if (required_bump && compare_bump_types(required_bump, existing_entry.bump_type) > 0) {
328
- // Dependencies require a larger bump than existing changesets provide
329
- const old_version = repo.package_json.version || '0.0.0';
330
- const new_version = calculate_next_version(old_version, required_bump);
331
-
332
- // Only mark as changed if version actually changed
333
- if (existing_entry.to !== new_version) {
334
- changed = true;
335
- verbose_new_changes++;
336
-
337
- // Capture verbose data before updating
338
- if (verbose) {
339
- const changeset_detail = verbose_changeset_details.find(
340
- (d) => d.package_name === pkg_name,
341
- );
342
- verbose_iteration_packages.push({
343
- name: pkg_name,
344
- changeset_count: changeset_detail?.files.length || 1,
345
- bump_from_changesets: existing_entry.bump_type,
346
- required_bump,
347
- triggering_dep,
348
- action: 'escalation',
349
- version_to: new_version,
350
- is_breaking: is_breaking_change(old_version, required_bump),
351
- });
352
- }
353
-
354
- existing_entry.needs_bump_escalation = true;
355
- existing_entry.existing_bump = existing_entry.bump_type;
356
- existing_entry.required_bump = required_bump;
357
- existing_entry.bump_type = required_bump;
358
- existing_entry.to = new_version;
359
- existing_entry.breaking = is_breaking_change(old_version, required_bump);
360
-
361
- // Update predicted versions
362
- predicted_versions.set(pkg_name, new_version);
363
-
364
- if (existing_entry.breaking) {
365
- breaking_packages.add(pkg_name);
366
- }
367
- }
518
+ // Dependencies may require a larger bump than the entry has. Only mark as
519
+ // changed if the version actually changes.
520
+ if (compare_bump_types(required_bump, existing_entry.bump_type) <= 0) continue;
521
+ if (existing_entry.to === new_version) continue;
522
+
523
+ changed = true;
524
+ verbose_new_changes++;
525
+
526
+ // An auto-changeset entry raised by a dependency that turned breaking later in the
527
+ // iteration (a dependent listed before its dependency) is still an auto-changeset:
528
+ // the executor generates its changeset at the bump the plan settles on. Only an
529
+ // entry with changesets of its own escalates.
530
+ const kind = version_change_kind(existing_entry);
531
+
532
+ if (verbose) {
533
+ const changeset_detail = changeset_details.find((d) => d.package_name === pkg_name);
534
+ verbose_iteration_packages.push({
535
+ name: pkg_name,
536
+ changeset_count: kind === 'auto' ? 0 : changeset_detail?.files.length || 1,
537
+ bump_from_changesets: kind === 'auto' ? null : existing_entry.bump_type,
538
+ required_bump,
539
+ triggering_dep,
540
+ action: kind === 'auto' ? 'auto_changeset' : 'escalation',
541
+ version_to: new_version,
542
+ is_breaking
543
+ });
544
+ }
545
+
546
+ if (kind !== 'auto') {
547
+ existing_entry.needs_bump_escalation = true;
548
+ existing_entry.existing_bump ??= existing_entry.bump_type;
549
+ existing_entry.required_bump = required_bump;
550
+ }
551
+ existing_entry.bump_type = required_bump;
552
+ existing_entry.to = new_version;
553
+ existing_entry.breaking = is_breaking;
554
+
555
+ predicted_versions.set(pkg_name, new_version);
556
+ if (is_breaking) {
557
+ breaking_packages.add(pkg_name);
368
558
  }
369
- } else if (required_bump) {
559
+ } else if (!predicted_versions.has(pkg_name)) {
370
560
  // No existing changesets but needs changeset for dependency updates
371
- const old_version = repo.package_json.version || '0.0.0';
372
- const new_version = calculate_next_version(old_version, required_bump);
373
-
374
- // Check if this is a new version (not already in version_changes)
375
- if (!predicted_versions.has(pkg_name)) {
376
- changed = true;
377
- verbose_new_changes++;
378
-
379
- const is_breaking = is_breaking_change(old_version, required_bump);
380
-
381
- // Capture verbose data
382
- if (verbose) {
383
- verbose_iteration_packages.push({
384
- name: pkg_name,
385
- changeset_count: 0,
386
- bump_from_changesets: null,
387
- required_bump,
388
- triggering_dep,
389
- action: 'auto_changeset',
390
- version_to: new_version,
391
- is_breaking,
392
- });
393
- }
394
-
395
- if (is_breaking) {
396
- breaking_packages.add(pkg_name);
397
- }
398
-
399
- version_changes.push({
400
- package_name: pkg_name,
401
- from: old_version,
402
- to: new_version,
403
- bump_type: required_bump,
404
- breaking: is_breaking,
405
- has_changesets: false,
406
- will_generate_changeset: true,
561
+ changed = true;
562
+ verbose_new_changes++;
563
+
564
+ if (verbose) {
565
+ verbose_iteration_packages.push({
566
+ name: pkg_name,
567
+ changeset_count: 0,
568
+ bump_from_changesets: null,
569
+ required_bump,
570
+ triggering_dep,
571
+ action: 'auto_changeset',
572
+ version_to: new_version,
573
+ is_breaking
407
574
  });
575
+ }
408
576
 
409
- // Update predicted versions
410
- predicted_versions.set(pkg_name, new_version);
577
+ if (is_breaking) {
578
+ breaking_packages.add(pkg_name);
411
579
  }
580
+
581
+ version_changes.push({
582
+ package_name: pkg_name,
583
+ from: old_version,
584
+ to: new_version,
585
+ bump_type: required_bump,
586
+ breaking: is_breaking,
587
+ has_changesets: false,
588
+ will_generate_changeset: true
589
+ });
590
+
591
+ predicted_versions.set(pkg_name, new_version);
412
592
  }
413
593
  }
414
594
 
@@ -417,147 +597,131 @@ export const generate_publishing_plan = async (
417
597
  verbose_iterations.push({
418
598
  iteration,
419
599
  packages: verbose_iteration_packages,
420
- new_changes: verbose_new_changes,
600
+ new_changes: verbose_new_changes
421
601
  });
422
602
  }
423
603
  }
424
604
 
425
- // Check if we hit iteration limit without convergence
426
- if (iteration === GITOPS_MAX_ITERATIONS_DEFAULT && changed) {
427
- // Calculate how many packages still need processing
428
- const pending_packages: Array<string> = [];
429
-
430
- // Recalculate one more time to see what's pending
431
- const {dependency_updates: pending_updates} = calculate_dependency_updates(
432
- repos,
433
- predicted_versions,
434
- breaking_packages,
435
- );
436
-
437
- for (const repo of repos) {
438
- const pkg_name = repo.library.name;
439
- if (repo.package_json.private) continue; // private packages never publish
440
- const required_bump = get_required_bump_for_dependencies(
441
- repo,
442
- pending_updates,
443
- breaking_packages,
444
- );
445
-
446
- // Check if this package would need processing
447
- const existing_entry = version_changes.find((vc) => vc.package_name === pkg_name);
448
- const needs_escalation =
449
- existing_entry &&
450
- required_bump &&
451
- compare_bump_types(required_bump, existing_entry.bump_type) > 0;
452
- const needs_auto_changeset = !existing_entry && required_bump;
453
-
454
- if (needs_escalation || needs_auto_changeset) {
455
- pending_packages.push(pkg_name);
456
- }
457
- }
605
+ return { iterations: iteration, converged: !changed, verbose_iterations };
606
+ };
458
607
 
459
- // Add warning with diagnostics
460
- const pending_count = pending_packages.length;
461
- const estimated_iterations = Math.ceil(pending_count / 2); // Rough estimate
462
- warnings.push(
463
- `Reached maximum iterations (${GITOPS_MAX_ITERATIONS_DEFAULT}) without full convergence - ` +
464
- `${pending_count} package(s) may still need processing: ${pending_packages.join(', ')}. ` +
465
- `Estimated ${estimated_iterations} more iteration(s) needed.`,
466
- );
467
- }
608
+ /**
609
+ * Words the warning for a plan that hit `GITOPS_MAX_ITERATIONS_DEFAULT` still
610
+ * changing, naming the packages one more pass would change.
611
+ *
612
+ * @returns the warning, or `null` when one more pass would change nothing (the last pass converged it)
613
+ */
614
+ const plan_convergence_warning = (repos: Array<LocalRepo>, state: CascadeState): string | null => {
615
+ const { version_changes, predicted_versions, breaking_packages } = state;
616
+ const pending_packages: Array<string> = [];
468
617
 
469
- // Final dependency updates calculation after convergence
470
- const {dependency_updates, breaking_cascades} = calculate_dependency_updates(
618
+ // Recalculate one more time to see what's pending
619
+ const { dependency_updates: pending_updates } = calculate_dependency_updates(
471
620
  repos,
472
621
  predicted_versions,
473
- breaking_packages,
622
+ breaking_packages
474
623
  );
475
624
 
476
- // Identify packages with no changes
477
625
  for (const repo of repos) {
478
- const has_version_change = version_changes.some((vc) => vc.package_name === repo.library.name);
479
- if (!has_version_change) {
480
- const has_result = await ops.has_changesets({repo});
481
- if (has_result.ok && !has_result.value) {
482
- info.push(repo.library.name);
483
- }
626
+ const pkg_name = repo.library.name;
627
+ if (repo.package_json.private) continue; // private packages never publish
628
+ const required_bump = get_required_bump_for_dependencies(
629
+ repo,
630
+ pending_updates,
631
+ breaking_packages
632
+ );
633
+ if (!required_bump) continue;
634
+
635
+ const existing_entry = version_changes.find((vc) => vc.package_name === pkg_name);
636
+ if (!existing_entry || compare_bump_types(required_bump, existing_entry.bump_type) > 0) {
637
+ pending_packages.push(pkg_name);
484
638
  }
485
639
  }
486
640
 
487
- // Build verbose data if requested
488
- let verbose_data: VerboseData | undefined;
489
- if (verbose) {
490
- // Build propagation chains from breaking_cascades
491
- const propagation_chains: Array<VerbosePropagationChain> = [];
492
- for (const [source, affected] of breaking_cascades) {
493
- const chain: Array<{pkg: string; dep_type: 'prod' | 'peer'; action: string}> = [];
494
- for (const pkg of affected) {
495
- // Determine dep type and action
496
- const update = dependency_updates.find(
497
- (u) => u.dependent_package === pkg && u.updated_dependency === source,
498
- );
499
- const dep_type: 'prod' | 'peer' = update?.type === 'peerDependencies' ? 'peer' : 'prod';
500
- const version_change = version_changes.find((vc) => vc.package_name === pkg);
501
- let action = 'update';
502
- if (version_change?.will_generate_changeset) {
503
- action = 'auto-changeset';
504
- } else if (version_change?.needs_bump_escalation) {
505
- action = 'bump escalation';
506
- }
507
- chain.push({pkg, dep_type, action});
508
- }
509
- if (chain.length > 0) {
510
- propagation_chains.push({source, chain});
511
- }
641
+ if (pending_packages.length === 0) return null;
642
+
643
+ return (
644
+ `Reached maximum iterations (${GITOPS_MAX_ITERATIONS_DEFAULT}) without full convergence - ` +
645
+ `${pending_packages.length} package(s) may still need processing: ${pending_packages.join(', ')}.`
646
+ );
647
+ };
648
+
649
+ interface PlanVerboseDataOptions {
650
+ graph: DependencyGraph;
651
+ version_changes: Array<VersionChange>;
652
+ dependency_updates: Array<DependencyUpdate>;
653
+ breaking_cascades: Map<string, Array<string>>;
654
+ changeset_details: Array<VerboseChangesetDetail>;
655
+ iterations: Array<VerboseIteration>;
656
+ total_iterations: number;
657
+ prod_cycle_count: number;
658
+ dev_cycle_count: number;
659
+ }
660
+
661
+ /**
662
+ * Assembles the verbose diagnostics: propagation chains from the breaking
663
+ * cascades and a summary of the dependency graph, beside the changeset and
664
+ * iteration details the passes collected.
665
+ */
666
+ const plan_verbose_data = (options: PlanVerboseDataOptions): VerboseData => {
667
+ const { graph, version_changes, dependency_updates, breaking_cascades } = options;
668
+
669
+ // Build propagation chains from breaking_cascades
670
+ const propagation_chains: Array<VerbosePropagationChain> = [];
671
+ for (const [source, affected] of breaking_cascades) {
672
+ const chain: Array<{ pkg: string; dep_type: 'prod' | 'peer'; action: string }> = [];
673
+ for (const pkg of affected) {
674
+ // Determine dep type and action
675
+ const update = dependency_updates.find(
676
+ (u) => u.dependent_package === pkg && u.updated_dependency === source
677
+ );
678
+ const dep_type: 'prod' | 'peer' = update?.type === 'peerDependencies' ? 'peer' : 'prod';
679
+ const version_change = version_changes.find((vc) => vc.package_name === pkg);
680
+ const kind = version_change && version_change_kind(version_change);
681
+ const action =
682
+ kind === 'auto' ? 'auto-changeset' : kind === 'escalation' ? 'bump escalation' : 'update';
683
+ chain.push({ pkg, dep_type, action });
684
+ }
685
+ if (chain.length > 0) {
686
+ propagation_chains.push({ source, chain });
512
687
  }
688
+ }
513
689
 
514
- // Build graph summary
515
- const prod_peer_edges: Array<{from: string; to: string; type: 'prod' | 'peer'}> = [];
516
- const dev_edges: Array<{from: string; to: string}> = [];
517
- let internal_dep_count = 0;
518
-
519
- for (const [pkg_name, node] of graph.nodes) {
520
- for (const [dep_name, spec] of node.dependencies) {
521
- // Only count internal dependencies (deps that are also in the graph)
522
- if (graph.nodes.has(dep_name)) {
523
- internal_dep_count++;
524
- if (spec.type === 'dev') {
525
- dev_edges.push({from: pkg_name, to: dep_name});
526
- } else {
527
- prod_peer_edges.push({
528
- from: pkg_name,
529
- to: dep_name,
530
- type: spec.type === 'peer' ? 'peer' : 'prod',
531
- });
532
- }
690
+ // Build graph summary
691
+ const prod_peer_edges: Array<{ from: string; to: string; type: 'prod' | 'peer' }> = [];
692
+ const dev_edges: Array<{ from: string; to: string }> = [];
693
+ let internal_dep_count = 0;
694
+
695
+ for (const [pkg_name, node] of graph.nodes) {
696
+ for (const [dep_name, spec] of node.dependencies) {
697
+ // Only count internal dependencies (deps that are also in the graph)
698
+ if (graph.nodes.has(dep_name)) {
699
+ internal_dep_count++;
700
+ if (spec.type === 'dev') {
701
+ dev_edges.push({ from: pkg_name, to: dep_name });
702
+ } else {
703
+ prod_peer_edges.push({
704
+ from: pkg_name,
705
+ to: dep_name,
706
+ type: spec.type === 'peer' ? 'peer' : 'prod'
707
+ });
533
708
  }
534
709
  }
535
710
  }
536
-
537
- verbose_data = {
538
- changeset_details: verbose_changeset_details,
539
- iterations: verbose_iterations,
540
- propagation_chains,
541
- graph_summary: {
542
- package_count: graph.nodes.size,
543
- internal_dep_count,
544
- prod_peer_edges,
545
- dev_edges,
546
- prod_cycle_count: production_cycles.length,
547
- dev_cycle_count: dev_cycles.length,
548
- },
549
- total_iterations: iteration,
550
- };
551
711
  }
552
712
 
553
713
  return {
554
- publishing_order,
555
- version_changes,
556
- dependency_updates,
557
- breaking_cascades,
558
- warnings,
559
- info,
560
- errors,
561
- verbose_data,
714
+ changeset_details: options.changeset_details,
715
+ iterations: options.iterations,
716
+ propagation_chains,
717
+ graph_summary: {
718
+ package_count: graph.nodes.size,
719
+ internal_dep_count,
720
+ prod_peer_edges,
721
+ dev_edges,
722
+ prod_cycle_count: options.prod_cycle_count,
723
+ dev_cycle_count: options.dev_cycle_count
724
+ },
725
+ total_iterations: options.total_iterations
562
726
  };
563
727
  };