Skip to content

Commit 1d6a307

Browse files
feat(tests): update docs
Statistics: 6 files changed, 179 insertions, 27 deletions Summary: - Dirs: .=2, src=2, tests=2 - Exts: .py=4, .md=1, .lock=1 - A/M/D: 0/6/0 - Symbols: test_code2llm_analysis_emits_refactor_items, _scan_code2llm_analysis, test_code2llm_analysis_emits_dup_ticket, test_code2llm_analysis_emits_cc_ticket Modified files: - README.md (+4/-4) - src/koru/autonomous.py (+23/-2) - src/koru/scan.py (+112/-18) - tests/test_autonomous.py (+1/-0) - tests/test_scan.py (+38/-2) - uv.lock (+1/-1) Changes (notes): - README.md (+4/-4): update documentation - src/koru/autonomous.py (+23/-2): update - src/koru/scan.py (+112/-18): update - tests/test_autonomous.py (+1/-0): update - tests/test_scan.py (+38/-2): add functions: test_code2llm_analysis_emits_cc_ticket, test_code2llm_analysis_emits_dup_ticket, test_code2llm_analysis_emits_refactor_items - uv.lock (+1/-1): update Implementation notes (heuristics): - Type inferred from file paths + diff keywords + add/delete ratio - Scope prefers 'goal' when goal/* is touched; otherwise based on top-level dirs - For <=6 files: generate short per-file notes from added lines (defs/classes/click options/headings) - A/M/D derived from git name-status; per-file +X/-X from git numstat
1 parent 9ee272c commit 1d6a307

9 files changed

Lines changed: 193 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7474
`quality:regix``gate:regix`, `quality:redup*``gate:redup`,
7575
`quality:sumr:*``gate:sumr`.
7676

77+
## [0.1.61] - 2026-05-12
78+
79+
### Docs
80+
- Update README.md
81+
82+
### Test
83+
- Update tests/test_autonomous.py
84+
- Update tests/test_scan.py
85+
86+
### Other
87+
- Update uv.lock
88+
7789
## [0.1.60] - 2026-05-12
7890

7991
### Docs

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
## AI Cost Tracking
66

7-
![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.60-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
8-
![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.42-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-27.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
7+
![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.61-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
8+
![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.34-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-27.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
99

10-
- 🤖 **LLM usage:** $3.4184 (84 commits)
11-
- 👤 **Human dev:** ~$2718 (27.2h @ $100/h, 30min dedup)
10+
- 🤖 **LLM usage:** $3.3440 (85 commits)
11+
- 👤 **Human dev:** ~$2795 (27.9h @ $100/h, 30min dedup)
1212

1313
Generated on 2026-05-12 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
1414

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.60
1+
0.1.61

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "koru"
7-
version = "0.1.60"
7+
version = "0.1.61"
88
description = "Closed-loop automation across semcod/* repositories."
99
readme = "README.md"
1010
requires-python = ">=3.12"

src/koru/autonomous.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,28 @@ def _build_parser() -> argparse.ArgumentParser:
159159
action="store_true",
160160
help="Force `koru --init` re-initialization if project is already initialized.",
161161
)
162+
up.add_argument(
163+
"--semcod-artifacts",
164+
action="store_true",
165+
default=None,
166+
help=(
167+
"Include semcod quality artifacts in `koru scan` "
168+
"(jscpd, code2llm/SUMR refactor analysis, testql export, redup). "
169+
"Enabled by default in autonomous mode."
170+
),
171+
)
172+
up.add_argument(
173+
"--no-semcod-artifacts",
174+
dest="semcod_artifacts",
175+
action="store_false",
176+
help="Disable semcod artifact scanning.",
177+
)
162178
up.set_defaults(
163179
submit=True,
164180
enable_autopilot=True,
165181
enable_serve=True,
166182
stop_on_waiting_input=True,
183+
semcod_artifacts=True,
167184
)
168185

169186
return parser
@@ -217,12 +234,15 @@ def _run_cycle(
217234
autopilot_ide: str,
218235
drive_prompt: str,
219236
submit: bool,
237+
include_semcod_artifacts: bool | None,
220238
client: AutopilotClient | None,
221239
) -> tuple[ScanResult | None, QueueLoopResult, str]:
222240
scan_result: ScanResult | None = None
223241
if enable_scan:
224-
print("+ koru scan --apply")
225-
scan_result = run_scan(project=project, apply=True)
242+
print("+ koru scan --apply" + (" --semcod-artifacts" if include_semcod_artifacts else ""))
243+
scan_result = run_scan(
244+
project=project, apply=True, include_semcod_artifacts=include_semcod_artifacts
245+
)
226246
print(
227247
f" scan: suggestions={len(scan_result.suggestions)} "
228248
f"applied={len(scan_result.applied)} skipped={len(scan_result.skipped)}"
@@ -340,6 +360,7 @@ def _action_up(args: argparse.Namespace) -> int:
340360
autopilot_ide=autopilot_ide,
341361
drive_prompt=args.drive_prompt,
342362
submit=args.submit,
363+
include_semcod_artifacts=args.semcod_artifacts,
343364
client=client,
344365
)
345366

src/koru/scan.py

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,23 @@ def _scan_jscpd_report(project: Path) -> list[Suggestion]:
473473
]
474474

475475

476+
_TOON_GOD_RE = re.compile(
477+
r"^\s*🔴\s+GOD\s+(?P<path>\S+)\s*=\s*(?P<loc>\d+)L,\s*(?P<classes>\d+)\s+classes?,\s*(?P<methods>\d+)m",
478+
re.MULTILINE,
479+
)
480+
_TOON_CC_RE = re.compile(
481+
r"^\s*🟡\s+CC\s+(?P<func>\S+)\s+CC=(?P<cc>\d+)\s+\(limit:\s*(?P<limit>\d+)\)",
482+
re.MULTILINE,
483+
)
484+
_TOON_DUP_RE = re.compile(
485+
r"^\s*🔴\s+DUP\s+(?P<count>\d+)\s+classes?\s+duplicated",
486+
re.MULTILINE,
487+
)
488+
_TOON_REFACTOR_ITEM_RE = re.compile(
489+
r"^\s*(?P<num>\d+)\.\s+(?P<desc>.+?)\s*\((?P<note>[^)]+)\)\s*$"
490+
)
491+
492+
476493
def _scan_code2llm_analysis(project: Path) -> list[Suggestion]:
477494
candidates = (
478495
project / "project" / "analysis.toon.yaml",
@@ -483,28 +500,105 @@ def _scan_code2llm_analysis(project: Path) -> list[Suggestion]:
483500
if path is None:
484501
return []
485502
try:
486-
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
503+
text = path.read_text(encoding="utf-8", errors="ignore")
487504
except OSError:
488505
return []
489-
god = sum(1 for ln in lines if "🔴 GOD" in ln)
490-
critical = sum(1 for ln in lines if "🔴" in ln)
491-
if god < 1 and critical < 8:
492-
return []
493506
rel = str(path.relative_to(project))
494-
pr = "high" if god >= 3 else "normal"
495-
return [
496-
Suggestion(
497-
signal="code2llm_analysis",
498-
title="Execute code2llm REFACTOR plan (god modules / critical)",
499-
description=(
500-
f"`{rel}` lists ~{god} god-module rows and {critical} critical (🔴) markers. "
501-
"Work through the REFACTOR section; re-run `code2llm ./ -f toon` to refresh."
502-
),
503-
priority=pr,
504-
labels=("code2llm", "refactor", "scan"),
505-
files=(rel,),
507+
508+
suggestions: list[Suggestion] = []
509+
510+
# --- HEALTH: DUP ---
511+
dup_match = _TOON_DUP_RE.search(text)
512+
if dup_match:
513+
count = int(dup_match.group("count"))
514+
suggestions.append(
515+
Suggestion(
516+
signal="code2llm_dup",
517+
title=f"Remove {count} duplicated classes (code2llm analysis)",
518+
description=(
519+
f"`{rel}` reports **{count}** duplicated classes. "
520+
"Extract shared helpers/modules; re-run `code2llm ./ -f toon` to refresh."
521+
),
522+
priority="high",
523+
labels=("code2llm", "duplication", "refactor", "scan"),
524+
files=(rel,),
525+
)
506526
)
507-
]
527+
528+
# --- HEALTH: GOD modules ---
529+
for m in _TOON_GOD_RE.finditer(text):
530+
file_path = m.group("path").strip()
531+
loc = m.group("loc")
532+
classes = m.group("classes")
533+
methods = m.group("methods")
534+
suggestions.append(
535+
Suggestion(
536+
signal="code2llm_god",
537+
title=f"Split god module: {file_path}",
538+
description=(
539+
f"`{rel}` flags `{file_path}` as a god module "
540+
f"({loc} lines, {classes} classes, {methods} methods). "
541+
"Split into focused submodules by responsibility."
542+
),
543+
priority="high",
544+
labels=("code2llm", "god-module", "refactor", "scan"),
545+
files=(file_path, rel),
546+
)
547+
)
548+
549+
# --- HEALTH: high-CC methods ---
550+
cc_seen: set[str] = set()
551+
for m in _TOON_CC_RE.finditer(text):
552+
func = m.group("func").strip()
553+
cc = int(m.group("cc"))
554+
limit = int(m.group("limit"))
555+
if func in cc_seen:
556+
continue
557+
cc_seen.add(func)
558+
suggestions.append(
559+
Suggestion(
560+
signal="code2llm_cc",
561+
title=f"Reduce cyclomatic complexity: {func} (CC={cc}, limit={limit})",
562+
description=(
563+
f"`{rel}` reports `{func}` with CC={cc} (limit={limit}). "
564+
"Extract sub-functions, simplify conditionals, or split into strategy pattern."
565+
),
566+
priority="normal",
567+
labels=("code2llm", "complexity", "refactor", "scan"),
568+
files=(rel,),
569+
)
570+
)
571+
572+
# --- REFACTOR plan items ---
573+
in_refactor = False
574+
for line in text.splitlines():
575+
if line.startswith("REFACTOR"):
576+
in_refactor = True
577+
continue
578+
if in_refactor and line.startswith(("HEALTH", "PIPELINES", "LAYERS")):
579+
break
580+
if not in_refactor:
581+
continue
582+
rm = _TOON_REFACTOR_ITEM_RE.match(line)
583+
if not rm:
584+
continue
585+
desc = rm.group("desc").strip()
586+
note = rm.group("note").strip()
587+
suggestions.append(
588+
Suggestion(
589+
signal="code2llm_refactor",
590+
title=f"code2llm refactor: {desc}",
591+
description=(
592+
f"REFACTOR item from `{rel}`: **{desc}** ({note}). "
593+
"Execute this refactor step; re-run `code2llm ./ -f toon` to verify."
594+
),
595+
priority="normal",
596+
labels=("code2llm", "refactor", "scan"),
597+
files=(rel,),
598+
)
599+
)
600+
601+
return suggestions
508602

509603

510604
def _scan_testql_export(project: Path) -> list[Suggestion]:

tests/test_autonomous.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ def drive(self, *_args, **_kwargs):
258258
autopilot_ide="auto",
259259
drive_prompt="continue with the next ticket",
260260
submit=True,
261+
include_semcod_artifacts=True,
261262
client=FailIfDrivenClient(),
262263
)
263264

tests/test_scan.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,11 +370,47 @@ def test_code2llm_analysis_emits_when_god_rows(self) -> None:
370370
project = Path(tmp)
371371
(project / "project").mkdir()
372372
(project / "project" / "analysis.toon.yaml").write_text(
373-
"HEALTH:\n 🔴 GOD big.py = 900L\n",
373+
"HEALTH[1]:\n 🔴 GOD big.py = 900L, 5 classes, 16m, max CC=11\n",
374374
encoding="utf-8",
375375
)
376376
out = scan_semcod_quality_artifacts(project)
377-
self.assertTrue(any(s.signal == "code2llm_analysis" for s in out))
377+
self.assertTrue(any(s.signal == "code2llm_god" for s in out))
378+
379+
def test_code2llm_analysis_emits_dup_ticket(self) -> None:
380+
with tempfile.TemporaryDirectory() as tmp:
381+
project = Path(tmp)
382+
(project / "project").mkdir()
383+
(project / "project" / "analysis.toon.yaml").write_text(
384+
"HEALTH[1]:\n 🔴 DUP 28 classes duplicated\n",
385+
encoding="utf-8",
386+
)
387+
out = scan_semcod_quality_artifacts(project)
388+
self.assertTrue(any(s.signal == "code2llm_dup" for s in out))
389+
390+
def test_code2llm_analysis_emits_cc_ticket(self) -> None:
391+
with tempfile.TemporaryDirectory() as tmp:
392+
project = Path(tmp)
393+
(project / "project").mkdir()
394+
(project / "project" / "analysis.toon.yaml").write_text(
395+
"HEALTH[1]:\n 🟡 CC my_func CC=18 (limit:15)\n",
396+
encoding="utf-8",
397+
)
398+
out = scan_semcod_quality_artifacts(project)
399+
self.assertTrue(any(s.signal == "code2llm_cc" for s in out))
400+
401+
def test_code2llm_analysis_emits_refactor_items(self) -> None:
402+
with tempfile.TemporaryDirectory() as tmp:
403+
project = Path(tmp)
404+
(project / "project").mkdir()
405+
(project / "project" / "analysis.toon.yaml").write_text(
406+
"REFACTOR[2]:\n"
407+
" 1. rm duplicates (-28 dup classes)\n"
408+
" 2. split big.py (god module)\n",
409+
encoding="utf-8",
410+
)
411+
out = scan_semcod_quality_artifacts(project)
412+
refactor_tickets = [s for s in out if s.signal == "code2llm_refactor"]
413+
self.assertEqual(len(refactor_tickets), 2)
378414

379415
def test_testql_export_emits_when_many_failures(self) -> None:
380416
with tempfile.TemporaryDirectory() as tmp:

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)