From 2acfc85482590cd4c648d21e0ae425078b565a08 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Thu, 15 Oct 2026 02:42:30 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Loop-Erkennung=20meldet=20bei=20?= =?UTF-8?q?=E2=80=9EBlocker:=20Keine=E2=80=9C=20keinen=20falschen=20Schlei?= =?UTF-8?q?fenabbruch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Judge-Runden mit „Blocker: Keine“ und verschiedenen Major-Punkten galten als „gleicher Blocker zweimal“. loopKey() vergleicht jetzt die Blocker, bei leerem Blocker-Abschnitt die Major-Punkte, sonst den ganzen Judge-Text. Co-Authored-By: Claude Opus 5.5 --- BEDIENUNGSANLEITUNG.md | 9 ++++++--- pi-coding-extension.ts | 22 +++++++++++++++++++--- tests/extension-guards.test.mjs | 2 +- tests/pure-logic.test.mjs | 24 ++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index a633c06..6484690 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -430,12 +430,15 @@ Stand und der Fix-Zyklus läuft automatisch bis PASS oder max. N Runden. ### Loop-Erkennung -Wenn zweimal hintereinander genau dieselben Blocker oder Concerns auftreten, bricht -`/optimize` ab: +Wenn zweimal hintereinander dieselbe Beanstandung auftritt, bricht `/optimize` ab: ``` -⚠ Derselbe Blocker tritt erneut auf – Schleife abgebrochen. Bitte manuell prüfen. +⚠ Schleife: Gleicher Blocker zweimal – manuelle Intervention nötig ``` +Verglichen wird der Blocker-Abschnitt des Judges. Steht dort nur „Keine“, werden stattdessen die Major-Punkte +verglichen; fehlen beide Abschnitte, der ganze Judge-Text. Zwei Runden mit „Blocker: Keine“, aber verschiedenen +Major-Punkten sind also keine Schleife. + In diesem Fall: `/judge` manuell ausführen, Blocker lesen, mit `/fix` manuell eingreifen. ### Max. Runden ohne PASS diff --git a/pi-coding-extension.ts b/pi-coding-extension.ts index 883fc22..12b24cb 100644 --- a/pi-coding-extension.ts +++ b/pi-coding-extension.ts @@ -677,6 +677,23 @@ function parseBlockers(text: string): string { return m ? m[1].trim() : ""; } +// Schlüssel für die Loop-Erkennung im /optimize-Loop: der Blocker-Abschnitt; steht dort nur „Keine“ o. ä., +// der Major-Abschnitt; fehlen beide, der ganze Judge-Text. Ohne diese Unterscheidung galten zwei Runden mit +// „Blocker: Keine“ und unterschiedlichen Major-Punkten als „gleicher Blocker“ (Fehlalarm). +function loopKey(text: string): string { + const stop = "(?:\\n(?:\\*\\*(?:Blocker|Major|Minor)\\*\\*|##\\s*(?:Blocker|Major|Minor)|[-–*]\\s*(?:Blocker|Major|Minor))|$)"; + const section = (name) => { + const m = text.match(new RegExp(`(?:\\*\\*${name}\\*\\*|##\\s*${name}|[-–*]\\s*${name})[:\\n]([\\s\\S]*?)${stop}`, "i")); + return m ? m[1].trim() : ""; + }; + const isNone = (s) => /^[\s\-–*•:]*(keine?r?|none|n\/a|nichts|—|-)?[\s.!]*$/i.test(s); + const blockers = section("Blocker"); + if (blockers && !isNone(blockers)) return blockers; + const majors = section("Major"); + if (majors && !isNone(majors)) return "MAJOR: " + majors; + return text; +} + // Gibt geänderte Quelldateien seit einem Git-Tag zurück. // null = Tag existiert nicht (erster Lauf) → alles verarbeiten // [] = nichts geändert → Phase überspringen @@ -1372,10 +1389,9 @@ export default function (pi: ExtensionAPI) { } // Loop-Erkennung: gleiche Beanstandung zweimal → manuell eingreifen. - // Fallback auf den gesamten Judge-Text, wenn kein Blocker-Abschnitt vorhanden ist - // (z.B. reine Minor-Concerns) — sonst würde die Erkennung dort nie greifen. + // Schlüssel: Blocker, bei „Blocker: Keine“ die Major-Punkte, sonst der ganze Judge-Text (loopKey). // Normalisierung verhindert False-Negatives durch minimale Formulierungsunterschiede. - const currentBlockers = parseBlockers(judgeText) || judgeText; + const currentBlockers = loopKey(judgeText); if (currentBlockers && normalizeForComparison(currentBlockers) === normalizeForComparison(lastBlockers)) { ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`); finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig"); diff --git a/tests/extension-guards.test.mjs b/tests/extension-guards.test.mjs index 9297e6e..0f281f5 100644 --- a/tests/extension-guards.test.mjs +++ b/tests/extension-guards.test.mjs @@ -38,7 +38,7 @@ test("Fix-Auslösung: PASS WITH CONCERNS gilt nur mit --approve-concerns als bes }); test("Loop-Erkennung greift auch ohne Blocker-Abschnitt (Fallback auf Gesamttext)", () => { - assert.match(extSource, /parseBlockers\(judgeText\)\s*\|\|\s*judgeText/); + assert.match(extSource, /const currentBlockers = loopKey\(judgeText\)/); }); test("currentRole wird nach jedem Kommando auf 'idle' zurückgesetzt (kein Persona-Leak in freien Chat)", () => { diff --git a/tests/pure-logic.test.mjs b/tests/pure-logic.test.mjs index af1f446..de86901 100644 --- a/tests/pure-logic.test.mjs +++ b/tests/pure-logic.test.mjs @@ -9,6 +9,7 @@ const parseVerdict = extractFn("parseVerdict"); const parseBlockers = extractFn("parseBlockers"); const normalizeForComparison = extractFn("normalizeForComparison"); const licenceMd = extractFn("licenceMd"); +const loopKey = extractFn("loopKey"); test("parseVerdict: klares PASS", () => { assert.equal(parseVerdict("Review fertig.\nUrteil: PASS\nKeine Blocker."), "PASS"); @@ -78,3 +79,26 @@ test("licenceMd: proprietärer Default mit eingesetztem Jahr und Inhaber", () => test("licenceMd: Platzhalter-Inhaber wird unverändert eingesetzt", () => { assert.match(licenceMd("2030", "[Rechteinhaber eintragen]"), /Copyright \(c\) 2030 \[Rechteinhaber eintragen\]/); }); + +test("loopKey: echte Blocker sind der Schlüssel", () => { + const txt = "## Blocker\n- B1 Absturz bei leerer Eingabe\n\n## Major\n- M1 x"; + assert.match(loopKey(txt), /B1 Absturz/); + assert.doesNotMatch(loopKey(txt), /M1/); +}); + +test("loopKey: 'Blocker: Keine' → Major-Punkte statt Fehlalarm (Strata-Probe 15.10.)", () => { + const r1 = "## Urteil: **PASS WITH CONCERNS**\n\n## Blocker\n- Keine.\n\n## Major\n- M1 Flags als Name\n\n## Minor\n- N1"; + const r2 = "## Urteil: **PASS WITH CONCERNS**\n\n## Blocker\n- Keine.\n\n## Major\n- M1 UTF-8-Absturz\n\n## Minor\n- N1"; + assert.notEqual(loopKey(r1), loopKey(r2)); + assert.match(loopKey(r1), /^MAJOR: - M1 Flags/); +}); + +test("loopKey: gleiche Major-Punkte zweimal → gleicher Schlüssel (Loop wird erkannt)", () => { + const r = "**Blocker**\nkeine\n**Major**\n- M1 gleich\n**Minor**\n- n"; + assert.equal(loopKey(r), loopKey(r + "\n")); +}); + +test("loopKey: weder Blocker noch Major → ganzer Text", () => { + assert.equal(loopKey("Urteil: FAIL\nirgendwas"), "Urteil: FAIL\nirgendwas"); + assert.equal(loopKey("## Blocker\nNone\n## Major\n-"), "## Blocker\nNone\n## Major\n-"); +});