Skip to content

Commit 2915021

Browse files
refactor(goal): CLI interface improvements
changes: - file: cli.py area: cli modified: [main] - file: metrics.py area: quality added: [_count_elements_ast] modified: [ReproductionMetrics, _compute_structural_metrics] removed: [count_elements] - file: toon_format.py area: core added: [_item_count, generate_hybrid] modified: [TOONGenerator] dependencies: flow: "cli→toon_format" - cli.py -> toon_format.py stats: lines: "+159/-25 (net +134)" files: 3 complexity: "Large structural change (normalized)"
1 parent bd82d65 commit 2915021

File tree

11 files changed

+179
-32
lines changed

11 files changed

+179
-32
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
## [1.0.43] - 2026-02-25
2+
3+
### Summary
4+
5+
refactor(goal): CLI interface improvements
6+
7+
### Other
8+
9+
- update code2logic/cli.py
10+
- update code2logic/metrics.py
11+
- update code2logic/toon_format.py
12+
13+
114
## [1.0.42] - 2026-02-25
215

316
### Summary

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.42
1+
1.0.43

code2logic/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
>>> print(output)
1919
"""
2020

21-
__version__ = "1.0.42"
21+
__version__ = "1.0.43"
2222
__author__ = "Softreck"
2323
__email__ = "info@softreck.dev"
2424
__license__ = "MIT"

code2logic/cli.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,11 +978,18 @@ def _maybe_print_pretty_help() -> bool:
978978
# For TOON, --compact means ultra-compact format
979979
compact = args.compact if hasattr(args, 'compact') else False
980980
ultra_compact = args.ultra_compact if hasattr(args, 'ultra_compact') else False
981+
use_hybrid = args.hybrid if hasattr(args, 'hybrid') else False
981982

982983
# Use compact or ultra_compact flag (compact takes precedence for TOON)
983984
use_ultra_compact = ultra_compact or compact
984985

985-
if use_ultra_compact:
986+
if use_hybrid:
987+
output = generator.generate_hybrid(
988+
project,
989+
detail='full',
990+
no_repeat_name=args.no_repeat_module,
991+
)
992+
elif use_ultra_compact:
986993
output = generator.generate_ultra_compact(project)
987994
else:
988995
detail_map = {

code2logic/metrics.py

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -303,23 +303,11 @@ def _cosine_similarity(self, words1: List[str], words2: List[str]) -> float:
303303
return (dot_product / (magnitude1 * magnitude2)) * 100
304304

305305
def _compute_structural_metrics(self, original: str, generated: str) -> StructuralMetrics:
306-
"""Compute structural metrics."""
306+
"""Compute structural metrics using AST when possible, regex as fallback."""
307307
metrics = StructuralMetrics()
308308

309-
# Count elements
310-
def count_elements(code: str) -> Dict[str, int]:
311-
return {
312-
'classes': len(re.findall(r'^class\s+\w+', code, re.MULTILINE)),
313-
'functions': len(re.findall(r'^(?:async\s+)?def\s+\w+', code, re.MULTILINE)),
314-
'methods': len(re.findall(r'^\s+(?:async\s+)?def\s+\w+', code, re.MULTILINE)),
315-
'imports': len(re.findall(r'^(?:from|import)\s+', code, re.MULTILINE)),
316-
# Capture both annotated attributes and simple assignments.
317-
# This is still heuristic, but avoids undercounting common code.
318-
'attributes': len(re.findall(r'^\s+\w+\s*(?::\s*[^=\n]+)?\s*=', code, re.MULTILINE)),
319-
}
320-
321-
orig = count_elements(original)
322-
gen = count_elements(generated)
309+
orig = self._count_elements_ast(original)
310+
gen = self._count_elements_ast(generated)
323311

324312
metrics.classes_original = orig['classes']
325313
metrics.classes_generated = gen['classes']
@@ -341,15 +329,15 @@ def count_elements(code: str) -> Dict[str, int]:
341329
metrics.attributes_generated = gen['attributes']
342330
metrics.attributes_match = orig['attributes'] == gen['attributes']
343331

344-
# Structural score
345-
matches = sum([
346-
metrics.classes_match,
347-
metrics.functions_match,
348-
metrics.methods_match,
349-
metrics.imports_match,
350-
metrics.attributes_match,
351-
])
352-
metrics.structural_score = (matches / 5) * 100
332+
# Ratio-based structural score (partial credit instead of binary)
333+
total = 0.0
334+
for key in ('classes', 'functions', 'methods', 'imports', 'attributes'):
335+
ov, gv = orig[key], gen[key]
336+
if ov == 0 and gv == 0:
337+
total += 1.0
338+
elif max(ov, gv) > 0:
339+
total += min(ov, gv) / max(ov, gv)
340+
metrics.structural_score = (total / 5) * 100
353341

354342
# Element coverage
355343
total_orig = sum(orig.values())
@@ -359,6 +347,59 @@ def count_elements(code: str) -> Dict[str, int]:
359347

360348
return metrics
361349

350+
@staticmethod
351+
def _count_elements_ast(code: str) -> Dict[str, int]:
352+
"""Count structural elements using Python AST, with regex fallback."""
353+
import ast as _ast
354+
355+
try:
356+
tree = _ast.parse(code)
357+
except SyntaxError:
358+
# Fallback to regex for unparseable code
359+
return {
360+
'classes': len(re.findall(r'^class\s+\w+', code, re.MULTILINE)),
361+
'functions': len(re.findall(r'^(?:async\s+)?def\s+\w+', code, re.MULTILINE)),
362+
'methods': len(re.findall(r'^\s+(?:async\s+)?def\s+\w+', code, re.MULTILINE)),
363+
'imports': len(re.findall(r'^(?:from|import)\s+', code, re.MULTILINE)),
364+
'attributes': len(re.findall(r'^\s+\w+\s*(?::\s*[^=\n]+)?\s*=', code, re.MULTILINE)),
365+
}
366+
367+
classes = 0
368+
functions = 0
369+
methods = 0
370+
imports = 0
371+
attributes = 0
372+
373+
for node in _ast.walk(tree):
374+
if isinstance(node, _ast.ClassDef):
375+
classes += 1
376+
# Count methods inside classes
377+
for item in node.body:
378+
if isinstance(item, (_ast.FunctionDef, _ast.AsyncFunctionDef)):
379+
methods += 1
380+
# Count class-level attributes (annotated or assigned)
381+
elif isinstance(item, (_ast.Assign, _ast.AnnAssign)):
382+
attributes += 1
383+
elif isinstance(node, (_ast.FunctionDef, _ast.AsyncFunctionDef)):
384+
# Only count as top-level function if not inside a class
385+
# (methods already counted above)
386+
pass
387+
elif isinstance(node, (_ast.Import, _ast.ImportFrom)):
388+
imports += 1
389+
390+
# Count top-level functions (not methods)
391+
for node in _ast.iter_child_nodes(tree):
392+
if isinstance(node, (_ast.FunctionDef, _ast.AsyncFunctionDef)):
393+
functions += 1
394+
395+
return {
396+
'classes': classes,
397+
'functions': functions,
398+
'methods': methods,
399+
'imports': imports,
400+
'attributes': attributes,
401+
}
402+
362403
def _compute_semantic_metrics(self, original: str, generated: str) -> SemanticMetrics:
363404
"""Compute semantic preservation metrics."""
364405
metrics = SemanticMetrics()

code2logic/toon_format.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,92 @@ def generate(self, project: ProjectInfo, detail: str = 'standard', no_repeat_nam
132132

133133
return '\n'.join(lines)
134134

135+
def generate_hybrid(
136+
self,
137+
project: ProjectInfo,
138+
detail: str = 'full',
139+
no_repeat_name: bool = True,
140+
hub_top_n: int = 5,
141+
hub_functions_detail: str = 'full',
142+
) -> str:
143+
"""Generate TOON-Hybrid: project structure + function-logic for hub modules.
144+
145+
Combines project-level TOON (classes, imports, structure) with
146+
selective function-logic details for the most important modules.
147+
148+
Args:
149+
project: Analyzed project info
150+
detail: Detail level for project structure
151+
no_repeat_name: Compress repeated directory prefixes
152+
hub_top_n: Number of top modules to include function details for
153+
hub_functions_detail: Detail level for function-logic ('standard', 'full')
154+
155+
Returns:
156+
Hybrid TOON string
157+
"""
158+
from .function_logic import FunctionLogicGenerator
159+
from .shared_utils import remove_self_from_params
160+
161+
# Generate base project TOON
162+
base = self.generate(project, detail=detail, no_repeat_name=no_repeat_name)
163+
164+
# Identify hub modules: use dependency_metrics if available, otherwise sort by function count
165+
hub_paths: set = set()
166+
dep_metrics = getattr(project, 'dependency_metrics', {}) or {}
167+
if dep_metrics:
168+
ranked = sorted(dep_metrics.items(), key=lambda x: getattr(x[1], 'pagerank', 0), reverse=True)
169+
hub_paths = {path for path, node in ranked[:hub_top_n]}
170+
else:
171+
# Fallback: rank by total functions + methods
172+
def _item_count(m):
173+
return len(getattr(m, 'functions', []) or []) + sum(
174+
len(getattr(c, 'methods', []) or []) for c in (getattr(m, 'classes', []) or [])
175+
)
176+
ranked_modules = sorted(project.modules, key=_item_count, reverse=True)
177+
hub_paths = {m.path for m in ranked_modules[:hub_top_n]}
178+
179+
if not hub_paths:
180+
return base
181+
182+
# Generate function-logic section for hub modules only
183+
hub_modules = [m for m in project.modules if m.path in hub_paths]
184+
if not hub_modules:
185+
return base
186+
187+
logic_gen = FunctionLogicGenerator()
188+
lines = [base, "", "# === Hub Module Function Details ==="]
189+
190+
for m in hub_modules:
191+
items = logic_gen._module_items(m)
192+
if not items:
193+
continue
194+
lines.append(f" {self._quote(m.path)}:")
195+
196+
# Emit class context
197+
classes = getattr(m, 'classes', []) or []
198+
for cls in classes:
199+
bases = ','.join(getattr(cls, 'bases', []) or []) or '-'
200+
lines.append(f" CLASS {self._quote(cls.name)}({bases})")
201+
202+
# Emit function table
203+
header = f"line{self.delim_marker}name{self.delim_marker}sig{self.delim_marker}does"
204+
lines.append(f" functions[{len(items)}]{{{header}}}:")
205+
206+
for kind, qname, func in items:
207+
sig = logic_gen._build_sig(func, include_async_prefix=False, language=m.language)
208+
start_line = str(getattr(func, 'start_line', 0) or 0)
209+
display_name = qname
210+
if getattr(func, 'is_async', False):
211+
display_name = f"~{qname}"
212+
cc = getattr(func, 'complexity', 1) or 1
213+
if cc > 1:
214+
display_name = f"{display_name} cc:{cc}"
215+
does = logic_gen._build_does(func)
216+
row = [start_line, self._quote(display_name), self._quote(sig), self._quote(does)]
217+
lines.append(f" {self.delimiter.join(row)}")
218+
219+
return '\n'.join(lines)
220+
135221
def _generate_modules(self, modules: List[ModuleInfo], detail: str, no_repeat_name: bool = False) -> List[str]:
136222
"""Generate modules section."""
137223
lines = []

logic2code/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@
1414
from .generator import CodeGenerator, GeneratorConfig, GenerationResult
1515
from .renderers import PythonRenderer
1616

17-
__version__ = '1.0.42'
17+
__version__ = '1.0.43'
1818
__all__ = ['CodeGenerator', 'GeneratorConfig', 'GenerationResult', 'PythonRenderer']

logic2test/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@
1515
from .parsers import LogicParser
1616
from .templates import TestTemplate
1717

18-
__version__ = '1.0.42'
18+
__version__ = '1.0.43'
1919
__all__ = ['TestGenerator', 'GeneratorConfig', 'GenerationResult', 'LogicParser', 'TestTemplate']

lolm/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
)
7777
from .clients import LLMRateLimitError
7878

79-
__version__ = '1.0.42'
79+
__version__ = '1.0.43'
8080
__all__ = [
8181
# Config
8282
'LLMConfig',

pyproject.toml

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

55
[tool.poetry]
66
name = "code2logic"
7-
version = "1.0.42"
7+
version = "1.0.43"
88
description = "Code2Logic - Source code to logical representation converter for LLM analysis, featuring Tree-sitter parsing, dependency graph analysis, and multi-language support."
99
readme = "README.md"
1010
license = "Apache-2.0"

0 commit comments

Comments
 (0)