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/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/confidence.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Confidence scoring for rules and patterns.
|
|
2
|
+
|
|
3
|
+
Confidence represents structural inertia - how hard it would be for the system
|
|
4
|
+
to stop believing a rule. It reflects accumulated mass from reinforcement,
|
|
5
|
+
not objective correctness.
|
|
6
|
+
|
|
7
|
+
A rule gains mass when:
|
|
8
|
+
- It shows up again (frequency)
|
|
9
|
+
- It shows up recently (recency)
|
|
10
|
+
- It survives contradictions
|
|
11
|
+
|
|
12
|
+
A rule loses mass when:
|
|
13
|
+
- It's unused (time decay)
|
|
14
|
+
- It's contradicted
|
|
15
|
+
- It's contextually bypassed
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from typing import TypedDict
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"ConfidenceTier",
|
|
28
|
+
"ConfidenceConfig",
|
|
29
|
+
"ConfidenceMetrics",
|
|
30
|
+
"ConfidenceMetricsDict",
|
|
31
|
+
"calculate_confidence",
|
|
32
|
+
"get_confidence_tier",
|
|
33
|
+
"merge_confidence_metrics",
|
|
34
|
+
"add_contradiction",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ConfidenceTier(str, Enum):
|
|
39
|
+
"""Descriptive tiers for rule confidence.
|
|
40
|
+
|
|
41
|
+
These are purely descriptive labels for human interpretation.
|
|
42
|
+
No logic gates or hard thresholds are enforced by the system.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
SPECULATIVE = "speculative" # Low mass, recently introduced
|
|
46
|
+
PROVISIONAL = "provisional" # Growing mass, some reinforcement
|
|
47
|
+
STABLE = "stable" # Consistent reinforcement, moderate mass
|
|
48
|
+
ENTRENCHED = "entrenched" # High mass, sustained over time
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class ConfidenceConfig:
|
|
53
|
+
"""Configuration parameters for confidence calculation.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
tau: Half-life for recency decay (in days). Smaller = twitchier system.
|
|
57
|
+
k: Saturation constant for frequency. Larger = slower saturation.
|
|
58
|
+
lambda_: Decay constant for contradiction penalty.
|
|
59
|
+
tier_thresholds: Confidence score thresholds for each tier.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
tau: float = 30.0 # 30-day half-life by default
|
|
63
|
+
k: float = 5.0 # Frequency saturation constant
|
|
64
|
+
lambda_: float = 2.0 # Contradiction decay constant
|
|
65
|
+
tier_thresholds: tuple[float, float, float] = (0.2, 0.4, 0.7)
|
|
66
|
+
|
|
67
|
+
def __post_init__(self) -> None:
|
|
68
|
+
if self.tau <= 0:
|
|
69
|
+
raise ValueError("tau must be positive")
|
|
70
|
+
if self.k <= 0:
|
|
71
|
+
raise ValueError("k must be positive")
|
|
72
|
+
if self.lambda_ <= 0:
|
|
73
|
+
raise ValueError("lambda_ must be positive")
|
|
74
|
+
low, mid, high = self.tier_thresholds
|
|
75
|
+
if not (0 <= low <= mid <= high <= 1):
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"tier_thresholds must be monotonically increasing in [0, 1]"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ConfidenceMetricsDict(TypedDict):
|
|
82
|
+
"""Serializable form of confidence metrics."""
|
|
83
|
+
|
|
84
|
+
reinforcement_count: int
|
|
85
|
+
last_reinforced: str # ISO format timestamp
|
|
86
|
+
contradiction_count: int
|
|
87
|
+
first_seen: str # ISO format timestamp
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class ConfidenceMetrics:
|
|
92
|
+
"""Tracked metrics for confidence calculation.
|
|
93
|
+
|
|
94
|
+
These are the raw inputs that feed into the confidence formula.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
reinforcement_count: int = 1
|
|
98
|
+
last_reinforced: datetime = field(
|
|
99
|
+
default_factory=lambda: datetime.now(timezone.utc)
|
|
100
|
+
)
|
|
101
|
+
contradiction_count: int = 0
|
|
102
|
+
first_seen: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
103
|
+
|
|
104
|
+
def __post_init__(self) -> None:
|
|
105
|
+
if self.reinforcement_count < 0:
|
|
106
|
+
raise ValueError("reinforcement_count must be non-negative")
|
|
107
|
+
if self.contradiction_count < 0:
|
|
108
|
+
raise ValueError("contradiction_count must be non-negative")
|
|
109
|
+
|
|
110
|
+
def to_dict(self) -> ConfidenceMetricsDict:
|
|
111
|
+
"""Convert to serializable dictionary."""
|
|
112
|
+
return {
|
|
113
|
+
"reinforcement_count": self.reinforcement_count,
|
|
114
|
+
"last_reinforced": self.last_reinforced.isoformat(),
|
|
115
|
+
"contradiction_count": self.contradiction_count,
|
|
116
|
+
"first_seen": self.first_seen.isoformat(),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_dict(cls, data: ConfidenceMetricsDict) -> ConfidenceMetrics:
|
|
121
|
+
"""Reconstruct from serialized dictionary.
|
|
122
|
+
|
|
123
|
+
Note: Timezone-naive datetimes are assumed to be UTC.
|
|
124
|
+
"""
|
|
125
|
+
last_reinforced = datetime.fromisoformat(data["last_reinforced"])
|
|
126
|
+
first_seen = datetime.fromisoformat(data["first_seen"])
|
|
127
|
+
|
|
128
|
+
# Ensure timezone awareness (assume UTC for naive datetimes)
|
|
129
|
+
if last_reinforced.tzinfo is None:
|
|
130
|
+
last_reinforced = last_reinforced.replace(tzinfo=timezone.utc)
|
|
131
|
+
if first_seen.tzinfo is None:
|
|
132
|
+
first_seen = first_seen.replace(tzinfo=timezone.utc)
|
|
133
|
+
|
|
134
|
+
return cls(
|
|
135
|
+
reinforcement_count=data["reinforcement_count"],
|
|
136
|
+
last_reinforced=last_reinforced,
|
|
137
|
+
contradiction_count=data["contradiction_count"],
|
|
138
|
+
first_seen=first_seen,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def calculate_frequency_weight(n: int, k: float) -> float:
|
|
143
|
+
"""Calculate frequency weight with saturation.
|
|
144
|
+
|
|
145
|
+
Uses bounded exponential: 1 - exp(-n/k)
|
|
146
|
+
This makes early reinforcement matter more than late spam.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
n: Reinforcement count
|
|
150
|
+
k: Saturation constant (larger = slower saturation)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Weight in range (0, 1), approaching 1 as n grows
|
|
154
|
+
"""
|
|
155
|
+
return 1.0 - math.exp(-n / k)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def calculate_recency_weight(
|
|
159
|
+
t_last: datetime,
|
|
160
|
+
t_now: datetime,
|
|
161
|
+
tau: float,
|
|
162
|
+
) -> float:
|
|
163
|
+
"""Calculate recency weight with exponential decay.
|
|
164
|
+
|
|
165
|
+
Uses: exp(-(t_now - t_last) / tau)
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
t_last: Timestamp of last reinforcement
|
|
169
|
+
t_now: Current timestamp
|
|
170
|
+
tau: Half-life in days
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Weight in range (0, 1], decaying over time.
|
|
174
|
+
If t_last is in the future, clamps to 1.0.
|
|
175
|
+
"""
|
|
176
|
+
days_elapsed = (t_now - t_last).total_seconds() / (24 * 60 * 60)
|
|
177
|
+
if days_elapsed < 0:
|
|
178
|
+
return 1.0 # Future timestamps treated as "just now"
|
|
179
|
+
return math.exp(-days_elapsed / tau)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def calculate_contradiction_penalty(c: int, lambda_: float) -> float:
|
|
183
|
+
"""Calculate contradiction penalty (drag).
|
|
184
|
+
|
|
185
|
+
Rules don't die from contradictions, they get heavy and sink.
|
|
186
|
+
Uses: exp(-c / lambda)
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
c: Contradiction count
|
|
190
|
+
lambda_: Decay constant
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Penalty multiplier in range (0, 1]
|
|
194
|
+
"""
|
|
195
|
+
return math.exp(-c / lambda_)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def calculate_confidence(
|
|
199
|
+
metrics: ConfidenceMetrics,
|
|
200
|
+
config: ConfidenceConfig | None = None,
|
|
201
|
+
t_now: datetime | None = None,
|
|
202
|
+
) -> float:
|
|
203
|
+
"""Calculate confidence score for a rule.
|
|
204
|
+
|
|
205
|
+
Confidence = frequency_weight * recency_weight * contradiction_penalty
|
|
206
|
+
|
|
207
|
+
This gives a scalar that:
|
|
208
|
+
- Rises fast early
|
|
209
|
+
- Decays naturally over time
|
|
210
|
+
- Never quite hits zero
|
|
211
|
+
- Never explodes to infinity
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
metrics: Tracked metrics for the rule
|
|
215
|
+
config: Scoring configuration (uses defaults if None)
|
|
216
|
+
t_now: Current time (uses now if None)
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Confidence score in range (0, 1)
|
|
220
|
+
"""
|
|
221
|
+
if config is None:
|
|
222
|
+
config = ConfidenceConfig()
|
|
223
|
+
if t_now is None:
|
|
224
|
+
t_now = datetime.now(timezone.utc)
|
|
225
|
+
|
|
226
|
+
freq = calculate_frequency_weight(metrics.reinforcement_count, config.k)
|
|
227
|
+
recency = calculate_recency_weight(metrics.last_reinforced, t_now, config.tau)
|
|
228
|
+
penalty = calculate_contradiction_penalty(
|
|
229
|
+
metrics.contradiction_count, config.lambda_
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return freq * recency * penalty
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_confidence_tier(
|
|
236
|
+
score: float,
|
|
237
|
+
config: ConfidenceConfig | None = None,
|
|
238
|
+
) -> ConfidenceTier:
|
|
239
|
+
"""Map confidence score to descriptive tier.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
score: Confidence score in range [0, 1]
|
|
243
|
+
config: Configuration with tier thresholds
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Descriptive tier label
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
ValueError: If score is outside [0, 1] range
|
|
250
|
+
"""
|
|
251
|
+
if not (0.0 <= score <= 1.0):
|
|
252
|
+
raise ValueError(f"score must be in [0, 1], got {score}")
|
|
253
|
+
|
|
254
|
+
if config is None:
|
|
255
|
+
config = ConfidenceConfig()
|
|
256
|
+
|
|
257
|
+
low, mid, high = config.tier_thresholds
|
|
258
|
+
|
|
259
|
+
if score < low:
|
|
260
|
+
return ConfidenceTier.SPECULATIVE
|
|
261
|
+
elif score < mid:
|
|
262
|
+
return ConfidenceTier.PROVISIONAL
|
|
263
|
+
elif score < high:
|
|
264
|
+
return ConfidenceTier.STABLE
|
|
265
|
+
else:
|
|
266
|
+
return ConfidenceTier.ENTRENCHED
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def merge_confidence_metrics(
|
|
270
|
+
existing: ConfidenceMetrics,
|
|
271
|
+
new_occurrence: datetime | None = None,
|
|
272
|
+
) -> ConfidenceMetrics:
|
|
273
|
+
"""Merge a new occurrence into existing metrics.
|
|
274
|
+
|
|
275
|
+
This is called when a rule is reinforced (seen again).
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
existing: Current metrics for the rule
|
|
279
|
+
new_occurrence: Timestamp of new occurrence (uses now if None)
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Updated metrics with incremented count and updated timestamp
|
|
283
|
+
"""
|
|
284
|
+
if new_occurrence is None:
|
|
285
|
+
new_occurrence = datetime.now(timezone.utc)
|
|
286
|
+
|
|
287
|
+
return ConfidenceMetrics(
|
|
288
|
+
reinforcement_count=existing.reinforcement_count + 1,
|
|
289
|
+
last_reinforced=new_occurrence,
|
|
290
|
+
contradiction_count=existing.contradiction_count,
|
|
291
|
+
first_seen=existing.first_seen,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def add_contradiction(metrics: ConfidenceMetrics) -> ConfidenceMetrics:
|
|
296
|
+
"""Record a contradiction against a rule.
|
|
297
|
+
|
|
298
|
+
Contradictions add drag but don't invalidate rules.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
metrics: Current metrics for the rule
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Updated metrics with incremented contradiction count
|
|
305
|
+
"""
|
|
306
|
+
return ConfidenceMetrics(
|
|
307
|
+
reinforcement_count=metrics.reinforcement_count,
|
|
308
|
+
last_reinforced=metrics.last_reinforced,
|
|
309
|
+
contradiction_count=metrics.contradiction_count + 1,
|
|
310
|
+
first_seen=metrics.first_seen,
|
|
311
|
+
)
|
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,
|