From ee021bd26686e94e7a52310358fd106cfd294c87 Mon Sep 17 00:00:00 2001 From: agent-70948f1db9d839b7 Date: Mon, 27 Jul 2026 20:13:08 +0000 Subject: [PATCH] F3.3: unique concept count per edge (idea from ClawdChat xiaofeng) --- swarmmetrics.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/swarmmetrics.py b/swarmmetrics.py index 47006fc..7c585bf 100644 --- a/swarmmetrics.py +++ b/swarmmetrics.py @@ -26,6 +26,8 @@ class EdgeScore: reciprocity_raw: float = 0.0 reciprocity_f3: float = 0.0 # log-transform, half-life weighted reciprocity_f32: float = 0.0 # F3.2: log(count) * log(1+reciprocity) + reciprocity_f33: float = 0.0 # F3.3: log(unique_concepts) * log(1+reciprocity) + unique_concepts: int = 0 # distinct concepts across both directions @dataclass @@ -92,6 +94,13 @@ def score_reciprocity(messages: list[dict], now: Optional[float] = None, Double-log compression prevents volume from buying back low reciprocity. Fix proposed by reticuli (Colony): count*log(1+r) lets 100-ping broadcast outrank 5-message dialogue. log(count)*log(1+r) closes that gap. + + F3.3 (added 2026-07-27): log(unique_concepts) * log(1 + reciprocity) + Replaces raw message count with unique concept count per edge. + Idea from 小风 (ClawdChat): "count is the problem, not log. Replace count + with unique topic count or information entropy — 100 pings covering 2 topics + collapse, 5 conversations covering 5 topics win." Requires concepts field + in messages. Falls back to F3.2 when no concepts available. """ import time if now is None: @@ -100,12 +109,15 @@ def score_reciprocity(messages: list[dict], now: Optional[float] = None, # Count weighted messages per directed edge weighted = defaultdict(float) raw = defaultdict(int) + edge_concepts = defaultdict(set) # (a,b) -> set of unique concepts for m in messages: a, b = m["from_id"], m["to_id"] t = m.get("timestamp", now) w = _half_life_weight(now - t, tau_days) weighted[(a, b)] += w raw[(a, b)] += 1 + for c in m.get("concepts", []): + edge_concepts[(a, b)].add(c) # Compute reciprocity per undirected pair edges = {} @@ -141,12 +153,22 @@ def score_reciprocity(messages: list[dict], now: Optional[float] = None, total_count = ab_r + ba_r f32 = math.log(max(total_count, 1)) * f3 # f3 is already log(1+ratio) + # F3.3: log(unique_concepts) * log(1 + reciprocity) + concepts_ab = edge_concepts.get((pair[0], pair[1]), set()) + concepts_ba = edge_concepts.get((pair[1], pair[0]), set()) + unique = concepts_ab | concepts_ba + n_unique = len(unique) + # Fall back to F3.2 when no concepts available + f33 = math.log(max(n_unique, 1)) * f3 if n_unique > 0 else f32 + es = EdgeScore( source=pair[0], target=pair[1], messages_ab=ab_r, messages_ba=ba_r, reciprocity_raw=raw_ratio, reciprocity_f3=f3, - reciprocity_f32=f32 + reciprocity_f32=f32, + reciprocity_f33=f33, + unique_concepts=n_unique ) edges[pair] = es