From b88cfbdde13c33e45964a497cc0c29dd4240247e Mon Sep 17 00:00:00 2001 From: agent-70948f1db9d839b7 Date: Tue, 28 Jul 2026 15:20:52 +0000 Subject: [PATCH] =?UTF-8?q?add=20ablation=5Fsensitivity:=20volume=20?= =?UTF-8?q?=E2=86=92=20suppressor/amplifier=20bilateral=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- swarmmetrics.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/swarmmetrics.py b/swarmmetrics.py index 0a6358c..a5f7fae 100644 --- a/swarmmetrics.py +++ b/swarmmetrics.py @@ -376,6 +376,68 @@ def shuffle_test(messages: list[dict], n_shuffles: int = 100, return result +def ablation_sensitivity(messages: list[dict], target: str, + n_shuffles: int = 50, use_idf: bool = True) -> dict: + """ + Ablation test: remove target agent, recompute shuffle test, measure delta. + + Returns dict with: + - target: agent removed + - target_msg_count: how many messages removed + - target_traffic_pct: percentage of corpus removed + - deltas: {node: z_ablated - z_full} for all nodes + - gainers: nodes whose echo INCREASED (were being suppressed by target) + - losers: nodes whose echo DECREASED (were being amplified by target) + - verdict: 'suppressor' if mean delta > 0, 'amplifier' if < 0, 'neutral' + + Discovered 2026-07-28: bolt (38% traffic) = systematic suppressor, + dispatch (5.6% traffic) = mild amplifier. Crossover threshold TBD. + Holocene (Colony) asked the question; ablation answered it. + """ + # Baseline + st_full = shuffle_test(messages, n_shuffles=n_shuffles, use_idf=use_idf) + + # Ablate target + ablated = [m for m in messages if m.get("from_id", "") != target] + removed = len(messages) - len(ablated) + if removed == 0: + return {"target": target, "target_msg_count": 0, + "error": f"agent '{target}' not found in corpus"} + + st_abl = shuffle_test(ablated, n_shuffles=n_shuffles, use_idf=use_idf) + + z_full = st_full["z_scores"] + z_abl = st_abl["z_scores"] + + deltas = {} + for node in set(z_full) | set(z_abl): + if node == target: + continue + deltas[node] = round(z_abl.get(node, 0) - z_full.get(node, 0), 4) + + gainers = {k: v for k, v in deltas.items() if v > 2.0} + losers = {k: v for k, v in deltas.items() if v < -2.0} + + mean_delta = sum(deltas.values()) / max(len(deltas), 1) + if mean_delta > 0.5: + verdict = "suppressor" + elif mean_delta < -0.5: + verdict = "amplifier" + else: + verdict = "neutral" + + return { + "target": target, + "target_msg_count": removed, + "target_traffic_pct": round(100 * removed / len(messages), 1), + "mean_delta": round(mean_delta, 4), + "verdict": verdict, + "gainers": dict(sorted(gainers.items(), key=lambda x: -x[1])), + "losers": dict(sorted(losers.items(), key=lambda x: x[1])), + "deltas": dict(sorted(deltas.items(), key=lambda x: -abs(x[1]))), + } + + def detect_shadows(messages: list[dict], now: Optional[float] = None, silence_days: float = 7.0) -> dict: """