152 lines
5.8 KiB
Python
152 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Adversarial property test for the refactored chunker.
|
|
|
|
Targeted at the one invariant the docstring makes a strong claim about:
|
|
``reconstruct(chunk_text(t, m, o), o) == t.strip()`` for EVERY input,
|
|
including:
|
|
|
|
* repetitive sentences whose carried-overlap tail can coincidentally
|
|
equal a prefix of the next chunk's content (the genuine-vs-context
|
|
discriminator in ``reconstruct``);
|
|
* long repetitive-token sentences that hard-split, immediately followed
|
|
by a sentence-grouped chunk whose content begins with the same run
|
|
the overlap tail carries (genuine-overlap branch mis-taken as context);
|
|
* arbitrary inter-sentence whitespace (double space, tab, newline,
|
|
mixed runs) that the original bug collapsed.
|
|
|
|
If this exits 0, the lossless claim is backed by >= N randomised trials
|
|
across all three adversarial shapes. If it exits 1, it prints the first
|
|
counterexample with a full structural dump so the bug is reproducible.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
import sys
|
|
|
|
sys.path.insert(0, "/root/.hermes/wizard-outputs/forge")
|
|
import chunker # noqa: E402
|
|
|
|
S = chunker._OVERLAP_SENTINEL
|
|
|
|
|
|
def shape_long_rep_then_sentence(seed: int) -> str:
|
|
"""Hard-split long repetitive sentence immediately followed by a
|
|
sentence chunk whose content begins with the *same* token run as the
|
|
long sentence's tail -- forces the genuine-overlap discriminator to be
|
|
exercised on a context-overlap site."""
|
|
rng = random.Random(seed)
|
|
tok = rng.choice(["word", "x", "abc", "node"])
|
|
# long sentence of repeated tokens (no real EOS until the end)
|
|
long_sent = " ".join([tok] * rng.randint(40, 220)) + "."
|
|
# next sentence repeats the tail token so the overlap tail (last oc chars
|
|
# of the long sentence) coincides with the next chunk's prefix.
|
|
next_sent = (tok + " ") * rng.randint(2, 8) + "tail."
|
|
# vary inter-sentence whitespace
|
|
sep = rng.choice([" ", " ", "\t", "\n", "\n\n", " \n ", " \t "])
|
|
return long_sent + sep + next_sent + (sep + "More " + tok + " words.\n\n") * 2
|
|
|
|
|
|
def shape_repetitive_sentences(seed: int) -> str:
|
|
"""Repeated sentence groups (the docstring's primary adversarial class)."""
|
|
rng = random.Random(seed)
|
|
groups = []
|
|
for _ in range(rng.randint(10, 60)):
|
|
g = rng.choice(
|
|
[
|
|
"Sentence one. ",
|
|
"Sentence two. ",
|
|
"Sentence three. ",
|
|
"Alpha beta. ",
|
|
"Gamma delta. ",
|
|
]
|
|
)
|
|
# occasionally insert weird interior whitespace
|
|
if rng.random() < 0.3:
|
|
g = g.replace(" ", rng.choice([" ", "\t", " \n "]))
|
|
groups.append(g)
|
|
return "".join(groups)
|
|
|
|
|
|
def shape_whitespace_variety(seed: int) -> str:
|
|
"""Lots of short sentences joined by weird whitespace runs."""
|
|
rng = random.Random(seed)
|
|
sents = ["Hello world", "How are you", "I am fine", "Test sentence"]
|
|
seps = [" ", " ", " ", "\t", "\n", "\n\n", " \n ", " \t "]
|
|
parts = []
|
|
for _ in range(rng.randint(5, 40)):
|
|
parts.append(rng.choice(sents))
|
|
parts.append(rng.choice(seps))
|
|
# trailing whitespace to exercise strip boundary
|
|
parts.append(rng.choice(seps))
|
|
return "".join(parts)
|
|
|
|
|
|
def shape_fuzz(seed: int) -> str:
|
|
rng = random.Random(seed)
|
|
words = ["alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
|
|
"golf", "hotel", "india", "juliet"]
|
|
seps = [" ", " ", " ", "\t", "\n", "\n\n"]
|
|
parts = []
|
|
n = rng.randint(1, 8)
|
|
for _ in range(n):
|
|
nw = rng.randint(2, 12)
|
|
parts.append(" ".join(rng.choice(words) for _ in range(nw)))
|
|
parts.append(rng.choice([".", ".", ".", "?", "!", "..."]))
|
|
parts.append(rng.choice(seps))
|
|
return "".join(parts).strip() + rng.choice(["", "", " ", " \n "])
|
|
|
|
|
|
def check(text: str, mt: int, ot: int) -> bool:
|
|
if not text.strip():
|
|
return True
|
|
expect = text.strip()
|
|
cs = chunker.chunk_text(text, max_tokens=mt, overlap_tokens=ot)
|
|
rec = chunker.reconstruct(cs, ot)
|
|
return rec == expect
|
|
|
|
|
|
def main() -> int:
|
|
shapes = [shape_long_rep_then_sentence, shape_repetitive_sentences,
|
|
shape_whitespace_variety, shape_fuzz]
|
|
mts = [8, 10, 12, 16, 20, 24, 32, 40, 48, 64, 72, 100, 128]
|
|
ots = [0, 1, 2, 3, 4, 6, 8, 12, 16]
|
|
trials_per_shape = 600
|
|
total = 0
|
|
fails = 0
|
|
for si, shape in enumerate(shapes):
|
|
for i in range(trials_per_shape):
|
|
seed = si * 100000 + i
|
|
text = shape(seed)
|
|
mt = random.Random(seed).choice(mts)
|
|
ot = random.Random(seed + 1).choice(ots)
|
|
if ot >= mt:
|
|
ot = max(0, mt - 1)
|
|
total += 1
|
|
if not check(text, mt, ot):
|
|
fails += 1
|
|
print(f"FAIL shape={shape.__name__} seed={seed} mt={mt} ot={ot}")
|
|
print(f" text(:120)={text[:120]!r}... len={len(text)}")
|
|
cs = chunker.chunk_text(text, max_tokens=mt, overlap_tokens=ot)
|
|
rec = chunker.reconstruct(cs, ot)
|
|
exp = text.strip()
|
|
for j in range(min(len(rec), len(exp))):
|
|
if rec[j] != exp[j]:
|
|
print(f" FIRST DIFF at {j}")
|
|
print(f" exp: {exp[max(0,j-25):j+25]!r}")
|
|
print(f" rec: {rec[max(0,j-25):j+25]!r}")
|
|
break
|
|
else:
|
|
print(f" len exp={len(exp)} rec={len(rec)} "
|
|
f"(common prefix OK, lengths differ)")
|
|
if fails >= 3:
|
|
print("Stopping after 3 failures.")
|
|
return 1
|
|
print(f"trials={total} fails={fails}")
|
|
print("PASS: lossless round-trip across all adversarial shapes."
|
|
if fails == 0 else "FAIL: invariant violated.")
|
|
return 0 if fails == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|