newpr 1.0.23 → 1.0.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newpr",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "AI-powered large PR review tool - understand PRs with 1000+ lines of changes",
5
5
  "module": "src/cli/index.ts",
6
6
  "type": "module",
@@ -119,7 +119,7 @@ describe("extractDeltas", () => {
119
119
  });
120
120
 
121
121
  describe("computeGroupStats", () => {
122
- test("uses merged dependency baseline for multi-parent DAG groups", async () => {
122
+ test("uses first dependency as baseline (matching publish base branch) for multi-parent DAG groups", async () => {
123
123
  const repoPath = mkdtempSync(join(tmpdir(), "group-stats-dag-test-"));
124
124
  try {
125
125
  await Bun.$`git init ${repoPath}`.quiet();
@@ -180,14 +180,16 @@ describe("computeGroupStats", () => {
180
180
  expect(stats.get("A")?.deletions).toBe(1);
181
181
  expect(stats.get("B")?.additions).toBe(1);
182
182
  expect(stats.get("B")?.deletions).toBe(1);
183
- expect(stats.get("C")?.additions).toBe(1);
184
- expect(stats.get("C")?.deletions).toBe(1);
183
+ expect(stats.get("C")?.additions).toBe(2);
184
+ expect(stats.get("C")?.deletions).toBe(2);
185
185
  expect(detailed.get("A")?.file_stats["a.txt"]?.additions).toBe(1);
186
186
  expect(detailed.get("A")?.file_stats["a.txt"]?.deletions).toBe(1);
187
187
  expect(detailed.get("B")?.file_stats["b.txt"]?.additions).toBe(1);
188
188
  expect(detailed.get("B")?.file_stats["b.txt"]?.deletions).toBe(1);
189
189
  expect(detailed.get("C")?.file_stats["c.txt"]?.additions).toBe(1);
190
190
  expect(detailed.get("C")?.file_stats["c.txt"]?.deletions).toBe(1);
191
+ expect(detailed.get("C")?.file_stats["b.txt"]?.additions).toBe(1);
192
+ expect(detailed.get("C")?.file_stats["b.txt"]?.deletions).toBe(1);
191
193
  } finally {
192
194
  rmSync(repoPath, { recursive: true, force: true });
193
195
  }
@@ -196,48 +196,7 @@ async function resolveParentTree(
196
196
  return resolveTree(repoPath, baseSha);
197
197
  }
198
198
 
199
- if (parentIds.length === 1) {
200
- return expectedTrees.get(parentIds[0]!) ?? resolveTree(repoPath, baseSha);
201
- }
202
-
203
- const parentTrees = parentIds.map((p) => expectedTrees.get(p)).filter((t): t is string => Boolean(t));
204
- if (parentTrees.length === 0) return resolveTree(repoPath, baseSha);
205
- if (parentTrees.length === 1) return parentTrees[0]!;
206
-
207
- const syntheticCommitByTree = new Map<string, string>();
208
- const ensureSyntheticCommit = async (treeSha: string): Promise<string | null> => {
209
- const existing = syntheticCommitByTree.get(treeSha);
210
- if (existing) return existing;
211
-
212
- const result = await Bun.$`git -C ${repoPath} commit-tree ${treeSha} -p ${baseSha} -m "newpr synthetic parent"`.quiet().nothrow();
213
- if (result.exitCode !== 0) return null;
214
- const commitSha = result.stdout.toString().trim();
215
- if (!commitSha) return null;
216
- syntheticCommitByTree.set(treeSha, commitSha);
217
- return commitSha;
218
- };
219
-
220
- let mergedTree = parentTrees[0]!;
221
- let mergedCommit = await ensureSyntheticCommit(mergedTree);
222
- if (!mergedCommit) return null;
223
-
224
- for (let i = 1; i < parentTrees.length; i++) {
225
- const nextCommit = await ensureSyntheticCommit(parentTrees[i]!);
226
- if (!nextCommit) return null;
227
-
228
- const mergeResult = await Bun.$`git -C ${repoPath} merge-tree --write-tree --allow-unrelated-histories ${mergedCommit} ${nextCommit}`.quiet().nothrow();
229
- if (mergeResult.exitCode !== 0) return null;
230
- const nextTree = mergeResult.stdout.toString().trim().split("\n")[0]?.trim();
231
- if (!nextTree) return null;
232
- mergedTree = nextTree;
233
-
234
- const mergedCommitResult = await Bun.$`git -C ${repoPath} commit-tree ${mergedTree} -p ${mergedCommit} -p ${nextCommit} -m "newpr synthetic merged parent"`.quiet().nothrow();
235
- if (mergedCommitResult.exitCode !== 0) return null;
236
- mergedCommit = mergedCommitResult.stdout.toString().trim();
237
- if (!mergedCommit) return null;
238
- }
239
-
240
- return mergedTree;
199
+ return expectedTrees.get(parentIds[0]!) ?? resolveTree(repoPath, baseSha);
241
200
  }
242
201
 
243
202
  export interface StackGroupComputedStats {
@@ -181,4 +181,115 @@ describe("checkFeasibility", () => {
181
181
  const order = result.ordered_group_ids!;
182
182
  expect(order.indexOf("group-a")).toBeLessThan(order.indexOf("group-b"));
183
183
  });
184
+
185
+ test("dependency edge wins over conflicting path-order edge", () => {
186
+ const deltas: DeltaEntry[] = [
187
+ makeDelta("c1", "base", [{ status: "A", path: "ui.tsx" }]),
188
+ makeDelta("c2", "c1", [{ status: "A", path: "lib.ts" }]),
189
+ ];
190
+
191
+ const ownership = new Map([
192
+ ["ui.tsx", "ui-group"],
193
+ ["lib.ts", "lib-group"],
194
+ ]);
195
+
196
+ const declaredDeps = new Map([
197
+ ["ui-group", ["lib-group"]],
198
+ ]);
199
+
200
+ const result = checkFeasibility({ deltas, ownership, declared_deps: declaredDeps });
201
+
202
+ expect(result.feasible).toBe(true);
203
+ const order = result.ordered_group_ids!;
204
+ expect(order.indexOf("lib-group")).toBeLessThan(order.indexOf("ui-group"));
205
+ expect(result.dependency_edges).toBeDefined();
206
+ const depEdge = result.dependency_edges!.find(
207
+ (e) => e.from === "lib-group" && e.to === "ui-group",
208
+ );
209
+ expect(depEdge).toBeDefined();
210
+ });
211
+
212
+ test("dependency edge preserved when path-order creates opposing cycle", () => {
213
+ const deltas: DeltaEntry[] = [
214
+ makeDelta("c1", "base", [
215
+ { status: "A", path: "ui.tsx" },
216
+ { status: "A", path: "hook.ts" },
217
+ ]),
218
+ makeDelta("c2", "c1", [{ status: "M", path: "ui.tsx" }]),
219
+ ];
220
+
221
+ const ownership = new Map([
222
+ ["ui.tsx", "ui-group"],
223
+ ["hook.ts", "lib-group"],
224
+ ]);
225
+
226
+ const declaredDeps = new Map([
227
+ ["ui-group", ["lib-group"]],
228
+ ]);
229
+
230
+ const result = checkFeasibility({ deltas, ownership, declared_deps: declaredDeps });
231
+
232
+ expect(result.feasible).toBe(true);
233
+ const order = result.ordered_group_ids!;
234
+ expect(order.indexOf("lib-group")).toBeLessThan(order.indexOf("ui-group"));
235
+ });
236
+
237
+ test("mutual dependency edges both survive when no path-order edges exist", () => {
238
+ const deltas: DeltaEntry[] = [
239
+ makeDelta("c1", "base", [
240
+ { status: "A", path: "a.ts" },
241
+ { status: "A", path: "b.ts" },
242
+ ]),
243
+ ];
244
+
245
+ const ownership = new Map([
246
+ ["a.ts", "group-a"],
247
+ ["b.ts", "group-b"],
248
+ ]);
249
+
250
+ const declaredDeps = new Map([
251
+ ["group-a", ["group-b"]],
252
+ ["group-b", ["group-a"]],
253
+ ]);
254
+
255
+ const result = checkFeasibility({ deltas, ownership, declared_deps: declaredDeps });
256
+
257
+ expect(result.feasible).toBe(true);
258
+ expect(result.ordered_group_ids).toHaveLength(2);
259
+ });
260
+
261
+ test("layer hint yields to import dep when they conflict", () => {
262
+ const deltas: DeltaEntry[] = [
263
+ makeDelta("c1", "base", [
264
+ { status: "A", path: "api.ts" },
265
+ { status: "A", path: "ui.tsx" },
266
+ { status: "A", path: "types.ts" },
267
+ ]),
268
+ ];
269
+
270
+ const ownership = new Map([
271
+ ["api.ts", "api-group"],
272
+ ["ui.tsx", "ui-group"],
273
+ ["types.ts", "types-group"],
274
+ ]);
275
+
276
+ const declaredDeps = new Map([
277
+ ["ui-group", ["api-group"]],
278
+ ]);
279
+
280
+ const layerHints = new Map([
281
+ ["api-group", ["ui-group"]],
282
+ ]);
283
+
284
+ const result = checkFeasibility({
285
+ deltas,
286
+ ownership,
287
+ declared_deps: declaredDeps,
288
+ layer_hints: layerHints,
289
+ });
290
+
291
+ expect(result.feasible).toBe(true);
292
+ const order = result.ordered_group_ids!;
293
+ expect(order.indexOf("api-group")).toBeLessThan(order.indexOf("ui-group"));
294
+ });
184
295
  });
@@ -9,10 +9,11 @@ interface FeasibilityInput {
9
9
  deltas: DeltaEntry[];
10
10
  ownership: Map<string, string>;
11
11
  declared_deps?: Map<string, string[]>;
12
+ layer_hints?: Map<string, string[]>;
12
13
  }
13
14
 
14
15
  export function checkFeasibility(input: FeasibilityInput): FeasibilityResult {
15
- const { deltas, ownership, declared_deps } = input;
16
+ const { deltas, ownership, declared_deps, layer_hints } = input;
16
17
 
17
18
  const allGroups = new Set<string>(ownership.values());
18
19
  if (allGroups.size <= 1) {
@@ -28,6 +29,9 @@ export function checkFeasibility(input: FeasibilityInput): FeasibilityResult {
28
29
  if (declared_deps) {
29
30
  addDeclaredDepEdges(declared_deps, allGroups, edges);
30
31
  }
32
+ if (layer_hints) {
33
+ addLayerHintEdges(layer_hints, allGroups, edges);
34
+ }
31
35
 
32
36
  const deduped = deduplicateEdges(edges);
33
37
  const acyclic = breakAllCycles(deduped);
@@ -37,11 +41,22 @@ export function checkFeasibility(input: FeasibilityInput): FeasibilityResult {
37
41
  }
38
42
 
39
43
  function breakAllCycles(edges: ConstraintEdge[]): ConstraintEdge[] {
40
- const edgeSet = new Set(edges.map((e) => `${e.from}→${e.to}`));
44
+ const edgeMap = new Map<string, ConstraintEdge>();
45
+ for (const e of edges) edgeMap.set(`${e.from}→${e.to}`, e);
46
+
41
47
  const withoutMutual: ConstraintEdge[] = [];
42
48
  for (const edge of edges) {
43
49
  const reverseKey = `${edge.to}→${edge.from}`;
44
- if (edgeSet.has(reverseKey) && edge.kind === "dependency") continue;
50
+ const reverse = edgeMap.get(reverseKey);
51
+ if (!reverse) {
52
+ // No mutual conflict — keep
53
+ withoutMutual.push(edge);
54
+ continue;
55
+ }
56
+ // Mutual conflict: drop the LESS important edge.
57
+ // dependency > path-order (dependency is build-critical)
58
+ if (edge.kind === "path-order" && reverse.kind === "dependency") continue;
59
+ // Same kind: keep both, let breakRemainingCycles handle it
45
60
  withoutMutual.push(edge);
46
61
  }
47
62
  return breakRemainingCycles(withoutMutual);
@@ -77,7 +92,7 @@ function breakRemainingCycles(edges: ConstraintEdge[]): ConstraintEdge[] {
77
92
 
78
93
  const prioritized = [...edges].sort((a, b) => {
79
94
  const kindScore = (e: ConstraintEdge) =>
80
- e.kind === "path-order" ? 0 : e.kind === "dependency" ? 1 : 2;
95
+ e.kind === "dependency" ? 0 : e.kind === "path-order" ? 1 : 2;
81
96
  return kindScore(a) - kindScore(b);
82
97
  });
83
98
 
@@ -174,6 +189,27 @@ function addDeclaredDepEdges(
174
189
  }
175
190
  }
176
191
 
192
+ function addLayerHintEdges(
193
+ layerHints: Map<string, string[]>,
194
+ allGroups: Set<string>,
195
+ edges: ConstraintEdge[],
196
+ ): void {
197
+ for (const [groupId, deps] of layerHints) {
198
+ if (!allGroups.has(groupId)) continue;
199
+
200
+ for (const depGroupId of deps) {
201
+ if (!allGroups.has(depGroupId)) continue;
202
+ if (groupId === depGroupId) continue;
203
+
204
+ edges.push({
205
+ from: depGroupId,
206
+ to: groupId,
207
+ kind: "path-order",
208
+ });
209
+ }
210
+ }
211
+ }
212
+
177
213
  function deduplicateEdges(edges: ConstraintEdge[]): ConstraintEdge[] {
178
214
  const seen = new Set<string>();
179
215
  const result: ConstraintEdge[] = [];