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 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
  ]
@@ -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.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=ZR3cTn19WCCB2DjDRN2jyJOhXs7FazDkulRXYAZnquU,24757
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=07N1gRiPQQTBtLp9rsEErh39sgXWZSlEKWBn708SoQk,412
9
- buildlog/core/operations.py,sha256=o01z2Sy0fgiBK6Z90Lkg6ACoqihH3-HC-hkPBSdj9mA,10656
10
- buildlog/mcp/__init__.py,sha256=Eaoa7aRWa428ORxyvneH1cKW7XxuBpF4qvM9mTEH7Ds,268
11
- buildlog/mcp/server.py,sha256=sgEMYBYq1tNKddvtyTpQ_M1dZHj7FhePDzwz7saIPH0,512
12
- buildlog/mcp/tools.py,sha256=KbO1NjogCWHECTIzFYSeAikJGG8fIMDNDOBicdKXDdY,2818
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.3.0.data/data/share/buildlog/copier.yml,sha256=A-1JKV59kOe0BQosGUBgRCg7iQozP_qyA3zfoHwpBKY,927
20
- buildlog-0.3.0.data/data/share/buildlog/post_gen.py,sha256=XFlo40LuPpAsBhIRRRtHqvU3_5POss4L401hp35ijhw,1744
21
- buildlog-0.3.0.data/data/share/buildlog/template/buildlog/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- buildlog-0.3.0.data/data/share/buildlog/template/buildlog/2026-01-01-example.md,sha256=7x9sKmydfmfKyNz9hV7MtYnQJuBwbxNanbPOcpQDDZQ,7040
23
- buildlog-0.3.0.data/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md,sha256=osclytWwl5jUiTgSpuT4cT3h3oPvCkZ5GPCnFuJZNcY,3802
24
- buildlog-0.3.0.data/data/share/buildlog/template/buildlog/_TEMPLATE.md,sha256=CUvxgcx1-9XT_EdQ8e_vnuPq_h-u1uhXJgForJU2Pso,2932
25
- buildlog-0.3.0.data/data/share/buildlog/template/buildlog/assets/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- buildlog-0.3.0.dist-info/METADATA,sha256=v2RWbkwAEgpZtshu6jiqr5X9cV713Ml_SGhhVywmLLA,24188
27
- buildlog-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
28
- buildlog-0.3.0.dist-info/entry_points.txt,sha256=BMFclPOomp_sgaa0OqBg6LfqCMlqzjZV88ww5TrPPoo,87
29
- buildlog-0.3.0.dist-info/licenses/LICENSE,sha256=fAgt-akug9nAwIj6M-SIf8u3ck-T7pJTwfmy9vWYASk,1074
30
- buildlog-0.3.0.dist-info/RECORD,,
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,,