newpr 1.0.18 → 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 +1 -1
- package/src/cli/args.ts +1 -1
- package/src/config/index.ts +1 -6
- package/src/stack/feasibility.ts +9 -8
- package/src/stack/partition.ts +45 -19
- package/src/web/server/routes.ts +6 -4
- package/src/web/server/stack-manager.ts +13 -6
package/package.json
CHANGED
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
|
|
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).
|
package/src/config/index.ts
CHANGED
|
@@ -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;
|
package/src/stack/feasibility.ts
CHANGED
|
@@ -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
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
}
|
package/src/stack/partition.ts
CHANGED
|
@@ -193,7 +193,25 @@ function parsePartitionResponse(
|
|
|
193
193
|
const warnings: string[] = [];
|
|
194
194
|
const structuredWarnings: StackWarning[] = [];
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
let sharedFoundation: FileGroup | undefined;
|
|
197
|
+
if (data.shared_foundation && typeof data.shared_foundation === "object") {
|
|
198
|
+
const sf = data.shared_foundation as Record<string, unknown>;
|
|
199
|
+
sharedFoundation = {
|
|
200
|
+
name: String(sf.name ?? "Shared Foundation"),
|
|
201
|
+
type: "chore",
|
|
202
|
+
description: String(sf.description ?? "Common infrastructure changes"),
|
|
203
|
+
files: Array.isArray(sf.files) ? sf.files.map(String) : [],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const groupNameLookup = new Map<string, string>();
|
|
208
|
+
for (const group of groups) {
|
|
209
|
+
groupNameLookup.set(group.name.toLowerCase(), group.name);
|
|
210
|
+
}
|
|
211
|
+
if (sharedFoundation) {
|
|
212
|
+
groupNameLookup.set(sharedFoundation.name.toLowerCase(), sharedFoundation.name);
|
|
213
|
+
groupNameLookup.set("shared foundation", sharedFoundation.name);
|
|
214
|
+
}
|
|
197
215
|
|
|
198
216
|
for (const item of assignments) {
|
|
199
217
|
const entry = item as Record<string, unknown>;
|
|
@@ -206,7 +224,26 @@ function parsePartitionResponse(
|
|
|
206
224
|
continue;
|
|
207
225
|
}
|
|
208
226
|
|
|
209
|
-
|
|
227
|
+
const normalizedGroup = group.toLowerCase().replace(/["'`]/g, "").trim();
|
|
228
|
+
const isSharedFoundationAlias = /shared[\s_-]*foundation/.test(normalizedGroup);
|
|
229
|
+
let canonicalGroup = groupNameLookup.get(normalizedGroup);
|
|
230
|
+
if (!canonicalGroup && isSharedFoundationAlias) {
|
|
231
|
+
if (!sharedFoundation) {
|
|
232
|
+
sharedFoundation = {
|
|
233
|
+
name: "Shared Foundation",
|
|
234
|
+
type: "chore",
|
|
235
|
+
description: "Common infrastructure changes",
|
|
236
|
+
files: [],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
groupNameLookup.set(sharedFoundation.name.toLowerCase(), sharedFoundation.name);
|
|
240
|
+
groupNameLookup.set("shared-foundation", sharedFoundation.name);
|
|
241
|
+
groupNameLookup.set("shared_foundation", sharedFoundation.name);
|
|
242
|
+
groupNameLookup.set("shared foundation", sharedFoundation.name);
|
|
243
|
+
canonicalGroup = sharedFoundation.name;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!canonicalGroup) {
|
|
210
247
|
warnings.push(`Unknown group "${group}" for file "${path}", skipping`);
|
|
211
248
|
structuredWarnings.push({
|
|
212
249
|
category: "system",
|
|
@@ -224,19 +261,22 @@ function parsePartitionResponse(
|
|
|
224
261
|
reattributed.push({
|
|
225
262
|
path,
|
|
226
263
|
from_groups: ambiguousEntry.groups,
|
|
227
|
-
to_group:
|
|
264
|
+
to_group: canonicalGroup,
|
|
228
265
|
reason,
|
|
229
266
|
});
|
|
230
267
|
} else if (isUnassigned) {
|
|
231
268
|
reattributed.push({
|
|
232
269
|
path,
|
|
233
270
|
from_groups: [],
|
|
234
|
-
to_group:
|
|
271
|
+
to_group: canonicalGroup,
|
|
235
272
|
reason,
|
|
236
273
|
});
|
|
237
274
|
}
|
|
238
275
|
|
|
239
|
-
ownership.set(path,
|
|
276
|
+
ownership.set(path, canonicalGroup);
|
|
277
|
+
if (sharedFoundation && canonicalGroup === sharedFoundation.name && !sharedFoundation.files.includes(path)) {
|
|
278
|
+
sharedFoundation.files.push(path);
|
|
279
|
+
}
|
|
240
280
|
}
|
|
241
281
|
|
|
242
282
|
const stillUnassigned = report.unassigned.filter((p) => !ownership.has(p));
|
|
@@ -274,18 +314,6 @@ function parsePartitionResponse(
|
|
|
274
314
|
});
|
|
275
315
|
}
|
|
276
316
|
|
|
277
|
-
let sharedFoundation: FileGroup | undefined;
|
|
278
|
-
if (data.shared_foundation && typeof data.shared_foundation === "object") {
|
|
279
|
-
const sf = data.shared_foundation as Record<string, unknown>;
|
|
280
|
-
sharedFoundation = {
|
|
281
|
-
name: String(sf.name ?? "Shared Foundation"),
|
|
282
|
-
type: "chore",
|
|
283
|
-
description: String(sf.description ?? "Common infrastructure changes"),
|
|
284
|
-
files: Array.isArray(sf.files) ? sf.files.map(String) : [],
|
|
285
|
-
};
|
|
286
|
-
validGroupNames.add(sharedFoundation.name);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
317
|
return {
|
|
290
318
|
ownership,
|
|
291
319
|
reattributed,
|
|
@@ -294,5 +322,3 @@ function parsePartitionResponse(
|
|
|
294
322
|
structured_warnings: structuredWarnings,
|
|
295
323
|
};
|
|
296
324
|
}
|
|
297
|
-
|
|
298
|
-
|
package/src/web/server/routes.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
446
|
-
if (
|
|
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,
|
|
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 "${
|
|
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
|
|
460
|
-
message: `AI could not classify these files — assigned to "${
|
|
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) {
|