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, ".")
|
sys.path.insert(0, ".")
|
||||||
from swarmmetrics import (
|
from swarmmetrics import (
|
||||||
_gini, _half_life_weight, score_reciprocity, score_channels,
|
_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()
|
now = time.time()
|
||||||
|
|
@ -408,6 +408,57 @@ def test_genuine_novel_concept_zero_echo():
|
||||||
# for concepts that have had time to propagate.
|
# 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():
|
def test_snapshot_idf_vs_corpus_idf():
|
||||||
"""snapshot_idf=True should compute IDF at emission time, not on full corpus.
|
"""snapshot_idf=True should compute IDF at emission time, not on full corpus.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue