From 6cf804a2c7f2851502964b1822506bb05bf03c6c Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Thu, 8 Oct 2026 15:05:06 +0200 Subject: [PATCH] fix: import current code2llm HEALTH alerts (ticket-206) --- planfile/importers/code2llm_importer.py | 171 +++++++++++++++++++----- project/ticket-206/README.md | 35 +++++ project/ticket-206/intent.json | 76 +++++++++++ tests/test_code2llm_importer.py | 127 ++++++++++++++++++ 4 files changed, 373 insertions(+), 36 deletions(-) create mode 100644 project/ticket-206/README.md create mode 100644 project/ticket-206/intent.json create mode 100644 tests/test_code2llm_importer.py diff --git a/planfile/importers/code2llm_importer.py b/planfile/importers/code2llm_importer.py index a1385bd..3b3f24a 100644 --- a/planfile/importers/code2llm_importer.py +++ b/planfile/importers/code2llm_importer.py @@ -1,16 +1,31 @@ """Import tickets from code2llm evolution.toon and analysis.toon.""" - -def import_code2llm(toon_path: str, auto_priority: bool = True, - sprint: str = "backlog", **kwargs) -> list[dict]: - """Parse evolution.toon NEXT[] → ticket dicts.""" - content = open(toon_path, encoding="utf-8").read() - - if "NEXT[" in content: - return _parse_evolution(content, auto_priority) - elif "HEALTH[" in content: - return _parse_health(content, auto_priority) - return [] +import hashlib +import re +from pathlib import Path + + +def import_code2llm( + toon_path: str, auto_priority: bool = True, sprint: str = "backlog", **kwargs +) -> list[dict]: + """Parse current HEALTH or legacy NEXT sections into persistable candidates.""" + path = Path(toon_path) + raw = path.read_bytes() + content = raw.decode("utf-8") + if re.search(r"^\s*HEALTH\[\d+\]:\s*$", content, re.MULTILINE): + tickets = _parse_health(content, auto_priority) + digest = hashlib.sha256(raw).hexdigest() + for ticket in tickets: + ticket["source"]["context"].update( + analysis_path=str(path.resolve()), analysis_sha256=digest + ) + elif re.search(r"^\s*NEXT\[\d+\]:", content, re.MULTILINE): + tickets = _parse_evolution(content, auto_priority) + else: + return [] + for ticket in tickets: + ticket["name"] = ticket["title"] + return tickets def _parse_evolution(content: str, auto_priority: bool) -> list[dict]: @@ -65,10 +80,12 @@ def _handle_in_next(self, line: str): # End of NEXT section - check if line has content but doesn't start with space # and is not a property line (WHY:, EFFORT:, IMPACT:) - elif (stripped and - not line.startswith(" ") and - not stripped.startswith("[") and - ":" not in stripped): + elif ( + stripped + and not line.startswith(" ") + and not stripped.startswith("[") + and ":" not in stripped + ): self.state = "outside" # Property lines @@ -102,28 +119,110 @@ def _evolution_item_to_ticket(item: dict, auto_priority: bool) -> dict: "description": item.get("why", ""), "priority": priority, "labels": ["tech-debt", "refactoring", action.lower()], - "source": {"tool": "code2llm", "context": { - "impact": impact, - "effort": item.get("effort"), - }}, + "source": { + "tool": "code2llm", + "context": { + "impact": impact, + "effort": item.get("effort"), + }, + }, } def _parse_health(content: str, auto_priority: bool) -> list[dict]: - """Parse HEALTH[] section from analysis.toon.""" - tickets = [] - for line in content.split("\n"): - if "CC" in line and "limit:" in line: - parts = line.strip().split() - func = parts[2] if len(parts) > 2 else "unknown" - cc_str = [p for p in parts if p.startswith("CC=")] - cc = int(cc_str[0].split("=")[1]) if cc_str else 0 - tickets.append({ - "title": f"Reduce CC: {func} (CC={cc})", - "priority": "high" if cc > 20 else "normal", - "labels": ["complexity", "cc-violation"], - "source": {"tool": "code2llm", "context": { - "function": func, "cc": cc, - }}, - }) - return tickets + """Import only HEALTH alerts; other sections contain repeated metrics.""" + tickets = {} + in_health = False + declared_count = 0 + for line in content.splitlines(): + header = re.fullmatch(r"\s*HEALTH\[(\d+)\]:\s*", line) + if header: + in_health = True + declared_count += int(header[1]) + continue + if not in_health: + continue + if re.match(r"^[A-Z][A-Z0-9_]*\[\d+\]:", line): + in_health = False + continue + ticket = _health_alert(line.strip(), auto_priority) + if ticket: + key = ticket["source"]["context"]["dedupe_key"] + tickets.setdefault(key, ticket) + if declared_count and not tickets: + raise ValueError("Non-empty code2llm HEALTH section has no supported alerts") + return list(tickets.values()) + + +def _health_ticket( + title: str, description: str, priority: str, labels: list[str], context: dict, key: str +) -> dict: + context["dedupe_key"] = key + return { + "title": title, + "description": description, + "priority": priority, + "labels": ["tech-debt", *labels, f"dedupe:{key}"], + "source": {"tool": "code2llm", "context": context}, + } + + +def _health_alert(line: str, auto_priority: bool) -> dict | None: + god = re.search(r"\bGOD\s+(\S+)\s*=\s*(\d+)L\b", line) + if god: + path = god[1] + if Path(path).is_absolute() or "\\" in path or ":" in path or ".." in Path(path).parts: + raise ValueError(f"Unsafe code2llm module path: {path}") + cc = re.search(r"max CC=(\d+)", line) + context = {"signal": "god", "module": path, "lines": int(god[2])} + if cc: + context["cc"] = int(cc[1]) + ticket = _health_ticket( + f"Split god module: {path}", + f"Review {path} ({god[2]} lines) and split cohesive responsibilities. " + "Preserve public behavior, run relevant tests and compare a fresh analysis. " + "A native ticket and editing admission are required before execution.", + "high" if auto_priority else "normal", + ["refactoring", "god-module"], + context, + f"code2llm:god:{path}", + ) + ticket["files"] = [path] + return ticket + dup = re.search(r"\bDUP\s+(\d+) duplicate class groups\b", line) + if dup and int(dup[1]): + return _health_ticket( + f"Review duplicated classes ({dup[1]} groups)", + "Inspect concrete duplicate groups before choosing a shared abstraction. " + "The aggregate alert does not identify editable files.", + "high" if auto_priority else "normal", + ["duplication", "triage"], + {"signal": "dup", "groups": int(dup[1])}, + "code2llm:dup:class-groups", + ) + cycle = re.search(r"\bCYCLE\s+Circular dependency detected:\s*(.+)", line) + if cycle: + chain = cycle[1].split(". This indicates", 1)[0].rstrip(".") + key = hashlib.sha256(chain.encode()).hexdigest()[:20] + return _health_ticket( + f"Inspect circular dependency: {chain[:120]}", + f"Reported call chain: {chain}. Check whether recursion is intentional " + "and whether the code is vendored before proposing a change. " + "Resolve source files and a bounded native scope during triage.", + "high" if auto_priority else "normal", + ["circular-dependency", "triage"], + {"signal": "cycle", "chain": chain}, + f"code2llm:cycle:{key}", + ) + legacy = re.search(r"\bCC\s+(\S+)\s+(?:CC=|=\s*)(\d+)\s*\(limit:\s*\d+\)", line) + if legacy: + function, cc = legacy[1], int(legacy[2]) + return _health_ticket( + f"Reduce CC: {function} (CC={cc})", + "Reduce complexity while preserving behavior and run relevant tests.", + "high" if auto_priority and cc > 20 else "normal", + ["complexity", "cc-violation"], + {"function": function, "cc": cc}, + f"code2llm:cc:{function}", + ) + return None diff --git a/project/ticket-206/README.md b/project/ticket-206/README.md new file mode 100644 index 0000000..2c8e2a3 --- /dev/null +++ b/project/ticket-206/README.md @@ -0,0 +1,35 @@ +# Ticket 206: Import current code2llm health reports as actionable quality tickets + +- **ID**: ticket-206 +- **Owner**: agent:codex +- **Status**: IN_PROGRESS +- **Workflow state**: PUBLICATION +- **Created**: 2026-10-08 + +## Goal and scope + +SESSION_EXECUTION_AUTHORIZATION: The user requests continuation of the +Willman/Koru code quality pipeline based on project/analysis.toon.yaml. +Workspace PLF-013 / PLF-121 tracks this bounded parser repair. Current HEALTH +reports contain GOD, DUP and CYCLE alerts; the installed importer silently +returns no tickets because it only recognizes an older CC line format. +Parse current health alerts, preserve legacy CC/evolution support, restrict +parsing to HEALTH, and return ticket names accepted by Planfile persistence. +Retain report provenance, concrete module paths and stable dedupe labels. +Duplicate and cycle signals require triage; no executor or lease is inferred. + +## Acceptance criteria + +- [x] AC-01: Current GOD/DUP/CYCLE health alerts produce file-scoped or triage candidates instead of an empty result. +- [x] AC-02: Legacy CC and evolution imports retain their existing fields and behavior. +- [x] AC-03: Imported records persist with Planfile's normal bulk API; unrelated report sections cannot create tickets. +- [x] AC-04: Focused importer and persistence tests plus governance pass. + +SESSION_EXECUTION_AUTHORIZATION (2026-10-08 continuation): The user says +"kontynuuj, scalaj, naprawiaj" after reviewing the tested local repair and +explicit publication question. This authorizes push, PR, independent protected +validation and gated merge of this bounded importer repair. Record actual merge +and activation externally; session prose never substitutes for trusted approval. + +No dispatcher activation, Koru quality editing, protected policy changes or +version bump is part of this slice. diff --git a/project/ticket-206/intent.json b/project/ticket-206/intent.json new file mode 100644 index 0000000..671c79c --- /dev/null +++ b/project/ticket-206/intent.json @@ -0,0 +1,76 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-206", + "summary": "Import current code2llm health reports as actionable quality tickets", + "workstream": "application", + "classification": { + "kind": "BUG", + "priority": "P1", + "origin": "requested" + }, + "allowedPaths": [ + "project/ticket-206/**", + "TODO.md", + "project/TICKETS.md", + "planfile/importers/code2llm_importer.py", + "tests/test_code2llm_importer.py" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "a5bc9be973a2eca012590c65d271ac8286622e35", + "targetBranch": "main", + "outcome": "Validated current code2llm HEALTH import published through protected independent approval and merge, then observed in the installed primary checkout.", + "nonGoals": [ + "No dispatcher activation, Koru quality editing, protected admission changes or version bump" + ], + "complexity": "S", + "estimatedMinutes": 45, + "budgets": { + "maxImplementationFiles": 2, + "maxAffectedComponents": 2, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Parse only HEALTH alerts into named advisory records; preserve legacy evolution and CC fields.", + "components": [ + { + "name": "code2llm-import", + "paths": [ + "planfile/importers/code2llm_importer.py", + "tests/test_code2llm_importer.py" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Revert the ticket implementation commit." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-04", + "commands": [ + "pytest -q tests/test_code2llm_importer.py tests/test_store_tickets_id_fallback.py", + "ruff check --no-cache planfile/importers/code2llm_importer.py tests/test_code2llm_importer.py", + "ruff format --check --no-cache planfile/importers/code2llm_importer.py tests/test_code2llm_importer.py", + "bash project/governance-check.sh" + ], + "evidence": "17 focused tests pass with managed governance 0 errors/0 warnings; lint and formatting pass. Actual workspace report produces and persists20 candidates:7 GOD,1 DUP,12 CYCLE. External receipt: .subactor/recovery/code2llm-health-206-20261008/current-report-import.json. Final standalone gate recorded there before commit." + } + ] + } +} diff --git a/tests/test_code2llm_importer.py b/tests/test_code2llm_importer.py new file mode 100644 index 0000000..8152247 --- /dev/null +++ b/tests/test_code2llm_importer.py @@ -0,0 +1,127 @@ +"""Current report intake must reach persisted tickets without execution authority.""" + +import hashlib + +import pytest + +from planfile import Planfile +from planfile.importers.code2llm_importer import import_code2llm + + +def import_report(tmp_path, content, **kwargs): + path = tmp_path / "analysis.toon.yaml" + path.write_text(content, encoding="utf-8") + return import_code2llm(str(path), **kwargs) + + +def test_current_health_has_concrete_modules_and_advisory_triage(tmp_path): + content = """# CC average and a NEXT[9] mention are not sections +HEALTH[4]: + 🔴 DUP 30 duplicate class groups + 🔴 GOD engine/pipeline.py = 760L, 5 classes, 18m, max CC=50 + 🔴 GOD engine/delta.rs = 520L, 4 classes, 14m, max CC=17 + 🔴 CYCLE Circular dependency detected: pkg.policy -> pkg.Strategy.evaluate. This indicates high coupling. +REFACTOR[1]: + 🔴 GOD ignored/summary.py = 999L +PIPELINES[1]: + 🔴 CC ignored = 99 (limit: 15) +""" + tickets = import_report(tmp_path, content) + assert len(tickets) == 4 + modules = [t for t in tickets if "god-module" in t["labels"]] + assert [t["files"] for t in modules] == [["engine/pipeline.py"], ["engine/delta.rs"]] + assert modules[0]["source"]["context"]["cc"] == 50 + assert tickets[0]["source"]["context"]["groups"] == 30 + assert tickets[-1]["source"]["context"]["chain"] == "pkg.policy -> pkg.Strategy.evaluate" + for ticket in tickets: + assert ticket["name"] == ticket["title"] + assert "executor" not in ticket + assert ( + ticket["source"]["context"]["analysis_sha256"] + == hashlib.sha256(content.encode()).hexdigest() + ) + assert ticket["source"]["context"]["analysis_path"] == str(tmp_path / "analysis.toon.yaml") + assert all("files" not in t for t in tickets if "triage" in t["labels"]) + + +def test_imported_health_records_persist_through_normal_bulk_api(tmp_path): + data = import_report( + tmp_path, + """HEALTH[3]: + 🔴 GOD engine/core.py = 955L, max CC=49 + 🔴 DUP 30 duplicate class groups + 🔴 CYCLE Circular dependency detected: pkg.a -> pkg.b. This indicates high coupling. +""", + ) + pf = Planfile(str(tmp_path / "project")) + created = pf.create_tickets_bulk(data, source="code2llm", sprint="backlog") + assert len(created) == 3 + for item, original in zip(created, data, strict=True): + stored = pf.get_ticket(item.id) + assert stored.name == original["name"] + assert stored.source.context == original["source"]["context"] + assert stored.executor is None + assert stored.execution is None + assert pf.get_ticket(created[0].id).files == ["engine/core.py"] + + +def test_repeated_health_alerts_share_stable_identity_within_report(tmp_path): + line = " 🔴 GOD engine/core.py = 955L, max CC=49\n" + first = import_report(tmp_path, "HEALTH[2]:\n" + line * 2) + second = import_report(tmp_path, "HEALTH[1]:\n" + line.replace("955L", "960L")) + assert len(first) == len(second) == 1 + assert ( + first[0]["source"]["context"]["dedupe_key"] == second[0]["source"]["context"]["dedupe_key"] + ) + + +@pytest.mark.parametrize("metric", ["CC=25", "= 25"]) +def test_legacy_cc_retains_numeric_fields_and_priority(tmp_path, metric): + (ticket,) = import_report(tmp_path, f"HEALTH[1]:\n 🔴 CC pkg.function {metric} (limit: 15)\n") + assert ticket["title"] == "Reduce CC: pkg.function (CC=25)" + assert ticket["source"]["context"]["function"] == "pkg.function" + assert ticket["source"]["context"]["cc"] == 25 + assert ticket["priority"] == "high" + + +@pytest.mark.parametrize( + "alert", + [ + "GOD engine/core.py = 955L", + "DUP 30 duplicate class groups", + "CYCLE Circular dependency detected: pkg.a -> pkg.b", + "CC pkg.f CC=25 (limit:15)", + ], +) +def test_disabled_auto_priority_is_respected(tmp_path, alert): + (ticket,) = import_report(tmp_path, f"HEALTH[1]:\n 🔴 {alert}\n", auto_priority=False) + assert ticket["priority"] == "normal" + + +def test_legacy_evolution_keeps_fields_and_gains_persistable_name(tmp_path): + (ticket,) = import_report( + tmp_path, + """NEXT[1]: + [1] ! SPLIT pkg.engine + WHY: Isolate transport + EFFORT: 2h + IMPACT: 6000 +""", + ) + assert ticket["title"] == ticket["name"] == "SPLIT pkg.engine" + assert ticket["priority"] == "critical" + assert ticket["description"] == "Isolate transport" + assert ticket["source"]["context"] == {"impact": 6000, "effort": "2h"} + + +def test_empty_and_unrecognized_reports_do_not_silently_lose_positive_health(tmp_path): + assert import_report(tmp_path, "HEALTH[0]:\n ✅ Healthy\n") == [] + assert import_report(tmp_path, "Not a code2llm report") == [] + with pytest.raises(ValueError, match="no supported alerts"): + import_report(tmp_path, "HEALTH[1]:\n 🔴 UNKNOWN unsupported format\n") + + +@pytest.mark.parametrize("path", ["../outside.py", "/outside.py", "C:\\outside.py"]) +def test_module_paths_cannot_escape_report_repository(tmp_path, path): + with pytest.raises(ValueError, match="Unsafe.*path"): + import_report(tmp_path, f"HEALTH[1]:\n 🔴 GOD {path} = 955L\n")