buildlog 0.1.0__py3-none-any.whl → 0.3.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/cli.py +46 -23
- buildlog/confidence.py +311 -0
- buildlog/core/operations.py +11 -15
- buildlog/distill.py +3 -3
- buildlog/embeddings.py +108 -16
- buildlog/mcp/tools.py +4 -4
- buildlog/render/__init__.py +34 -11
- buildlog/render/claude_md.py +3 -24
- buildlog/render/settings_json.py +3 -23
- buildlog/render/skill.py +175 -0
- buildlog/render/tracking.py +43 -0
- buildlog/skills.py +229 -47
- buildlog/stats.py +7 -5
- {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/post_gen.py +11 -7
- buildlog-0.3.0.dist-info/METADATA +763 -0
- buildlog-0.3.0.dist-info/RECORD +30 -0
- buildlog-0.1.0.dist-info/METADATA +0 -664
- buildlog-0.1.0.dist-info/RECORD +0 -27
- {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/copier.yml +0 -0
- {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
- {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
- {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
- {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
- {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
- {buildlog-0.1.0.dist-info → buildlog-0.3.0.dist-info}/WHEEL +0 -0
- {buildlog-0.1.0.dist-info → buildlog-0.3.0.dist-info}/entry_points.txt +0 -0
- {buildlog-0.1.0.dist-info → buildlog-0.3.0.dist-info}/licenses/LICENSE +0 -0
buildlog/embeddings.py
CHANGED
|
@@ -46,20 +46,112 @@ Embedding = list[float]
|
|
|
46
46
|
BackendName = Literal["token", "sentence-transformers", "openai"]
|
|
47
47
|
|
|
48
48
|
# Stop words to filter in token-based approach
|
|
49
|
-
STOP_WORDS: Final[frozenset[str]] = frozenset(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
STOP_WORDS: Final[frozenset[str]] = frozenset(
|
|
50
|
+
{
|
|
51
|
+
"a",
|
|
52
|
+
"an",
|
|
53
|
+
"the",
|
|
54
|
+
"is",
|
|
55
|
+
"are",
|
|
56
|
+
"was",
|
|
57
|
+
"were",
|
|
58
|
+
"be",
|
|
59
|
+
"been",
|
|
60
|
+
"being",
|
|
61
|
+
"have",
|
|
62
|
+
"has",
|
|
63
|
+
"had",
|
|
64
|
+
"do",
|
|
65
|
+
"does",
|
|
66
|
+
"did",
|
|
67
|
+
"will",
|
|
68
|
+
"would",
|
|
69
|
+
"could",
|
|
70
|
+
"should",
|
|
71
|
+
"may",
|
|
72
|
+
"might",
|
|
73
|
+
"must",
|
|
74
|
+
"shall",
|
|
75
|
+
"can",
|
|
76
|
+
"need",
|
|
77
|
+
"dare",
|
|
78
|
+
"ought",
|
|
79
|
+
"used",
|
|
80
|
+
"to",
|
|
81
|
+
"of",
|
|
82
|
+
"in",
|
|
83
|
+
"for",
|
|
84
|
+
"on",
|
|
85
|
+
"with",
|
|
86
|
+
"at",
|
|
87
|
+
"by",
|
|
88
|
+
"from",
|
|
89
|
+
"as",
|
|
90
|
+
"into",
|
|
91
|
+
"through",
|
|
92
|
+
"during",
|
|
93
|
+
"before",
|
|
94
|
+
"after",
|
|
95
|
+
"above",
|
|
96
|
+
"below",
|
|
97
|
+
"between",
|
|
98
|
+
"under",
|
|
99
|
+
"again",
|
|
100
|
+
"further",
|
|
101
|
+
"then",
|
|
102
|
+
"once",
|
|
103
|
+
"here",
|
|
104
|
+
"there",
|
|
105
|
+
"when",
|
|
106
|
+
"where",
|
|
107
|
+
"why",
|
|
108
|
+
"how",
|
|
109
|
+
"all",
|
|
110
|
+
"each",
|
|
111
|
+
"few",
|
|
112
|
+
"more",
|
|
113
|
+
"most",
|
|
114
|
+
"other",
|
|
115
|
+
"some",
|
|
116
|
+
"such",
|
|
117
|
+
"no",
|
|
118
|
+
"nor",
|
|
119
|
+
"not",
|
|
120
|
+
"only",
|
|
121
|
+
"own",
|
|
122
|
+
"same",
|
|
123
|
+
"so",
|
|
124
|
+
"than",
|
|
125
|
+
"too",
|
|
126
|
+
"very",
|
|
127
|
+
"just",
|
|
128
|
+
"also",
|
|
129
|
+
"now",
|
|
130
|
+
"always",
|
|
131
|
+
"never",
|
|
132
|
+
"often",
|
|
133
|
+
"still",
|
|
134
|
+
"already",
|
|
135
|
+
"ever",
|
|
136
|
+
"it",
|
|
137
|
+
"its",
|
|
138
|
+
"this",
|
|
139
|
+
"that",
|
|
140
|
+
"these",
|
|
141
|
+
"those",
|
|
142
|
+
"i",
|
|
143
|
+
"you",
|
|
144
|
+
"he",
|
|
145
|
+
"she",
|
|
146
|
+
"we",
|
|
147
|
+
"they",
|
|
148
|
+
"what",
|
|
149
|
+
"which",
|
|
150
|
+
"who",
|
|
151
|
+
"whom",
|
|
152
|
+
"whose",
|
|
153
|
+
}
|
|
154
|
+
)
|
|
63
155
|
|
|
64
156
|
# Common synonyms for normalization
|
|
65
157
|
SYNONYMS: Final[dict[str, str]] = {
|
|
@@ -249,7 +341,7 @@ class SentenceTransformerBackend(EmbeddingBackend):
|
|
|
249
341
|
"Install with: pip install buildlog[embeddings]"
|
|
250
342
|
) from e
|
|
251
343
|
|
|
252
|
-
self._model = SentenceTransformer(self._model_name)
|
|
344
|
+
self._model = SentenceTransformer(self._model_name) # type: ignore[assignment]
|
|
253
345
|
|
|
254
346
|
return self._model
|
|
255
347
|
|
|
@@ -298,7 +390,7 @@ class OpenAIBackend(EmbeddingBackend):
|
|
|
298
390
|
"Install with: pip install openai"
|
|
299
391
|
) from e
|
|
300
392
|
|
|
301
|
-
self._client = openai.OpenAI()
|
|
393
|
+
self._client = openai.OpenAI() # type: ignore[assignment]
|
|
302
394
|
|
|
303
395
|
return self._client
|
|
304
396
|
|
buildlog/mcp/tools.py
CHANGED
|
@@ -39,17 +39,17 @@ def buildlog_status(
|
|
|
39
39
|
|
|
40
40
|
def buildlog_promote(
|
|
41
41
|
skill_ids: list[str],
|
|
42
|
-
target: Literal["claude_md", "settings_json"] = "claude_md",
|
|
42
|
+
target: Literal["claude_md", "settings_json", "skill"] = "claude_md",
|
|
43
43
|
buildlog_dir: str = "buildlog",
|
|
44
44
|
) -> dict:
|
|
45
45
|
"""Promote skills to your agent's rules.
|
|
46
46
|
|
|
47
|
-
Writes selected skills to CLAUDE.md
|
|
48
|
-
|
|
47
|
+
Writes selected skills to CLAUDE.md, .claude/settings.json, or
|
|
48
|
+
.claude/skills/buildlog-learned/SKILL.md (Anthropic Agent Skills format).
|
|
49
49
|
|
|
50
50
|
Args:
|
|
51
51
|
skill_ids: List of skill IDs to promote (e.g., ["arch-b0fcb62a1e"])
|
|
52
|
-
target: Where to write rules ("claude_md" or "
|
|
52
|
+
target: Where to write rules ("claude_md", "settings_json", or "skill")
|
|
53
53
|
buildlog_dir: Path to buildlog directory
|
|
54
54
|
|
|
55
55
|
Returns:
|
buildlog/render/__init__.py
CHANGED
|
@@ -3,39 +3,62 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Literal
|
|
6
|
+
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
10
|
from buildlog.render.settings_json import SettingsJsonRenderer
|
|
11
|
+
from buildlog.render.skill import SkillRenderer
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from typing import Any
|
|
11
15
|
|
|
12
16
|
__all__ = [
|
|
13
17
|
"RenderTarget",
|
|
14
18
|
"ClaudeMdRenderer",
|
|
15
19
|
"SettingsJsonRenderer",
|
|
20
|
+
"SkillRenderer",
|
|
16
21
|
"get_renderer",
|
|
22
|
+
"RENDERERS",
|
|
17
23
|
]
|
|
18
24
|
|
|
25
|
+
# Registry of available renderers
|
|
26
|
+
# Using RenderTarget Protocol allows easy extension without modifying types
|
|
27
|
+
RENDERERS: dict[str, type[RenderTarget]] = {
|
|
28
|
+
"claude_md": ClaudeMdRenderer,
|
|
29
|
+
"settings_json": SettingsJsonRenderer,
|
|
30
|
+
"skill": SkillRenderer,
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
|
|
20
34
|
def get_renderer(
|
|
21
|
-
target: Literal["claude_md", "settings_json"],
|
|
35
|
+
target: Literal["claude_md", "settings_json", "skill"],
|
|
22
36
|
path: Path | None = None,
|
|
23
|
-
|
|
37
|
+
**kwargs: Any,
|
|
38
|
+
) -> RenderTarget:
|
|
24
39
|
"""Get renderer for target.
|
|
25
40
|
|
|
26
41
|
Args:
|
|
27
|
-
target: Target format - "claude_md" or "
|
|
42
|
+
target: Target format - "claude_md", "settings_json", or "skill".
|
|
28
43
|
path: Optional custom path for the target file.
|
|
44
|
+
**kwargs: Additional arguments passed to the renderer constructor.
|
|
45
|
+
Common kwargs (accepted by all renderers):
|
|
46
|
+
- tracking_path: Path to promoted.json for tracking promoted IDs.
|
|
47
|
+
Skill-specific kwargs:
|
|
48
|
+
- skill_name: Name of the skill directory (default: "buildlog-learned").
|
|
49
|
+
Must not contain path separators.
|
|
29
50
|
|
|
30
51
|
Returns:
|
|
31
|
-
Renderer instance.
|
|
52
|
+
Renderer instance implementing RenderTarget protocol.
|
|
32
53
|
|
|
33
54
|
Raises:
|
|
34
55
|
ValueError: If target is not recognized.
|
|
35
56
|
"""
|
|
36
|
-
if target
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
if target not in RENDERERS:
|
|
58
|
+
available = ", ".join(f"'{k}'" for k in RENDERERS.keys())
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Unknown render target: '{target}'. Must be one of: {available}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
renderer_cls = RENDERERS[target]
|
|
64
|
+
return renderer_cls(path=path, **kwargs) # type: ignore[call-arg]
|
buildlog/render/claude_md.py
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import json
|
|
6
5
|
from datetime import datetime
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
from typing import TYPE_CHECKING
|
|
9
8
|
|
|
9
|
+
from buildlog.render.tracking import track_promoted
|
|
10
10
|
from buildlog.skills import _to_imperative
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -79,28 +79,7 @@ class ClaudeMdRenderer:
|
|
|
79
79
|
else:
|
|
80
80
|
self.path.write_text(content)
|
|
81
81
|
|
|
82
|
-
# Track promoted skill IDs
|
|
83
|
-
self.
|
|
82
|
+
# Track promoted skill IDs using shared utility
|
|
83
|
+
track_promoted(skills, self.tracking_path)
|
|
84
84
|
|
|
85
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))
|
buildlog/render/settings_json.py
CHANGED
|
@@ -7,6 +7,7 @@ from datetime import datetime
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import TYPE_CHECKING
|
|
9
9
|
|
|
10
|
+
from buildlog.render.tracking import track_promoted
|
|
10
11
|
from buildlog.skills import _to_imperative
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
@@ -69,28 +70,7 @@ class SettingsJsonRenderer:
|
|
|
69
70
|
# Write back
|
|
70
71
|
self.path.write_text(json.dumps(settings, indent=2))
|
|
71
72
|
|
|
72
|
-
# Track promoted skill IDs
|
|
73
|
-
self.
|
|
73
|
+
# Track promoted skill IDs using shared utility
|
|
74
|
+
track_promoted(skills, self.tracking_path)
|
|
74
75
|
|
|
75
76
|
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))
|
buildlog/render/skill.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Render skills to Anthropic Agent Skills format.
|
|
2
|
+
|
|
3
|
+
Creates .claude/skills/buildlog-learned/SKILL.md that can be loaded
|
|
4
|
+
on-demand by Claude Code and other Anthropic tools.
|
|
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 track_promoted
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from buildlog.skills import Skill
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SkillRenderer:
|
|
20
|
+
"""Creates .claude/skills/buildlog-learned/SKILL.md
|
|
21
|
+
|
|
22
|
+
This renderer produces Anthropic Agent Skills format, which allows
|
|
23
|
+
for on-demand loading of project-specific patterns by Claude.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
path: Path | None = None,
|
|
29
|
+
tracking_path: Path | None = None,
|
|
30
|
+
skill_name: str = "buildlog-learned",
|
|
31
|
+
):
|
|
32
|
+
"""Initialize renderer.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
path: Path to SKILL.md file. Defaults to .claude/skills/{skill_name}/SKILL.md.
|
|
36
|
+
tracking_path: Path to promoted.json tracking file.
|
|
37
|
+
Defaults to .buildlog/promoted.json.
|
|
38
|
+
skill_name: Name of the skill directory. Defaults to "buildlog-learned".
|
|
39
|
+
Must not contain path separators or parent references.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If skill_name contains path traversal characters.
|
|
43
|
+
"""
|
|
44
|
+
# Security: Validate skill_name to prevent path traversal
|
|
45
|
+
if "/" in skill_name or "\\" in skill_name or ".." in skill_name:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Invalid skill_name: {skill_name!r}. "
|
|
48
|
+
"Must not contain path separators or '..'."
|
|
49
|
+
)
|
|
50
|
+
self.skill_name = skill_name
|
|
51
|
+
self.path = path or Path(f".claude/skills/{skill_name}/SKILL.md")
|
|
52
|
+
self.tracking_path = tracking_path or Path(".buildlog/promoted.json")
|
|
53
|
+
|
|
54
|
+
def render(self, skills: list[Skill]) -> str:
|
|
55
|
+
"""Render skills to SKILL.md format.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
skills: List of skills to render.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Confirmation message describing what was written.
|
|
62
|
+
"""
|
|
63
|
+
if not skills:
|
|
64
|
+
return "No skills to promote"
|
|
65
|
+
|
|
66
|
+
# Group by confidence, then category
|
|
67
|
+
by_confidence: dict[str, dict[str, list[Skill]]] = {
|
|
68
|
+
"high": {},
|
|
69
|
+
"medium": {},
|
|
70
|
+
"low": {},
|
|
71
|
+
}
|
|
72
|
+
for skill in skills:
|
|
73
|
+
conf = skill.confidence
|
|
74
|
+
cat = skill.category
|
|
75
|
+
by_confidence[conf].setdefault(cat, []).append(skill)
|
|
76
|
+
|
|
77
|
+
# Build SKILL.md content
|
|
78
|
+
categories = sorted(set(s.category for s in skills))
|
|
79
|
+
category_display = ", ".join(self._category_title(c) for c in categories)
|
|
80
|
+
|
|
81
|
+
lines = [
|
|
82
|
+
"---",
|
|
83
|
+
f"name: {self.skill_name}",
|
|
84
|
+
f"description: Project-specific patterns learned from development history. "
|
|
85
|
+
f"Use when writing code, making architectural decisions, reviewing PRs, "
|
|
86
|
+
f"or ensuring consistency. Contains {len(skills)} rules across "
|
|
87
|
+
f"{category_display}.",
|
|
88
|
+
"---",
|
|
89
|
+
"",
|
|
90
|
+
"# Learned Patterns",
|
|
91
|
+
"",
|
|
92
|
+
f"*{len(skills)} rules extracted from buildlog entries on "
|
|
93
|
+
f"{datetime.now().strftime('%Y-%m-%d')}*",
|
|
94
|
+
"",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# High confidence = Must Follow
|
|
98
|
+
if by_confidence["high"]:
|
|
99
|
+
lines.extend(
|
|
100
|
+
self._render_confidence_section(
|
|
101
|
+
"Must Follow (High Confidence)",
|
|
102
|
+
"These patterns have been reinforced multiple times.",
|
|
103
|
+
by_confidence["high"],
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Medium confidence = Should Consider
|
|
108
|
+
if by_confidence["medium"]:
|
|
109
|
+
lines.extend(
|
|
110
|
+
self._render_confidence_section(
|
|
111
|
+
"Should Consider (Medium Confidence)",
|
|
112
|
+
"These patterns appear frequently but may have exceptions.",
|
|
113
|
+
by_confidence["medium"],
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Low confidence = Worth Knowing
|
|
118
|
+
if by_confidence["low"]:
|
|
119
|
+
lines.extend(
|
|
120
|
+
self._render_confidence_section(
|
|
121
|
+
"Worth Knowing (Low Confidence)",
|
|
122
|
+
"Emerging patterns worth being aware of.",
|
|
123
|
+
by_confidence["low"],
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
content = "\n".join(lines)
|
|
128
|
+
|
|
129
|
+
# Write file
|
|
130
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
self.path.write_text(content)
|
|
132
|
+
|
|
133
|
+
# Track promoted using shared utility
|
|
134
|
+
track_promoted(skills, self.tracking_path)
|
|
135
|
+
|
|
136
|
+
return f"Created skill at {self.path}"
|
|
137
|
+
|
|
138
|
+
def _category_title(self, category: str) -> str:
|
|
139
|
+
"""Convert category slug to display title."""
|
|
140
|
+
titles = {
|
|
141
|
+
"architectural": "Architectural",
|
|
142
|
+
"workflow": "Workflow",
|
|
143
|
+
"tool_usage": "Tool Usage",
|
|
144
|
+
"domain_knowledge": "Domain Knowledge",
|
|
145
|
+
}
|
|
146
|
+
return titles.get(category, category.replace("_", " ").title())
|
|
147
|
+
|
|
148
|
+
def _render_confidence_section(
|
|
149
|
+
self,
|
|
150
|
+
title: str,
|
|
151
|
+
description: str,
|
|
152
|
+
by_category: dict[str, list[Skill]],
|
|
153
|
+
) -> list[str]:
|
|
154
|
+
"""Render a confidence-level section.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
title: Section title (e.g., "Must Follow (High Confidence)").
|
|
158
|
+
description: Description of what this confidence level means.
|
|
159
|
+
by_category: Skills grouped by category.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of markdown lines for this section.
|
|
163
|
+
"""
|
|
164
|
+
lines = [f"## {title}", "", description, ""]
|
|
165
|
+
|
|
166
|
+
for category, cat_skills in sorted(by_category.items()):
|
|
167
|
+
cat_title = self._category_title(category)
|
|
168
|
+
lines.append(f"### {cat_title}")
|
|
169
|
+
lines.append("")
|
|
170
|
+
for skill in cat_skills:
|
|
171
|
+
# Don't add confidence prefix - section already indicates confidence
|
|
172
|
+
lines.append(f"- {skill.rule}")
|
|
173
|
+
lines.append("")
|
|
174
|
+
|
|
175
|
+
return lines
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Shared tracking utilities for render adapters."""
|
|
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
|
+
if TYPE_CHECKING:
|
|
11
|
+
from buildlog.skills import Skill
|
|
12
|
+
|
|
13
|
+
__all__ = ["track_promoted"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def track_promoted(skills: list[Skill], tracking_path: Path) -> None:
|
|
17
|
+
"""Track which skills have been promoted.
|
|
18
|
+
|
|
19
|
+
Writes skill IDs and promotion timestamps to a JSON file.
|
|
20
|
+
Handles corrupt JSON gracefully by starting fresh.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
skills: Skills that were promoted.
|
|
24
|
+
tracking_path: Path to the tracking JSON file.
|
|
25
|
+
"""
|
|
26
|
+
tracking_path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
# Load existing tracking data (handle corrupt JSON)
|
|
29
|
+
tracking: dict = {"skill_ids": [], "promoted_at": {}}
|
|
30
|
+
if tracking_path.exists():
|
|
31
|
+
try:
|
|
32
|
+
tracking = json.loads(tracking_path.read_text())
|
|
33
|
+
except json.JSONDecodeError:
|
|
34
|
+
pass # Start fresh if corrupted
|
|
35
|
+
|
|
36
|
+
# Add new skill IDs
|
|
37
|
+
now = datetime.now().isoformat()
|
|
38
|
+
for skill in skills:
|
|
39
|
+
if skill.id not in tracking["skill_ids"]:
|
|
40
|
+
tracking["skill_ids"].append(skill.id)
|
|
41
|
+
tracking["promoted_at"][skill.id] = now
|
|
42
|
+
|
|
43
|
+
tracking_path.write_text(json.dumps(tracking, indent=2))
|