buildlog 0.6.1__py3-none-any.whl → 0.8.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 +589 -44
- buildlog/confidence.py +27 -0
- buildlog/core/__init__.py +12 -0
- buildlog/core/bandit.py +699 -0
- buildlog/core/operations.py +499 -11
- 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 +461 -0
- buildlog/mcp/server.py +12 -6
- buildlog/mcp/tools.py +166 -13
- buildlog/render/__init__.py +19 -2
- buildlog/render/claude_md.py +74 -26
- buildlog/render/continue_dev.py +102 -0
- buildlog/render/copilot.py +100 -0
- buildlog/render/cursor.py +105 -0
- buildlog/render/tracking.py +20 -1
- buildlog/render/windsurf.py +95 -0
- buildlog/seeds.py +41 -0
- buildlog/skills.py +69 -6
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/copier.yml +0 -4
- buildlog-0.8.0.data/data/share/buildlog/template/buildlog/_TEMPLATE_QUICK.md +21 -0
- buildlog-0.8.0.dist-info/METADATA +151 -0
- buildlog-0.8.0.dist-info/RECORD +54 -0
- buildlog-0.6.1.dist-info/METADATA +0 -490
- buildlog-0.6.1.dist-info/RECORD +0 -41
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/post_gen.py +0 -0
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
- {buildlog-0.6.1.dist-info → buildlog-0.8.0.dist-info}/WHEEL +0 -0
- {buildlog-0.6.1.dist-info → buildlog-0.8.0.dist-info}/entry_points.txt +0 -0
- {buildlog-0.6.1.dist-info → buildlog-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
buildlog/render/tracking.py
CHANGED
|
@@ -10,7 +10,26 @@ from typing import TYPE_CHECKING
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from buildlog.skills import Skill
|
|
12
12
|
|
|
13
|
-
__all__ = ["track_promoted"]
|
|
13
|
+
__all__ = ["track_promoted", "get_promoted_ids"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_promoted_ids(tracking_path: Path) -> set[str]:
|
|
17
|
+
"""Get the set of already-promoted skill IDs.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
tracking_path: Path to the tracking JSON file.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Set of skill IDs that have been promoted.
|
|
24
|
+
"""
|
|
25
|
+
if not tracking_path.exists():
|
|
26
|
+
return set()
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
tracking = json.loads(tracking_path.read_text())
|
|
30
|
+
return set(tracking.get("skill_ids", []))
|
|
31
|
+
except json.JSONDecodeError:
|
|
32
|
+
return set()
|
|
14
33
|
|
|
15
34
|
|
|
16
35
|
def track_promoted(skills: list[Skill], tracking_path: Path) -> None:
|
|
@@ -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/seeds.py
CHANGED
|
@@ -156,6 +156,36 @@ class SeedFile:
|
|
|
156
156
|
)
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
def _validate_seed_schema(data: dict) -> bool:
|
|
160
|
+
"""Validate seed file has expected schema structure.
|
|
161
|
+
|
|
162
|
+
Defense-in-depth validation for seed files. While yaml.safe_load
|
|
163
|
+
prevents code execution, this ensures data structure matches expectations.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
data: Parsed YAML data.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
True if schema is valid, False otherwise.
|
|
170
|
+
"""
|
|
171
|
+
if not isinstance(data, dict):
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
# Rules must be a list if present
|
|
175
|
+
rules = data.get("rules", [])
|
|
176
|
+
if not isinstance(rules, list):
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
# Each rule must be a dict with at least a "rule" key
|
|
180
|
+
for rule in rules:
|
|
181
|
+
if not isinstance(rule, dict):
|
|
182
|
+
return False
|
|
183
|
+
if "rule" not in rule:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
|
|
159
189
|
def load_seed_file(path: Path) -> SeedFile | None:
|
|
160
190
|
"""Load a single seed file from disk.
|
|
161
191
|
|
|
@@ -164,6 +194,10 @@ def load_seed_file(path: Path) -> SeedFile | None:
|
|
|
164
194
|
|
|
165
195
|
Returns:
|
|
166
196
|
Parsed SeedFile or None if loading fails.
|
|
197
|
+
|
|
198
|
+
Note:
|
|
199
|
+
Uses yaml.safe_load which is safe from code execution attacks.
|
|
200
|
+
Additional schema validation ensures data structure is as expected.
|
|
167
201
|
"""
|
|
168
202
|
if not path.exists():
|
|
169
203
|
logger.warning(f"Seed file not found: {path}")
|
|
@@ -171,7 +205,14 @@ def load_seed_file(path: Path) -> SeedFile | None:
|
|
|
171
205
|
|
|
172
206
|
try:
|
|
173
207
|
with open(path) as f:
|
|
208
|
+
# yaml.safe_load is safe - no arbitrary code execution
|
|
174
209
|
data = yaml.safe_load(f)
|
|
210
|
+
|
|
211
|
+
# Validate schema before parsing
|
|
212
|
+
if not _validate_seed_schema(data):
|
|
213
|
+
logger.error(f"Invalid seed file schema: {path}")
|
|
214
|
+
return None
|
|
215
|
+
|
|
175
216
|
return SeedFile.from_dict(data)
|
|
176
217
|
except (yaml.YAMLError, KeyError, TypeError) as e:
|
|
177
218
|
logger.error(f"Failed to parse seed file {path}: {e}")
|
buildlog/skills.py
CHANGED
|
@@ -23,7 +23,10 @@ import re
|
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
24
|
from datetime import date, datetime, timezone
|
|
25
25
|
from pathlib import Path
|
|
26
|
-
from typing import Final, Literal, TypedDict
|
|
26
|
+
from typing import TYPE_CHECKING, Final, Literal, TypedDict
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from buildlog.llm import LLMBackend
|
|
27
30
|
|
|
28
31
|
from buildlog.confidence import ConfidenceConfig, ConfidenceMetrics
|
|
29
32
|
from buildlog.confidence import calculate_confidence as calculate_continuous_confidence
|
|
@@ -83,6 +86,10 @@ class SkillDict(_SkillDictRequired, total=False):
|
|
|
83
86
|
antipattern: str # What does violation look like?
|
|
84
87
|
rationale: str # Why does this matter?
|
|
85
88
|
persona_tags: list[str] # Which reviewers use this rule?
|
|
89
|
+
# LLM-extracted scoring fields
|
|
90
|
+
severity: str # critical/major/minor/info
|
|
91
|
+
scope: str # global/module/function
|
|
92
|
+
applicability: list[str] # contexts where relevant
|
|
86
93
|
|
|
87
94
|
|
|
88
95
|
class SkillSetDict(TypedDict):
|
|
@@ -115,6 +122,9 @@ class Skill:
|
|
|
115
122
|
antipattern: What does violation look like? (defensibility)
|
|
116
123
|
rationale: Why does this rule matter? (defensibility)
|
|
117
124
|
persona_tags: Which reviewer personas use this rule?
|
|
125
|
+
severity: How bad is ignoring this rule? (critical/major/minor/info)
|
|
126
|
+
scope: How broadly does this rule apply? (global/module/function)
|
|
127
|
+
applicability: Contexts where this rule is relevant.
|
|
118
128
|
"""
|
|
119
129
|
|
|
120
130
|
id: str
|
|
@@ -131,6 +141,10 @@ class Skill:
|
|
|
131
141
|
antipattern: str | None = None
|
|
132
142
|
rationale: str | None = None
|
|
133
143
|
persona_tags: list[str] = field(default_factory=list)
|
|
144
|
+
# LLM-extracted scoring
|
|
145
|
+
severity: str | None = None
|
|
146
|
+
scope: str | None = None
|
|
147
|
+
applicability: list[str] = field(default_factory=list)
|
|
134
148
|
|
|
135
149
|
def to_dict(self) -> SkillDict:
|
|
136
150
|
"""Convert to dictionary for serialization.
|
|
@@ -159,6 +173,12 @@ class Skill:
|
|
|
159
173
|
result["rationale"] = self.rationale
|
|
160
174
|
if self.persona_tags:
|
|
161
175
|
result["persona_tags"] = self.persona_tags
|
|
176
|
+
if self.severity is not None:
|
|
177
|
+
result["severity"] = self.severity
|
|
178
|
+
if self.scope is not None:
|
|
179
|
+
result["scope"] = self.scope
|
|
180
|
+
if self.applicability:
|
|
181
|
+
result["applicability"] = self.applicability
|
|
162
182
|
return result
|
|
163
183
|
|
|
164
184
|
|
|
@@ -326,6 +346,7 @@ def _deduplicate_insights(
|
|
|
326
346
|
patterns: list[PatternDict],
|
|
327
347
|
threshold: float = MIN_SIMILARITY_THRESHOLD,
|
|
328
348
|
backend: EmbeddingBackend | None = None,
|
|
349
|
+
llm_backend: LLMBackend | None = None,
|
|
329
350
|
) -> list[tuple[str, int, list[str], date | None, date | None]]:
|
|
330
351
|
"""Deduplicate similar insights into merged rules.
|
|
331
352
|
|
|
@@ -366,9 +387,17 @@ def _deduplicate_insights(
|
|
|
366
387
|
results: list[tuple[str, int, list[str], date | None, date | None]] = []
|
|
367
388
|
|
|
368
389
|
for group in groups:
|
|
369
|
-
# Use
|
|
370
|
-
|
|
371
|
-
|
|
390
|
+
# Use LLM to select canonical form if available and group has >1 member
|
|
391
|
+
if llm_backend is not None and len(group) > 1:
|
|
392
|
+
try:
|
|
393
|
+
candidates = [p["insight"] for p in group]
|
|
394
|
+
rule = llm_backend.select_canonical(candidates)
|
|
395
|
+
except Exception:
|
|
396
|
+
canonical = min(group, key=lambda p: len(p["insight"]))
|
|
397
|
+
rule = canonical["insight"]
|
|
398
|
+
else:
|
|
399
|
+
canonical = min(group, key=lambda p: len(p["insight"]))
|
|
400
|
+
rule = canonical["insight"]
|
|
372
401
|
frequency = len(group)
|
|
373
402
|
sources = sorted(set(p["source"] for p in group))
|
|
374
403
|
|
|
@@ -434,6 +463,7 @@ def generate_skills(
|
|
|
434
463
|
embedding_backend: str | None = None,
|
|
435
464
|
confidence_config: ConfidenceConfig | None = None,
|
|
436
465
|
include_review_learnings: bool = True,
|
|
466
|
+
llm: bool = False,
|
|
437
467
|
) -> SkillSet:
|
|
438
468
|
"""Generate skills from buildlog patterns and review learnings.
|
|
439
469
|
|
|
@@ -449,12 +479,21 @@ def generate_skills(
|
|
|
449
479
|
include_review_learnings: Whether to include learnings from code reviews.
|
|
450
480
|
When True, loads .buildlog/review_learnings.json and merges
|
|
451
481
|
review learnings into the skill set.
|
|
482
|
+
llm: If True and an LLM backend is available, use LLM for extraction,
|
|
483
|
+
canonical selection, and scoring. Falls back gracefully.
|
|
452
484
|
|
|
453
485
|
Returns:
|
|
454
486
|
SkillSet with generated skills.
|
|
455
487
|
"""
|
|
488
|
+
# Resolve LLM backend if requested
|
|
489
|
+
llm_backend = None
|
|
490
|
+
if llm:
|
|
491
|
+
from buildlog.llm import get_llm_backend
|
|
492
|
+
|
|
493
|
+
llm_backend = get_llm_backend(buildlog_dir=buildlog_dir)
|
|
494
|
+
|
|
456
495
|
# Get distilled patterns
|
|
457
|
-
result = distill_all(buildlog_dir, since=since_date)
|
|
496
|
+
result = distill_all(buildlog_dir, since=since_date, llm=llm)
|
|
458
497
|
|
|
459
498
|
# Get embedding backend
|
|
460
499
|
backend = (
|
|
@@ -471,7 +510,9 @@ def generate_skills(
|
|
|
471
510
|
|
|
472
511
|
for category in CATEGORIES:
|
|
473
512
|
patterns = result.patterns.get(category, [])
|
|
474
|
-
deduplicated = _deduplicate_insights(
|
|
513
|
+
deduplicated = _deduplicate_insights(
|
|
514
|
+
patterns, backend=backend, llm_backend=llm_backend
|
|
515
|
+
)
|
|
475
516
|
|
|
476
517
|
skills: list[Skill] = []
|
|
477
518
|
for rule, frequency, sources, most_recent, earliest in deduplicated:
|
|
@@ -490,6 +531,25 @@ def generate_skills(
|
|
|
490
531
|
confidence_score, confidence_config
|
|
491
532
|
).value
|
|
492
533
|
|
|
534
|
+
# LLM scoring for severity/scope/applicability
|
|
535
|
+
severity: str | None = None
|
|
536
|
+
scope: str | None = None
|
|
537
|
+
applicability_tags: list[str] = []
|
|
538
|
+
if llm_backend is not None:
|
|
539
|
+
try:
|
|
540
|
+
scoring = llm_backend.score_rule(rule, category)
|
|
541
|
+
severity = scoring.severity
|
|
542
|
+
scope = scoring.scope
|
|
543
|
+
applicability_tags = scoring.applicability
|
|
544
|
+
except Exception:
|
|
545
|
+
pass # Keep defaults (None/empty)
|
|
546
|
+
|
|
547
|
+
# Apply severity weighting to confidence score
|
|
548
|
+
if confidence_score is not None and severity is not None:
|
|
549
|
+
from buildlog.confidence import apply_severity_weight
|
|
550
|
+
|
|
551
|
+
confidence_score = apply_severity_weight(confidence_score, severity)
|
|
552
|
+
|
|
493
553
|
skill = Skill(
|
|
494
554
|
id=_generate_skill_id(category, rule),
|
|
495
555
|
category=category,
|
|
@@ -500,6 +560,9 @@ def generate_skills(
|
|
|
500
560
|
tags=_extract_tags(rule),
|
|
501
561
|
confidence_score=confidence_score,
|
|
502
562
|
confidence_tier=confidence_tier,
|
|
563
|
+
severity=severity,
|
|
564
|
+
scope=scope,
|
|
565
|
+
applicability=applicability_tags,
|
|
503
566
|
)
|
|
504
567
|
skills.append(skill)
|
|
505
568
|
|
|
@@ -20,10 +20,6 @@ update_claude_md:
|
|
|
20
20
|
help: Add buildlog instructions to CLAUDE.md if it exists?
|
|
21
21
|
default: true
|
|
22
22
|
|
|
23
|
-
# Post-generation tasks
|
|
24
|
-
_tasks:
|
|
25
|
-
- "{{ 'python3 post_gen.py' if update_claude_md else 'echo Skipping CLAUDE.md update' }}"
|
|
26
|
-
|
|
27
23
|
_message_after_copy: |
|
|
28
24
|
Build journal installed!
|
|
29
25
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Build Journal: [TITLE]
|
|
2
|
+
|
|
3
|
+
**Date:** [YYYY-MM-DD]
|
|
4
|
+
**Duration:** [X hours]
|
|
5
|
+
|
|
6
|
+
## What I Did
|
|
7
|
+
|
|
8
|
+
[What you built, fixed, or changed. 2-3 sentences.]
|
|
9
|
+
|
|
10
|
+
## What Went Wrong
|
|
11
|
+
|
|
12
|
+
[Mistakes, surprises, dead ends. Be specific — these become rules.]
|
|
13
|
+
|
|
14
|
+
## What I Learned
|
|
15
|
+
|
|
16
|
+
### Improvements
|
|
17
|
+
|
|
18
|
+
- [One thing to do differently next time]
|
|
19
|
+
- [One thing that worked well to repeat]
|
|
20
|
+
|
|
21
|
+
*More sections: see _TEMPLATE.md for the full format.*
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: buildlog
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Engineering notebook for AI-assisted development
|
|
5
|
+
Project-URL: Homepage, https://github.com/Peleke/buildlog-template
|
|
6
|
+
Project-URL: Repository, https://github.com/Peleke/buildlog-template
|
|
7
|
+
Author: Peleke Sengstacke
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai,buildlog,development,documentation,journal
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Documentation
|
|
21
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: click>=8.0.0
|
|
24
|
+
Requires-Dist: copier>=9.0.0
|
|
25
|
+
Requires-Dist: numpy>=1.21.0
|
|
26
|
+
Requires-Dist: pymupdf>=1.26.7
|
|
27
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: anthropic>=0.40.0; extra == 'all'
|
|
30
|
+
Requires-Dist: mcp>=1.0.0; extra == 'all'
|
|
31
|
+
Requires-Dist: ollama>=0.4.0; extra == 'all'
|
|
32
|
+
Requires-Dist: openai>=1.0.0; extra == 'all'
|
|
33
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == 'all'
|
|
34
|
+
Provides-Extra: anthropic
|
|
35
|
+
Requires-Dist: anthropic>=0.40.0; extra == 'anthropic'
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: black>=24.0.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: flake8>=7.0.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: isort>=5.13.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: mkdocs-material>=9.5.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: pre-commit>=3.6.0; extra == 'dev'
|
|
43
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
44
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
|
|
47
|
+
Provides-Extra: embeddings
|
|
48
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == 'embeddings'
|
|
49
|
+
Provides-Extra: engine
|
|
50
|
+
Provides-Extra: llm
|
|
51
|
+
Requires-Dist: anthropic>=0.40.0; extra == 'llm'
|
|
52
|
+
Requires-Dist: ollama>=0.4.0; extra == 'llm'
|
|
53
|
+
Provides-Extra: mcp
|
|
54
|
+
Requires-Dist: mcp>=1.0.0; extra == 'mcp'
|
|
55
|
+
Provides-Extra: ollama
|
|
56
|
+
Requires-Dist: ollama>=0.4.0; extra == 'ollama'
|
|
57
|
+
Provides-Extra: openai
|
|
58
|
+
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
59
|
+
Description-Content-Type: text/markdown
|
|
60
|
+
|
|
61
|
+
<div align="center">
|
|
62
|
+
|
|
63
|
+
# buildlog
|
|
64
|
+
|
|
65
|
+
### The Only Agent Learning System You Can Prove Works
|
|
66
|
+
|
|
67
|
+
[](https://pypi.org/project/buildlog/)
|
|
68
|
+
[](https://python.org/)
|
|
69
|
+
[](https://github.com/Peleke/buildlog-template/actions/workflows/ci.yml)
|
|
70
|
+
[](https://opensource.org/licenses/MIT)
|
|
71
|
+
[](https://peleke.github.io/buildlog-template/)
|
|
72
|
+
|
|
73
|
+
**Falsifiable claims. Measurable outcomes. No vibes.**
|
|
74
|
+
|
|
75
|
+
<img src="assets/hero-banner-perfectdeliberate.png" alt="buildlog - The Only Agent Learning System You Can Prove Works" width="800"/>
|
|
76
|
+
|
|
77
|
+
> **RE: The art** — Yes, it's AI-generated. Yes, that's hypocritical for a project about rigor over vibes. Looking for an actual artist to pay for a real logo. If you know someone good, [open an issue](https://github.com/Peleke/buildlog-template/issues) or DM me. Budget exists.
|
|
78
|
+
|
|
79
|
+
**[Read the full documentation](https://peleke.github.io/buildlog-template/)**
|
|
80
|
+
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
Everyone's building "agent memory." Blog posts announce breakthroughs. Products ship with "learning" in the tagline. Ask them one question: **How do you know it works?**
|
|
86
|
+
|
|
87
|
+
buildlog gives you the infrastructure to answer with data. It captures engineering knowledge from work sessions, extracts rules, selects which rules to surface using a Thompson Sampling bandit, and measures impact via Repeated Mistake Rate (RMR) across tracked experiments.
|
|
88
|
+
|
|
89
|
+
## Features
|
|
90
|
+
|
|
91
|
+
- **Structured capture** — Document work sessions as entries with mistakes, decisions, and outcomes
|
|
92
|
+
- **Rule extraction** — Distill and deduplicate patterns into actionable rules
|
|
93
|
+
- **Thompson Sampling bandit** — Automatic rule selection that balances exploration and exploitation
|
|
94
|
+
- **Experiment tracking** — Sessions, mistakes, RMR calculation with statistical rigor
|
|
95
|
+
- **Review gauntlet** — Curated reviewer personas (Security Karen, Test Terrorist) with HITL checkpoints
|
|
96
|
+
- **Multi-agent support** — Render rules to Claude Code, Cursor, GitHub Copilot, Windsurf, Continue.dev
|
|
97
|
+
- **MCP server** — Full Claude Code integration via `buildlog-mcp`
|
|
98
|
+
|
|
99
|
+
## Quick Start
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uv pip install buildlog # or: pip install buildlog (inside a venv)
|
|
103
|
+
buildlog init
|
|
104
|
+
buildlog new my-feature
|
|
105
|
+
buildlog distill && buildlog skills
|
|
106
|
+
buildlog experiment start
|
|
107
|
+
# ... work ...
|
|
108
|
+
buildlog experiment end
|
|
109
|
+
buildlog experiment report
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Documentation
|
|
113
|
+
|
|
114
|
+
| Section | Description |
|
|
115
|
+
|---------|------------|
|
|
116
|
+
| [Installation](https://peleke.github.io/buildlog-template/getting-started/installation/) | Setup, extras, and initialization |
|
|
117
|
+
| [Quick Start](https://peleke.github.io/buildlog-template/getting-started/quick-start/) | Full pipeline walkthrough |
|
|
118
|
+
| [Core Concepts](https://peleke.github.io/buildlog-template/getting-started/concepts/) | The problem, the claim, and the metric |
|
|
119
|
+
| [CLI Reference](https://peleke.github.io/buildlog-template/guides/cli-reference/) | Every command documented |
|
|
120
|
+
| [MCP Integration](https://peleke.github.io/buildlog-template/guides/mcp-integration/) | Claude Code setup and available tools |
|
|
121
|
+
| [Experiments](https://peleke.github.io/buildlog-template/guides/experiments/) | Running and measuring experiments |
|
|
122
|
+
| [Review Gauntlet](https://peleke.github.io/buildlog-template/guides/review-gauntlet/) | Reviewer personas and the gauntlet loop |
|
|
123
|
+
| [Multi-Agent Setup](https://peleke.github.io/buildlog-template/guides/multi-agent/) | Render rules to any AI coding agent |
|
|
124
|
+
| [Theory](https://peleke.github.io/buildlog-template/theory/00-background/) | The math behind Thompson Sampling |
|
|
125
|
+
| [Philosophy](https://peleke.github.io/buildlog-template/philosophy/) | Principles and honest limitations |
|
|
126
|
+
|
|
127
|
+
## Contributing
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
git clone https://github.com/Peleke/buildlog-template
|
|
131
|
+
cd buildlog-template
|
|
132
|
+
uv venv && source .venv/bin/activate
|
|
133
|
+
uv pip install -e ".[dev]"
|
|
134
|
+
pytest
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
We're especially interested in better context representations, credit assignment approaches, statistical methodology improvements, and real-world experiment results (positive or negative).
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT License — see [LICENSE](./LICENSE)
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
<div align="center">
|
|
146
|
+
|
|
147
|
+
**"Agent learning" without measurement is just prompt engineering with extra steps.**
|
|
148
|
+
|
|
149
|
+
**buildlog is measurement.**
|
|
150
|
+
|
|
151
|
+
</div>
|