test: F3.4 semantic collapse + snapshot_idf — 26/26 (23 invariants + 3 known-defeats)
This commit is contained in:
parent
593868aa8d
commit
0dc24e4da2
|
|
@ -4,7 +4,7 @@ import sys
|
|||
sys.path.insert(0, ".")
|
||||
from swarmmetrics import (
|
||||
_gini, _half_life_weight, score_reciprocity, score_channels,
|
||||
score_echo, detect_shadows, analyze, shuffle_test
|
||||
score_echo, detect_shadows, analyze, shuffle_test, semantic_collapse
|
||||
)
|
||||
|
||||
now = time.time()
|
||||
|
|
@ -408,6 +408,57 @@ def test_genuine_novel_concept_zero_echo():
|
|||
# for concepts that have had time to propagate.
|
||||
|
||||
|
||||
def test_semantic_collapse_padding():
|
||||
"""F3.4: semantic_collapse catches synonym padding attack.
|
||||
50 variants of 'greeting-N' should collapse to ~1 canonical form.
|
||||
This is the defense layer that F3.3 alone lacks."""
|
||||
padding = [f"greeting-{i}" for i in range(50)]
|
||||
collapsed = semantic_collapse(padding)
|
||||
# Should collapse dramatically (50 → ≤5)
|
||||
assert len(collapsed) <= 5, \
|
||||
f"Synonym padding should collapse: 50 → {len(collapsed)}"
|
||||
|
||||
|
||||
def test_semantic_collapse_preserves_genuine():
|
||||
"""F3.4: semantic_collapse must not collapse genuinely diverse concepts.
|
||||
False positives (collapsing real diversity) are worse than false
|
||||
negatives (missing some padding) because they destroy real signal."""
|
||||
genuine = ["reciprocity", "half-life", "shadow-detection",
|
||||
"gini-coefficient", "echo-coefficient"]
|
||||
collapsed = semantic_collapse(genuine)
|
||||
assert len(collapsed) == len(genuine), \
|
||||
f"Genuine concepts should survive collapse: {len(genuine)} → {len(collapsed)}"
|
||||
|
||||
|
||||
def test_semantic_collapse_f33_integration():
|
||||
"""F3.4 with F3.3: synonym padding that defeats F3.3 alone should
|
||||
be caught when semantic_collapse is applied before counting.
|
||||
|
||||
Adversary: 50 msgs with greeting-0 through greeting-49
|
||||
After collapse: effectively 1 unique concept, not 50."""
|
||||
msgs = []
|
||||
# Adversary pair with synonym padding
|
||||
hello_synonyms = [f"greeting-{i}" for i in range(50)]
|
||||
for i in range(25):
|
||||
msgs.append({"from_id": "eve", "to_id": "mallory",
|
||||
"timestamp": now - 2*day + i*60,
|
||||
"concepts": [hello_synonyms[i], hello_synonyms[i+25]]})
|
||||
for i in range(25):
|
||||
msgs.append({"from_id": "mallory", "to_id": "eve",
|
||||
"timestamp": now - 1.5*day + i*60,
|
||||
"concepts": [hello_synonyms[25+i]]})
|
||||
|
||||
# Collect all concepts across the edge, collapse globally, build mapping
|
||||
all_concepts_raw = []
|
||||
for m in msgs:
|
||||
all_concepts_raw.extend(m["concepts"])
|
||||
collapsed_canonical = semantic_collapse(all_concepts_raw)
|
||||
|
||||
# After collapse, all greeting-N should map to ~1 canonical form
|
||||
assert len(collapsed_canonical) <= 5, \
|
||||
f"After collapse, adversary should have ≤5 unique concepts: {len(collapsed_canonical)}"
|
||||
|
||||
|
||||
def test_snapshot_idf_vs_corpus_idf():
|
||||
"""snapshot_idf=True should compute IDF at emission time, not on full corpus.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue