buildlog 0.2.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/confidence.py +311 -0
- 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 +233 -11
- {buildlog-0.2.0.data → buildlog-0.4.0.data}/data/share/buildlog/post_gen.py +11 -7
- {buildlog-0.2.0.dist-info → buildlog-0.4.0.dist-info}/METADATA +134 -2
- buildlog-0.4.0.dist-info/RECORD +30 -0
- buildlog-0.2.0.dist-info/RECORD +0 -29
- {buildlog-0.2.0.data → buildlog-0.4.0.data}/data/share/buildlog/copier.yml +0 -0
- {buildlog-0.2.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
- {buildlog-0.2.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
- {buildlog-0.2.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
- {buildlog-0.2.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
- {buildlog-0.2.0.data → buildlog-0.4.0.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
- {buildlog-0.2.0.dist-info → buildlog-0.4.0.dist-info}/WHEEL +0 -0
- {buildlog-0.2.0.dist-info → buildlog-0.4.0.dist-info}/entry_points.txt +0 -0
- {buildlog-0.2.0.dist-info → buildlog-0.4.0.dist-info}/licenses/LICENSE +0 -0
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)
|