Skip to content

Commit ab31c0c

Browse files
refactor(docs): code analysis engine
changes: - file: file_analyzer.py area: analyzer modified: [analyze_directory, FileAnalyzer] - file: generator.py area: analyzer modified: [_make_serializable, PlanfileGenerator] - file: yaml_parser.py area: analyzer modified: [extract_from_yaml_structure] - file: models.py area: model modified: [to_yaml, export, Strategy] - file: store.py area: core modified: [_write_yaml, PlanfileStore] - file: state.py area: core modified: [SyncState, save_sync] stats: lines: "+413/-330 (net +83)" files: 19 complexity: "Large structural change (normalized)"
1 parent 3e3ba3a commit ab31c0c

23 files changed

+434
-333
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.1.49] - 2026-03-27
11+
12+
### Docs
13+
- Update project/context.md
14+
15+
### Other
16+
- Update planfile/analysis/file_analyzer.py
17+
- Update planfile/analysis/generator.py
18+
- Update planfile/analysis/parsers/yaml_parser.py
19+
- Update planfile/core/models.py
20+
- Update planfile/core/store.py
21+
- Update planfile/sync/state.py
22+
- Update project.sh
23+
- Update project/analysis.toon.yaml
24+
- Update project/calls.mmd
25+
- Update project/calls.png
26+
- ... and 8 more files
27+
1028
## [0.1.48] - 2026-03-27
1129

1230
### Docs

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.48
1+
0.1.49

planfile/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- CLI and API for applying and reviewing strategies
99
"""
1010

11-
__version__ = "0.1.48"
11+
__version__ = "0.1.49"
1212
__author__ = "Tom Sapletta"
1313
__email__ = "tom@sapletta.com"
1414

planfile/analysis/file_analyzer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ def analyze_directory(self, directory: Path, patterns: List[str] = None) -> Dict
8282
for pattern in patterns:
8383
for file_path in directory.rglob(pattern):
8484
# Skip hidden files and common exclusions
85-
if file_path.name.startswith('.') or any(skip in str(file_path) for skip in ['__pycache__', '.git', 'node_modules', '.pytest_cache']):
85+
if file_path.name.startswith('.') or any(skip in str(file_path) for skip in ['__pycache__', '.git', 'node_modules', '.pytest_cache', '.planfile_analysis']):
86+
continue
87+
88+
# Skip analysis files to prevent recursive analysis
89+
if 'analysis_summary.json' in file_path.name or 'local-strategy.yaml' in file_path.name:
8690
continue
8791

8892
issues, metrics, tasks = self.analyze_file(file_path)

planfile/analysis/generator.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -261,16 +261,46 @@ def _generate_success_criteria(self, metrics: Dict[str, Any]) -> List[str]:
261261
def _create_strategy_object(self, strategy_data: Dict[str, Any]) -> Strategy:
262262
return strategy_data
263263

264-
def _make_serializable(self, obj: Any) -> Any:
264+
def _make_serializable(self, obj: Any, visited: set = None) -> Any:
265+
"""Convert object to serializable format with cycle detection."""
266+
if visited is None:
267+
visited = set()
268+
269+
# Prevent infinite recursion
270+
obj_id = id(obj)
271+
if obj_id in visited:
272+
return f"<circular_reference_{obj_id}>"
273+
visited.add(obj_id)
274+
265275
if hasattr(obj, '__dict__'):
266-
return {k: self._make_serializable(v) for k, v in obj.__dict__.items()}
276+
result = {}
277+
for k, v in obj.__dict__.items():
278+
# Skip private attributes and large data structures
279+
if k.startswith('_') or k in ['content', 'file_contents', 'raw_data']:
280+
continue
281+
result[k] = self._make_serializable(v, visited)
282+
return result
267283
elif isinstance(obj, dict):
268-
return {k: self._make_serializable(v) for k, v in obj.items()}
284+
result = {}
285+
for k, v in obj.items():
286+
# Skip large values
287+
if isinstance(v, str) and len(v) > 1000:
288+
result[k] = f"<string_length_{len(v)}>"
289+
else:
290+
result[k] = self._make_serializable(v, visited)
291+
return result
269292
elif isinstance(obj, list):
270-
return [self._make_serializable(item) for item in obj]
293+
# Limit list size to prevent bloat
294+
if len(obj) > 100:
295+
return f"<list_length_{len(obj)}>"
296+
return [self._make_serializable(item, visited) for item in obj]
297+
elif isinstance(obj, (str, int, float, bool)) or obj is None:
298+
if isinstance(obj, str) and len(obj) > 1000:
299+
return f"<string_length_{len(obj)}>"
300+
return obj
271301
elif hasattr(obj, '__name__'):
272302
return str(obj)
273303
else:
274-
return obj
304+
return f"<type_{type(obj).__name__}>"
275305

276306
generator = PlanfileGenerator()

planfile/analysis/parsers/yaml_parser.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,45 @@
77
from planfile.analysis.parsers.text_parser import analyze_text
88
from planfile.analysis.parsers.toon_parser import analyze_toon
99

10-
def extract_from_yaml_structure(data: Any, path: str, parent_key: str = "") -> List[ExtractedIssue]:
11-
"""Extract issues from YAML structure."""
10+
def extract_from_yaml_structure(data: Any, path: str, parent_key: str = "", visited: set = None) -> List[ExtractedIssue]:
11+
"""Extract issues from YAML structure with recursion protection."""
12+
if visited is None:
13+
visited = set()
14+
1215
issues = []
1316

17+
# Prevent infinite recursion
18+
if id(data) in visited:
19+
return issues
20+
visited.add(id(data))
21+
1422
if isinstance(data, dict):
1523
for key, value in data.items():
1624
full_key = f"{parent_key}.{key}" if parent_key else key
1725

18-
# Look for common issue indicators
19-
if isinstance(value, str):
26+
# Skip if we're already processing issues (prevent self-reference)
27+
if 'issues' in full_key.lower():
28+
continue
29+
30+
# Look for common issue indicators, but not in our own generated content
31+
if isinstance(value, str) and len(value) < 500: # Limit string length
2032
if any(keyword in value.lower() for keyword in ['error', 'fail', 'bug', 'issue']):
21-
issues.append(ExtractedIssue(
22-
title=f"Issue in {full_key}",
23-
description=value,
24-
priority="medium",
25-
category="bug",
26-
file_path=path
27-
))
33+
# Skip if this looks like our own generated issue
34+
if not any(skip in value.lower() for skip in ['extractedissue', 'file_path', 'priority:', 'category:']):
35+
issues.append(ExtractedIssue(
36+
title=f"Issue in {full_key}",
37+
description=value[:200], # Limit description length
38+
priority="medium",
39+
category="bug",
40+
file_path=path
41+
))
2842

29-
# Recurse
30-
issues.extend(extract_from_yaml_structure(value, path, full_key))
43+
# Recurse with protection
44+
issues.extend(extract_from_yaml_structure(value, path, full_key, visited))
3145

3246
elif isinstance(data, list):
3347
for i, item in enumerate(data):
34-
issues.extend(extract_from_yaml_structure(item, path, f"{parent_key}[{i}]"))
48+
issues.extend(extract_from_yaml_structure(item, path, f"{parent_key}[{i}]", visited))
3549

3650
return issues
3751

planfile/core/models.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,12 @@ def merge(self, others: List['Strategy'], name: str = None) -> 'Strategy':
354354
def export(self, format: str = 'yaml') -> str:
355355
"""Export strategy to specified format."""
356356
if format.lower() == 'yaml':
357-
return yaml.dump(self.model_dump(), default_flow_style=False, sort_keys=False)
357+
try:
358+
return yaml.safe_dump(self.model_dump(), default_flow_style=False, sort_keys=False)
359+
except Exception as e:
360+
# Fallback to regular dump if safe_dump fails
361+
print(f"Warning: safe_dump failed in export, using regular dump: {e}")
362+
return yaml.dump(self.model_dump(), default_flow_style=False, sort_keys=False)
358363
elif format.lower() == 'json':
359364
import json
360365
return json.dumps(self.model_dump(), indent=2, default=str)
@@ -445,7 +450,12 @@ def _convert_old_format(data: Dict) -> Dict:
445450

446451
def to_yaml(self) -> str:
447452
"""Export to YAML with clean formatting."""
448-
return yaml.dump(self.model_dump(exclude_none=True), default_flow_style=False, sort_keys=False)
453+
try:
454+
return yaml.safe_dump(self.model_dump(exclude_none=True), default_flow_style=False, sort_keys=False)
455+
except Exception as e:
456+
# Fallback to regular dump if safe_dump fails
457+
print(f"Warning: safe_dump failed in to_yaml, using regular dump: {e}")
458+
return yaml.dump(self.model_dump(exclude_none=True), default_flow_style=False, sort_keys=False)
449459

450460

451461
# ─── Ticket types (new in Sprint 3) ───

planfile/core/store.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,16 @@ def _write_yaml(self, path: Path, data: dict):
308308
path.parent.mkdir(parents=True, exist_ok=True)
309309
lock = FileLock(str(path) + ".lock", timeout=5)
310310
with lock:
311-
path.write_text(
312-
yaml.dump(data, default_flow_style=False,
313-
allow_unicode=True, sort_keys=False),
314-
encoding="utf-8",
315-
)
311+
try:
312+
# Use safe_dump to prevent circular reference issues
313+
content = yaml.safe_dump(data, default_flow_style=False,
314+
allow_unicode=True, sort_keys=False)
315+
except Exception as e:
316+
# Fallback to regular dump if safe_dump fails
317+
print(f"Warning: safe_dump failed, using regular dump: {e}")
318+
content = yaml.dump(data, default_flow_style=False,
319+
allow_unicode=True, sort_keys=False)
320+
path.write_text(content, encoding="utf-8")
316321

317322
def load_sprint(self, sprint: str = "current") -> dict:
318323
"""Load sprint data as dictionary."""

planfile/sync/state.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@ def save_sync(self, ticket_map: dict):
2222
state["ticket_map"] = {**state.get("ticket_map", {}), **ticket_map}
2323
state["synced_at"] = datetime.utcnow().isoformat()
2424
self.state_file.parent.mkdir(parents=True, exist_ok=True)
25-
self.state_file.write_text(
26-
yaml.dump(state, default_flow_style=False, sort_keys=False),
27-
encoding="utf-8",
28-
)
25+
try:
26+
# Use safe_dump to prevent circular reference issues
27+
content = yaml.safe_dump(state, default_flow_style=False, sort_keys=False)
28+
except Exception as e:
29+
# Fallback to regular dump if safe_dump fails
30+
print(f"Warning: safe_dump failed in save_sync, using regular dump: {e}")
31+
content = yaml.dump(state, default_flow_style=False, sort_keys=False)
32+
self.state_file.write_text(content, encoding="utf-8")
2933

3034
def get_remote_id(self, local_id: str) -> str | None:
3135
"""Look up remote ID for a local ticket."""

project.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env bash
22
clear
33
pip install -e .
4+
venv/bin/pip install prefact --upgrade
45
venv/bin/pip install vallm --upgrade
56
venv/bin/pip install redup --upgrade
67
venv/bin/pip install glon --upgrade
@@ -19,4 +20,5 @@ venv/bin/redup scan . --format toon --output ./project
1920
#venv/bin/redup scan . --functions-only -f toon --output ./project
2021
#venv/bin/vallm batch ./src --recursive --semantic --model qwen2.5-coder:7b
2122
#vallm batch --parallel .
22-
venv/bin/vallm batch . --recursive --format toon --output ./project
23+
venv/bin/vallm batch . --recursive --format toon --output ./project
24+
venv/bin/prefact -a

0 commit comments

Comments
 (0)