buildlog 0.7.0__py3-none-any.whl → 0.9.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.
- buildlog/__init__.py +1 -1
- buildlog/cli.py +659 -48
- buildlog/confidence.py +27 -0
- buildlog/core/__init__.py +2 -0
- buildlog/core/bandit.py +699 -0
- buildlog/core/operations.py +284 -24
- buildlog/distill.py +80 -1
- buildlog/engine/__init__.py +61 -0
- buildlog/engine/bandit.py +23 -0
- buildlog/engine/confidence.py +28 -0
- buildlog/engine/embeddings.py +28 -0
- buildlog/engine/experiments.py +619 -0
- buildlog/engine/types.py +31 -0
- buildlog/llm.py +508 -0
- buildlog/mcp/server.py +10 -6
- buildlog/mcp/tools.py +61 -13
- buildlog/render/__init__.py +19 -2
- buildlog/render/claude_md.py +67 -32
- buildlog/render/continue_dev.py +102 -0
- buildlog/render/copilot.py +100 -0
- buildlog/render/cursor.py +105 -0
- buildlog/render/windsurf.py +95 -0
- buildlog/seed_engine/__init__.py +2 -0
- buildlog/seed_engine/llm_extractor.py +121 -0
- buildlog/seed_engine/pipeline.py +45 -1
- buildlog/skills.py +69 -6
- {buildlog-0.7.0.data → buildlog-0.9.0.data}/data/share/buildlog/copier.yml +0 -4
- buildlog-0.9.0.data/data/share/buildlog/template/buildlog/_TEMPLATE_QUICK.md +21 -0
- buildlog-0.9.0.dist-info/METADATA +248 -0
- buildlog-0.9.0.dist-info/RECORD +55 -0
- buildlog-0.7.0.dist-info/METADATA +0 -544
- buildlog-0.7.0.dist-info/RECORD +0 -41
- {buildlog-0.7.0.data → buildlog-0.9.0.data}/data/share/buildlog/post_gen.py +0 -0
- {buildlog-0.7.0.data → buildlog-0.9.0.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
- {buildlog-0.7.0.data → buildlog-0.9.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
- {buildlog-0.7.0.data → buildlog-0.9.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
- {buildlog-0.7.0.data → buildlog-0.9.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
- {buildlog-0.7.0.data → buildlog-0.9.0.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
- {buildlog-0.7.0.dist-info → buildlog-0.9.0.dist-info}/WHEEL +0 -0
- {buildlog-0.7.0.dist-info → buildlog-0.9.0.dist-info}/entry_points.txt +0 -0
- {buildlog-0.7.0.dist-info → buildlog-0.9.0.dist-info}/licenses/LICENSE +0 -0
buildlog/render/__init__.py
CHANGED
|
@@ -7,8 +7,12 @@ from typing import TYPE_CHECKING, Literal
|
|
|
7
7
|
|
|
8
8
|
from buildlog.render.base import RenderTarget
|
|
9
9
|
from buildlog.render.claude_md import ClaudeMdRenderer
|
|
10
|
+
from buildlog.render.continue_dev import ContinueRenderer
|
|
11
|
+
from buildlog.render.copilot import CopilotRenderer
|
|
12
|
+
from buildlog.render.cursor import CursorRenderer
|
|
10
13
|
from buildlog.render.settings_json import SettingsJsonRenderer
|
|
11
14
|
from buildlog.render.skill import SkillRenderer
|
|
15
|
+
from buildlog.render.windsurf import WindsurfRenderer
|
|
12
16
|
|
|
13
17
|
if TYPE_CHECKING:
|
|
14
18
|
from typing import Any
|
|
@@ -18,8 +22,13 @@ __all__ = [
|
|
|
18
22
|
"ClaudeMdRenderer",
|
|
19
23
|
"SettingsJsonRenderer",
|
|
20
24
|
"SkillRenderer",
|
|
25
|
+
"CursorRenderer",
|
|
26
|
+
"CopilotRenderer",
|
|
27
|
+
"WindsurfRenderer",
|
|
28
|
+
"ContinueRenderer",
|
|
21
29
|
"get_renderer",
|
|
22
30
|
"RENDERERS",
|
|
31
|
+
"RENDER_TARGETS",
|
|
23
32
|
]
|
|
24
33
|
|
|
25
34
|
# Registry of available renderers
|
|
@@ -28,18 +37,26 @@ RENDERERS: dict[str, type[RenderTarget]] = {
|
|
|
28
37
|
"claude_md": ClaudeMdRenderer,
|
|
29
38
|
"settings_json": SettingsJsonRenderer,
|
|
30
39
|
"skill": SkillRenderer,
|
|
40
|
+
"cursor": CursorRenderer,
|
|
41
|
+
"copilot": CopilotRenderer,
|
|
42
|
+
"windsurf": WindsurfRenderer,
|
|
43
|
+
"continue_dev": ContinueRenderer,
|
|
31
44
|
}
|
|
32
45
|
|
|
46
|
+
# Valid target names (useful for CLI choices and type hints)
|
|
47
|
+
RENDER_TARGETS = list(RENDERERS.keys())
|
|
48
|
+
|
|
33
49
|
|
|
34
50
|
def get_renderer(
|
|
35
|
-
target:
|
|
51
|
+
target: str,
|
|
36
52
|
path: Path | None = None,
|
|
37
53
|
**kwargs: Any,
|
|
38
54
|
) -> RenderTarget:
|
|
39
55
|
"""Get renderer for target.
|
|
40
56
|
|
|
41
57
|
Args:
|
|
42
|
-
target: Target format -
|
|
58
|
+
target: Target format - one of: claude_md, settings_json, skill,
|
|
59
|
+
cursor, copilot, windsurf, continue_dev.
|
|
43
60
|
path: Optional custom path for the target file.
|
|
44
61
|
**kwargs: Additional arguments passed to the renderer constructor.
|
|
45
62
|
Common kwargs (accepted by all renderers):
|
buildlog/render/claude_md.py
CHANGED
|
@@ -12,9 +12,18 @@ from buildlog.skills import _to_imperative
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from buildlog.skills import Skill
|
|
14
14
|
|
|
15
|
+
# Markers to identify the buildlog-managed section in CLAUDE.md
|
|
16
|
+
_SECTION_START = "<!-- buildlog:rules:start -->"
|
|
17
|
+
_SECTION_END = "<!-- buildlog:rules:end -->"
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
class ClaudeMdRenderer:
|
|
17
|
-
"""
|
|
21
|
+
"""Manages a dedicated rules section in CLAUDE.md.
|
|
22
|
+
|
|
23
|
+
Uses HTML comment markers to identify the buildlog-managed section.
|
|
24
|
+
On each promote, the section is replaced (not appended) with ALL
|
|
25
|
+
currently promoted rules, preventing duplicates.
|
|
26
|
+
"""
|
|
18
27
|
|
|
19
28
|
def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
|
|
20
29
|
"""Initialize renderer.
|
|
@@ -31,12 +40,14 @@ class ClaudeMdRenderer:
|
|
|
31
40
|
self.tracking_path = tracking_path
|
|
32
41
|
|
|
33
42
|
def render(self, skills: list[Skill]) -> str:
|
|
34
|
-
"""
|
|
43
|
+
"""Write skills to CLAUDE.md, replacing the buildlog-managed section.
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
On first run, appends a marked section. On subsequent runs, finds
|
|
46
|
+
and replaces the marked section with updated rules. This prevents
|
|
47
|
+
the duplicate accumulation that append-only causes.
|
|
37
48
|
|
|
38
49
|
Args:
|
|
39
|
-
skills: List of skills to
|
|
50
|
+
skills: List of skills to write.
|
|
40
51
|
|
|
41
52
|
Returns:
|
|
42
53
|
Confirmation message.
|
|
@@ -44,25 +55,52 @@ class ClaudeMdRenderer:
|
|
|
44
55
|
if not skills:
|
|
45
56
|
return "No skills to promote"
|
|
46
57
|
|
|
47
|
-
# Filter out already-promoted skills
|
|
58
|
+
# Filter out already-promoted skills for tracking purposes,
|
|
59
|
+
# but we still rebuild the full section from ALL promoted skills
|
|
48
60
|
already_promoted = get_promoted_ids(self.tracking_path)
|
|
49
61
|
new_skills = [s for s in skills if s.id not in already_promoted]
|
|
50
62
|
|
|
51
63
|
if not new_skills:
|
|
52
64
|
return f"All {len(skills)} skills already promoted"
|
|
53
65
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
for skill in new_skills:
|
|
57
|
-
by_category.setdefault(skill.category, []).append(skill)
|
|
66
|
+
# Track the new skills first so the section includes them
|
|
67
|
+
track_promoted(new_skills, self.tracking_path)
|
|
58
68
|
|
|
59
|
-
# Build section
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
# Build the section content from ALL skills being promoted now
|
|
70
|
+
# (not just new ones — we replace the entire section)
|
|
71
|
+
all_skills = skills # All skills passed in this call
|
|
72
|
+
section = self._build_section(all_skills)
|
|
73
|
+
|
|
74
|
+
# Read existing file
|
|
75
|
+
if self.path.exists():
|
|
76
|
+
existing = self.path.read_text()
|
|
77
|
+
else:
|
|
78
|
+
existing = ""
|
|
79
|
+
|
|
80
|
+
# Replace or append the buildlog section
|
|
81
|
+
if _SECTION_START in existing and _SECTION_END in existing:
|
|
82
|
+
# Replace existing section
|
|
83
|
+
start_idx = existing.index(_SECTION_START)
|
|
84
|
+
end_idx = existing.index(_SECTION_END) + len(_SECTION_END)
|
|
85
|
+
updated = existing[:start_idx] + section + existing[end_idx:]
|
|
86
|
+
elif _SECTION_START in existing:
|
|
87
|
+
# Malformed: start marker but no end. Replace from start to EOF.
|
|
88
|
+
start_idx = existing.index(_SECTION_START)
|
|
89
|
+
updated = existing[:start_idx] + section
|
|
90
|
+
else:
|
|
91
|
+
# No existing section — append
|
|
92
|
+
updated = existing.rstrip() + "\n\n" + section + "\n"
|
|
93
|
+
|
|
94
|
+
self.path.write_text(updated)
|
|
95
|
+
|
|
96
|
+
skipped = len(skills) - len(new_skills)
|
|
97
|
+
msg = f"Wrote {len(new_skills)} new rules to {self.path} ({len(all_skills)} total in section)"
|
|
98
|
+
if skipped > 0:
|
|
99
|
+
msg += f" ({skipped} already tracked)"
|
|
100
|
+
return msg
|
|
65
101
|
|
|
102
|
+
def _build_section(self, skills: list[Skill]) -> str:
|
|
103
|
+
"""Build the marked section content."""
|
|
66
104
|
category_titles = {
|
|
67
105
|
"architectural": "Architectural",
|
|
68
106
|
"workflow": "Workflow",
|
|
@@ -70,6 +108,18 @@ class ClaudeMdRenderer:
|
|
|
70
108
|
"domain_knowledge": "Domain Knowledge",
|
|
71
109
|
}
|
|
72
110
|
|
|
111
|
+
# Group by category
|
|
112
|
+
by_category: dict[str, list[Skill]] = {}
|
|
113
|
+
for skill in skills:
|
|
114
|
+
by_category.setdefault(skill.category, []).append(skill)
|
|
115
|
+
|
|
116
|
+
lines = [
|
|
117
|
+
_SECTION_START,
|
|
118
|
+
"",
|
|
119
|
+
f"## Learned Rules (buildlog, updated {datetime.now().strftime('%Y-%m-%d')})",
|
|
120
|
+
"",
|
|
121
|
+
]
|
|
122
|
+
|
|
73
123
|
for category, cat_skills in by_category.items():
|
|
74
124
|
title = category_titles.get(category, category.replace("_", " ").title())
|
|
75
125
|
lines.append(f"### {title}")
|
|
@@ -79,20 +129,5 @@ class ClaudeMdRenderer:
|
|
|
79
129
|
lines.append(f"- {rule}")
|
|
80
130
|
lines.append("")
|
|
81
131
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# Append to file
|
|
85
|
-
if self.path.exists():
|
|
86
|
-
existing = self.path.read_text()
|
|
87
|
-
self.path.write_text(existing + content)
|
|
88
|
-
else:
|
|
89
|
-
self.path.write_text(content)
|
|
90
|
-
|
|
91
|
-
# Track promoted skill IDs using shared utility
|
|
92
|
-
track_promoted(new_skills, self.tracking_path)
|
|
93
|
-
|
|
94
|
-
skipped = len(skills) - len(new_skills)
|
|
95
|
-
msg = f"Appended {len(new_skills)} rules to {self.path}"
|
|
96
|
-
if skipped > 0:
|
|
97
|
-
msg += f" ({skipped} already promoted, skipped)"
|
|
98
|
-
return msg
|
|
132
|
+
lines.append(_SECTION_END)
|
|
133
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Render skills to Continue.dev rules format.
|
|
2
|
+
|
|
3
|
+
Creates .continue/rules/buildlog-rules.md with YAML frontmatter and
|
|
4
|
+
Markdown body containing learned rules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from buildlog.render.tracking import get_promoted_ids, track_promoted
|
|
14
|
+
from buildlog.skills import _to_imperative
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from buildlog.skills import Skill
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ContinueRenderer:
|
|
21
|
+
"""Creates .continue/rules/buildlog-rules.md for Continue.dev."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
|
|
24
|
+
"""Initialize renderer.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
path: Path to rules file.
|
|
28
|
+
Defaults to .continue/rules/buildlog-rules.md.
|
|
29
|
+
tracking_path: Path to promoted.json tracking file.
|
|
30
|
+
Defaults to .buildlog/promoted.json.
|
|
31
|
+
"""
|
|
32
|
+
self.path = path or Path(".continue/rules/buildlog-rules.md")
|
|
33
|
+
self.tracking_path = tracking_path or Path(".buildlog/promoted.json")
|
|
34
|
+
|
|
35
|
+
def render(self, skills: list[Skill]) -> str:
|
|
36
|
+
"""Render skills to Continue.dev rules file.
|
|
37
|
+
|
|
38
|
+
Overwrites the file with all promoted skills.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
skills: List of skills to render.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Confirmation message.
|
|
45
|
+
"""
|
|
46
|
+
if not skills:
|
|
47
|
+
return "No skills to promote"
|
|
48
|
+
|
|
49
|
+
# Filter out already-promoted skills
|
|
50
|
+
already_promoted = get_promoted_ids(self.tracking_path)
|
|
51
|
+
new_skills = [s for s in skills if s.id not in already_promoted]
|
|
52
|
+
|
|
53
|
+
if not new_skills:
|
|
54
|
+
return f"All {len(skills)} skills already promoted"
|
|
55
|
+
|
|
56
|
+
# Group by category
|
|
57
|
+
by_category: dict[str, list[Skill]] = {}
|
|
58
|
+
for skill in new_skills:
|
|
59
|
+
by_category.setdefault(skill.category, []).append(skill)
|
|
60
|
+
|
|
61
|
+
category_titles = {
|
|
62
|
+
"architectural": "Architectural",
|
|
63
|
+
"workflow": "Workflow",
|
|
64
|
+
"tool_usage": "Tool Usage",
|
|
65
|
+
"domain_knowledge": "Domain Knowledge",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Build content with YAML frontmatter
|
|
69
|
+
lines = [
|
|
70
|
+
"---",
|
|
71
|
+
"name: buildlog-learned-rules",
|
|
72
|
+
"---",
|
|
73
|
+
"",
|
|
74
|
+
"# Learned Rules",
|
|
75
|
+
"",
|
|
76
|
+
f"*Auto-generated by buildlog on {datetime.now().strftime('%Y-%m-%d')}*",
|
|
77
|
+
"",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
for category, cat_skills in by_category.items():
|
|
81
|
+
title = category_titles.get(category, category.replace("_", " ").title())
|
|
82
|
+
lines.append(f"## {title}")
|
|
83
|
+
lines.append("")
|
|
84
|
+
for skill in cat_skills:
|
|
85
|
+
rule = _to_imperative(skill.rule, skill.confidence)
|
|
86
|
+
lines.append(f"- {rule}")
|
|
87
|
+
lines.append("")
|
|
88
|
+
|
|
89
|
+
content = "\n".join(lines)
|
|
90
|
+
|
|
91
|
+
# Write file
|
|
92
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
self.path.write_text(content)
|
|
94
|
+
|
|
95
|
+
# Track promoted skill IDs
|
|
96
|
+
track_promoted(new_skills, self.tracking_path)
|
|
97
|
+
|
|
98
|
+
skipped = len(skills) - len(new_skills)
|
|
99
|
+
msg = f"Wrote {len(new_skills)} rules to {self.path}"
|
|
100
|
+
if skipped > 0:
|
|
101
|
+
msg += f" ({skipped} already promoted, skipped)"
|
|
102
|
+
return msg
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Render skills to GitHub Copilot instructions format.
|
|
2
|
+
|
|
3
|
+
Appends a learned rules section to .github/copilot-instructions.md.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from buildlog.render.tracking import get_promoted_ids, track_promoted
|
|
13
|
+
from buildlog.skills import _to_imperative
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from buildlog.skills import Skill
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CopilotRenderer:
|
|
20
|
+
"""Appends promoted skills to .github/copilot-instructions.md."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
|
|
23
|
+
"""Initialize renderer.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
path: Path to copilot instructions file.
|
|
27
|
+
Defaults to .github/copilot-instructions.md.
|
|
28
|
+
tracking_path: Path to promoted.json tracking file.
|
|
29
|
+
Defaults to .buildlog/promoted.json.
|
|
30
|
+
"""
|
|
31
|
+
self.path = path or Path(".github/copilot-instructions.md")
|
|
32
|
+
self.tracking_path = tracking_path or Path(".buildlog/promoted.json")
|
|
33
|
+
|
|
34
|
+
def render(self, skills: list[Skill]) -> str:
|
|
35
|
+
"""Append skills to copilot-instructions.md.
|
|
36
|
+
|
|
37
|
+
Filters out already-promoted skills and appends a new section.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
skills: List of skills to render.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Confirmation message.
|
|
44
|
+
"""
|
|
45
|
+
if not skills:
|
|
46
|
+
return "No skills to promote"
|
|
47
|
+
|
|
48
|
+
# Filter out already-promoted skills
|
|
49
|
+
already_promoted = get_promoted_ids(self.tracking_path)
|
|
50
|
+
new_skills = [s for s in skills if s.id not in already_promoted]
|
|
51
|
+
|
|
52
|
+
if not new_skills:
|
|
53
|
+
return f"All {len(skills)} skills already promoted"
|
|
54
|
+
|
|
55
|
+
# Group by category
|
|
56
|
+
by_category: dict[str, list[Skill]] = {}
|
|
57
|
+
for skill in new_skills:
|
|
58
|
+
by_category.setdefault(skill.category, []).append(skill)
|
|
59
|
+
|
|
60
|
+
category_titles = {
|
|
61
|
+
"architectural": "Architectural",
|
|
62
|
+
"workflow": "Workflow",
|
|
63
|
+
"tool_usage": "Tool Usage",
|
|
64
|
+
"domain_knowledge": "Domain Knowledge",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Build section
|
|
68
|
+
lines = [
|
|
69
|
+
"",
|
|
70
|
+
f"## Learned Rules (buildlog {datetime.now().strftime('%Y-%m-%d')})",
|
|
71
|
+
"",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
for category, cat_skills in by_category.items():
|
|
75
|
+
title = category_titles.get(category, category.replace("_", " ").title())
|
|
76
|
+
lines.append(f"### {title}")
|
|
77
|
+
lines.append("")
|
|
78
|
+
for skill in cat_skills:
|
|
79
|
+
rule = _to_imperative(skill.rule, skill.confidence)
|
|
80
|
+
lines.append(f"- {rule}")
|
|
81
|
+
lines.append("")
|
|
82
|
+
|
|
83
|
+
content = "\n".join(lines)
|
|
84
|
+
|
|
85
|
+
# Append to file (create if needed)
|
|
86
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
if self.path.exists():
|
|
88
|
+
existing = self.path.read_text()
|
|
89
|
+
self.path.write_text(existing + content)
|
|
90
|
+
else:
|
|
91
|
+
self.path.write_text(content)
|
|
92
|
+
|
|
93
|
+
# Track promoted skill IDs
|
|
94
|
+
track_promoted(new_skills, self.tracking_path)
|
|
95
|
+
|
|
96
|
+
skipped = len(skills) - len(new_skills)
|
|
97
|
+
msg = f"Appended {len(new_skills)} rules to {self.path}"
|
|
98
|
+
if skipped > 0:
|
|
99
|
+
msg += f" ({skipped} already promoted, skipped)"
|
|
100
|
+
return msg
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Render skills to Cursor rules format.
|
|
2
|
+
|
|
3
|
+
Creates .cursor/rules/buildlog-rules.mdc with YAML frontmatter and
|
|
4
|
+
Markdown body containing learned rules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from buildlog.render.tracking import get_promoted_ids, track_promoted
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from buildlog.skills import Skill
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CursorRenderer:
|
|
20
|
+
"""Creates .cursor/rules/buildlog-rules.mdc for Cursor IDE."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
|
|
23
|
+
"""Initialize renderer.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
path: Path to .mdc rules file. Defaults to .cursor/rules/buildlog-rules.mdc.
|
|
27
|
+
tracking_path: Path to promoted.json tracking file.
|
|
28
|
+
Defaults to .buildlog/promoted.json.
|
|
29
|
+
"""
|
|
30
|
+
self.path = path or Path(".cursor/rules/buildlog-rules.mdc")
|
|
31
|
+
self.tracking_path = tracking_path or Path(".buildlog/promoted.json")
|
|
32
|
+
|
|
33
|
+
def render(self, skills: list[Skill]) -> str:
|
|
34
|
+
"""Render skills to Cursor .mdc rules file.
|
|
35
|
+
|
|
36
|
+
Overwrites the file with all promoted skills (not append-only) since
|
|
37
|
+
Cursor reads the entire file on each invocation.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
skills: List of skills to render.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Confirmation message.
|
|
44
|
+
"""
|
|
45
|
+
if not skills:
|
|
46
|
+
return "No skills to promote"
|
|
47
|
+
|
|
48
|
+
# Filter out already-promoted skills
|
|
49
|
+
already_promoted = get_promoted_ids(self.tracking_path)
|
|
50
|
+
new_skills = [s for s in skills if s.id not in already_promoted]
|
|
51
|
+
|
|
52
|
+
if not new_skills:
|
|
53
|
+
return f"All {len(skills)} skills already promoted"
|
|
54
|
+
|
|
55
|
+
# Group by category
|
|
56
|
+
by_category: dict[str, list[Skill]] = {}
|
|
57
|
+
for skill in new_skills:
|
|
58
|
+
by_category.setdefault(skill.category, []).append(skill)
|
|
59
|
+
|
|
60
|
+
category_titles = {
|
|
61
|
+
"architectural": "Architectural",
|
|
62
|
+
"workflow": "Workflow",
|
|
63
|
+
"tool_usage": "Tool Usage",
|
|
64
|
+
"domain_knowledge": "Domain Knowledge",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Build MDC content with YAML frontmatter
|
|
68
|
+
lines = [
|
|
69
|
+
"---",
|
|
70
|
+
"description: Buildlog-learned rules for code quality",
|
|
71
|
+
"alwaysApply: true",
|
|
72
|
+
"---",
|
|
73
|
+
"",
|
|
74
|
+
"# Learned Rules",
|
|
75
|
+
"",
|
|
76
|
+
f"*Auto-generated by buildlog on {datetime.now().strftime('%Y-%m-%d')}*",
|
|
77
|
+
"",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
for category, cat_skills in by_category.items():
|
|
81
|
+
title = category_titles.get(category, category.replace("_", " ").title())
|
|
82
|
+
lines.append(f"## {title}")
|
|
83
|
+
lines.append("")
|
|
84
|
+
for skill in cat_skills:
|
|
85
|
+
conf = skill.confidence
|
|
86
|
+
freq = skill.frequency
|
|
87
|
+
lines.append(
|
|
88
|
+
f"- **{skill.rule}** (confidence: {conf}, frequency: {freq})"
|
|
89
|
+
)
|
|
90
|
+
lines.append("")
|
|
91
|
+
|
|
92
|
+
content = "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
# Write file
|
|
95
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
self.path.write_text(content)
|
|
97
|
+
|
|
98
|
+
# Track promoted skill IDs
|
|
99
|
+
track_promoted(new_skills, self.tracking_path)
|
|
100
|
+
|
|
101
|
+
skipped = len(skills) - len(new_skills)
|
|
102
|
+
msg = f"Wrote {len(new_skills)} rules to {self.path}"
|
|
103
|
+
if skipped > 0:
|
|
104
|
+
msg += f" ({skipped} already promoted, skipped)"
|
|
105
|
+
return msg
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Render skills to Windsurf rules format.
|
|
2
|
+
|
|
3
|
+
Creates .windsurf/rules/buildlog-rules.md with learned rules in
|
|
4
|
+
plain Markdown format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from buildlog.render.tracking import get_promoted_ids, track_promoted
|
|
14
|
+
from buildlog.skills import _to_imperative
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from buildlog.skills import Skill
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WindsurfRenderer:
|
|
21
|
+
"""Creates .windsurf/rules/buildlog-rules.md for Windsurf IDE."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
|
|
24
|
+
"""Initialize renderer.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
path: Path to rules file. Defaults to .windsurf/rules/buildlog-rules.md.
|
|
28
|
+
tracking_path: Path to promoted.json tracking file.
|
|
29
|
+
Defaults to .buildlog/promoted.json.
|
|
30
|
+
"""
|
|
31
|
+
self.path = path or Path(".windsurf/rules/buildlog-rules.md")
|
|
32
|
+
self.tracking_path = tracking_path or Path(".buildlog/promoted.json")
|
|
33
|
+
|
|
34
|
+
def render(self, skills: list[Skill]) -> str:
|
|
35
|
+
"""Render skills to Windsurf rules file.
|
|
36
|
+
|
|
37
|
+
Overwrites the file with all promoted skills.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
skills: List of skills to render.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Confirmation message.
|
|
44
|
+
"""
|
|
45
|
+
if not skills:
|
|
46
|
+
return "No skills to promote"
|
|
47
|
+
|
|
48
|
+
# Filter out already-promoted skills
|
|
49
|
+
already_promoted = get_promoted_ids(self.tracking_path)
|
|
50
|
+
new_skills = [s for s in skills if s.id not in already_promoted]
|
|
51
|
+
|
|
52
|
+
if not new_skills:
|
|
53
|
+
return f"All {len(skills)} skills already promoted"
|
|
54
|
+
|
|
55
|
+
# Group by category
|
|
56
|
+
by_category: dict[str, list[Skill]] = {}
|
|
57
|
+
for skill in new_skills:
|
|
58
|
+
by_category.setdefault(skill.category, []).append(skill)
|
|
59
|
+
|
|
60
|
+
category_titles = {
|
|
61
|
+
"architectural": "Architectural",
|
|
62
|
+
"workflow": "Workflow",
|
|
63
|
+
"tool_usage": "Tool Usage",
|
|
64
|
+
"domain_knowledge": "Domain Knowledge",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Build Markdown content
|
|
68
|
+
lines = [
|
|
69
|
+
f"## Learned Rules (buildlog {datetime.now().strftime('%Y-%m-%d')})",
|
|
70
|
+
"",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
for category, cat_skills in by_category.items():
|
|
74
|
+
title = category_titles.get(category, category.replace("_", " ").title())
|
|
75
|
+
lines.append(f"### {title}")
|
|
76
|
+
lines.append("")
|
|
77
|
+
for skill in cat_skills:
|
|
78
|
+
rule = _to_imperative(skill.rule, skill.confidence)
|
|
79
|
+
lines.append(f"- {rule}")
|
|
80
|
+
lines.append("")
|
|
81
|
+
|
|
82
|
+
content = "\n".join(lines)
|
|
83
|
+
|
|
84
|
+
# Write file
|
|
85
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
self.path.write_text(content)
|
|
87
|
+
|
|
88
|
+
# Track promoted skill IDs
|
|
89
|
+
track_promoted(new_skills, self.tracking_path)
|
|
90
|
+
|
|
91
|
+
skipped = len(skills) - len(new_skills)
|
|
92
|
+
msg = f"Wrote {len(new_skills)} rules to {self.path}"
|
|
93
|
+
if skipped > 0:
|
|
94
|
+
msg += f" ({skipped} already promoted, skipped)"
|
|
95
|
+
return msg
|
buildlog/seed_engine/__init__.py
CHANGED
|
@@ -33,6 +33,7 @@ from buildlog.seed_engine.categorizers import (
|
|
|
33
33
|
)
|
|
34
34
|
from buildlog.seed_engine.extractors import ManualExtractor, RuleExtractor
|
|
35
35
|
from buildlog.seed_engine.generators import SeedGenerator
|
|
36
|
+
from buildlog.seed_engine.llm_extractor import LLMExtractor
|
|
36
37
|
from buildlog.seed_engine.models import (
|
|
37
38
|
CandidateRule,
|
|
38
39
|
CategorizedRule,
|
|
@@ -59,6 +60,7 @@ __all__ = [
|
|
|
59
60
|
# Extractors
|
|
60
61
|
"RuleExtractor",
|
|
61
62
|
"ManualExtractor",
|
|
63
|
+
"LLMExtractor",
|
|
62
64
|
# Categorizers
|
|
63
65
|
"Categorizer",
|
|
64
66
|
"TagBasedCategorizer",
|