From 0dcd2ab6dfffb3b677771b766182d9112113f6ba Mon Sep 17 00:00:00 2001 From: Nitsu45 Date: Wed, 14 Oct 2026 22:42:27 +0200 Subject: [PATCH] Prepare bounded casebuilding trial with explicit phase stop --- docs/casebuilding-trial.md | 50 +++++++++++++++++++++++++++++++++ src/viano_bug_analysis/trial.py | 32 +++++++++++++++++++-- tests/test_trial.py | 23 +++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 docs/casebuilding-trial.md diff --git a/docs/casebuilding-trial.md b/docs/casebuilding-trial.md new file mode 100644 index 0000000..daaee51 --- /dev/null +++ b/docs/casebuilding-trial.md @@ -0,0 +1,50 @@ +# Begrenzter Casebuilding-Test + +Der Test vom 14.10.2026 verwendet eine frische Fallakte aus den ursprünglichen +wowdiag/wowsrc-Paketen, die lokale World-Testdatenbank und den festgelegten Git-Commit. +Die vorherige gestoppte Analyse wird nicht fortgesetzt. + +Workspace: `service-data/trials/casebuilding-20261014` +Case-ID: `BUG-000020` +Package-ID: `trial-casebuilding-BUG-000020-20261014` + +Der deterministische CaseBuilder wurde bereits ohne LLM ausgeführt. Das gültige +Paket enthält vier belegte Projektabfragen: die Git-Schemafundstellen für Creature +und Quest sowie DB-Namen/Details für die gemeldete Creature und Quest. Insbesondere +ist `23486 = Goldark Snipehunter` vor der Interpretation sichtbar. +Die Lösung des bekannten Bugs wurde nicht vorgegeben. Snapshots für die Prüfung +liegen als `case.before-llm.json`, `case.before-llm.md` und `preparation.json` im Workspace. + +In der PowerShell mit gesetztem API-Key aus dem Repository-Verzeichnis starten: + +```powershell +.\.venv\Scripts\python.exe -m viano_bug_analysis.trial run --workspace service-data/trials/casebuilding-20261014 --llm-config config/llm.timeout300.local.toml --project-config config/project.local.toml --case-id BUG-000020 --package-id trial-casebuilding-BUG-000020-20261014 --stop-after interpretation --max-dispatches 6 +``` + +Dieser Befehl führt kostenpflichtige LLM-Anfragen aus. `--stop-after interpretation` +beendet den Lauf bei der bestätigten Interpretation mit `PHASE_COMPLETE`, auch bei +einer erneuten Ausführung desselben Befehls. Er startet keine Hypothesengenerierung +oder Investigation. `--max-dispatches 6` begrenzt zusätzlich die einzelnen Schritte; +die bestehende Korrekturpolicy begrenzt ungültige Antworten separat. Provider-Retries +folgen der LLM-Konfiguration; der Dienst wartet nicht aktiv auf einen Retry-Zeitpunkt. + +Bei `RETRY_PENDING`/`DEFERRED` nach dem angegebenen Zeitpunkt denselben Befehl +erneut ausführen. `YIELDED` bedeutet, dass das Schrittlimit vor einem bestätigten +Ergebnis erreicht wurde; derselbe Befehl setzt fort. Bei `ERROR`, `BLOCKED` oder +`REVIEW` zuerst die Ausgabe auswerten. + +Prüfkriterien nach `PHASE_COMPLETE`: + +- Goldark wird korrekt von den betroffenen Wolpertingern unterschieden. +- Testerbeobachtung, erwartetes Verhalten und Annahmen bleiben getrennt. +- Neue technische Aussagen zitieren die DB-/Git-Fakten aus dem Case. +- Nicht gelesene Spawns werden nicht als bekannte Daten ausgegeben. +- DB-Snapshot und Sourcecode-Schemata werden nicht mit Live-Serverbeobachtungen verwechselt. +- Verbleibende Wissenslücken sind sichtbar; die Interpretation behauptet noch keine Root Cause. +- Modellaufrufe, Input-/Output-Tokens, Korrekturversuche und Laufzeit werden aus der + gespeicherten LLM-Historie ausgewertet. Ohne konfigurierte Preise ist `estimated_cost` + gegebenenfalls null; tatsächliche Kosten sind zusätzlich im Provider-Dashboard sichtbar. + +Der JSON-Abschluss zeigt den Pfad der neuen Paketgeneration. Die vollständige +Interpretation, JSON/Markdown-Zusammenfassung, Prompt und Tokenmetadaten liegen +in diesem Paket. Der vorbereitete Case bleibt append-only erhalten. diff --git a/src/viano_bug_analysis/trial.py b/src/viano_bug_analysis/trial.py index 65d05cc..824eef1 100644 --- a/src/viano_bug_analysis/trial.py +++ b/src/viano_bug_analysis/trial.py @@ -59,9 +59,11 @@ def describe(path, status, *, code=None, exported=None, retry_not_before=None): async def drive(store, gateway, case_id, package_id, *, max_dispatches=100, - attempt_id="default", policies=None, progress=None): + attempt_id="default", policies=None, progress=None, stop_after=None): if type(max_dispatches) is not int or max_dispatches <= 0: raise TrialError("TRIAL_DISPATCH_LIMIT") + if stop_after not in {None, "case", "interpretation"}: + raise TrialError("TRIAL_STOP_PHASE") policies = policies or {} identity = (case_id, package_id) correction = policies.get("correction", CorrectionConfig()) @@ -86,6 +88,8 @@ async def drive(store, gateway, case_id, package_id, *, max_dispatches=100, if state == State.BLOCKED: return describe(latest, "BLOCKED", code=routing["reason"], retry_not_before=routing["retry_not_before"]) + if phase_complete(latest, stop_after): + return describe(latest, "PHASE_COMPLETE") if progress: progress({"dispatch": dispatch + 1, "state": state}) if state in {State.OUTPUT_READY, State.FAILED_TO_ANALYZE, State.HANDED_OFF}: @@ -124,7 +128,27 @@ async def drive(store, gateway, case_id, package_id, *, max_dispatches=100, return describe(result.package, status, code=getattr(result, "code", None), exported=getattr(result, "exported", None), retry_not_before=getattr(result, "retry_not_before", None)) - return describe(store.latest(*identity), "YIELDED") + latest = store.latest(*identity) + return describe(latest, "PHASE_COMPLETE" if phase_complete(latest, stop_after) else "YIELDED") + + +def phase_complete(path, stop_after): + if stop_after is None: + return False + from .case.contracts import read_json + with PackageReader(path) as reader: + manifest = reader.manifest + if stop_after == "case": + ref = manifest["artifacts"].get("case") + if manifest["state"] != State.BUILDING_CASE or not ref: + return False + doc = read_json(reader, ref, "case", 8 * 1024 * 1024)["case"] + else: + ref = manifest["artifacts"].get("case_interpretation") + if manifest["state"] != State.INTERPRETING_CASE or not ref: + return False + doc = read_json(reader, ref, "case-interpretation", 2 * 1024 * 1024) + return all(doc[field] == manifest[field] for field in ("case_id", "package_id", "analysis_revision")) def positive(text): @@ -153,6 +177,7 @@ def parser(): run.add_argument("--package-id", help="Stable managed package identity") run.add_argument("--attempt-id", default="default") run.add_argument("--max-dispatches", type=positive, default=100) + run.add_argument("--stop-after", choices=["case", "interpretation"], help="Stop at the confirmed phase artifact, including on resume") run.add_argument("--telemetry-config", type=Path) run.add_argument("--project-config", type=Path, help="Read-only source/DB research providers") for name in ("analysis", "budget", "investigation", "diagnosis", "change-request", "correction"): @@ -210,12 +235,13 @@ def main(argv=None): with FileLock(workspace.runtime / "locks" / ("trial-" + key(case_id) + ".lock")): result = asyncio.run(drive(queue.store, gateway, case_id, package_id, max_dispatches=args.max_dispatches, attempt_id=args.attempt_id, policies=policies, + stop_after=args.stop_after, progress=lambda event: print(json.dumps(event), file=sys.stderr, flush=True))) except Exception as error: telemetry.guard(telemetry.exception, error) raise print(json.dumps(result, ensure_ascii=True)) - return 0 if result["status"] in {"CONFIG_READY", "PREPARED", "OUTPUT_READY", "REVIEW", "HANDED_OFF"} else 3 + return 0 if result["status"] in {"CONFIG_READY", "PREPARED", "PHASE_COMPLETE", "OUTPUT_READY", "REVIEW", "HANDED_OFF"} else 3 except KeyboardInterrupt: print(json.dumps({"status": "INTERRUPTED"}), file=sys.stderr) return 130 diff --git a/tests/test_trial.py b/tests/test_trial.py index 0942703..61ac828 100644 --- a/tests/test_trial.py +++ b/tests/test_trial.py @@ -65,6 +65,18 @@ def test_initial_builder_interpretation_and_hypotheses(case_setup): assert adapter.calls == 2 +@pytest.mark.parametrize("phase,dispatches,calls",[("case",1,0),("interpretation",2,1),("interpretation",10,1)]) +def test_stop_after_confirmed_phase_and_safe_repeated_command(case_setup,phase,dispatches,calls): + store=case_setup[0] + adapter=PipelineAdapter();g=gateway(adapter) + result=asyncio.run(drive(store,g,*CASE,max_dispatches=dispatches,stop_after=phase)) + assert result["status"]=="PHASE_COMPLETE" and adapter.calls==calls + with PackageReader(result["package"]) as reader: + assert not reader.manifest["artifacts"].get("hypotheses") + replay=asyncio.run(drive(store,g,*CASE,max_dispatches=10,stop_after=phase)) + assert replay["package"]==result["package"] and adapter.calls==calls + + def test_retry_returns_without_sleep_and_blocked_budget_kept(tool_case): store = tool_case[0] adapter = LoopAdapter(unavailable=True) @@ -105,6 +117,17 @@ def test_cli_rejects_ambiguous_identity(): main(["run", "--package", "one.zip", "--case-id", "a", "--package-id", "b"]) +def test_cli_interpretation_stop_exits_successfully(case_setup,monkeypatch,capsys): + import viano_bug_analysis.trial as trial + store=case_setup[0];adapter=PipelineAdapter() + monkeypatch.setattr(trial,"preflight",lambda path:gateway(adapter)) + command=["run","--workspace",str(store.workspace.root),"--case-id",CASE[0],"--package-id",CASE[1], + "--stop-after","interpretation","--max-dispatches","6"] + assert main(command)==0 + assert json.loads(capsys.readouterr().out)["status"]=="PHASE_COMPLETE" and adapter.calls==1 + assert main(command)==0 and adapter.calls==1 + + def test_cli_run_connects_telemetry_and_publishes(tool_case, monkeypatch, capsys): import viano_bug_analysis.trial as trial store = tool_case[0]