newpr 1.0.24 → 1.0.26

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.
@@ -15,11 +15,11 @@ import { splitOversizedGroups } from "../../stack/split.ts";
15
15
  import { rebalanceGroups } from "../../stack/balance.ts";
16
16
  import { mergeGroups, mergeEmptyGroups } from "../../stack/merge-groups.ts";
17
17
  import { checkFeasibility } from "../../stack/feasibility.ts";
18
- import { createStackPlan } from "../../stack/plan.ts";
18
+ import { createStackPlan, buildDagParents, buildAncestorSets } from "../../stack/plan.ts";
19
19
  import { executeStack } from "../../stack/execute.ts";
20
20
  import { verifyStack } from "../../stack/verify.ts";
21
- import { runStackQualityGate } from "../../stack/quality-gate.ts";
22
- import { analyzeImportDependencies } from "../../stack/import-deps.ts";
21
+ import { runStackQualityGate, type QualityGateResult } from "../../stack/quality-gate.ts";
22
+ import { analyzeImportDependencies, rebuildGroupDeps, mergeImportCycleGroups } from "../../stack/import-deps.ts";
23
23
  import { extractSymbols } from "../../stack/symbol-flow.ts";
24
24
  import { buildCoChangePairs, buildHistoricalCoChangePairs } from "../../stack/co-change.ts";
25
25
  import { computeConfidenceReassignments, classifyGroupLayer } from "../../stack/confidence-score.ts";
@@ -115,6 +115,7 @@ export interface StackStateSnapshot {
115
115
  plan: StackPlanData | null;
116
116
  execResult: StackExecResult | null;
117
117
  verifyResult: StackVerifyData | null;
118
+ qualityGateResult: QualityGateResult | null;
118
119
  publishResult: StackPublishData | null;
119
120
  publishPreview: StackPublishPreviewData | null;
120
121
  startedAt: number;
@@ -127,12 +128,14 @@ interface StackSession {
127
128
  phase: StackPhase | null;
128
129
  error: string | null;
129
130
  maxGroups: number | null;
131
+ customEnv: Record<string, string> | null;
130
132
  context: StackContext | null;
131
133
  partition: StackPartitionData | null;
132
134
  feasibility: FeasibilityResult | null;
133
135
  plan: StackPlanData | null;
134
136
  execResult: StackExecResult | null;
135
137
  verifyResult: StackVerifyData | null;
138
+ qualityGateResult: QualityGateResult | null;
136
139
  publishResult: StackPublishData | null;
137
140
  publishPreview: StackPublishPreviewData | null;
138
141
  events: StackEvent[];
@@ -171,6 +174,7 @@ function toSnapshot(session: StackSession): StackStateSnapshot {
171
174
  plan: session.plan,
172
175
  execResult: session.execResult,
173
176
  verifyResult: session.verifyResult,
177
+ qualityGateResult: session.qualityGateResult,
174
178
  publishResult: session.publishResult,
175
179
  publishPreview: session.publishPreview,
176
180
  startedAt: session.startedAt,
@@ -248,6 +252,7 @@ export function startStack(
248
252
  maxGroups: number | null,
249
253
  token: string,
250
254
  config: NewprConfig,
255
+ customEnv?: Record<string, string> | null,
251
256
  ): { ok: true } | { error: string; status: number } {
252
257
  const existing = sessions.get(analysisSessionId);
253
258
  if (existing?.status === "running") {
@@ -272,6 +277,8 @@ export function startStack(
272
277
  subscribers: new Set(),
273
278
  startedAt: Date.now(),
274
279
  finishedAt: null,
280
+ customEnv: customEnv ?? null,
281
+ qualityGateResult: null,
275
282
  abortController: new AbortController(),
276
283
  };
277
284
  sessions.set(analysisSessionId, session);
@@ -335,6 +342,8 @@ export async function restoreCompletedStacks(sessionIds: string[]): Promise<void
335
342
  verifyResult: snapshot.verifyResult,
336
343
  publishResult: snapshot.publishResult ?? null,
337
344
  publishPreview: snapshot.publishPreview ?? null,
345
+ customEnv: null,
346
+ qualityGateResult: snapshot.qualityGateResult ?? null,
338
347
  events: [],
339
348
  subscribers: new Set(),
340
349
  startedAt: snapshot.startedAt,
@@ -621,16 +630,36 @@ async function runStackPipeline(
621
630
  groupLayerOrder.set(g.name, layer === "schema" ? 0 : layer === "refactor" ? 1 : layer === "core" ? 2 : layer === "integration" ? 3 : layer === "ui" ? 4 : layer === "test" ? 5 : 2);
622
631
  }
623
632
 
633
+ let finalGroupDeps = rebuildGroupDeps(importDeps.fileDeps, mergedOwnership);
634
+
635
+ const cycleMerge = mergeImportCycleGroups(currentGroups, mergedOwnership, finalGroupDeps);
636
+ if (cycleMerge.mergedCycles.length > 0) {
637
+ currentGroups = cycleMerge.groups;
638
+ for (const [path, gid] of cycleMerge.ownership) mergedOwnership.set(path, gid);
639
+ finalGroupDeps = rebuildGroupDeps(importDeps.fileDeps, mergedOwnership);
640
+ const cycleDetails = cycleMerge.mergedCycles.map((c) => c.join(" + "));
641
+ allWarnings.push(`Merged ${cycleMerge.mergedCycles.length} import dependency cycle(s): ${cycleDetails.join("; ")}`);
642
+ allStructuredWarnings.push({
643
+ category: "coupling",
644
+ severity: "warn",
645
+ title: `${cycleMerge.mergedCycles.length} group cycle(s) merged`,
646
+ message: "Groups with circular import dependencies were merged to prevent build failures",
647
+ details: cycleDetails,
648
+ });
649
+ emit(session, "partitioning", `Merged ${cycleMerge.mergedCycles.length} import dependency cycle(s)...`);
650
+ }
651
+
624
652
  const importDepEdges = new Set<string>();
625
- for (const [group, deps] of importDeps.groupDeps) {
653
+ for (const [group, deps] of finalGroupDeps) {
626
654
  for (const dep of deps) importDepEdges.add(`${dep}→${group}`);
627
655
  }
628
656
 
629
657
  const mergedDeclaredDeps = new Map<string, string[]>();
630
- for (const [group, deps] of importDeps.groupDeps) {
658
+ for (const [group, deps] of finalGroupDeps) {
631
659
  mergedDeclaredDeps.set(group, [...deps]);
632
660
  }
633
661
 
662
+ const layerHints = new Map<string, string[]>();
634
663
  const sortedByLayer = [...currentGroups].sort((a, b) => (groupLayerOrder.get(a.name) ?? 2) - (groupLayerOrder.get(b.name) ?? 2));
635
664
  for (let i = 1; i < sortedByLayer.length; i++) {
636
665
  const prev = sortedByLayer[i - 1]!;
@@ -638,10 +667,10 @@ async function runStackPipeline(
638
667
  if ((groupLayerOrder.get(prev.name) ?? 2) >= (groupLayerOrder.get(curr.name) ?? 2)) continue;
639
668
  const reverseKey = `${curr.name}→${prev.name}`;
640
669
  if (importDepEdges.has(reverseKey)) continue;
641
- const existing = mergedDeclaredDeps.get(curr.name) ?? [];
670
+ const existing = layerHints.get(curr.name) ?? [];
642
671
  if (!existing.includes(prev.name)) {
643
672
  existing.push(prev.name);
644
- mergedDeclaredDeps.set(curr.name, existing);
673
+ layerHints.set(curr.name, existing);
645
674
  }
646
675
  }
647
676
 
@@ -650,6 +679,7 @@ async function runStackPipeline(
650
679
  deltas,
651
680
  ownership: mergedOwnership,
652
681
  declared_deps: mergedDeclaredDeps,
682
+ layer_hints: layerHints,
653
683
  });
654
684
  const ownershipObj = Object.fromEntries(mergedOwnership);
655
685
 
@@ -708,6 +738,14 @@ async function runStackPipeline(
708
738
  for (const [path, groupId] of emptyMerged.ownership) {
709
739
  ownership.set(path, groupId);
710
740
  }
741
+
742
+ const postMergeOrder = plan.groups.map((g) => g.id);
743
+ const postMergeDeps = plan.groups.flatMap((g) => (g.deps ?? []).map((dep) => ({ from: dep, to: g.id })));
744
+ const postMergeDagParents = buildDagParents(postMergeOrder, postMergeDeps);
745
+ plan.ancestor_sets = new Map(
746
+ [...buildAncestorSets(postMergeOrder, postMergeDagParents)].map(([k, v]) => [k, [...v]]),
747
+ );
748
+
711
749
  const emptyDetails = emptyMerged.merges.map((m) => `"${m.absorbed}" → "${m.into}"`);
712
750
  allWarnings.push(`Merged ${emptyMerged.merges.length} empty group(s): ${emptyDetails.join(", ")}`);
713
751
  allStructuredWarnings.push({
@@ -742,6 +780,65 @@ async function runStackPipeline(
742
780
  group.file_stats = computed.file_stats;
743
781
  }
744
782
 
783
+ const MAX_EMPTY_MERGE_ROUNDS = 5;
784
+ for (let round = 0; round < MAX_EMPTY_MERGE_ROUNDS; round++) {
785
+ const hasEmpty = plan.groups.some((g) =>
786
+ g.stats && g.stats.additions === 0 && g.stats.deletions === 0,
787
+ );
788
+ if (!hasEmpty || plan.groups.length <= 1) break;
789
+
790
+ const postMerge = mergeEmptyGroups(plan.groups, ownership, plan.expected_trees);
791
+ if (postMerge.merges.length === 0) break;
792
+
793
+ plan.groups = postMerge.groups;
794
+ plan.expected_trees = postMerge.expectedTrees;
795
+ for (const [path, groupId] of postMerge.ownership) {
796
+ ownership.set(path, groupId);
797
+ }
798
+
799
+ const loopMergeOrder = plan.groups.map((g) => g.id);
800
+ const loopMergeDeps = plan.groups.flatMap((g) => (g.deps ?? []).map((dep) => ({ from: dep, to: g.id })));
801
+ const loopDagParents = buildDagParents(loopMergeOrder, loopMergeDeps);
802
+ plan.ancestor_sets = new Map(
803
+ [...buildAncestorSets(loopMergeOrder, loopDagParents)].map(([k, v]) => [k, [...v]]),
804
+ );
805
+
806
+ const mergeDetails = postMerge.merges.map((m) => `"${m.absorbed}" → "${m.into}"`);
807
+ allWarnings.push(`Merged ${postMerge.merges.length} empty group(s) (post-recompute): ${mergeDetails.join(", ")}`);
808
+ allStructuredWarnings.push({
809
+ category: "grouping",
810
+ severity: "info",
811
+ title: `${postMerge.merges.length} empty group(s) merged (post-recompute)`,
812
+ message: "Groups with zero effective changes after final stats recomputation were absorbed into adjacent groups",
813
+ details: mergeDetails,
814
+ });
815
+ emit(session, "planning", `Merged ${postMerge.merges.length} empty group(s) after recomputation...`);
816
+
817
+ const reDagParents = new Map(plan.groups.map((g) => [g.id, g.deps ?? []]));
818
+ const reStats = await computeGroupStatsWithFiles(
819
+ repoPath,
820
+ baseSha,
821
+ plan.groups.map((g) => g.id),
822
+ plan.expected_trees,
823
+ reDagParents,
824
+ );
825
+ for (const group of plan.groups) {
826
+ const computed = reStats.get(group.id);
827
+ if (!computed) continue;
828
+ group.stats = computed.stats;
829
+ group.file_stats = computed.file_stats;
830
+ }
831
+ }
832
+
833
+ if (session.partition) {
834
+ session.partition = {
835
+ ...session.partition,
836
+ ownership: Object.fromEntries(ownership),
837
+ warnings: allWarnings,
838
+ structured_warnings: allStructuredWarnings,
839
+ };
840
+ }
841
+
745
842
  emit(session, "planning", "Generating PR titles...");
746
843
  const prTitles = await generatePrTitles(llmClient, plan.groups, stored.meta.pr_title, config.language);
747
844
  for (const group of plan.groups) {
@@ -800,18 +897,25 @@ async function runStackPipeline(
800
897
  throw new Error(`Verification failed: ${verifyResult.errors.join(", ")}`);
801
898
  }
802
899
 
803
- emit(session, "executing", "Running lint/build quality gate for each stack PR...");
900
+ emit(session, "executing", "Running quality gate (typecheck, lint, test, build) for each stack PR...");
804
901
  const qualityGateResult = await runStackQualityGate({
805
902
  repo_path: repoPath,
806
903
  exec_result: execResult,
904
+ custom_env: session.customEnv ?? undefined,
807
905
  onProgress: (message) => emit(session, "executing", message),
808
906
  checkAborted: () => checkAborted(session),
809
907
  });
908
+ session.qualityGateResult = qualityGateResult;
810
909
 
811
910
  if (qualityGateResult.skippedReason) {
812
911
  emit(session, "executing", `Quality gate skipped: ${qualityGateResult.skippedReason}`);
813
912
  } else if (qualityGateResult.ran) {
814
- emit(session, "executing", "Quality gate passed for all stack PRs");
913
+ const failedGroups = qualityGateResult.groupResults.filter((g) => !g.passed && !g.skipped);
914
+ if (failedGroups.length > 0) {
915
+ emit(session, "executing", `Quality gate: ${failedGroups.length} group(s) had failures (warning only, not blocking)`);
916
+ } else {
917
+ emit(session, "executing", "Quality gate passed for all stack PRs");
918
+ }
815
919
  }
816
920
 
817
921
  // ---- Done ----