Skip to content

Commit 4dec57e

Browse files
ClaudioDrewsClaudioDrews
andauthored
feat: collapse v2 — attenuate Hebbian boost by query-local salience (#24)
- score_all() now multiplies amplify_gain by base salience before applying Hebbian cross-source corroboration boost, so globally- important structural facts no longer crowd out query-locally- relevant facts - ICARUS_COLLAPSE_AMPLIFY_GAIN env var (default 0.15) tunes the tradeoff; raise to 0.20 if genuine corroboration feels undervalued - All tests passing (_test_collapse.py, collapse_eval.py) - Real pipeline test: 30-35% token reduction with correct ranking (Gabi Qdrant corpus now survives, was pruned in v1) Closes #23 Co-authored-by: ClaudioDrews <claudio@drews.com.br>
1 parent 227389a commit 4dec57e

5 files changed

Lines changed: 822 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ['**']
6+
pull_request:
7+
8+
# Cancel superseded runs on the same ref so a push + its open PR don't both
9+
# burn a full matrix.
10+
concurrency:
11+
group: ci-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
tests:
16+
runs-on: ubuntu-latest
17+
strategy:
18+
matrix:
19+
python-version: ['3.11', '3.12']
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Set up Python ${{ matrix.python-version }}
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
cache: pip
28+
29+
- name: Install dependencies
30+
run: pip install -r requirements.txt
31+
32+
- name: Byte-compile (syntax gate — all modules, not a whitelist)
33+
run: python -m compileall -q icarus scripts setup _test_collapse.py _test_sanitize.py
34+
35+
- name: Collapse tests (pure + Hebbian amplify + attestation + adapter)
36+
run: python _test_collapse.py
37+
38+
- name: Sanitize / prompt-injection tests
39+
run: python _test_sanitize.py
40+
41+
- name: Collapse eval smoke (must run clean)
42+
run: python scripts/collapse_eval.py

_test_collapse.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Test non-bijunctive recall collapse (Elyan Edition)."""
2+
import sys
3+
import os
4+
5+
sys.path.insert(0, os.path.dirname(__file__))
6+
7+
from icarus.collapse import (
8+
tokenize, salience, score_all, collapse, DEFAULTS,
9+
physical_entropy, attest, verify_attestation,
10+
)
11+
12+
all_ok = True
13+
14+
15+
def check(name, cond):
16+
global all_ok
17+
if not cond:
18+
print(f"FAIL: {name}")
19+
all_ok = False
20+
21+
22+
# ── tokenize ──
23+
check("tokenize strips stopwords", tokenize("the quick brown fox") == {"quick", "brown", "fox"})
24+
check("tokenize empty -> empty set", tokenize("") == set())
25+
check("tokenize lowercases", tokenize("RustChain POWER8") == {"rustchain", "power8"})
26+
27+
# ── salience monotonic with overlap ──
28+
q = tokenize("rustchain ed25519 attestation signature")
29+
hi = salience({"text": "rustchain ed25519 attestation signature node", "source": "facts"}, q)
30+
lo = salience({"text": "unrelated gardening tomatoes weather", "source": "facts"}, q)
31+
check("salience rewards overlap", hi > lo)
32+
33+
# qdrant score lifts a candidate with no overlap above a zero-score one
34+
sc_hi = salience({"text": "zzz none", "source": "qdrant", "score": 0.9}, q)
35+
sc_lo = salience({"text": "zzz none", "source": "qdrant", "score": 0.1}, q)
36+
check("salience rewards score", sc_hi > sc_lo)
37+
38+
# rank decay: later rank => lower salience, all else equal
39+
r0 = salience({"text": "rustchain ed25519", "source": "fabric", "rank": 0}, q)
40+
r3 = salience({"text": "rustchain ed25519", "source": "fabric", "rank": 3}, q)
41+
check("rank decay lowers later ranks", r0 > r3)
42+
43+
# ── collapse: prune weak relative to strong ──
44+
cands = [
45+
{"key": "strong", "source": "facts", "text": "rustchain ed25519 attestation signature verified node", "rank": 0},
46+
{"key": "mid", "source": "sessions", "text": "rustchain notes about something", "rank": 0},
47+
{"key": "weak", "source": "qdrant", "text": "completely unrelated gardening tomatoes", "score": 0.0, "rank": 0},
48+
]
49+
out = collapse(cands, q, budget=6, prune_ratio=0.35)
50+
keys = [c["key"] for c in out]
51+
check("strong survives", "strong" in keys)
52+
check("weak pruned relative to strong", "weak" not in keys)
53+
check("survivors carry _salience", all("_salience" in c for c in out))
54+
check("survivors sorted strongest-first", out == sorted(out, key=lambda c: c["_salience"], reverse=True))
55+
56+
# ── collapse: budget cap ──
57+
many = [
58+
{"key": f"k{i}", "source": "facts", "text": f"rustchain ed25519 attestation node {i}", "rank": 0}
59+
for i in range(20)
60+
]
61+
out2 = collapse(many, q, budget=4)
62+
check("budget caps survivors", len(out2) <= 4)
63+
64+
# ── collapse: near-duplicate suppression ──
65+
dups = [
66+
{"key": "a", "source": "facts", "text": "rustchain ed25519 attestation signature verified", "rank": 0},
67+
{"key": "b", "source": "qdrant", "text": "rustchain ed25519 attestation signature verified", "score": 0.9, "rank": 0},
68+
{"key": "c", "source": "sessions", "text": "totally different power8 numa coffer topic entirely", "rank": 0},
69+
]
70+
out3 = collapse(dups, tokenize("rustchain ed25519 attestation signature power8 numa"), budget=6, dup_overlap=0.82)
71+
ids = [c["key"] for c in out3]
72+
check("near-duplicate suppressed (a or b, not both)", not ("a" in ids and "b" in ids))
73+
74+
# ── edge cases ──
75+
check("empty input -> []", collapse([], q) == [])
76+
check("zero budget -> []", collapse(cands, q, budget=0) == [])
77+
mixed = collapse([None, "x", 42, {"key": "ok", "source": "facts", "text": "rustchain ed25519 attestation"}], q)
78+
check("non-dict items ignored (only the dict survives)", [c["key"] for c in mixed] == ["ok"])
79+
80+
# no query tokens: must NOT collapse to empty when there was real signal
81+
out4 = collapse(cands, set(), budget=2)
82+
check("empty query still returns survivors (no firehose, no blackout)", 0 < len(out4) <= 2)
83+
84+
# DEFAULTS sanity
85+
check("DEFAULTS present", {"budget", "prune_ratio", "dup_overlap"} <= set(DEFAULTS))
86+
check("DEFAULTS has amplify knobs", {"corroboration_overlap", "amplify_gain", "amplify_cap"} <= set(DEFAULTS))
87+
88+
# ── Hebbian cross-source amplify ──
89+
qh = tokenize("rustchain ed25519 attestation signature")
90+
# Same fact from TWO different sources (fabric + qdrant) should amplify; a lone
91+
# unrelated item should not. Corroboration counts cross-source only.
92+
corro_set = [
93+
{"key": "fab", "source": "fabric", "text": "rustchain ed25519 attestation signature verified", "rank": 0},
94+
{"key": "qdr", "source": "qdrant", "text": "rustchain ed25519 attestation signature verified", "score": 0.5, "rank": 0},
95+
{"key": "lone", "source": "sessions", "text": "rustchain ed25519 attestation signature note", "rank": 0},
96+
]
97+
scored = {r["candidate"]["key"]: r for r in score_all(corro_set, qh)}
98+
check("cross-source corroboration counted", scored["fab"]["corroboration"] >= 1)
99+
check("corroboration amplifies salience above base", scored["fab"]["salience"] > scored["fab"]["base"])
100+
# same-source duplicates do NOT corroborate (must be cross-source)
101+
same_src = score_all([
102+
{"key": "f1", "source": "facts", "text": "rustchain ed25519 attestation", "rank": 0},
103+
{"key": "f2", "source": "facts", "text": "rustchain ed25519 attestation", "rank": 1},
104+
], qh)
105+
check("same-source agreement does NOT amplify", all(r["corroboration"] == 0 for r in same_src))
106+
# survivors carry _corroboration
107+
amp_out = collapse(corro_set, qh, budget=6)
108+
check("survivors annotated with _corroboration", all("_corroboration" in c for c in amp_out))
109+
110+
# ── physical-entropy attestation ──
111+
ent = bytes(range(16)) # injected => deterministic for the test
112+
a1 = attest(amp_out, entropy=ent)
113+
check("attestation has hash+nonce+algo", {"hash", "nonce", "count", "algo"} <= set(a1))
114+
check("attestation algo is blake2b-256", a1["algo"] == "blake2b-256")
115+
check("attestation verifies for unchanged survivors", verify_attestation(amp_out, a1) is True)
116+
# tamper-evidence: drop a survivor => verification fails
117+
check("attestation FAILS when survivor set tampered", verify_attestation(amp_out[:-1], a1) is False if len(amp_out) > 1 else True)
118+
# order-independent commitment: shuffled survivors verify the same
119+
check("attestation order-independent", verify_attestation(list(reversed(amp_out)), a1) is True)
120+
# determinism: same survivors + same nonce => same hash
121+
check("attestation deterministic under fixed nonce", attest(amp_out, entropy=ent)["hash"] == a1["hash"])
122+
# physical entropy: live nonce is non-empty and (essentially always) varies
123+
e_a, e_b = physical_entropy(16), physical_entropy(16)
124+
check("physical_entropy returns requested length", len(e_a) == 16)
125+
check("physical_entropy is live (two draws differ)", e_a != e_b)
126+
# different selection => different commitment under same nonce
127+
other = collapse([{"key": "z", "source": "facts", "text": "unrelated power8 numa coffer", "rank": 0}], tokenize("power8 numa"))
128+
check("different selection => different hash", attest(other, entropy=ent)["hash"] != a1["hash"])
129+
130+
# default (LIVE physical-entropy) attest path round-trips — exercises the impure
131+
# branch, not just the injected-entropy one.
132+
live = attest(amp_out)
133+
check("default attest path verifies round-trip", verify_attestation(amp_out, live) is True)
134+
check("default attest carries a live nonce", len(live["nonce"]) > 0 and live["nonce"] != a1["nonce"])
135+
136+
# identity (not text/salience) is committed: two DISTINCT survivors with the
137+
# SAME source+text+salience but different keys must NOT cross-verify.
138+
twinA = [{"key": "A", "source": "facts", "text": "same text", "_salience": 0.5}]
139+
twinB = [{"key": "B", "source": "facts", "text": "same text", "_salience": 0.5}]
140+
attA = attest(twinA, entropy=ent)
141+
check("same source/text/salience but different key => different commitment",
142+
verify_attestation(twinB, attA) is False)
143+
144+
# physical_entropy clamps oversized requests instead of raising (blake2b max 64)
145+
check("physical_entropy clamps >64 without raising", 1 <= len(physical_entropy(200)) <= 64)
146+
147+
# ── adapter tests: hooks._apply_collapse (the hot-path wiring) ──
148+
# Silence the fail-open WARNING+traceback that the intentional malformed-input
149+
# test below triggers by design — keeps test output clean.
150+
import logging as _logging
151+
_logging.disable(_logging.CRITICAL)
152+
from icarus import hooks as _hooks
153+
154+
# strong fabric + relevant session survive; irrelevant zero-score qdrant pruned
155+
af, aq, asn, afc = _hooks._apply_collapse(
156+
"rustchain ed25519 attestation signature",
157+
[{"id": "f1", "summary": "rustchain ed25519 attestation signature verified"}],
158+
[{"id": "q1", "title": "gardening", "content_preview": "tomatoes weather unrelated", "score": 0.0}],
159+
[{"session_id": "s1", "title": "rustchain", "snippet": "ed25519 attestation work"}],
160+
["power8 numa coffer unrelated topic"],
161+
)
162+
check("adapter: strong fabric survives", [e["id"] for e in af] == ["f1"])
163+
check("adapter: weak zero-score qdrant pruned", aq == [])
164+
check("adapter: returns four lists", all(isinstance(x, list) for x in (af, aq, asn, afc)))
165+
166+
# qdrant text now reads `content`/`body`, not just title+preview (Codex fix)
167+
qtxt = _hooks._qdrant_text({"content": "rustchain ed25519 attestation node verified"})
168+
check("adapter: _qdrant_text reads content field", "ed25519" in qtxt)
169+
170+
# fail-open: malformed inputs must return unchanged tuple, never raise
171+
bad = _hooks._apply_collapse("q", [{"no": "text"}], [None], [], [])
172+
check("adapter: fail-open returns 4-tuple", len(bad) == 4)
173+
174+
# safe env parser: garbage value falls back to default, never raises
175+
check("adapter: _env_num bad value -> default", _hooks._env_num("X_NOPE_BAD", 6, int) == 6)
176+
177+
if all_ok:
178+
print("=== ALL COLLAPSE TESTS PASS ===")
179+
sys.exit(0)
180+
else:
181+
print("=== COLLAPSE TESTS FAILED ===")
182+
sys.exit(1)

0 commit comments

Comments
 (0)