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.
Files changed (27) hide show
  1. buildlog/cli.py +46 -23
  2. buildlog/confidence.py +311 -0
  3. buildlog/core/operations.py +11 -15
  4. buildlog/distill.py +3 -3
  5. buildlog/embeddings.py +108 -16
  6. buildlog/mcp/tools.py +4 -4
  7. buildlog/render/__init__.py +34 -11
  8. buildlog/render/claude_md.py +3 -24
  9. buildlog/render/settings_json.py +3 -23
  10. buildlog/render/skill.py +175 -0
  11. buildlog/render/tracking.py +43 -0
  12. buildlog/skills.py +229 -47
  13. buildlog/stats.py +7 -5
  14. {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/post_gen.py +11 -7
  15. buildlog-0.3.0.dist-info/METADATA +763 -0
  16. buildlog-0.3.0.dist-info/RECORD +30 -0
  17. buildlog-0.1.0.dist-info/METADATA +0 -664
  18. buildlog-0.1.0.dist-info/RECORD +0 -27
  19. {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/copier.yml +0 -0
  20. {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
  21. {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
  22. {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
  23. {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
  24. {buildlog-0.1.0.data → buildlog-0.3.0.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
  25. {buildlog-0.1.0.dist-info → buildlog-0.3.0.dist-info}/WHEEL +0 -0
  26. {buildlog-0.1.0.dist-info → buildlog-0.3.0.dist-info}/entry_points.txt +0 -0
  27. {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 generate_skills, format_skills
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, "-m", "copier", "copy",
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, "-m", "copier", "copy",
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("--date", "-d", "entry_date", default=None, help="Date for entry (YYYY-MM-DD)")
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("No _TEMPLATE.md found in buildlog/. Run 'buildlog init' first.", err=True)
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 = first_line.replace("# Build Journal: ", "").replace("# ", "").strip()
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("Failed to update. Try running 'copier update' directly.", err=True)
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("Failed to update. Try running 'copier update' directly.", err=True)
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(f"Wrote {result.statistics.get('total_patterns', 0)} patterns to {output_path}")
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("--detailed", is_flag=True, help="Show detailed breakdown including top sources")
297
- @click.option("--since", "since_date", default=None, help="Only include entries since date (YYYY-MM-DD)")
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
+ )
@@ -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 settings_json)."""
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 "settings_json").
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 with custom paths
281
- if target == "claude_md":
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() for s in skill_list
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 UTC, date, datetime
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(UTC).isoformat().replace("+00:00", "Z"),
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,