buildlog 0.3.0__py3-none-any.whl → 0.4.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/core/__init__.py +8 -0
- buildlog/core/operations.py +343 -2
- buildlog/mcp/__init__.py +2 -0
- buildlog/mcp/server.py +2 -0
- buildlog/mcp/tools.py +46 -1
- buildlog/skills.py +122 -1
- {buildlog-0.3.0.dist-info → buildlog-0.4.0.dist-info}/METADATA +132 -1
- {buildlog-0.3.0.dist-info → buildlog-0.4.0.dist-info}/RECORD +18 -18
- {buildlog-0.3.0.data → buildlog-0.4.0.data}/data/share/buildlog/copier.yml +0 -0
- {buildlog-0.3.0.data → buildlog-0.4.0.data}/data/share/buildlog/post_gen.py +0 -0
- {buildlog-0.3.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
- {buildlog-0.3.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
- {buildlog-0.3.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
- {buildlog-0.3.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
- {buildlog-0.3.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
- {buildlog-0.3.0.dist-info → buildlog-0.4.0.dist-info}/WHEEL +0 -0
- {buildlog-0.3.0.dist-info → buildlog-0.4.0.dist-info}/entry_points.txt +0 -0
- {buildlog-0.3.0.dist-info → buildlog-0.4.0.dist-info}/licenses/LICENSE +0 -0
buildlog/core/__init__.py
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from buildlog.core.operations import (
|
|
4
4
|
DiffResult,
|
|
5
|
+
LearnFromReviewResult,
|
|
5
6
|
PromoteResult,
|
|
6
7
|
RejectResult,
|
|
8
|
+
ReviewIssue,
|
|
9
|
+
ReviewLearning,
|
|
7
10
|
StatusResult,
|
|
8
11
|
diff,
|
|
9
12
|
find_skills_by_ids,
|
|
13
|
+
learn_from_review,
|
|
10
14
|
promote,
|
|
11
15
|
reject,
|
|
12
16
|
status,
|
|
@@ -17,9 +21,13 @@ __all__ = [
|
|
|
17
21
|
"PromoteResult",
|
|
18
22
|
"RejectResult",
|
|
19
23
|
"DiffResult",
|
|
24
|
+
"ReviewIssue",
|
|
25
|
+
"ReviewLearning",
|
|
26
|
+
"LearnFromReviewResult",
|
|
20
27
|
"status",
|
|
21
28
|
"promote",
|
|
22
29
|
"reject",
|
|
23
30
|
"diff",
|
|
24
31
|
"find_skills_by_ids",
|
|
32
|
+
"learn_from_review",
|
|
25
33
|
]
|
buildlog/core/operations.py
CHANGED
|
@@ -6,12 +6,14 @@ MCP, CLI, HTTP, or any other interface.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import hashlib
|
|
9
10
|
import json
|
|
10
11
|
from dataclasses import dataclass, field
|
|
11
|
-
from datetime import datetime
|
|
12
|
+
from datetime import datetime, timezone
|
|
12
13
|
from pathlib import Path
|
|
13
|
-
from typing import Literal
|
|
14
|
+
from typing import Literal, TypedDict
|
|
14
15
|
|
|
16
|
+
from buildlog.confidence import ConfidenceMetrics, merge_confidence_metrics
|
|
15
17
|
from buildlog.render import get_renderer
|
|
16
18
|
from buildlog.skills import Skill, SkillSet, generate_skills
|
|
17
19
|
|
|
@@ -20,11 +22,15 @@ __all__ = [
|
|
|
20
22
|
"PromoteResult",
|
|
21
23
|
"RejectResult",
|
|
22
24
|
"DiffResult",
|
|
25
|
+
"ReviewIssue",
|
|
26
|
+
"ReviewLearning",
|
|
27
|
+
"LearnFromReviewResult",
|
|
23
28
|
"status",
|
|
24
29
|
"promote",
|
|
25
30
|
"reject",
|
|
26
31
|
"diff",
|
|
27
32
|
"find_skills_by_ids",
|
|
33
|
+
"learn_from_review",
|
|
28
34
|
]
|
|
29
35
|
|
|
30
36
|
|
|
@@ -108,6 +114,175 @@ class DiffResult:
|
|
|
108
114
|
"""Error message if operation failed."""
|
|
109
115
|
|
|
110
116
|
|
|
117
|
+
# -----------------------------------------------------------------------------
|
|
118
|
+
# Review Learning Data Structures
|
|
119
|
+
# -----------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ReviewIssueDict(TypedDict, total=False):
|
|
123
|
+
"""Serializable form of ReviewIssue."""
|
|
124
|
+
|
|
125
|
+
severity: str
|
|
126
|
+
category: str
|
|
127
|
+
description: str
|
|
128
|
+
rule_learned: str
|
|
129
|
+
location: str | None
|
|
130
|
+
why_it_matters: str | None
|
|
131
|
+
functional_principle: str | None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class ReviewIssue:
|
|
136
|
+
"""A single issue identified during code review.
|
|
137
|
+
|
|
138
|
+
Attributes:
|
|
139
|
+
severity: How serious the issue is (critical/major/minor/nitpick).
|
|
140
|
+
category: What kind of issue (architectural/workflow/tool_usage/domain_knowledge).
|
|
141
|
+
description: What's wrong (concrete).
|
|
142
|
+
rule_learned: The generalizable rule extracted from this issue.
|
|
143
|
+
location: File:line where the issue was found.
|
|
144
|
+
why_it_matters: Why this issue matters (consequences).
|
|
145
|
+
functional_principle: Related FP principle, if applicable.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
severity: Literal["critical", "major", "minor", "nitpick"]
|
|
149
|
+
category: Literal["architectural", "workflow", "tool_usage", "domain_knowledge"]
|
|
150
|
+
description: str
|
|
151
|
+
rule_learned: str
|
|
152
|
+
location: str | None = None
|
|
153
|
+
why_it_matters: str | None = None
|
|
154
|
+
functional_principle: str | None = None
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dict(cls, data: dict) -> "ReviewIssue":
|
|
158
|
+
"""Construct from dictionary (e.g., from JSON)."""
|
|
159
|
+
return cls(
|
|
160
|
+
severity=data.get("severity", "minor"),
|
|
161
|
+
category=data.get("category", "workflow"),
|
|
162
|
+
description=data.get("description", ""),
|
|
163
|
+
rule_learned=data.get("rule_learned", ""),
|
|
164
|
+
location=data.get("location"),
|
|
165
|
+
why_it_matters=data.get("why_it_matters"),
|
|
166
|
+
functional_principle=data.get("functional_principle"),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ReviewLearningDict(TypedDict, total=False):
|
|
171
|
+
"""Serializable form of ReviewLearning."""
|
|
172
|
+
|
|
173
|
+
id: str
|
|
174
|
+
rule: str
|
|
175
|
+
category: str
|
|
176
|
+
severity: str
|
|
177
|
+
source: str
|
|
178
|
+
first_seen: str
|
|
179
|
+
last_reinforced: str
|
|
180
|
+
reinforcement_count: int
|
|
181
|
+
contradiction_count: int
|
|
182
|
+
functional_principle: str | None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class ReviewLearning:
|
|
187
|
+
"""A learning extracted from review, with confidence tracking.
|
|
188
|
+
|
|
189
|
+
Attributes:
|
|
190
|
+
id: Deterministic hash of rule_learned (category prefix + hash).
|
|
191
|
+
rule: The generalizable rule text.
|
|
192
|
+
category: Category of the learning.
|
|
193
|
+
severity: Severity of the original issue.
|
|
194
|
+
source: Where this learning came from (e.g., "review:PR#13").
|
|
195
|
+
first_seen: When this rule was first identified.
|
|
196
|
+
last_reinforced: When this rule was last seen/reinforced.
|
|
197
|
+
reinforcement_count: How many times this rule has been seen.
|
|
198
|
+
contradiction_count: How many times this rule was contradicted.
|
|
199
|
+
functional_principle: Related FP principle, if applicable.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
id: str
|
|
203
|
+
rule: str
|
|
204
|
+
category: str
|
|
205
|
+
severity: str
|
|
206
|
+
source: str
|
|
207
|
+
first_seen: datetime
|
|
208
|
+
last_reinforced: datetime
|
|
209
|
+
reinforcement_count: int = 1
|
|
210
|
+
contradiction_count: int = 0
|
|
211
|
+
functional_principle: str | None = None
|
|
212
|
+
|
|
213
|
+
def to_confidence_metrics(self) -> ConfidenceMetrics:
|
|
214
|
+
"""Convert to ConfidenceMetrics for scoring."""
|
|
215
|
+
return ConfidenceMetrics(
|
|
216
|
+
reinforcement_count=self.reinforcement_count,
|
|
217
|
+
last_reinforced=self.last_reinforced,
|
|
218
|
+
contradiction_count=self.contradiction_count,
|
|
219
|
+
first_seen=self.first_seen,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def to_dict(self) -> ReviewLearningDict:
|
|
223
|
+
"""Convert to serializable dictionary."""
|
|
224
|
+
result: ReviewLearningDict = {
|
|
225
|
+
"id": self.id,
|
|
226
|
+
"rule": self.rule,
|
|
227
|
+
"category": self.category,
|
|
228
|
+
"severity": self.severity,
|
|
229
|
+
"source": self.source,
|
|
230
|
+
"first_seen": self.first_seen.isoformat(),
|
|
231
|
+
"last_reinforced": self.last_reinforced.isoformat(),
|
|
232
|
+
"reinforcement_count": self.reinforcement_count,
|
|
233
|
+
"contradiction_count": self.contradiction_count,
|
|
234
|
+
}
|
|
235
|
+
if self.functional_principle:
|
|
236
|
+
result["functional_principle"] = self.functional_principle
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def from_dict(cls, data: ReviewLearningDict) -> "ReviewLearning":
|
|
241
|
+
"""Reconstruct from serialized dictionary."""
|
|
242
|
+
first_seen = datetime.fromisoformat(data["first_seen"])
|
|
243
|
+
last_reinforced = datetime.fromisoformat(data["last_reinforced"])
|
|
244
|
+
|
|
245
|
+
# Ensure timezone awareness
|
|
246
|
+
if first_seen.tzinfo is None:
|
|
247
|
+
first_seen = first_seen.replace(tzinfo=timezone.utc)
|
|
248
|
+
if last_reinforced.tzinfo is None:
|
|
249
|
+
last_reinforced = last_reinforced.replace(tzinfo=timezone.utc)
|
|
250
|
+
|
|
251
|
+
return cls(
|
|
252
|
+
id=data["id"],
|
|
253
|
+
rule=data["rule"],
|
|
254
|
+
category=data["category"],
|
|
255
|
+
severity=data["severity"],
|
|
256
|
+
source=data["source"],
|
|
257
|
+
first_seen=first_seen,
|
|
258
|
+
last_reinforced=last_reinforced,
|
|
259
|
+
reinforcement_count=data.get("reinforcement_count", 1),
|
|
260
|
+
contradiction_count=data.get("contradiction_count", 0),
|
|
261
|
+
functional_principle=data.get("functional_principle"),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclass
|
|
266
|
+
class LearnFromReviewResult:
|
|
267
|
+
"""Result of learning from a review.
|
|
268
|
+
|
|
269
|
+
Attributes:
|
|
270
|
+
new_learnings: IDs of newly created learnings.
|
|
271
|
+
reinforced_learnings: IDs of existing learnings that were reinforced.
|
|
272
|
+
total_issues_processed: Total number of issues processed.
|
|
273
|
+
source: Review source identifier.
|
|
274
|
+
message: Human-readable summary.
|
|
275
|
+
error: Error message if operation failed.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
new_learnings: list[str]
|
|
279
|
+
reinforced_learnings: list[str]
|
|
280
|
+
total_issues_processed: int
|
|
281
|
+
source: str
|
|
282
|
+
message: str = ""
|
|
283
|
+
error: str | None = None
|
|
284
|
+
|
|
285
|
+
|
|
111
286
|
def _get_rejected_path(buildlog_dir: Path) -> Path:
|
|
112
287
|
"""Get path to rejected.json file."""
|
|
113
288
|
return buildlog_dir / ".buildlog" / "rejected.json"
|
|
@@ -386,3 +561,169 @@ def diff(
|
|
|
386
561
|
already_promoted=len(promoted_ids),
|
|
387
562
|
already_rejected=len(rejected_ids),
|
|
388
563
|
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# -----------------------------------------------------------------------------
|
|
567
|
+
# Review Learning Operations
|
|
568
|
+
# -----------------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _get_learnings_path(buildlog_dir: Path) -> Path:
|
|
572
|
+
"""Get path to review_learnings.json file."""
|
|
573
|
+
return buildlog_dir / ".buildlog" / "review_learnings.json"
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _generate_learning_id(category: str, rule: str) -> str:
|
|
577
|
+
"""Generate deterministic ID for a learning.
|
|
578
|
+
|
|
579
|
+
Uses category prefix + first 10 chars of SHA256 hash.
|
|
580
|
+
"""
|
|
581
|
+
# Normalize: lowercase, strip whitespace
|
|
582
|
+
normalized = rule.lower().strip()
|
|
583
|
+
hash_input = f"{category}:{normalized}".encode("utf-8")
|
|
584
|
+
hash_hex = hashlib.sha256(hash_input).hexdigest()[:10]
|
|
585
|
+
|
|
586
|
+
# Category prefix mapping
|
|
587
|
+
prefix_map = {
|
|
588
|
+
"architectural": "arch",
|
|
589
|
+
"workflow": "wf",
|
|
590
|
+
"tool_usage": "tool",
|
|
591
|
+
"domain_knowledge": "dom",
|
|
592
|
+
}
|
|
593
|
+
prefix = prefix_map.get(category, category[:4])
|
|
594
|
+
|
|
595
|
+
return f"{prefix}-{hash_hex}"
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _load_learnings(path: Path) -> dict:
|
|
599
|
+
"""Load learnings from JSON file."""
|
|
600
|
+
if not path.exists():
|
|
601
|
+
return {"learnings": {}, "review_history": []}
|
|
602
|
+
try:
|
|
603
|
+
return json.loads(path.read_text())
|
|
604
|
+
except (json.JSONDecodeError, OSError):
|
|
605
|
+
return {"learnings": {}, "review_history": []}
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _save_learnings(path: Path, data: dict) -> None:
|
|
609
|
+
"""Save learnings to JSON file."""
|
|
610
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
611
|
+
path.write_text(json.dumps(data, indent=2))
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def learn_from_review(
|
|
615
|
+
buildlog_dir: Path,
|
|
616
|
+
issues: list[dict],
|
|
617
|
+
source: str | None = None,
|
|
618
|
+
) -> LearnFromReviewResult:
|
|
619
|
+
"""Capture learnings from a code review and update confidence metrics.
|
|
620
|
+
|
|
621
|
+
For each issue:
|
|
622
|
+
1. Generate deterministic ID from rule text
|
|
623
|
+
2. If exists: reinforce (increment count, update timestamp)
|
|
624
|
+
3. If new: create ReviewLearning with initial metrics
|
|
625
|
+
4. Persist to .buildlog/review_learnings.json
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
buildlog_dir: Path to buildlog directory.
|
|
629
|
+
issues: List of review issues with rule_learned field.
|
|
630
|
+
source: Optional source identifier (defaults to timestamp).
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
LearnFromReviewResult with new/reinforced learning IDs.
|
|
634
|
+
"""
|
|
635
|
+
if not issues:
|
|
636
|
+
return LearnFromReviewResult(
|
|
637
|
+
new_learnings=[],
|
|
638
|
+
reinforced_learnings=[],
|
|
639
|
+
total_issues_processed=0,
|
|
640
|
+
source=source or "",
|
|
641
|
+
error="No issues provided",
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# Default source to timestamp
|
|
645
|
+
now = datetime.now(timezone.utc)
|
|
646
|
+
if source is None:
|
|
647
|
+
source = f"review:{now.isoformat()}"
|
|
648
|
+
elif not source.startswith("review:"):
|
|
649
|
+
source = f"review:{source}"
|
|
650
|
+
|
|
651
|
+
learnings_path = _get_learnings_path(buildlog_dir)
|
|
652
|
+
data = _load_learnings(learnings_path)
|
|
653
|
+
|
|
654
|
+
new_ids: list[str] = []
|
|
655
|
+
reinforced_ids: list[str] = []
|
|
656
|
+
processed = 0
|
|
657
|
+
|
|
658
|
+
for issue_dict in issues:
|
|
659
|
+
# Skip issues without rule_learned
|
|
660
|
+
rule = issue_dict.get("rule_learned", "").strip()
|
|
661
|
+
if not rule:
|
|
662
|
+
continue
|
|
663
|
+
|
|
664
|
+
# Parse issue
|
|
665
|
+
issue = ReviewIssue.from_dict(issue_dict)
|
|
666
|
+
learning_id = _generate_learning_id(issue.category, rule)
|
|
667
|
+
|
|
668
|
+
if learning_id in data["learnings"]:
|
|
669
|
+
# Reinforce existing learning
|
|
670
|
+
existing_data = data["learnings"][learning_id]
|
|
671
|
+
existing = ReviewLearning.from_dict(existing_data)
|
|
672
|
+
|
|
673
|
+
# Use merge_confidence_metrics pattern
|
|
674
|
+
updated_metrics = merge_confidence_metrics(
|
|
675
|
+
existing.to_confidence_metrics(), now
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# Update the learning
|
|
679
|
+
existing_data["last_reinforced"] = now.isoformat()
|
|
680
|
+
existing_data["reinforcement_count"] = updated_metrics.reinforcement_count
|
|
681
|
+
reinforced_ids.append(learning_id)
|
|
682
|
+
else:
|
|
683
|
+
# Create new learning
|
|
684
|
+
learning = ReviewLearning(
|
|
685
|
+
id=learning_id,
|
|
686
|
+
rule=rule,
|
|
687
|
+
category=issue.category,
|
|
688
|
+
severity=issue.severity,
|
|
689
|
+
source=source,
|
|
690
|
+
first_seen=now,
|
|
691
|
+
last_reinforced=now,
|
|
692
|
+
reinforcement_count=1,
|
|
693
|
+
contradiction_count=0,
|
|
694
|
+
functional_principle=issue.functional_principle,
|
|
695
|
+
)
|
|
696
|
+
data["learnings"][learning_id] = learning.to_dict()
|
|
697
|
+
new_ids.append(learning_id)
|
|
698
|
+
|
|
699
|
+
processed += 1
|
|
700
|
+
|
|
701
|
+
# Record in review history
|
|
702
|
+
data["review_history"].append(
|
|
703
|
+
{
|
|
704
|
+
"timestamp": now.isoformat(),
|
|
705
|
+
"source": source,
|
|
706
|
+
"issues_count": processed,
|
|
707
|
+
"new_learning_ids": new_ids,
|
|
708
|
+
"reinforced_learning_ids": reinforced_ids,
|
|
709
|
+
}
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Persist
|
|
713
|
+
_save_learnings(learnings_path, data)
|
|
714
|
+
|
|
715
|
+
# Build message
|
|
716
|
+
msg_parts = []
|
|
717
|
+
if new_ids:
|
|
718
|
+
msg_parts.append(f"{len(new_ids)} new learning(s)")
|
|
719
|
+
if reinforced_ids:
|
|
720
|
+
msg_parts.append(f"{len(reinforced_ids)} reinforced")
|
|
721
|
+
message = ", ".join(msg_parts) if msg_parts else "No learnings captured"
|
|
722
|
+
|
|
723
|
+
return LearnFromReviewResult(
|
|
724
|
+
new_learnings=new_ids,
|
|
725
|
+
reinforced_learnings=reinforced_ids,
|
|
726
|
+
total_issues_processed=processed,
|
|
727
|
+
source=source,
|
|
728
|
+
message=message,
|
|
729
|
+
)
|
buildlog/mcp/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from buildlog.mcp.tools import (
|
|
4
4
|
buildlog_diff,
|
|
5
|
+
buildlog_learn_from_review,
|
|
5
6
|
buildlog_promote,
|
|
6
7
|
buildlog_reject,
|
|
7
8
|
buildlog_status,
|
|
@@ -12,4 +13,5 @@ __all__ = [
|
|
|
12
13
|
"buildlog_promote",
|
|
13
14
|
"buildlog_reject",
|
|
14
15
|
"buildlog_diff",
|
|
16
|
+
"buildlog_learn_from_review",
|
|
15
17
|
]
|
buildlog/mcp/server.py
CHANGED
|
@@ -6,6 +6,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
6
6
|
|
|
7
7
|
from buildlog.mcp.tools import (
|
|
8
8
|
buildlog_diff,
|
|
9
|
+
buildlog_learn_from_review,
|
|
9
10
|
buildlog_promote,
|
|
10
11
|
buildlog_reject,
|
|
11
12
|
buildlog_status,
|
|
@@ -18,6 +19,7 @@ mcp.tool()(buildlog_status)
|
|
|
18
19
|
mcp.tool()(buildlog_promote)
|
|
19
20
|
mcp.tool()(buildlog_reject)
|
|
20
21
|
mcp.tool()(buildlog_diff)
|
|
22
|
+
mcp.tool()(buildlog_learn_from_review)
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
def main() -> None:
|
buildlog/mcp/tools.py
CHANGED
|
@@ -9,7 +9,7 @@ from dataclasses import asdict
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Literal
|
|
11
11
|
|
|
12
|
-
from buildlog.core import diff, promote, reject, status
|
|
12
|
+
from buildlog.core import diff, learn_from_review, promote, reject, status
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _validate_skill_ids(skill_ids: list[str]) -> list[str]:
|
|
@@ -95,3 +95,48 @@ def buildlog_diff(
|
|
|
95
95
|
"""
|
|
96
96
|
result = diff(Path(buildlog_dir))
|
|
97
97
|
return asdict(result)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def buildlog_learn_from_review(
|
|
101
|
+
issues: list[dict],
|
|
102
|
+
source: str | None = None,
|
|
103
|
+
buildlog_dir: str = "buildlog",
|
|
104
|
+
) -> dict:
|
|
105
|
+
"""Capture learnings from code review feedback.
|
|
106
|
+
|
|
107
|
+
Call this after a review loop completes to persist learnings.
|
|
108
|
+
Each issue's rule_learned becomes a tracked learning that gains
|
|
109
|
+
confidence through reinforcement.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
issues: List of issues with structure:
|
|
113
|
+
{
|
|
114
|
+
"severity": "critical|major|minor|nitpick",
|
|
115
|
+
"category": "architectural|workflow|tool_usage|domain_knowledge",
|
|
116
|
+
"description": "What's wrong",
|
|
117
|
+
"rule_learned": "Generalizable rule",
|
|
118
|
+
"location": "file:line (optional)",
|
|
119
|
+
"why_it_matters": "Why this matters (optional)",
|
|
120
|
+
"functional_principle": "FP principle (optional)"
|
|
121
|
+
}
|
|
122
|
+
source: Optional identifier (e.g., "PR#13")
|
|
123
|
+
buildlog_dir: Path to buildlog directory
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Result with new_learnings, reinforced_learnings, total processed
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
buildlog_learn_from_review(
|
|
130
|
+
issues=[
|
|
131
|
+
{
|
|
132
|
+
"severity": "critical",
|
|
133
|
+
"category": "architectural",
|
|
134
|
+
"description": "Score bounds not validated",
|
|
135
|
+
"rule_learned": "Validate invariants at function boundaries"
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
source="PR#13"
|
|
139
|
+
)
|
|
140
|
+
"""
|
|
141
|
+
result = learn_from_review(Path(buildlog_dir), issues, source)
|
|
142
|
+
return asdict(result)
|
buildlog/skills.py
CHANGED
|
@@ -33,6 +33,18 @@ from buildlog.embeddings import EmbeddingBackend, get_backend, get_default_backe
|
|
|
33
33
|
|
|
34
34
|
logger = logging.getLogger(__name__)
|
|
35
35
|
|
|
36
|
+
|
|
37
|
+
def _load_review_learnings(buildlog_dir: Path) -> dict:
|
|
38
|
+
"""Load review learnings from .buildlog/review_learnings.json."""
|
|
39
|
+
learnings_path = buildlog_dir / ".buildlog" / "review_learnings.json"
|
|
40
|
+
if not learnings_path.exists():
|
|
41
|
+
return {"learnings": {}}
|
|
42
|
+
try:
|
|
43
|
+
return json.loads(learnings_path.read_text())
|
|
44
|
+
except (json.JSONDecodeError, OSError):
|
|
45
|
+
return {"learnings": {}}
|
|
46
|
+
|
|
47
|
+
|
|
36
48
|
# Configuration constants
|
|
37
49
|
MIN_SIMILARITY_THRESHOLD: Final[float] = 0.7
|
|
38
50
|
HIGH_CONFIDENCE_FREQUENCY: Final[int] = 3
|
|
@@ -398,8 +410,9 @@ def generate_skills(
|
|
|
398
410
|
since_date: date | None = None,
|
|
399
411
|
embedding_backend: str | None = None,
|
|
400
412
|
confidence_config: ConfidenceConfig | None = None,
|
|
413
|
+
include_review_learnings: bool = True,
|
|
401
414
|
) -> SkillSet:
|
|
402
|
-
"""Generate skills from buildlog patterns.
|
|
415
|
+
"""Generate skills from buildlog patterns and review learnings.
|
|
403
416
|
|
|
404
417
|
Args:
|
|
405
418
|
buildlog_dir: Path to the buildlog directory.
|
|
@@ -410,6 +423,9 @@ def generate_skills(
|
|
|
410
423
|
confidence_config: Configuration for continuous confidence scoring.
|
|
411
424
|
If provided, skills will include confidence_score and confidence_tier.
|
|
412
425
|
If None, only discrete confidence levels (high/medium/low) are computed.
|
|
426
|
+
include_review_learnings: Whether to include learnings from code reviews.
|
|
427
|
+
When True, loads .buildlog/review_learnings.json and merges
|
|
428
|
+
review learnings into the skill set.
|
|
413
429
|
|
|
414
430
|
Returns:
|
|
415
431
|
SkillSet with generated skills.
|
|
@@ -468,6 +484,111 @@ def generate_skills(
|
|
|
468
484
|
skills.sort(key=lambda s: (-s.frequency, s.rule))
|
|
469
485
|
skills_by_category[category] = skills
|
|
470
486
|
|
|
487
|
+
# Merge review learnings if requested
|
|
488
|
+
if include_review_learnings:
|
|
489
|
+
review_data = _load_review_learnings(buildlog_dir)
|
|
490
|
+
learnings = review_data.get("learnings", {})
|
|
491
|
+
|
|
492
|
+
for learning_id, learning_dict in learnings.items():
|
|
493
|
+
category = learning_dict.get("category", "workflow")
|
|
494
|
+
rule = learning_dict.get("rule", "")
|
|
495
|
+
|
|
496
|
+
if not rule:
|
|
497
|
+
continue
|
|
498
|
+
|
|
499
|
+
# Parse timestamps for confidence calculation
|
|
500
|
+
first_seen_str = learning_dict.get("first_seen", "")
|
|
501
|
+
last_reinforced_str = learning_dict.get("last_reinforced", "")
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
first_seen = datetime.fromisoformat(first_seen_str)
|
|
505
|
+
if first_seen.tzinfo is None:
|
|
506
|
+
first_seen = first_seen.replace(tzinfo=timezone.utc)
|
|
507
|
+
except (ValueError, TypeError):
|
|
508
|
+
first_seen = datetime.now(timezone.utc)
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
last_reinforced = datetime.fromisoformat(last_reinforced_str)
|
|
512
|
+
if last_reinforced.tzinfo is None:
|
|
513
|
+
last_reinforced = last_reinforced.replace(tzinfo=timezone.utc)
|
|
514
|
+
except (ValueError, TypeError):
|
|
515
|
+
last_reinforced = datetime.now(timezone.utc)
|
|
516
|
+
|
|
517
|
+
# Get frequency from reinforcement count
|
|
518
|
+
frequency = learning_dict.get("reinforcement_count", 1)
|
|
519
|
+
|
|
520
|
+
# Check for duplicate rules in existing skills (by ID match)
|
|
521
|
+
existing_skill = None
|
|
522
|
+
if category in skills_by_category:
|
|
523
|
+
for skill in skills_by_category[category]:
|
|
524
|
+
if skill.id == learning_id:
|
|
525
|
+
existing_skill = skill
|
|
526
|
+
break
|
|
527
|
+
|
|
528
|
+
if existing_skill is not None:
|
|
529
|
+
# Merge: boost the existing skill's frequency
|
|
530
|
+
existing_skill = Skill(
|
|
531
|
+
id=existing_skill.id,
|
|
532
|
+
category=existing_skill.category,
|
|
533
|
+
rule=existing_skill.rule,
|
|
534
|
+
frequency=existing_skill.frequency + frequency,
|
|
535
|
+
confidence=existing_skill.confidence,
|
|
536
|
+
sources=existing_skill.sources
|
|
537
|
+
+ [learning_dict.get("source", "review")],
|
|
538
|
+
tags=existing_skill.tags,
|
|
539
|
+
confidence_score=existing_skill.confidence_score,
|
|
540
|
+
confidence_tier=existing_skill.confidence_tier,
|
|
541
|
+
)
|
|
542
|
+
# Replace in list
|
|
543
|
+
skills_by_category[category] = [
|
|
544
|
+
existing_skill if s.id == existing_skill.id else s
|
|
545
|
+
for s in skills_by_category[category]
|
|
546
|
+
]
|
|
547
|
+
else:
|
|
548
|
+
# Create new skill from review learning
|
|
549
|
+
review_conf_score: float | None = None
|
|
550
|
+
review_conf_tier: str | None = None
|
|
551
|
+
|
|
552
|
+
if confidence_config is not None and t_now is not None:
|
|
553
|
+
metrics = ConfidenceMetrics(
|
|
554
|
+
reinforcement_count=frequency,
|
|
555
|
+
last_reinforced=last_reinforced,
|
|
556
|
+
contradiction_count=learning_dict.get("contradiction_count", 0),
|
|
557
|
+
first_seen=first_seen,
|
|
558
|
+
)
|
|
559
|
+
review_conf_score = calculate_continuous_confidence(
|
|
560
|
+
metrics, confidence_config, t_now
|
|
561
|
+
)
|
|
562
|
+
review_conf_tier = get_confidence_tier(
|
|
563
|
+
review_conf_score, confidence_config
|
|
564
|
+
).value
|
|
565
|
+
|
|
566
|
+
# Calculate discrete confidence from most recent date
|
|
567
|
+
discrete_confidence = _calculate_confidence(
|
|
568
|
+
frequency, last_reinforced.date()
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
skill = Skill(
|
|
572
|
+
id=learning_id,
|
|
573
|
+
category=category,
|
|
574
|
+
rule=rule,
|
|
575
|
+
frequency=frequency,
|
|
576
|
+
confidence=discrete_confidence,
|
|
577
|
+
sources=[learning_dict.get("source", "review")],
|
|
578
|
+
tags=_extract_tags(rule),
|
|
579
|
+
confidence_score=review_conf_score,
|
|
580
|
+
confidence_tier=review_conf_tier,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Add to category
|
|
584
|
+
if category not in skills_by_category:
|
|
585
|
+
skills_by_category[category] = []
|
|
586
|
+
skills_by_category[category].append(skill)
|
|
587
|
+
|
|
588
|
+
# Re-sort categories after adding review learnings
|
|
589
|
+
for category in skills_by_category:
|
|
590
|
+
skills_by_category[category].sort(key=lambda s: (-s.frequency, s.rule))
|
|
591
|
+
|
|
471
592
|
return SkillSet(
|
|
472
593
|
generated_at=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
473
594
|
source_entries=result.entry_count,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: buildlog
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Engineering notebook for AI-assisted development
|
|
5
5
|
Project-URL: Homepage, https://github.com/Peleke/buildlog-template
|
|
6
6
|
Project-URL: Repository, https://github.com/Peleke/buildlog-template
|
|
@@ -678,6 +678,7 @@ The MCP server lets Claude Code interact with your buildlog rules directly. Your
|
|
|
678
678
|
| `buildlog_promote` | Write rules to CLAUDE.md, settings.json, or **Agent Skills** |
|
|
679
679
|
| `buildlog_reject` | Mark rules to exclude from future suggestions |
|
|
680
680
|
| `buildlog_diff` | Show rules pending review (not yet promoted/rejected) |
|
|
681
|
+
| `buildlog_learn_from_review` | Capture learnings from code review feedback |
|
|
681
682
|
|
|
682
683
|
### Promotion Targets via MCP
|
|
683
684
|
|
|
@@ -717,6 +718,136 @@ Claude: [calls buildlog_promote with target="skill"]
|
|
|
717
718
|
|
|
718
719
|
---
|
|
719
720
|
|
|
721
|
+
## Review Learning System
|
|
722
|
+
|
|
723
|
+
Beyond manual buildlog entries, buildlog can **learn from code reviews** in real-time. Every review becomes a teaching moment—rules get extracted, persisted, and gain confidence through reinforcement.
|
|
724
|
+
|
|
725
|
+
### How It Works
|
|
726
|
+
|
|
727
|
+
```mermaid
|
|
728
|
+
flowchart LR
|
|
729
|
+
A["Code Review"] --> B["Extract Rules"]
|
|
730
|
+
B --> C["learn_from_review()"]
|
|
731
|
+
C --> D["Persist to<br/>.buildlog/review_learnings.json"]
|
|
732
|
+
D --> E["Rules gain confidence<br/>through reinforcement"]
|
|
733
|
+
E --> F["Inject into<br/>future sessions"]
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### The MCP Tool
|
|
737
|
+
|
|
738
|
+
```python
|
|
739
|
+
buildlog_learn_from_review(
|
|
740
|
+
issues=[
|
|
741
|
+
{
|
|
742
|
+
"severity": "critical",
|
|
743
|
+
"category": "architectural",
|
|
744
|
+
"description": "No bounds validation on score input",
|
|
745
|
+
"rule_learned": "Validate invariants at function boundaries"
|
|
746
|
+
}
|
|
747
|
+
],
|
|
748
|
+
source="PR#42"
|
|
749
|
+
)
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
When the same rule is learned from multiple reviews, its confidence increases automatically.
|
|
753
|
+
|
|
754
|
+
### Reviewer Skills (The Brutal Feedback Loop)
|
|
755
|
+
|
|
756
|
+
buildlog ships with **ruthless reviewer personas** that output structured JSON compatible with `buildlog_learn_from_review()`. Every review teaches the system.
|
|
757
|
+
|
|
758
|
+
| Skill | Trigger | Focus |
|
|
759
|
+
|-------|---------|-------|
|
|
760
|
+
| **Ruthless Reviewer** | `review`, `code review` | Code quality, FP principles, invariants |
|
|
761
|
+
| **Test Terrorist** | `test review`, `coverage audit` | ALL test types: unit, integration, E2E, contract, property-based |
|
|
762
|
+
| **Security Karen** | `security review`, `owasp` | OWASP Top 10, input validation, auth |
|
|
763
|
+
| **Review Gauntlet** | `gauntlet`, `destroy my code` | All three reviewers in sequence |
|
|
764
|
+
|
|
765
|
+
#### The Review Gauntlet
|
|
766
|
+
|
|
767
|
+
Run all three reviewers for maximum brutality:
|
|
768
|
+
|
|
769
|
+
```
|
|
770
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
771
|
+
│ THE REVIEW GAUNTLET │
|
|
772
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
773
|
+
│ │
|
|
774
|
+
│ Your Code │
|
|
775
|
+
│ │ │
|
|
776
|
+
│ ▼ │
|
|
777
|
+
│ ┌──────────────────┐ │
|
|
778
|
+
│ │ RUTHLESS │ "Is this pure? Would it compile │
|
|
779
|
+
│ │ REVIEWER │ in Haskell?" │
|
|
780
|
+
│ └────────┬─────────┘ │
|
|
781
|
+
│ ▼ │
|
|
782
|
+
│ ┌──────────────────┐ │
|
|
783
|
+
│ │ TEST │ "Where are your contract tests? │
|
|
784
|
+
│ │ TERRORIST │ Show me the Gherkin." │
|
|
785
|
+
│ └────────┬─────────┘ │
|
|
786
|
+
│ ▼ │
|
|
787
|
+
│ ┌──────────────────┐ │
|
|
788
|
+
│ │ SECURITY │ "I need to speak to your security │
|
|
789
|
+
│ │ KAREN │ manager about this SQL query." │
|
|
790
|
+
│ └────────┬─────────┘ │
|
|
791
|
+
│ ▼ │
|
|
792
|
+
│ Combined Issues → buildlog_learn_from_review() │
|
|
793
|
+
│ ▼ │
|
|
794
|
+
│ Future sessions get smarter │
|
|
795
|
+
│ │
|
|
796
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Each reviewer outputs structured JSON:
|
|
800
|
+
|
|
801
|
+
```json
|
|
802
|
+
{
|
|
803
|
+
"verdict": "BLOCKED",
|
|
804
|
+
"issues": [
|
|
805
|
+
{
|
|
806
|
+
"severity": "critical",
|
|
807
|
+
"category": "architectural",
|
|
808
|
+
"location": "src/api/handler.py:45",
|
|
809
|
+
"description": "Score bounds not validated",
|
|
810
|
+
"rule_learned": "Validate invariants at function boundaries",
|
|
811
|
+
"functional_principle": "Parse, don't validate"
|
|
812
|
+
}
|
|
813
|
+
]
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
After review, call `buildlog_learn_from_review(issues=...)` to persist the learnings.
|
|
818
|
+
|
|
819
|
+
### Test Terrorist Coverage
|
|
820
|
+
|
|
821
|
+
The Test Terrorist knows ALL test types:
|
|
822
|
+
|
|
823
|
+
| Layer | Test Types |
|
|
824
|
+
|-------|-----------|
|
|
825
|
+
| **Fundamentals** | Unit, Integration, E2E, Smoke |
|
|
826
|
+
| **User Flows** | BDD scenarios, persistence tests |
|
|
827
|
+
| **Advanced** | Contract (Pact), Property-based (Hypothesis), Metamorphic, Statistical, Mutation |
|
|
828
|
+
| **Specialized** | Chaos, Load/Performance, Accessibility |
|
|
829
|
+
|
|
830
|
+
**Contract tests are NON-NEGOTIABLE** for service boundaries. The Test Terrorist will find your missing ones.
|
|
831
|
+
|
|
832
|
+
### Security Karen's OWASP Obsession
|
|
833
|
+
|
|
834
|
+
Security Karen audits against OWASP Top 10 (2021):
|
|
835
|
+
|
|
836
|
+
- **A01**: Broken Access Control
|
|
837
|
+
- **A02**: Cryptographic Failures
|
|
838
|
+
- **A03**: Injection
|
|
839
|
+
- **A04**: Insecure Design
|
|
840
|
+
- **A05**: Security Misconfiguration
|
|
841
|
+
- **A06**: Vulnerable Components
|
|
842
|
+
- **A07**: Auth Failures
|
|
843
|
+
- **A08**: Integrity Failures
|
|
844
|
+
- **A09**: Logging Failures
|
|
845
|
+
- **A10**: SSRF
|
|
846
|
+
|
|
847
|
+
Plus: secrets management, input validation, API security, error handling.
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
720
851
|
## Philosophy
|
|
721
852
|
|
|
722
853
|
### 1. Write Fast, Not Pretty
|
|
@@ -3,28 +3,28 @@ buildlog/cli.py,sha256=cmg77_RVJx8mdtStApS1KXxBUUB8Id6psHZjtHo33iE,14350
|
|
|
3
3
|
buildlog/confidence.py,sha256=EOkPxIH1_y7k6B3Hl7Wn0iR2qK_lumvOyyyqUdafXVY,9382
|
|
4
4
|
buildlog/distill.py,sha256=fqXW_YyBFIFhwIWhnR-TQ7U65gypqG-mcAzNBr-qaag,11262
|
|
5
5
|
buildlog/embeddings.py,sha256=vPydWjJVkYp172zFou-lJ737qsu6vRMQAMs143RGIpA,12364
|
|
6
|
-
buildlog/skills.py,sha256=
|
|
6
|
+
buildlog/skills.py,sha256=C_tqspj9lVRkjF-g-KpKt8WoQ-Xq0e0l1VtXly_Naks,30055
|
|
7
7
|
buildlog/stats.py,sha256=2WdHdmzUNGobtWngmm9nA_UmqM7DQeAnZL8_rLQN8aw,13256
|
|
8
|
-
buildlog/core/__init__.py,sha256=
|
|
9
|
-
buildlog/core/operations.py,sha256=
|
|
10
|
-
buildlog/mcp/__init__.py,sha256=
|
|
11
|
-
buildlog/mcp/server.py,sha256=
|
|
12
|
-
buildlog/mcp/tools.py,sha256=
|
|
8
|
+
buildlog/core/__init__.py,sha256=ifByeEE3sM13EHWIKNAOjNc3zx_4qYEfyXdB4Woumuw,594
|
|
9
|
+
buildlog/core/operations.py,sha256=Q9wX8kfnpDBAC8ySHYXZi3vA9w0vuOyzOYl6JYB6YyM,22006
|
|
10
|
+
buildlog/mcp/__init__.py,sha256=jCLNUkYFrDcPd5dY9xbaaVANl-ZzdPim1BykgGY7f7U,334
|
|
11
|
+
buildlog/mcp/server.py,sha256=vlfXlRvGfoXBhkHv2y5e1jc7JH2Hkv7G1obSdxlQ14w,583
|
|
12
|
+
buildlog/mcp/tools.py,sha256=Rfk3dS4bLflvaUa5O2YLdvBx354ZQ_Lfy-Uoaz_AjGo,4397
|
|
13
13
|
buildlog/render/__init__.py,sha256=VxJWEmcX7pSiC3W-ytsHv9gNVUr4hJrVHBW084kEnAI,1993
|
|
14
14
|
buildlog/render/base.py,sha256=gQfvOsH1zttAo10xtEyNsAbqZ4NRSPiDihO-aiGgTsw,533
|
|
15
15
|
buildlog/render/claude_md.py,sha256=Z_E6MbJyVM_hJSoB4KL2rvbt5UEQHekTpJijj106lsI,2624
|
|
16
16
|
buildlog/render/settings_json.py,sha256=4DS5OWksPrFCa7MIgWIu0t4rxYmItpMdGfTqMX3aMNs,2495
|
|
17
17
|
buildlog/render/skill.py,sha256=_7umIS1Ms1oQ2_PopYueFjX41nMq1p28yJp6DhXFdgU,5981
|
|
18
18
|
buildlog/render/tracking.py,sha256=6O0RIU-1gjVG-_S5dmXLz6RCMsQoHOR2u5___UpqXEo,1294
|
|
19
|
-
buildlog-0.
|
|
20
|
-
buildlog-0.
|
|
21
|
-
buildlog-0.
|
|
22
|
-
buildlog-0.
|
|
23
|
-
buildlog-0.
|
|
24
|
-
buildlog-0.
|
|
25
|
-
buildlog-0.
|
|
26
|
-
buildlog-0.
|
|
27
|
-
buildlog-0.
|
|
28
|
-
buildlog-0.
|
|
29
|
-
buildlog-0.
|
|
30
|
-
buildlog-0.
|
|
19
|
+
buildlog-0.4.0.data/data/share/buildlog/copier.yml,sha256=A-1JKV59kOe0BQosGUBgRCg7iQozP_qyA3zfoHwpBKY,927
|
|
20
|
+
buildlog-0.4.0.data/data/share/buildlog/post_gen.py,sha256=XFlo40LuPpAsBhIRRRtHqvU3_5POss4L401hp35ijhw,1744
|
|
21
|
+
buildlog-0.4.0.data/data/share/buildlog/template/buildlog/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
buildlog-0.4.0.data/data/share/buildlog/template/buildlog/2026-01-01-example.md,sha256=7x9sKmydfmfKyNz9hV7MtYnQJuBwbxNanbPOcpQDDZQ,7040
|
|
23
|
+
buildlog-0.4.0.data/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md,sha256=osclytWwl5jUiTgSpuT4cT3h3oPvCkZ5GPCnFuJZNcY,3802
|
|
24
|
+
buildlog-0.4.0.data/data/share/buildlog/template/buildlog/_TEMPLATE.md,sha256=CUvxgcx1-9XT_EdQ8e_vnuPq_h-u1uhXJgForJU2Pso,2932
|
|
25
|
+
buildlog-0.4.0.data/data/share/buildlog/template/buildlog/assets/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
buildlog-0.4.0.dist-info/METADATA,sha256=adRpA8HpJlbAFLk7UCbHPqhWXNU7Xp9WEfYo-e4GqXI,30019
|
|
27
|
+
buildlog-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
28
|
+
buildlog-0.4.0.dist-info/entry_points.txt,sha256=BMFclPOomp_sgaa0OqBg6LfqCMlqzjZV88ww5TrPPoo,87
|
|
29
|
+
buildlog-0.4.0.dist-info/licenses/LICENSE,sha256=fAgt-akug9nAwIj6M-SIf8u3ck-T7pJTwfmy9vWYASk,1074
|
|
30
|
+
buildlog-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{buildlog-0.3.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md
RENAMED
|
File without changes
|
{buildlog-0.3.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md
RENAMED
|
File without changes
|
{buildlog-0.3.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/assets/.gitkeep
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|