11"""The janitor — consolidate the diary into the playbook.
22
3- Scores each tag's pattern by blending frequency, strength, and recency, and
4- promotes the ones that clear a tunable bar from the messy diary into the clean
5- playbook. The playbook is always *regenerated* from the diary (never edited in
6- place), so it can be rebuilt if it ever goes bad, and it is bounded to a top-K by
7- confidence. The outcome signal comes from B's own observations and their
8- recurrence — never B grading itself.
3+ Scores each tag's pattern by blending frequency, strength, and recency into a
4+ 0–100% confidence, and promotes the ones that clear a bar from the messy diary
5+ into the clean playbook. The playbook is always *regenerated* from the diary
6+ (never edited in place), bounded to a top-K by confidence. Each promoted rule
7+ cites the diary rows it was distilled from (provenance).
8+
9+ Tags are proposed by the LLM (reusing a shown vocabulary), so grouping by exact
10+ tag stays coherent without any clustering.
911"""
1012
1113from __future__ import annotations
1214
1315from collections import defaultdict
1416
15- from app .models .memory import DiaryEntry , PlaybookEntry
16- from app .repositories .diary import DiaryRepository
17- from app .repositories .playbook import PlaybookRepository
17+ from app .models .memory import ConfidenceBucket , DiaryEntry , PlaybookEntry , Strength
1818
1919RECENCY_DECAY = 0.8 # weight of an observation one run older
20+ STRENGTH_POINTS : dict [Strength , int ] = {"weak" : 15 , "medium" : 30 , "strong" : 50 }
2021MIN_RUNS = 2 # a pattern must recur across >= 2 training runs to be trusted
21- PROMOTION_BAR = 3.0 # min total confidence for a tag to promote at all
22- TOP_RULES_PER_TAG = 3 # keep the strongest few distinct lessons per tag
22+ PROMOTION_CONFIDENCE = 40 # min 0-100 confidence to promote (the low/medium boundary)
23+ TOP_RULES_PER_TAG = 3
2324TOP_K = 10 # bounded active playbook
2425
2526
26- def _weight (entry : DiaryEntry , now : int ) -> float :
27- return entry .strength * (RECENCY_DECAY ** (now - entry .run ))
27+ def _points (entry : DiaryEntry , now : int ) -> float :
28+ return STRENGTH_POINTS [entry .strength ] * (RECENCY_DECAY ** (now - entry .run ))
29+
30+
31+ def confidence_of (entries : list [DiaryEntry ], now : int ) -> int :
32+ """0–100% confidence from frequency × strength × recency (capped)."""
33+ return min (100 , round (sum (_points (e , now ) for e in entries )))
34+
35+
36+ def bucket_of (confidence : int ) -> ConfidenceBucket :
37+ if confidence >= 70 :
38+ return "high"
39+ if confidence >= 40 :
40+ return "medium"
41+ return "low"
2842
2943
3044def consolidate_entries (
3145 diary : list [DiaryEntry ],
3246 * ,
33- bar : float = PROMOTION_BAR ,
47+ bar : int = PROMOTION_CONFIDENCE ,
3448 top_k : int = TOP_K ,
3549) -> list [PlaybookEntry ]:
36- """Pure consolidation: diary entries -> the promoted, bounded playbook.
37-
38- A tag is trusted when its page-kind recurred across >= MIN_RUNS training runs
39- (that's the "proven pattern" gate — robust to the LLM phrasing lessons
40- differently each time). Within a trusted tag we keep the strongest few
41- distinct lessons by confidence (frequency x strength x recency).
42- """
50+ """Pure consolidation: diary entries -> the promoted, bounded playbook."""
4351 if not diary :
4452 return []
4553 now = max (e .run for e in diary )
@@ -51,33 +59,33 @@ def consolidate_entries(
5159
5260 promoted : list [PlaybookEntry ] = []
5361 for tag , entries in by_tag .items ():
54- runs = {e .run for e in entries }
55- if len (runs ) < MIN_RUNS :
62+ if len ({e .run for e in entries }) < MIN_RUNS :
5663 continue # pattern hasn't recurred across runs yet
57- if sum (_weight (e , now ) for e in entries ) < bar :
58- continue
5964
60- # Score each distinct lesson under this tag.
6165 by_lesson : dict [str , list [DiaryEntry ]] = defaultdict (list )
6266 for entry in entries :
6367 by_lesson [entry .lesson .strip ()].append (entry )
6468
6569 scored = sorted (
6670 (
67- (sum ( _weight ( e , now ) for e in les_entries ), lesson , les_entries )
71+ (confidence_of ( les_entries , now ), lesson , les_entries )
6872 for lesson , les_entries in by_lesson .items ()
6973 ),
7074 reverse = True ,
7175 key = lambda triple : triple [0 ],
7276 )
7377 for confidence , lesson , les_entries in scored [:TOP_RULES_PER_TAG ]:
78+ if confidence < bar :
79+ continue
7480 promoted .append (
7581 PlaybookEntry (
7682 tag = tag ,
7783 rule = lesson ,
78- confidence = round (confidence , 3 ),
84+ confidence = confidence ,
85+ confidence_bucket = bucket_of (confidence ),
7986 times_seen = len (les_entries ),
8087 last_run = max (e .run for e in les_entries ),
88+ source_ids = [e .id for e in les_entries if e .id ],
8189 )
8290 )
8391
@@ -86,13 +94,12 @@ def consolidate_entries(
8694
8795
8896async def run_janitor (
89- diary_repo : DiaryRepository | None = None ,
90- playbook_repo : PlaybookRepository | None = None ,
91- * ,
92- bar : float = PROMOTION_BAR ,
93- top_k : int = TOP_K ,
94- ) -> list [PlaybookEntry ]:
97+ diary_repo = None , playbook_repo = None , * , bar = PROMOTION_CONFIDENCE , top_k = TOP_K
98+ ):
9599 """Read the diary, rebuild the playbook, write it atomically."""
100+ from app .repositories .diary import DiaryRepository
101+ from app .repositories .playbook import PlaybookRepository
102+
96103 diary_repo = diary_repo or DiaryRepository ()
97104 playbook_repo = playbook_repo or PlaybookRepository ()
98105 diary = await diary_repo .all ()
0 commit comments