newpr 1.0.19 → 1.0.20

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.19",
3
+ "version": "1.0.20",
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",
package/src/cli/args.ts CHANGED
@@ -57,7 +57,7 @@ Options (review mode):
57
57
  -v, --version Show version
58
58
 
59
59
  Environment Variables:
60
- OPENROUTER_API_KEY Required. Your OpenRouter API key.
60
+ OPENROUTER_API_KEY Optional. Used when configured; otherwise local agent fallback is used.
61
61
  GITHUB_TOKEN Optional. Falls back to gh CLI token.
62
62
  NEWPR_MODEL Default model override.
63
63
  NEWPR_MAX_FILES Max files to analyze (default: 100).
@@ -61,12 +61,7 @@ export async function loadConfig(
61
61
  ): Promise<NewprConfig> {
62
62
  const stored = await (_readStore ?? readStoredConfig)();
63
63
 
64
- const apiKey = process.env.OPENROUTER_API_KEY || stored.openrouter_api_key;
65
- if (!apiKey) {
66
- throw new Error(
67
- "OPENROUTER_API_KEY is not set. Run `newpr auth` to configure, or set the environment variable.",
68
- );
69
- }
64
+ const apiKey = process.env.OPENROUTER_API_KEY || stored.openrouter_api_key || "";
70
65
 
71
66
  const agentVal = stored.agent as NewprConfig["agent"];
72
67
  const rawLang = process.env.NEWPR_LANGUAGE || stored.language || DEFAULT_CONFIG.language;
@@ -30,7 +30,7 @@ export function checkFeasibility(input: FeasibilityInput): FeasibilityResult {
30
30
  }
31
31
 
32
32
  const deduped = deduplicateEdges(edges);
33
- const result = topologicalSort(Array.from(allGroups), deduped, deltas);
33
+ const result = topologicalSort(Array.from(allGroups), deduped, deltas, ownership);
34
34
 
35
35
  return result;
36
36
  }
@@ -137,6 +137,7 @@ function topologicalSort(
137
137
  groups: string[],
138
138
  edges: ConstraintEdge[],
139
139
  deltas: DeltaEntry[],
140
+ ownership?: Map<string, string>,
140
141
  ): FeasibilityResult {
141
142
  const inDegree = new Map<string, number>();
142
143
  const adjacency = new Map<string, string[]>();
@@ -156,7 +157,7 @@ function topologicalSort(
156
157
  edgeMap.set(`${edge.from}→${edge.to}`, edge);
157
158
  }
158
159
 
159
- const firstCommitDate = buildFirstCommitDateMap(groups, deltas);
160
+ const firstCommitDate = buildFirstCommitDateMap(groups, deltas, ownership);
160
161
 
161
162
  const queue: string[] = [];
162
163
  for (const [g, deg] of inDegree) {
@@ -200,6 +201,7 @@ function topologicalSort(
200
201
  function buildFirstCommitDateMap(
201
202
  groups: string[],
202
203
  deltas: DeltaEntry[],
204
+ ownership?: Map<string, string>,
203
205
  ): Map<string, string> {
204
206
  const result = new Map<string, string>();
205
207
  for (const g of groups) {
@@ -207,12 +209,11 @@ function buildFirstCommitDateMap(
207
209
  }
208
210
  for (const delta of deltas) {
209
211
  for (const change of delta.changes) {
210
- const g = change.path;
211
- if (result.has(g)) {
212
- const current = result.get(g);
213
- if (!current || delta.date < current) {
214
- result.set(g, delta.date);
215
- }
212
+ const groupId = ownership?.get(change.path);
213
+ if (!groupId) continue;
214
+ const current = result.get(groupId);
215
+ if (current && delta.date < current) {
216
+ result.set(groupId, delta.date);
216
217
  }
217
218
  }
218
219
  }
@@ -1658,7 +1658,8 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
1658
1658
  },
1659
1659
 
1660
1660
  "POST /api/slides": async (req: Request) => {
1661
- if (!config.openrouter_api_key) return json({ error: "OpenRouter API key required" }, 400);
1661
+ const apiKey = config.openrouter_api_key;
1662
+ if (!apiKey) return json({ error: "OpenRouter API key required" }, 400);
1662
1663
 
1663
1664
  const body = await req.json() as { sessionId?: string; language?: string; resume?: boolean };
1664
1665
  const sessionId = body.sessionId;
@@ -1678,7 +1679,7 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
1678
1679
  (async () => {
1679
1680
  try {
1680
1681
  const deck = await generateSlides(
1681
- config.openrouter_api_key,
1682
+ apiKey,
1682
1683
  data,
1683
1684
  config.model,
1684
1685
  body.language ?? config.language,
@@ -1749,7 +1750,8 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
1749
1750
  const body = await req.json() as { sessionId?: string; resume?: boolean };
1750
1751
  const sessionId = body.sessionId;
1751
1752
  if (!sessionId) return json({ error: "Missing sessionId" }, 400);
1752
- if (!config.openrouter_api_key) return json({ error: "API key required" }, 400);
1753
+ const apiKey = config.openrouter_api_key;
1754
+ if (!apiKey) return json({ error: "API key required" }, 400);
1753
1755
 
1754
1756
  const plugin = getPlugin(pluginId);
1755
1757
  if (!plugin) return json({ error: `Unknown plugin: ${pluginId}` }, 404);
@@ -1770,7 +1772,7 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
1770
1772
  (async () => {
1771
1773
  try {
1772
1774
  const result = await plugin.generate(
1773
- { apiKey: config.openrouter_api_key, sessionId, data, language: config.language },
1775
+ { apiKey, sessionId, data, language: config.language },
1774
1776
  (event) => { job.message = event.message; job.current = event.current; job.total = event.total; },
1775
1777
  existingData,
1776
1778
  );
@@ -442,22 +442,22 @@ async function runStackPipeline(
442
442
 
443
443
  buildReattributionWarnings(partition, analysisSet, allStructuredWarnings);
444
444
 
445
- const lastGroup = groupOrder[groupOrder.length - 1];
446
- if (lastGroup) {
445
+ const backfillGroup = groupOrder[0] ?? groupOrder[groupOrder.length - 1];
446
+ if (backfillGroup) {
447
447
  const backfilled: string[] = [];
448
448
  for (const path of deltaFilePaths) {
449
449
  if (!mergedOwnership.has(path)) {
450
- mergedOwnership.set(path, lastGroup);
450
+ mergedOwnership.set(path, backfillGroup);
451
451
  backfilled.push(path);
452
452
  }
453
453
  }
454
454
  if (backfilled.length > 0) {
455
- allWarnings.push(`Files still unassigned after AI classification, fallback to "${lastGroup}": ${backfilled.join(", ")}`);
455
+ allWarnings.push(`Files still unassigned after AI classification, fallback to "${backfillGroup}": ${backfilled.join(", ")}`);
456
456
  allStructuredWarnings.push({
457
457
  category: "assignment",
458
458
  severity: "warn",
459
- title: `${backfilled.length} file(s) fell back to last group`,
460
- message: `AI could not classify these files — assigned to "${lastGroup}" as fallback`,
459
+ title: `${backfilled.length} file(s) fell back to first group`,
460
+ message: `AI could not classify these files — assigned to "${backfillGroup}" as fallback`,
461
461
  details: backfilled,
462
462
  });
463
463
  }
@@ -476,6 +476,13 @@ async function runStackPipeline(
476
476
  for (const [path, groupId] of balanced.ownership) {
477
477
  mergedOwnership.set(path, groupId);
478
478
  }
479
+ const balancedGroupFiles = new Map<string, string[]>();
480
+ for (const [path, groupId] of mergedOwnership) {
481
+ const files = balancedGroupFiles.get(groupId) ?? [];
482
+ files.push(path);
483
+ balancedGroupFiles.set(groupId, files);
484
+ }
485
+ currentGroups = currentGroups.map((g) => ({ ...g, files: (balancedGroupFiles.get(g.name) ?? g.files).sort() }));
479
486
  allStructuredWarnings.push(...balanced.warnings);
480
487
 
481
488
  if (session.maxGroups && session.maxGroups > 0 && currentGroups.length > session.maxGroups) {