daimon-briefing 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/PKG-INFO +1 -1
  2. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/carry.py +40 -7
  3. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/pyproject.toml +1 -1
  4. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_carry.py +70 -0
  5. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/uv.lock +1 -1
  6. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/.gitignore +0 -0
  7. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/README.md +0 -0
  8. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/__init__.py +0 -0
  9. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/anchor.py +0 -0
  10. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/briefing.py +0 -0
  11. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/cli.py +0 -0
  12. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/config.py +0 -0
  13. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/configure.py +0 -0
  14. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/harvest.py +0 -0
  15. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/hooks.py +0 -0
  16. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/llm.py +0 -0
  17. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/recall.py +0 -0
  18. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/render.py +0 -0
  19. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/scoring.py +0 -0
  20. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/serializer.py +0 -0
  21. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/store.py +0 -0
  22. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/teamsync.py +0 -0
  23. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/daimon_briefing/transcript.py +0 -0
  24. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/skills/daimon-briefing/SKILL.md +0 -0
  25. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/skills/daimon-end/SKILL.md +0 -0
  26. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/__init__.py +0 -0
  27. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/conftest.py +0 -0
  28. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/fixtures/sample_transcript.md +0 -0
  29. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_anchor.py +0 -0
  30. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_briefing.py +0 -0
  31. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_claude_hooks.py +0 -0
  32. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_cli.py +0 -0
  33. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_codex_hooks.py +0 -0
  34. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_config.py +0 -0
  35. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_configure.py +0 -0
  36. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_gemini_hooks.py +0 -0
  37. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_harvest.py +0 -0
  38. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_hooks.py +0 -0
  39. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_isolation.py +0 -0
  40. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_llm.py +0 -0
  41. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_recall.py +0 -0
  42. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_render.py +0 -0
  43. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_scoring.py +0 -0
  44. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_serializer.py +0 -0
  45. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_store.py +0 -0
  46. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_teamsync.py +0 -0
  47. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_transcript.py +0 -0
  48. {daimon_briefing-0.3.1 → daimon_briefing-0.3.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: daimon-briefing
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Dream-briefing hermes plugin: cognitive checkpoint at session end, 'while you were away' briefing at session start. Slice 1 (local-file, no Honcho).
5
5
  Author: Daily-Nerd / Daimon
6
6
  License: Apache-2.0
@@ -9,6 +9,7 @@ caller injects clock and knobs (scar: a default wall-clock anywhere silently
9
9
  freezes time math under simulation)."""
10
10
 
11
11
  import copy
12
+ from collections import Counter
12
13
 
13
14
  from . import recall, scoring, store
14
15
 
@@ -22,16 +23,41 @@ _CARRIED_KINDS = (
22
23
 
23
24
  _MIN_SHARED = 3 # shared salient terms for same-item
24
25
  _MIN_RATIO = 0.6 # or this fraction of the shorter term list
26
+ _GENERIC_DF = 3 # a term shared by >=3 items of one kind is that kind's
27
+ # vocabulary, not an item's identity. Filtering it out of
28
+ # dedup stops generic overlap (data/field/validation, the
29
+ # #13 live specimen) from forging a false merge. Computed
30
+ # per kind per merge — no static stoplist, so carry stays
31
+ # language-neutral (es i18n just shipped).
25
32
 
26
33
 
27
- def _same_item(a_text: str, b_text: str) -> bool:
34
+ def _generic_terms(texts, k: int = _GENERIC_DF) -> frozenset:
35
+ """Salient terms appearing in >= k DISTINCT texts of one kind — that kind's
36
+ shared vocabulary, which dedup must ignore. Document frequency counts a term
37
+ once per text (set per text), so repetition inside one item can't inflate
38
+ it."""
39
+ df: Counter = Counter()
40
+ for t in texts:
41
+ df.update(set(recall.salient_terms(t)))
42
+ return frozenset(term for term, n in df.items() if n >= k)
43
+
44
+
45
+ def _same_item(a_text: str, b_text: str, generic=frozenset()) -> bool:
28
46
  """Term-overlap identity: the serializer rewords constantly (run-01), so
29
47
  exact text misses twins. Shared >=3 salient terms, or >=60% of the shorter
30
- list, means same item. Short texts (<2 salient terms) never fuzzy-match
31
- the exact-text guard still catches identical ones."""
32
- a = set(recall.salient_terms(a_text))
33
- b = set(recall.salient_terms(b_text))
34
- if not a or not b:
48
+ list, means same item but only AFTER subtracting `generic` (the kind's
49
+ document-frequent vocabulary), so overlap on common words can't merge
50
+ unrelated items.
51
+
52
+ Floor: if either filtered set has <2 terms, never fuzzy-match. This blocks a
53
+ single surviving shared term from passing the ratio path (1/1 = 1.0). The
54
+ bias is deliberate and asymmetric: a false merge erases a loop and forges
55
+ its birth stamp, while a false non-merge only costs a duplicate item — so
56
+ tie-break toward NOT merging. The exact-text guard still catches identical
57
+ items regardless."""
58
+ a = set(recall.salient_terms(a_text)) - generic
59
+ b = set(recall.salient_terms(b_text)) - generic
60
+ if len(a) < 2 or len(b) < 2:
35
61
  return False
36
62
  shared = len(a & b)
37
63
  return shared >= _MIN_SHARED or shared / min(len(a), len(b)) >= _MIN_RATIO
@@ -64,6 +90,12 @@ def merge(new_cp: dict, prev_cp: dict | None, now: float,
64
90
  continue
65
91
  prev_items = (prev_cp.get(section) or {}).get(key) or []
66
92
  native_texts = {i.get("text") for i in native if isinstance(i, dict)}
93
+ # Generic vocabulary for THIS kind, from the same universe merge iterates
94
+ # (native + prev): terms this common are not identity, so dedup ignores
95
+ # them (#13). Computed once per kind, passed to every _same_item below.
96
+ generic = _generic_terms(
97
+ [str(i.get("text") or "") for i in native if isinstance(i, dict)]
98
+ + [str(i.get("text") or "") for i in prev_items if isinstance(i, dict)])
67
99
  carried = []
68
100
  for item in prev_items:
69
101
  if not isinstance(item, dict) or not str(item.get("text") or "").strip():
@@ -72,7 +104,8 @@ def merge(new_cp: dict, prev_cp: dict | None, now: float,
72
104
  if text in native_texts:
73
105
  continue # exact twin already present (idempotency)
74
106
  twin = next((n for n in native if isinstance(n, dict)
75
- and _same_item(text, str(n.get("text") or ""))), None)
107
+ and _same_item(text, str(n.get("text") or ""), generic)),
108
+ None)
76
109
  if twin is not None:
77
110
  # Session re-discussed it: the new wording wins, but the item's
78
111
  # AGE does not reset (run-01: 8-12 resets/20 cycles killed the
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "daimon-briefing"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "Dream-briefing hermes plugin: cognitive checkpoint at session end, 'while you were away' briefing at session start. Slice 1 (local-file, no Honcho)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -7,6 +7,18 @@ from daimon_briefing import carry
7
7
 
8
8
  NOW = 1_760_000_000.0 # arbitrary fixed epoch
9
9
 
10
+ # Live false-merge specimen (2026-07-02): two UNRELATED items that matched on
11
+ # exactly the generic terms {data, field, validation}. See #13.
12
+ _SPEC_A = ("First external user validation — the core adoption-arc objective "
13
+ "that unblocks _MIN_OVERLAP field data, DAIMON_TEAM validation, and "
14
+ "teammate-noise research questions")
15
+ _SPEC_B = ("Q-STALE + multi-cycle degradation validation — parked on LLM "
16
+ "budget. Need field data: what do 20 serialize cycles do to a "
17
+ "long-lived open loop, and how does that inform decay tuning?")
18
+ # Sibling native item carrying the same generic vocabulary, so {data, field,
19
+ # validation} each reach document-frequency 3 across the kind (A, sibling, B).
20
+ _SPEC_SIB = "extra validation of the data field mapping"
21
+
10
22
 
11
23
  def _iso(days_before_now):
12
24
  import datetime as dt
@@ -135,6 +147,64 @@ def test_same_item_short_texts_never_fuzzy_match():
135
147
  assert carry._same_item("ok", "ok go") is False
136
148
 
137
149
 
150
+ def test_generic_overlap_does_not_false_merge_specimen():
151
+ # Live #13 specimen: fresh native A and unrelated carried B share only the
152
+ # generic terms {data, field, validation}. B must survive as its OWN item;
153
+ # A must NOT inherit B's older birth stamp.
154
+ new = _cp("S-new", 0, questions=[
155
+ _item(_SPEC_A, days=0), _item(_SPEC_SIB, days=1)])
156
+ prev = _cp("S-prev", 1, questions=[_item(_SPEC_B, imp=7, days=45)])
157
+ out = carry.merge(new, prev, NOW)
158
+ qs = out["working_context"]["open_questions"]
159
+ texts = [q["text"] for q in qs]
160
+ assert _SPEC_B in texts # B kept, not erased
161
+ b_item = next(q for q in qs if q["text"] == _SPEC_B)
162
+ assert b_item["carried_from"] == "S-prev"
163
+ a_item = next(q for q in qs if q["text"] == _SPEC_A)
164
+ assert a_item["first_seen"] == _iso(0) # A did NOT inherit B's stamp
165
+ assert "carried_from" not in a_item
166
+ assert len(qs) == 3
167
+
168
+
169
+ def test_same_item_generic_filter_is_the_fix_not_a_threshold():
170
+ generic = frozenset({"data", "field", "validation"})
171
+ assert carry._same_item(_SPEC_A, _SPEC_B, generic) is False
172
+ assert carry._same_item(_SPEC_A, _SPEC_B) is True # unfiltered: the bug
173
+
174
+
175
+ def test_specific_twin_still_merges_and_inherits_age():
176
+ # Two rewordings sharing SPECIFIC low-DF terms must still match (run-02
177
+ # behavior): the guard filters vocabulary, not identity.
178
+ old = _item("quorint-ledger reconciliation drops entries when upstream "
179
+ "feed pauses", days=45)
180
+ new_twin = _item("quorint-ledger reconciliation still dropping entries on "
181
+ "feed pauses", days=0)
182
+ prev = _cp("S-prev", 1, questions=[
183
+ old, _item("unrelated gavotte pipeline flaking noise", days=3)])
184
+ new = _cp("S-new", 0, questions=[
185
+ new_twin, _item("tervane cache eviction unclear noise", days=1)])
186
+ out = carry.merge(new, prev, NOW)
187
+ qs = out["working_context"]["open_questions"]
188
+ twin = next(q for q in qs if "still dropping" in q["text"])
189
+ assert twin["first_seen"] == _iso(45) # matched -> age inherited
190
+ assert not any("drops entries" in q["text"] for q in qs) # not duplicated
191
+
192
+
193
+ def test_post_filter_floor_blocks_single_shared_term():
194
+ generic = frozenset({"data", "field", "validation"})
195
+ a = "data field validation alpha" # filtered -> {alpha}
196
+ b = "data field validation alpha bravo" # filtered -> {alpha, bravo}
197
+ # ratio would be 1/1 = 1.0 >= _MIN_RATIO without the floor; floor blocks it
198
+ assert carry._same_item(a, b, generic) is False
199
+
200
+
201
+ def test_generic_terms_df_boundary():
202
+ texts = ["zeta omega alpha", "zeta omega beta", "omega gamma delta"]
203
+ generic = carry._generic_terms(texts) # k defaults to _GENERIC_DF (3)
204
+ assert "omega" in generic # 3 distinct texts -> generic
205
+ assert "zeta" not in generic # exactly 2 distinct texts -> not generic
206
+
207
+
138
208
  def test_in_call_duplicate_prev_items_carry_once():
139
209
  # Two prev items with IDENTICAL text: native_texts must pick up the first
140
210
  # one as it's appended, so the second (an exact twin) is skipped too.
@@ -13,7 +13,7 @@ wheels = [
13
13
 
14
14
  [[package]]
15
15
  name = "daimon-briefing"
16
- version = "0.3.0"
16
+ version = "0.3.1"
17
17
  source = { editable = "." }
18
18
 
19
19
  [package.optional-dependencies]