buildlog 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ """Render skills to CLAUDE.md."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from buildlog.skills import _to_imperative
11
+
12
+ if TYPE_CHECKING:
13
+ from buildlog.skills import Skill
14
+
15
+
16
+ class ClaudeMdRenderer:
17
+ """Appends promoted skills to CLAUDE.md."""
18
+
19
+ def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
20
+ """Initialize renderer.
21
+
22
+ Args:
23
+ path: Path to CLAUDE.md file. Defaults to CLAUDE.md in current directory.
24
+ tracking_path: Path to promoted.json tracking file.
25
+ Defaults to .buildlog/promoted.json relative to path.
26
+ """
27
+ self.path = path or Path("CLAUDE.md")
28
+ if tracking_path is None:
29
+ self.tracking_path = self.path.parent / ".buildlog" / "promoted.json"
30
+ else:
31
+ self.tracking_path = tracking_path
32
+
33
+ def render(self, skills: list[Skill]) -> str:
34
+ """Append skills to CLAUDE.md.
35
+
36
+ Args:
37
+ skills: List of skills to append.
38
+
39
+ Returns:
40
+ Confirmation message.
41
+ """
42
+ if not skills:
43
+ return "No skills to promote"
44
+
45
+ # Group by category
46
+ by_category: dict[str, list[Skill]] = {}
47
+ for skill in skills:
48
+ by_category.setdefault(skill.category, []).append(skill)
49
+
50
+ # Build section
51
+ lines = [
52
+ "",
53
+ f"## Learned Rules (auto-generated {datetime.now().strftime('%Y-%m-%d')})",
54
+ "",
55
+ ]
56
+
57
+ category_titles = {
58
+ "architectural": "Architectural",
59
+ "workflow": "Workflow",
60
+ "tool_usage": "Tool Usage",
61
+ "domain_knowledge": "Domain Knowledge",
62
+ }
63
+
64
+ for category, cat_skills in by_category.items():
65
+ title = category_titles.get(category, category.replace("_", " ").title())
66
+ lines.append(f"### {title}")
67
+ lines.append("")
68
+ for skill in cat_skills:
69
+ rule = _to_imperative(skill.rule, skill.confidence)
70
+ lines.append(f"- {rule}")
71
+ lines.append("")
72
+
73
+ content = "\n".join(lines)
74
+
75
+ # Append to file
76
+ if self.path.exists():
77
+ existing = self.path.read_text()
78
+ self.path.write_text(existing + content)
79
+ else:
80
+ self.path.write_text(content)
81
+
82
+ # Track promoted skill IDs
83
+ self._track_promoted(skills)
84
+
85
+ return f"Appended {len(skills)} rules to {self.path}"
86
+
87
+ def _track_promoted(self, skills: list[Skill]) -> None:
88
+ """Track which skills have been promoted."""
89
+ self.tracking_path.parent.mkdir(parents=True, exist_ok=True)
90
+
91
+ # Load existing tracking data (handle corrupt JSON)
92
+ tracking = {"skill_ids": [], "promoted_at": {}}
93
+ if self.tracking_path.exists():
94
+ try:
95
+ tracking = json.loads(self.tracking_path.read_text())
96
+ except json.JSONDecodeError:
97
+ pass # Start fresh if corrupted
98
+
99
+ # Add new skill IDs
100
+ now = datetime.now().isoformat()
101
+ for skill in skills:
102
+ if skill.id not in tracking["skill_ids"]:
103
+ tracking["skill_ids"].append(skill.id)
104
+ tracking["promoted_at"][skill.id] = now
105
+
106
+ self.tracking_path.write_text(json.dumps(tracking, indent=2))
@@ -0,0 +1,96 @@
1
+ """Render skills to .claude/settings.json."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from buildlog.skills import _to_imperative
11
+
12
+ if TYPE_CHECKING:
13
+ from buildlog.skills import Skill
14
+
15
+
16
+ class SettingsJsonRenderer:
17
+ """Merges promoted skills into .claude/settings.json."""
18
+
19
+ def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
20
+ """Initialize renderer.
21
+
22
+ Args:
23
+ path: Path to settings.json file. Defaults to .claude/settings.json.
24
+ tracking_path: Path to promoted.json tracking file.
25
+ Defaults to .buildlog/promoted.json.
26
+ """
27
+ self.path = path or Path(".claude/settings.json")
28
+ self.tracking_path = tracking_path or Path(".buildlog/promoted.json")
29
+
30
+ def render(self, skills: list[Skill]) -> str:
31
+ """Merge skills into settings.json rules array.
32
+
33
+ Args:
34
+ skills: List of skills to add.
35
+
36
+ Returns:
37
+ Confirmation message.
38
+ """
39
+ if not skills:
40
+ return "No skills to promote"
41
+
42
+ # Load existing settings
43
+ if self.path.exists():
44
+ settings = json.loads(self.path.read_text())
45
+ else:
46
+ self.path.parent.mkdir(parents=True, exist_ok=True)
47
+ settings = {}
48
+
49
+ # Get or create rules array
50
+ rules: list[str] = settings.setdefault("rules", [])
51
+
52
+ # Add new rules (converted to imperative form)
53
+ added = 0
54
+ for skill in skills:
55
+ rule = _to_imperative(skill.rule, skill.confidence)
56
+ if rule not in rules:
57
+ rules.append(rule)
58
+ added += 1
59
+
60
+ # Update buildlog metadata (accumulate, don't replace)
61
+ buildlog_meta = settings.get("_buildlog", {"promoted_skill_ids": []})
62
+ existing_ids = set(buildlog_meta.get("promoted_skill_ids", []))
63
+ new_ids = existing_ids | {s.id for s in skills}
64
+ settings["_buildlog"] = {
65
+ "last_updated": datetime.now().isoformat(),
66
+ "promoted_skill_ids": sorted(new_ids),
67
+ }
68
+
69
+ # Write back
70
+ self.path.write_text(json.dumps(settings, indent=2))
71
+
72
+ # Track promoted skill IDs
73
+ self._track_promoted(skills)
74
+
75
+ return f"Added {added} rules to {self.path} ({len(skills) - added} duplicates skipped)"
76
+
77
+ def _track_promoted(self, skills: list[Skill]) -> None:
78
+ """Track which skills have been promoted."""
79
+ self.tracking_path.parent.mkdir(parents=True, exist_ok=True)
80
+
81
+ # Load existing tracking data (handle corrupt JSON)
82
+ tracking = {"skill_ids": [], "promoted_at": {}}
83
+ if self.tracking_path.exists():
84
+ try:
85
+ tracking = json.loads(self.tracking_path.read_text())
86
+ except json.JSONDecodeError:
87
+ pass # Start fresh if corrupted
88
+
89
+ # Add new skill IDs
90
+ now = datetime.now().isoformat()
91
+ for skill in skills:
92
+ if skill.id not in tracking["skill_ids"]:
93
+ tracking["skill_ids"].append(skill.id)
94
+ tracking["promoted_at"][skill.id] = now
95
+
96
+ self.tracking_path.write_text(json.dumps(tracking, indent=2))