From 1a01f7c427a9ed8f68b3bbaf1f793ab2ece7da87 Mon Sep 17 00:00:00 2001 From: Igor Gerasimov Date: Wed, 29 Jul 2026 13:42:57 +0200 Subject: [PATCH] =?UTF-8?q?feat(consilium):=20ADR-005=20Phase=201=20?= =?UTF-8?q?=E2=80=94=20tier-per-round=20develop=20scope=20(scope:=20top)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PDO-819 develop round shipped all 6 action points in one ~100-file MR the operator closed as unreviewable. The binary p0/all chooser (#580) dead-ends after the P0 merge: the next verdict starts at P1 and the p0 scope filters to empty. - filterDevScope (pure, exported, unit-tested): scope 'top' keeps only the HIGHEST priority tier still present (P0→P1→P2→P3→unprioritized) — each round ships one small MR and the next round's verdict naturally carries the following tier, all the way to convergence. - develop()/route/hook accept 'top'; the UI chooser offers 'Only (n) — small MR' with the tier resolved dynamically. - ADR-005 records the full mechanism: Phase 2 (stacked MRs in one round, bottom-up merge) and Phase 3 (repo-map size-aware grouping) as proposed follow-ups. loop-fsm + develop-route: 161 passed (4 new). tsc + vite build clean. --- client/src/hooks/use-consilium-loops.ts | 6 +- client/src/pages/ConsiliumLoopDetail.tsx | 30 +++++---- docs/adr/ADR-005-mr-splitting.md | 64 +++++++++++++++++++ server/routes/consilium-loops.ts | 4 +- .../consilium/consilium-loop-controller.ts | 37 ++++++++--- .../consilium-loops-develop-route.test.ts | 11 ++++ tests/unit/consilium/loop-fsm.test.ts | 21 ++++++ 7 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 docs/adr/ADR-005-mr-splitting.md diff --git a/client/src/hooks/use-consilium-loops.ts b/client/src/hooks/use-consilium-loops.ts index 12a9a40..bfd68df 100644 --- a/client/src/hooks/use-consilium-loops.ts +++ b/client/src/hooks/use-consilium-loops.ts @@ -518,9 +518,9 @@ export function useApproveMerge() { */ export function useDevelopLoop() { const qc = useQueryClient(); - return useMutation({ - // MR-size control: `scope: "p0"` develops only the P0s (small reviewable MR); - // omitted/"all" keeps the historical full-verdict round. + return useMutation({ + // MR-size control: `scope: "top"` develops only the highest remaining priority + // tier (small reviewable MR); omitted/"all" keeps the historical full round. mutationFn: ({ id, scope }) => apiRequest("POST", `${LIST_KEY}/${id}/develop`, scope ? { scope } : undefined), onSuccess: (_data, { id }) => { diff --git a/client/src/pages/ConsiliumLoopDetail.tsx b/client/src/pages/ConsiliumLoopDetail.tsx index 826dedd..8221652 100644 --- a/client/src/pages/ConsiliumLoopDetail.tsx +++ b/client/src/pages/ConsiliumLoopDetail.tsx @@ -2806,11 +2806,18 @@ export default function ConsiliumLoopDetail() { const latestActionPointCount = Array.isArray(latestRound?.openActionPoints) ? latestRound!.openActionPoints.length : 0; - // MR-size control: P0 slice of the latest verdict — offered as a smaller round. - const latestP0Count = Array.isArray(latestRound?.openActionPoints) - ? (latestRound!.openActionPoints as ActionPoint[]).filter((ap) => ap.priority === "P0") - .length - : 0; + // MR-size control (ADR-005 Phase 1): the HIGHEST priority tier still present in + // the latest verdict — offered as a smaller round (mirrors server filterDevScope). + const latestAps: ActionPoint[] = Array.isArray(latestRound?.openActionPoints) + ? (latestRound!.openActionPoints as ActionPoint[]) + : []; + const tierRankOf = (p?: string): number => + ({ P0: 0, P1: 1, P2: 2, P3: 3 } as Record)[p ?? ""] ?? 4; + const topTierRank = latestAps.length + ? Math.min(...latestAps.map((ap) => tierRankOf(ap.priority))) + : 4; + const topTierLabel = ["P0", "P1", "P2", "P3", "unprioritized"][topTierRank]; + const topTierCount = latestAps.filter((ap) => tierRankOf(ap.priority) === topTierRank).length; const canDevelop = isVerdictTerminalLoopState(loop.state) && latestActionPointCount > 0; @@ -2887,7 +2894,8 @@ export default function ConsiliumLoopDetail() { async function handleDevelop(scopeArg?: unknown) { // Normalize: plain onClick callers pass a click event — only the two literal // scopes ride to the server; anything else means "all" (historical behaviour). - const scope = scopeArg === "p0" || scopeArg === "all" ? scopeArg : undefined; + const scope = + scopeArg === "p0" || scopeArg === "top" || scopeArg === "all" ? scopeArg : undefined; setDevelopScopeOpen(false); try { await developLoop.mutateAsync({ id, scope }); @@ -2965,9 +2973,9 @@ export default function ConsiliumLoopDetail() {