buildlog 0.1.0__py3-none-any.whl → 0.2.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/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 +118 -37
- buildlog/stats.py +7 -5
- buildlog-0.2.0.dist-info/METADATA +762 -0
- buildlog-0.2.0.dist-info/RECORD +29 -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.2.0.data}/data/share/buildlog/copier.yml +0 -0
- {buildlog-0.1.0.data → buildlog-0.2.0.data}/data/share/buildlog/post_gen.py +0 -0
- {buildlog-0.1.0.data → buildlog-0.2.0.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
- {buildlog-0.1.0.data → buildlog-0.2.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
- {buildlog-0.1.0.data → buildlog-0.2.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
- {buildlog-0.1.0.data → buildlog-0.2.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
- {buildlog-0.1.0.data → buildlog-0.2.0.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
- {buildlog-0.1.0.dist-info → buildlog-0.2.0.dist-info}/WHEEL +0 -0
- {buildlog-0.1.0.dist-info → buildlog-0.2.0.dist-info}/entry_points.txt +0 -0
- {buildlog-0.1.0.dist-info → buildlog-0.2.0.dist-info}/licenses/LICENSE +0 -0
buildlog/cli.py
CHANGED
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
import click
|
|
10
10
|
|
|
11
11
|
from buildlog.distill import CATEGORIES, distill_all, format_output
|
|
12
|
-
from buildlog.skills import
|
|
12
|
+
from buildlog.skills import format_skills, generate_skills
|
|
13
13
|
from buildlog.stats import calculate_stats, format_dashboard, format_json
|
|
14
14
|
|
|
15
15
|
|
|
@@ -27,6 +27,7 @@ def get_template_dir() -> Path | None:
|
|
|
27
27
|
|
|
28
28
|
# 2. Check installed location (site-packages/../share/buildlog)
|
|
29
29
|
import sysconfig
|
|
30
|
+
|
|
30
31
|
data_dir = Path(sysconfig.get_path("data")) / "share" / "buildlog"
|
|
31
32
|
if (data_dir / "copier.yml").exists():
|
|
32
33
|
return data_dir
|
|
@@ -67,13 +68,16 @@ def init(no_claude_md: bool):
|
|
|
67
68
|
try:
|
|
68
69
|
subprocess.run(
|
|
69
70
|
[
|
|
70
|
-
sys.executable,
|
|
71
|
+
sys.executable,
|
|
72
|
+
"-m",
|
|
73
|
+
"copier",
|
|
74
|
+
"copy",
|
|
71
75
|
"--trust",
|
|
72
76
|
*(["--data", "update_claude_md=false"] if no_claude_md else []),
|
|
73
77
|
str(template_dir),
|
|
74
|
-
"."
|
|
78
|
+
".",
|
|
75
79
|
],
|
|
76
|
-
check=True
|
|
80
|
+
check=True,
|
|
77
81
|
)
|
|
78
82
|
except subprocess.CalledProcessError:
|
|
79
83
|
click.echo("Failed to initialize buildlog.", err=True)
|
|
@@ -84,13 +88,16 @@ def init(no_claude_md: bool):
|
|
|
84
88
|
try:
|
|
85
89
|
subprocess.run(
|
|
86
90
|
[
|
|
87
|
-
sys.executable,
|
|
91
|
+
sys.executable,
|
|
92
|
+
"-m",
|
|
93
|
+
"copier",
|
|
94
|
+
"copy",
|
|
88
95
|
"--trust",
|
|
89
96
|
*(["--data", "update_claude_md=false"] if no_claude_md else []),
|
|
90
97
|
"gh:Peleke/buildlog-template",
|
|
91
|
-
"."
|
|
98
|
+
".",
|
|
92
99
|
],
|
|
93
|
-
check=True
|
|
100
|
+
check=True,
|
|
94
101
|
)
|
|
95
102
|
except subprocess.CalledProcessError:
|
|
96
103
|
click.echo("Failed to initialize buildlog.", err=True)
|
|
@@ -102,7 +109,9 @@ def init(no_claude_md: bool):
|
|
|
102
109
|
|
|
103
110
|
@main.command()
|
|
104
111
|
@click.argument("slug")
|
|
105
|
-
@click.option(
|
|
112
|
+
@click.option(
|
|
113
|
+
"--date", "-d", "entry_date", default=None, help="Date for entry (YYYY-MM-DD)"
|
|
114
|
+
)
|
|
106
115
|
def new(slug: str, entry_date: str | None):
|
|
107
116
|
"""Create a new buildlog entry.
|
|
108
117
|
|
|
@@ -121,7 +130,9 @@ def new(slug: str, entry_date: str | None):
|
|
|
121
130
|
raise SystemExit(1)
|
|
122
131
|
|
|
123
132
|
if not template_file.exists():
|
|
124
|
-
click.echo(
|
|
133
|
+
click.echo(
|
|
134
|
+
"No _TEMPLATE.md found in buildlog/. Run 'buildlog init' first.", err=True
|
|
135
|
+
)
|
|
125
136
|
raise SystemExit(1)
|
|
126
137
|
|
|
127
138
|
# Determine date
|
|
@@ -170,8 +181,7 @@ def list():
|
|
|
170
181
|
raise SystemExit(1)
|
|
171
182
|
|
|
172
183
|
entries = sorted(
|
|
173
|
-
buildlog_dir.glob("20??-??-??-*.md"),
|
|
174
|
-
reverse=True # Most recent first
|
|
184
|
+
buildlog_dir.glob("20??-??-??-*.md"), reverse=True # Most recent first
|
|
175
185
|
)
|
|
176
186
|
|
|
177
187
|
if not entries:
|
|
@@ -183,7 +193,9 @@ def list():
|
|
|
183
193
|
# Extract title from first line if possible
|
|
184
194
|
try:
|
|
185
195
|
first_line = entry.read_text().split("\n")[0]
|
|
186
|
-
title =
|
|
196
|
+
title = (
|
|
197
|
+
first_line.replace("# Build Journal: ", "").replace("# ", "").strip()
|
|
198
|
+
)
|
|
187
199
|
if title == "[TITLE]":
|
|
188
200
|
title = "(untitled)"
|
|
189
201
|
except Exception:
|
|
@@ -202,21 +214,23 @@ def update():
|
|
|
202
214
|
click.echo("Updating from local template...")
|
|
203
215
|
try:
|
|
204
216
|
subprocess.run(
|
|
205
|
-
[sys.executable, "-m", "copier", "update", "--trust"],
|
|
206
|
-
check=True
|
|
217
|
+
[sys.executable, "-m", "copier", "update", "--trust"], check=True
|
|
207
218
|
)
|
|
208
219
|
except subprocess.CalledProcessError:
|
|
209
|
-
click.echo(
|
|
220
|
+
click.echo(
|
|
221
|
+
"Failed to update. Try running 'copier update' directly.", err=True
|
|
222
|
+
)
|
|
210
223
|
raise SystemExit(1)
|
|
211
224
|
else:
|
|
212
225
|
click.echo("Updating from GitHub...")
|
|
213
226
|
try:
|
|
214
227
|
subprocess.run(
|
|
215
|
-
[sys.executable, "-m", "copier", "update", "--trust"],
|
|
216
|
-
check=True
|
|
228
|
+
[sys.executable, "-m", "copier", "update", "--trust"], check=True
|
|
217
229
|
)
|
|
218
230
|
except subprocess.CalledProcessError:
|
|
219
|
-
click.echo(
|
|
231
|
+
click.echo(
|
|
232
|
+
"Failed to update. Try running 'copier update' directly.", err=True
|
|
233
|
+
)
|
|
220
234
|
raise SystemExit(1)
|
|
221
235
|
|
|
222
236
|
click.echo("\n✓ buildlog updated!")
|
|
@@ -273,7 +287,7 @@ def distill(output: str | None, fmt: str, since: datetime | None, category: str
|
|
|
273
287
|
|
|
274
288
|
# Format output
|
|
275
289
|
try:
|
|
276
|
-
formatted = format_output(result, fmt)
|
|
290
|
+
formatted = format_output(result, fmt) # type: ignore[arg-type]
|
|
277
291
|
except ImportError as e:
|
|
278
292
|
click.echo(str(e), err=True)
|
|
279
293
|
raise SystemExit(1)
|
|
@@ -283,7 +297,9 @@ def distill(output: str | None, fmt: str, since: datetime | None, category: str
|
|
|
283
297
|
output_path = Path(output)
|
|
284
298
|
try:
|
|
285
299
|
output_path.write_text(formatted, encoding="utf-8")
|
|
286
|
-
click.echo(
|
|
300
|
+
click.echo(
|
|
301
|
+
f"Wrote {result.statistics.get('total_patterns', 0)} patterns to {output_path}"
|
|
302
|
+
)
|
|
287
303
|
except Exception as e:
|
|
288
304
|
click.echo(f"Failed to write output: {e}", err=True)
|
|
289
305
|
raise SystemExit(1)
|
|
@@ -293,8 +309,15 @@ def distill(output: str | None, fmt: str, since: datetime | None, category: str
|
|
|
293
309
|
|
|
294
310
|
@main.command()
|
|
295
311
|
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
296
|
-
@click.option(
|
|
297
|
-
|
|
312
|
+
@click.option(
|
|
313
|
+
"--detailed", is_flag=True, help="Show detailed breakdown including top sources"
|
|
314
|
+
)
|
|
315
|
+
@click.option(
|
|
316
|
+
"--since",
|
|
317
|
+
"since_date",
|
|
318
|
+
default=None,
|
|
319
|
+
help="Only include entries since date (YYYY-MM-DD)",
|
|
320
|
+
)
|
|
298
321
|
def stats(output_json: bool, detailed: bool, since_date: str | None):
|
|
299
322
|
"""Show buildlog statistics and analytics.
|
|
300
323
|
|
|
@@ -409,7 +432,7 @@ def skills(
|
|
|
409
432
|
|
|
410
433
|
# Format output
|
|
411
434
|
try:
|
|
412
|
-
formatted = format_skills(skill_set, fmt)
|
|
435
|
+
formatted = format_skills(skill_set, fmt) # type: ignore[arg-type]
|
|
413
436
|
except ImportError as e:
|
|
414
437
|
click.echo(str(e), err=True)
|
|
415
438
|
raise SystemExit(1)
|
buildlog/core/operations.py
CHANGED
|
@@ -59,7 +59,7 @@ class PromoteResult:
|
|
|
59
59
|
"""IDs of skills that were promoted."""
|
|
60
60
|
|
|
61
61
|
target: str
|
|
62
|
-
"""Target format (claude_md or
|
|
62
|
+
"""Target format (claude_md, settings_json, or skill)."""
|
|
63
63
|
|
|
64
64
|
rules_added: int
|
|
65
65
|
"""Number of rules added."""
|
|
@@ -192,7 +192,7 @@ def status(
|
|
|
192
192
|
confidence_order = {"low": 0, "medium": 1, "high": 2}
|
|
193
193
|
min_level = confidence_order[min_confidence]
|
|
194
194
|
|
|
195
|
-
filtered: dict[str, list[dict]] = {}
|
|
195
|
+
filtered: dict[str, list[dict]] = {} # type: ignore[type-arg]
|
|
196
196
|
by_confidence = {"high": 0, "medium": 0, "low": 0}
|
|
197
197
|
promotable: list[str] = []
|
|
198
198
|
|
|
@@ -215,7 +215,7 @@ def status(
|
|
|
215
215
|
category_skills.append(skill.to_dict())
|
|
216
216
|
|
|
217
217
|
if category_skills:
|
|
218
|
-
filtered[category] = category_skills
|
|
218
|
+
filtered[category] = category_skills # type: ignore[assignment]
|
|
219
219
|
|
|
220
220
|
# Calculate actual total (sum of by_confidence, which excludes rejected)
|
|
221
221
|
actual_total = sum(by_confidence.values())
|
|
@@ -232,7 +232,7 @@ def status(
|
|
|
232
232
|
def promote(
|
|
233
233
|
buildlog_dir: Path,
|
|
234
234
|
skill_ids: list[str],
|
|
235
|
-
target: Literal["claude_md", "settings_json"] = "claude_md",
|
|
235
|
+
target: Literal["claude_md", "settings_json", "skill"] = "claude_md",
|
|
236
236
|
target_path: Path | None = None,
|
|
237
237
|
) -> PromoteResult:
|
|
238
238
|
"""Promote skills to agent rules.
|
|
@@ -240,7 +240,7 @@ def promote(
|
|
|
240
240
|
Args:
|
|
241
241
|
buildlog_dir: Path to buildlog directory.
|
|
242
242
|
skill_ids: List of skill IDs to promote.
|
|
243
|
-
target: Where to write rules ("claude_md" or "
|
|
243
|
+
target: Where to write rules ("claude_md", "settings_json", or "skill").
|
|
244
244
|
target_path: Optional custom path for the target file.
|
|
245
245
|
|
|
246
246
|
Returns:
|
|
@@ -277,13 +277,8 @@ def promote(
|
|
|
277
277
|
# Set up tracking path in buildlog directory
|
|
278
278
|
tracking_path = _get_promoted_path(buildlog_dir)
|
|
279
279
|
|
|
280
|
-
# Get renderer
|
|
281
|
-
|
|
282
|
-
from buildlog.render.claude_md import ClaudeMdRenderer
|
|
283
|
-
renderer = ClaudeMdRenderer(path=target_path, tracking_path=tracking_path)
|
|
284
|
-
else:
|
|
285
|
-
from buildlog.render.settings_json import SettingsJsonRenderer
|
|
286
|
-
renderer = SettingsJsonRenderer(path=target_path, tracking_path=tracking_path)
|
|
280
|
+
# Get renderer using the registry pattern
|
|
281
|
+
renderer = get_renderer(target, path=target_path, tracking_path=tracking_path)
|
|
287
282
|
|
|
288
283
|
message = renderer.render(found_skills)
|
|
289
284
|
|
|
@@ -372,16 +367,17 @@ def diff(
|
|
|
372
367
|
promoted_ids = _load_json_set(_get_promoted_path(buildlog_dir), "skill_ids")
|
|
373
368
|
|
|
374
369
|
# Find unpromoted, unrejected skills
|
|
375
|
-
pending: dict[str, list[dict]] = {}
|
|
370
|
+
pending: dict[str, list[dict]] = {} # type: ignore[type-arg]
|
|
376
371
|
total_pending = 0
|
|
377
372
|
|
|
378
373
|
for category, skill_list in skill_set.skills.items():
|
|
379
374
|
pending_skills = [
|
|
380
|
-
s.to_dict()
|
|
375
|
+
s.to_dict()
|
|
376
|
+
for s in skill_list
|
|
381
377
|
if s.id not in rejected_ids and s.id not in promoted_ids
|
|
382
378
|
]
|
|
383
379
|
if pending_skills:
|
|
384
|
-
pending[category] = pending_skills
|
|
380
|
+
pending[category] = pending_skills # type: ignore[assignment]
|
|
385
381
|
total_pending += len(pending_skills)
|
|
386
382
|
|
|
387
383
|
return DiffResult(
|
buildlog/distill.py
CHANGED
|
@@ -17,7 +17,7 @@ import logging
|
|
|
17
17
|
import re
|
|
18
18
|
from collections.abc import Iterator
|
|
19
19
|
from dataclasses import dataclass, field
|
|
20
|
-
from datetime import
|
|
20
|
+
from datetime import date, datetime, timezone
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
from typing import Final, Literal, TypedDict
|
|
23
23
|
|
|
@@ -81,7 +81,7 @@ class DistillResult:
|
|
|
81
81
|
extracted_at: str
|
|
82
82
|
entry_count: int
|
|
83
83
|
patterns: dict[str, list[PatternDict]] = field(default_factory=dict)
|
|
84
|
-
statistics: StatisticsDict = field(default_factory=dict)
|
|
84
|
+
statistics: StatisticsDict = field(default_factory=dict) # type: ignore[assignment]
|
|
85
85
|
|
|
86
86
|
def to_dict(self) -> DistillResultDict:
|
|
87
87
|
"""Convert to dictionary for JSON/YAML serialization."""
|
|
@@ -333,7 +333,7 @@ def distill_all(
|
|
|
333
333
|
statistics = _compute_statistics(patterns, by_month)
|
|
334
334
|
|
|
335
335
|
return DistillResult(
|
|
336
|
-
extracted_at=datetime.now(
|
|
336
|
+
extracted_at=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
337
337
|
entry_count=entry_count,
|
|
338
338
|
patterns=patterns,
|
|
339
339
|
statistics=statistics,
|
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))
|