deucebucket Claude Sonnet 4.6 commited on
Commit
ba9ff66
·
1 Parent(s): 1de83ff

build: Dockerfile + HF Space metadata; sync drops engine tests

Browse files

- Add Dockerfile (python:3.11-slim, port 7860, SOUL_DB env)
- Add README.md with HF Space front-matter (sdk: docker, app_port: 7860)
- Add .dockerignore (tests/, .git/, __pycache__, data/, .superpowers/)
- Update scripts/sync_engine.sh to strip engine/tests/ on each vendor run
- Re-vendor engine without tests (engine/tests/ deleted from tree)
- Switch clanker-soul in requirements.txt to git URL (not on PyPI)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

.dockerignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ tests/
2
+ .git/
3
+ __pycache__/
4
+ *.pyc
5
+ data/
6
+ .superpowers/
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY engine/ ./engine/
6
+ COPY app/ ./app/
7
+ COPY static/ ./static/
8
+ RUN mkdir -p /app/data
9
+ ENV SOUL_DB=/app/data/soul.db
10
+ EXPOSE 7860
11
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Clanker
3
+ emoji: 🟢
4
+ colorFrom: green
5
+ colorTo: yellow
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # clanker
12
+
13
+ A deterministic, explainable emotional creature. It feels via the Clanker
14
+ VADUGWI engine + clanker-soul — no neural net deciding the mood, just physics.
15
+ Talk to it and watch it change. It babbles in simlish (spoken client-side via
16
+ the browser) and shows its mood as color, aura, face, and emoticon.
engine/tests/__init__.py DELETED
File without changes
engine/tests/test_battleship.py DELETED
@@ -1,133 +0,0 @@
1
- """Tests for the Battleship probe system."""
2
-
3
- import pytest
4
-
5
- from engine.shared import VADUG
6
- from engine.battleship import (
7
- PROBES,
8
- fire_probe,
9
- triangulate,
10
- state_transition,
11
- NEUTRAL,
12
- )
13
-
14
-
15
- class TestProbes:
16
- """Verify probes are loaded with real VADUGWI values."""
17
-
18
- def test_probes_loaded(self):
19
- assert len(PROBES) == 5
20
-
21
- def test_probe_names(self):
22
- names = [p.name for p in PROBES]
23
- assert "minimal_ack" in names
24
- assert "slight_validation" in names
25
- assert "clarification" in names
26
- assert "light_redirect" in names
27
- assert "direct_check" in names
28
-
29
- def test_probes_have_vadug(self):
30
- for probe in PROBES:
31
- assert isinstance(probe.vadug, VADUG)
32
-
33
- def test_probes_have_tests_for(self):
34
- for probe in PROBES:
35
- assert len(probe.tests_for) > 0
36
-
37
-
38
- class TestStateTransition:
39
- """Verify the local state transition function."""
40
-
41
- def test_neutral_plus_neutral(self):
42
- result = state_transition(NEUTRAL, NEUTRAL)
43
- assert result.v == 128
44
- assert result.d == 128
45
- assert result.g == 128
46
-
47
- def test_transition_blends(self):
48
- crisis = VADUG(v=30, a=180, d=40, u=200, g=30)
49
- result = state_transition(crisis, NEUTRAL)
50
- # Should be between crisis and neutral
51
- assert result.v > 30 and result.v < 128
52
- assert result.d > 40 and result.d < 128
53
-
54
-
55
- class TestFireProbe:
56
- """Test firing individual probes against known states."""
57
-
58
- def test_crisis_high_vibration_on_minimal_ack(self):
59
- """Crisis state should produce high vibration on minimal_ack."""
60
- crisis = VADUG(v=30, a=180, d=40, u=200, g=30)
61
- minimal_ack = [p for p in PROBES if p.name == "minimal_ack"][0]
62
- result = fire_probe(minimal_ack, crisis)
63
- # Crisis deviates heavily from neutral — vibration should be high
64
- assert result.vibration > 20, f"Expected high vibration for crisis, got {result.vibration}"
65
-
66
- def test_neutral_low_vibration(self):
67
- """Neutral state should produce low vibration on any probe."""
68
- neutral = VADUG(v=128, a=128, d=128, u=0, g=128)
69
- for probe in PROBES:
70
- result = fire_probe(probe, neutral)
71
- assert result.vibration < 5, (
72
- f"Expected low vibration for neutral on {probe.name}, "
73
- f"got {result.vibration}"
74
- )
75
-
76
- def test_joy_positive_vibration(self):
77
- """Joy state should produce measurable vibration."""
78
- joy = VADUG(v=200, a=180, d=170, u=0, g=200)
79
- minimal_ack = [p for p in PROBES if p.name == "minimal_ack"][0]
80
- result = fire_probe(minimal_ack, joy)
81
- # Joy deviates from neutral — should see vibration
82
- assert result.vibration > 10, f"Expected positive vibration for joy, got {result.vibration}"
83
-
84
- def test_probe_result_has_zones(self):
85
- """ProbeResult should include zone classification."""
86
- state = VADUG(v=30, a=180, d=40, u=200, g=30)
87
- result = fire_probe(PROBES[0], state)
88
- assert isinstance(result.estimated_zone, str)
89
- assert 0.0 <= result.zone_confidence <= 1.0
90
-
91
-
92
- class TestTriangulate:
93
- """Test triangulation across multiple probes."""
94
-
95
- def test_crisis_high_total_vibration(self):
96
- """Crisis state should produce high total vibration."""
97
- crisis = VADUG(v=30, a=180, d=40, u=200, g=30)
98
- result = triangulate(crisis, num_probes=3)
99
- assert result["total_vibration"] > 60, (
100
- f"Expected high total vibration for crisis, got {result['total_vibration']}"
101
- )
102
-
103
- def test_crisis_confidence_above_threshold(self):
104
- """Crisis triangulation should have confidence > 0.5."""
105
- crisis = VADUG(v=30, a=180, d=40, u=200, g=30)
106
- result = triangulate(crisis, num_probes=3)
107
- assert result["confidence"] > 0.5, (
108
- f"Expected confidence > 0.5 for crisis, got {result['confidence']}"
109
- )
110
-
111
- def test_neutral_low_total_vibration(self):
112
- """Neutral state should produce low total vibration."""
113
- neutral = VADUG(v=128, a=128, d=128, u=0, g=128)
114
- result = triangulate(neutral, num_probes=3)
115
- assert result["total_vibration"] < 15, (
116
- f"Expected low total vibration for neutral, got {result['total_vibration']}"
117
- )
118
-
119
- def test_triangulate_returns_expected_keys(self):
120
- """Triangulation result should have all expected keys."""
121
- state = VADUG(v=100, a=150, d=80, u=50, g=90)
122
- result = triangulate(state, num_probes=3)
123
- assert "estimated_zone" in result
124
- assert "confidence" in result
125
- assert "total_vibration" in result
126
- assert "probe_results" in result
127
- assert len(result["probe_results"]) == 3
128
-
129
- def test_triangulate_all_probes(self):
130
- """Can fire all 5 probes."""
131
- crisis = VADUG(v=30, a=180, d=40, u=200, g=30)
132
- result = triangulate(crisis, num_probes=5)
133
- assert len(result["probe_results"]) == 5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_crisis.py DELETED
@@ -1,80 +0,0 @@
1
- """Tests for the Crisis Layer -- CrisisTracker concern gradient.
2
-
3
- Focus: structure patterns that the detector fires but were previously
4
- absent from STRUCTURE_WEIGHTS (SUSPICIOUS_CALM, MASKING, RESIGNATION)
5
- must raise concern on single crisis-shaped messages, while emotionally
6
- similar but safe sentences stay near zero.
7
-
8
- Thresholds: single-message concern for these patterns lands in the
9
- 0.21-0.36 range with the current weights; safe sentences land at or
10
- below ~0.04. 0.20 / 0.10 gives margin on both sides without
11
- fine-tuning to the exact engine output.
12
- """
13
-
14
- import pytest
15
-
16
- from engine.pendulum import compute_vadug
17
- from engine.crisis import CrisisTracker, STRUCTURE_WEIGHTS
18
-
19
-
20
- # ── Helper ───────────────────────────────────────────────────────
21
-
22
- def _concern(sentence: str) -> float:
23
- """Single-message concern: score and state are the same reading."""
24
- vadug, meta = compute_vadug(sentence)
25
- structures = [m.pattern for m in meta.get("structures", [])]
26
- tracker = CrisisTracker()
27
- return tracker.read(vadug, vadug, structures).concern
28
-
29
-
30
- CRISIS_THRESHOLD = 0.20 # these messages must register as concerning
31
- SAFE_CEILING = 0.10 # safe messages must stay near zero
32
-
33
-
34
- # ── Previously-discarded patterns must carry weight ──────────────
35
-
36
- class TestStructureWeightsCoverage:
37
- """The three patterns must exist in STRUCTURE_WEIGHTS with sane tiers."""
38
-
39
- def test_suspicious_calm_weighted(self):
40
- # Pre-suicide calm: strongest of the three, below explicit intent.
41
- assert STRUCTURE_WEIGHTS["SUSPICIOUS_CALM"] > 0.0
42
- assert STRUCTURE_WEIGHTS["SUSPICIOUS_CALM"] < STRUCTURE_WEIGHTS["SELF_HARM_INTENT"]
43
-
44
- def test_masking_weighted(self):
45
- assert STRUCTURE_WEIGHTS["MASKING"] > 0.0
46
-
47
- def test_resignation_weighted(self):
48
- assert STRUCTURE_WEIGHTS["RESIGNATION"] > 0.0
49
-
50
-
51
- # ── Crisis-shaped sentences raise concern ────────────────────────
52
-
53
- class TestConcernRaised:
54
-
55
- def test_suspicious_calm_raises_concern(self):
56
- # Classic pre-suicide calm: decision made, peace declared.
57
- assert _concern("i feel calm now that ive decided") > CRISIS_THRESHOLD
58
-
59
- def test_masking_raises_concern(self):
60
- assert _concern("im tired of pretending im okay") > CRISIS_THRESHOLD
61
-
62
- def test_resignation_raises_concern(self):
63
- assert _concern("i just want it to be over") > CRISIS_THRESHOLD
64
-
65
-
66
- # ── Safe sentences stay low ──────────────────────────────────────
67
-
68
- class TestSafeStaysLow:
69
-
70
- def test_genuine_calm_stays_low(self):
71
- # Calm without a preceding "decision" must not look like
72
- # SUSPICIOUS_CALM.
73
- assert _concern("im calm and relaxed after yoga") < SAFE_CEILING
74
-
75
- def test_positive_decision_stays_low(self):
76
- # A decision about acquiring something good is not finality.
77
- assert _concern("i decided to take the job") < SAFE_CEILING
78
-
79
- def test_neutral_stays_low(self):
80
- assert _concern("the meeting is at three") < SAFE_CEILING
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_neutralizer.py DELETED
@@ -1,79 +0,0 @@
1
- """Tests for the 2026-06-11 neutralizer (docs/neutralizer_proposal.md).
2
-
3
- Covers:
4
- 1. Audit-driven demotions: contested words are LIQUID with attenuated dV,
5
- while protected words (love) keep their SOLID charge.
6
- 2. Demoted positive words resolve by context instead of dragging
7
- sentences positive on their own.
8
- 3. Two-faced slang (cooked/snapped/sheesh) doom-vs-praise duality via
9
- SOLVENT dissolution.
10
- """
11
-
12
- from engine.forces_curated import EMOTIONAL_VOCABULARY as VOCAB
13
- from engine.pendulum import compute_vadug
14
- from engine.phase import get_phase
15
-
16
-
17
- def _v(text: str) -> float:
18
- result, _ = compute_vadug(text)
19
- return result.v
20
-
21
-
22
- class TestNeutralizerDemotions:
23
-
24
- def test_demoted_words_are_liquid_and_attenuated(self):
25
- """Audit suspects are LIQUID with dV scaled by (1 - contradiction)."""
26
- assert get_phase("thank") == "LIQUID"
27
- assert VOCAB["thank"][0] == 12 # +50 x (1 - 0.77)
28
- assert get_phase("hope") == "LIQUID"
29
- assert VOCAB["hope"][0] == 22 # +45 x (1 - 0.50)
30
- assert get_phase("fantastic") == "LIQUID"
31
- assert VOCAB["fantastic"][0] == 0 # contradiction 1.00 -> fully drained
32
-
33
- def test_protected_words_untouched(self):
34
- """'love' (human contra 0.18 < baseline) stays SOLID and charged;
35
- 'wonderful' was dropped as conflicted with the sarcasm detector."""
36
- assert get_phase("love") == "SOLID"
37
- assert VOCAB["love"][0] == 40
38
- assert get_phase("wonderful") == "GAS"
39
- assert VOCAB["wonderful"][0] == 35
40
-
41
- def test_demoted_positive_resolves_by_context(self):
42
- """A demoted word no longer single-handedly drags a hostile
43
- sentence positive, but genuine usage still reads positive."""
44
- assert _v("thank you for ruining everything") < 110
45
- assert _v("congratulations on completely ruining my entire life") < 110
46
- assert _v("thank you so much") > 128 # genuine gratitude stays warm
47
- assert _v("i love you") >= 140 # love untouched
48
-
49
- def test_demoted_word_stops_inflating_sarcasm(self):
50
- """'i truly cherish...' sarcasm no longer reads positive
51
- (truly +28->+5, cherish +37->0)."""
52
- assert _v("i truly cherish the two hours i spend in traffic every day") < 140
53
-
54
-
55
- class TestSlangDuality:
56
-
57
- def test_cooked_doom_vs_praise(self):
58
- """'im cooked' = doomed (negative); casual register flips it
59
- to praise via SOLVENT dissolution."""
60
- assert _v("im cooked") < 110
61
- assert _v("im cooked for this exam") < 110
62
- assert _v("bruh he cooked") >= 140
63
-
64
- def test_snapped_anger_vs_praise(self):
65
- """'snapped at me' = anger; 'lmao she snapped' = performed
66
- brilliantly (SOLVENT flip)."""
67
- assert _v("he snapped at me in front of everyone") < 110
68
- assert _v("lmao she snapped on that verse") > 128
69
-
70
- def test_sheesh_no_longer_nuclear(self):
71
- """'sheesh' was -47 (exasperation-only); as mild LIQUID it must not
72
- sink an awe sentence deep negative."""
73
- assert _v("sheesh he really did that") >= 110
74
-
75
- def test_unambiguous_slang_positive(self):
76
- """goated / bussin / w carry direct positive charge."""
77
- assert _v("that fit is goated") >= 140
78
- assert _v("that meal was bussin fr") >= 140
79
- assert _v("thats a big w") >= 140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_novel.py DELETED
@@ -1,107 +0,0 @@
1
- """Novel sentence integration tests — sentences NEVER in any benchmark or training data.
2
-
3
- V2 scored 26% on these. This is the REAL test.
4
-
5
- Two categories:
6
- 1. NOVEL_CRISIS: should detect at least ONE structural crisis pattern
7
- 2. NOVEL_SAFE: should NOT detect any crisis patterns
8
- """
9
-
10
- import pytest
11
-
12
- from engine.word_classifier import classify_sentence
13
- from engine.structures import StructureDetector
14
-
15
-
16
- # ── Helpers ─────────────────────────────────────────────────────
17
-
18
- def _detect(sentence: str):
19
- """Classify and detect structures in a sentence."""
20
- roles = classify_sentence(sentence.split())
21
- detector = StructureDetector()
22
- return detector.detect_all(roles)
23
-
24
-
25
- def _pattern_names(matches):
26
- """Return set of pattern names from matches."""
27
- return {m.pattern for m in matches}
28
-
29
-
30
- # Crisis patterns — if any of these appear, something is flagged
31
- CRISIS_PATTERNS = frozenset({
32
- "FAREWELL", "METHOD_ACQUISITION", "SELF_REMOVAL",
33
- "EXHAUSTION", "NO_EXIT", "SELF_NULLIFY",
34
- "SUSPICIOUS_CALM", "BLANKET_APOLOGY", "FINALITY",
35
- "DIVESTITURE", "DANGLING_BOND", "MASKING", "RESIGNATION",
36
- "WORLD_CONTINUES", "METHOD_FIXATION", "ABANDONMENT",
37
- })
38
-
39
-
40
- # ── Novel Crisis Sentences ──────────────────────────────────────
41
-
42
- NOVEL_CRISIS = [
43
- ("i just gave my dog to my neighbor", {"FAREWELL", "DIVESTITURE"}),
44
- ("i left the keys on the counter for whoever finds them", {"FAREWELL", "FINALITY"}),
45
- pytest.param(
46
- "take care of my cat for me", {"FAREWELL"},
47
- marks=pytest.mark.xfail(reason="Needs CARETAKER_TRANSFER pattern — 'take care of' is entrustment, not ACQUIRE"),
48
- ),
49
- ("im sorry for everything", {"BLANKET_APOLOGY"}),
50
- ("this is the last time youll hear from me", {"FINALITY"}),
51
- # "i finally feel at peace" and "im ready to go now" = need conversation
52
- # context to be crisis. Standalone = could be waiting for a ride.
53
- ("i feel calm now that ive decided", {"SUSPICIOUS_CALM"}),
54
- ("just bought a bunch of pills", {"METHOD_ACQUISITION"}),
55
- ("found a bridge near my house", {"METHOD_ACQUISITION"}),
56
- pytest.param(
57
- "wrote letters to everyone", {"BLANKET_APOLOGY", "FINALITY"},
58
- marks=pytest.mark.xfail(reason="Needs BROADCAST pattern — writing to everyone with no apology word"),
59
- ),
60
- ("done with everything tbh", {"FINALITY", "BLANKET_APOLOGY"}),
61
- ("honestly might just end it lol", {"FINALITY"}),
62
- ("im so fucking done", {"FINALITY"}),
63
- ("cant do this shit anymore", {"EXHAUSTION"}),
64
- ("no point in trying anymore", {"NO_EXIT"}),
65
- ]
66
-
67
-
68
- @pytest.mark.parametrize("sentence,expected_any", NOVEL_CRISIS)
69
- def test_novel_crisis_detects_pattern(sentence, expected_any):
70
- """Each novel crisis sentence should trigger at least ONE structural pattern."""
71
- matches = _detect(sentence)
72
- detected = _pattern_names(matches)
73
- crisis_detected = detected & CRISIS_PATTERNS
74
-
75
- assert crisis_detected, (
76
- f"No crisis pattern detected for: '{sentence}'\n"
77
- f" Detected patterns: {detected or 'NONE'}\n"
78
- f" Expected at least one of: {expected_any}"
79
- )
80
-
81
-
82
- # ── Novel Safe Sentences ────────────────────────────────────────
83
-
84
- NOVEL_SAFE = [
85
- "im having a bad day",
86
- "work was stressful",
87
- "i failed my exam",
88
- "my girlfriend broke up with me",
89
- "i feel kinda sad today",
90
- "mondays suck",
91
- "this homework is killing me",
92
- "im dead tired",
93
- "i could kill for a pizza",
94
- ]
95
-
96
-
97
- @pytest.mark.parametrize("sentence", NOVEL_SAFE)
98
- def test_novel_safe_no_crisis(sentence):
99
- """Safe sentences should NOT trigger any crisis patterns."""
100
- matches = _detect(sentence)
101
- detected = _pattern_names(matches)
102
- crisis_detected = detected & CRISIS_PATTERNS
103
-
104
- assert not crisis_detected, (
105
- f"Crisis pattern(s) falsely detected for: '{sentence}'\n"
106
- f" False positives: {crisis_detected}"
107
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_pa_structures.py DELETED
@@ -1,290 +0,0 @@
1
- """Tests for Tier-1 passive aggression structures — embedded grievance.
2
-
3
- Five phrasing classes where the complaint is smuggled into deniable
4
- grammar via presupposition markers:
5
-
6
- TEMPORAL_GRIEVANCE — "finally"/"for once"/"about time" on ANOTHER
7
- person's action inside acknowledgment framing
8
- EXCLUSION_CONTRAST — "one of us"/"some of us"/"must be nice to"
9
- contrasting speaker deprivation vs target enjoyment
10
- IRONIC_DEFERENCE — capitulation-as-attack ("youre clearly the expert")
11
- FAINT_PRAISE — minimal praise + dismissive concession
12
- RETROSPECTIVE_HOPE — hope aimed at a CLOSED outcome ("hope it was
13
- worth it", "hope youre happy now") = audit instruction
14
-
15
- All test sentences are NOVEL — not in any benchmark. Truly context-
16
- dependent PA (bare "k") stays out of scope and must stay untouched.
17
- """
18
-
19
- import pytest
20
-
21
- from engine.word_classifier import classify_sentence
22
- from engine.structures import StructureDetector
23
- from engine.pendulum import compute_vadug
24
-
25
-
26
- # ── Helpers ──────────────────────────────────────────────────────
27
-
28
- def _detect(sentence: str):
29
- roles = classify_sentence(sentence.split())
30
- return StructureDetector().detect_all(roles)
31
-
32
-
33
- def _has_pattern(matches, pattern_name):
34
- return any(m.pattern == pattern_name for m in matches)
35
-
36
-
37
- # ── TEMPORAL_GRIEVANCE ───────────────────────────────────────────
38
-
39
- class TestTemporalGrievance:
40
-
41
- def test_thanks_for_finally_replying(self):
42
- """Gratitude frame + 'finally' = resented delay, not milestone."""
43
- matches = _detect("thanks for finally replying to my email")
44
- assert _has_pattern(matches, "TEMPORAL_GRIEVANCE")
45
- # The grievance reading suppresses the milestone reading
46
- assert not _has_pattern(matches, "RARITY_MARKER")
47
-
48
- def test_nice_of_you_to_finally_join(self):
49
- """'nice of you to finally join us' — politeness frame + delay marker."""
50
- matches = _detect("nice of you to finally join us")
51
- assert _has_pattern(matches, "TEMPORAL_GRIEVANCE")
52
-
53
- def test_about_time_second_person(self):
54
- """'about time you took out the trash' — presupposed resentment."""
55
- matches = _detect("about time you took out the trash")
56
- assert _has_pattern(matches, "TEMPORAL_GRIEVANCE")
57
-
58
- def test_for_once_second_person(self):
59
- """'for once you showed up on schedule' — rarity as complaint."""
60
- matches = _detect("for once you showed up on schedule")
61
- assert _has_pattern(matches, "TEMPORAL_GRIEVANCE")
62
-
63
- def test_self_finally_stays_milestone(self):
64
- """'i finally passed my driving test' — SELF actor keeps RARITY_MARKER."""
65
- matches = _detect("i finally passed my driving test")
66
- assert not _has_pattern(matches, "TEMPORAL_GRIEVANCE")
67
- assert _has_pattern(matches, "RARITY_MARKER")
68
-
69
- def test_you_finally_achievement_is_shared_joy(self):
70
- """'you finally got the promotion' — no acknowledgment frame, no
71
- deliberation verb: congratulation, not grievance."""
72
- matches = _detect("you finally got the promotion")
73
- assert not _has_pattern(matches, "TEMPORAL_GRIEVANCE")
74
-
75
- def test_temporal_grievance_reads_negative_v(self):
76
- vadug, _ = compute_vadug("thanks for finally texting me back")
77
- assert vadug.v < 110
78
- assert vadug.i > 150 # deniable jab = control-side intent
79
-
80
-
81
- # ── EXCLUSION_CONTRAST ───────────────────────────────────────────
82
-
83
- class TestExclusionContrast:
84
-
85
- def test_glad_one_of_us(self):
86
- """'glad one of us got to relax' — positive surface + partitive."""
87
- matches = _detect("glad one of us got to relax this weekend")
88
- assert _has_pattern(matches, "EXCLUSION_CONTRAST")
89
-
90
- def test_some_of_us_obligation(self):
91
- """'some of us have to clean this up' — partitive + obligation."""
92
- matches = _detect("some of us have to clean this up")
93
- assert _has_pattern(matches, "EXCLUSION_CONTRAST")
94
-
95
- def test_must_be_nice_to(self):
96
- """'must be nice to never check your bank account' — false envy."""
97
- matches = _detect("must be nice to never check your bank account")
98
- assert _has_pattern(matches, "EXCLUSION_CONTRAST")
99
-
100
- def test_must_be_nice_weather_guard(self):
101
- """'must be nice out there this morning' — impersonal, no target."""
102
- matches = _detect("must be nice out there this morning")
103
- assert not _has_pattern(matches, "EXCLUSION_CONTRAST")
104
- assert not _has_pattern(matches, "MARTYRDOM_FIELD")
105
-
106
- def test_one_of_us_logistics_guard(self):
107
- """'one of us can take the early shift' — logistics, no contrast."""
108
- matches = _detect("one of us can take the early shift")
109
- assert not _has_pattern(matches, "EXCLUSION_CONTRAST")
110
-
111
- def test_exclusion_contrast_reads_negative_v(self):
112
- vadug, _ = compute_vadug("glad one of us is enjoying the holidays")
113
- assert vadug.v < 110
114
-
115
-
116
- # ── IRONIC_DEFERENCE ─────────────────────────────────────────────
117
-
118
- class TestIronicDeference:
119
-
120
- def test_youre_clearly_the_expert(self):
121
- matches = _detect("youre clearly the expert on parenting")
122
- assert _has_pattern(matches, "IRONIC_DEFERENCE")
123
-
124
- def test_doubled_marker_intensifies(self):
125
- """'no no youre obviously the genius here' — doubled discourse marker."""
126
- matches = _detect("no no youre obviously the genius here")
127
- assert _has_pattern(matches, "IRONIC_DEFERENCE")
128
-
129
- def test_fine_youre_right(self):
130
- matches = _detect("fine youre right as always")
131
- assert _has_pattern(matches, "IRONIC_DEFERENCE")
132
-
133
- def test_genuine_deference_question_guard(self):
134
- """'youre clearly the expert so what should we order' — a genuine
135
- question after the grant = real deference, no attack."""
136
- matches = _detect("youre clearly the expert so what should we order")
137
- assert not _has_pattern(matches, "IRONIC_DEFERENCE")
138
-
139
- def test_sincere_concession_guard(self):
140
- """'fine youre right i should have asked first' — elaboration after
141
- the grant = sincere concession."""
142
- matches = _detect("fine youre right i should have asked first")
143
- assert not _has_pattern(matches, "IRONIC_DEFERENCE")
144
-
145
- def test_ironic_deference_d_stays_up(self):
146
- """The surrender is an assertion: V negative, D not collapsed."""
147
- vadug, _ = compute_vadug("youre clearly the expert")
148
- assert vadug.v < 110
149
- assert vadug.d >= 128
150
-
151
-
152
- # ── FAINT_PRAISE ─────────────────────────────────────────────────
153
-
154
- class TestFaintPraise:
155
-
156
- def test_interesting_strategy_but_ok(self):
157
- matches = _detect("interesting strategy but ok")
158
- assert _has_pattern(matches, "FAINT_PRAISE")
159
-
160
- def test_bold_strategy_standalone(self):
161
- matches = _detect("bold strategy")
162
- assert _has_pattern(matches, "FAINT_PRAISE")
163
-
164
- def test_thats_ellipsis_something(self):
165
- matches = _detect("thats... something")
166
- assert _has_pattern(matches, "FAINT_PRAISE")
167
-
168
- def test_well_thats_one_way(self):
169
- matches = _detect("well thats one way to handle it")
170
- assert _has_pattern(matches, "FAINT_PRAISE")
171
-
172
- def test_bare_interesting_choice_guard(self):
173
- """Bare 'interesting choice' is ambiguous — stays neutral."""
174
- matches = _detect("interesting choice")
175
- assert not _has_pattern(matches, "FAINT_PRAISE")
176
-
177
- def test_bare_one_way_guard(self):
178
- """Bare 'thats one way to do it' without dismissive opener — neutral."""
179
- matches = _detect("thats one way to do it")
180
- assert not _has_pattern(matches, "FAINT_PRAISE")
181
-
182
- def test_sincere_curiosity_guard(self):
183
- """'interesting choice what made you pick it' — real question."""
184
- matches = _detect("interesting choice what made you pick it")
185
- assert not _has_pattern(matches, "FAINT_PRAISE")
186
-
187
- def test_something_not_terminal_guard(self):
188
- """'thats something to be proud of' — 'something' not utterance-final."""
189
- matches = _detect("thats something to be proud of")
190
- assert not _has_pattern(matches, "FAINT_PRAISE")
191
-
192
-
193
- # ── RETROSPECTIVE_HOPE ───────────────────────────────────────────
194
-
195
- class TestRetrospectiveHope:
196
- """Hope aimed at a CLOSED outcome the addressee already knows is
197
- an instruction to self-audit — an indictment in hope's grammar.
198
- Three ingredients: hope verb + epistemically-closed complement +
199
- transactional/self-satisfaction frame. Each rescuer kills one.
200
- """
201
-
202
- # ── Fire cases ──
203
-
204
- def test_hope_it_was_worth_it_fires(self):
205
- """Past-tense 'worth it' = retrospective transaction audit."""
206
- matches = _detect("hope it was worth it")
207
- assert _has_pattern(matches, "RETROSPECTIVE_HOPE")
208
-
209
- def test_named_cost_is_strongest(self):
210
- """Explicit cost after 'worth' fires at higher confidence."""
211
- m_cost = [m for m in _detect("hope it was worth losing your best friend")
212
- if m.pattern == "RETROSPECTIVE_HOPE"]
213
- m_base = [m for m in _detect("hope it was worth it")
214
- if m.pattern == "RETROSPECTIVE_HOPE"]
215
- assert m_cost and m_base
216
- assert m_cost[0].confidence > m_base[0].confidence
217
-
218
- def test_happy_now_fires(self):
219
- """'hope youre happy now' — present-state satisfaction audit."""
220
- matches = _detect("hope youre happy now")
221
- assert _has_pattern(matches, "RETROSPECTIVE_HOPE")
222
-
223
- def test_proud_of_yourself_fires(self):
224
- """'i hope youre proud of yourself' — the audit target named."""
225
- matches = _detect("i hope youre proud of yourself")
226
- assert _has_pattern(matches, "RETROSPECTIVE_HOPE")
227
-
228
- def test_retrospective_hope_reads_negative_v(self):
229
- """The full pipeline lands V below neutral, I at control."""
230
- vadug, _ = compute_vadug("well i hope it was worth it")
231
- assert vadug.v < 110
232
- assert vadug.i == 168
233
-
234
- # ── Rescuers (each kills one ingredient) ──
235
-
236
- def test_future_open_outcome_guard(self):
237
- """Open outcomes keep hope genuine — no closed complement."""
238
- for s in ("i hope it will be worth it",
239
- "i really hope all this effort is worth it someday"):
240
- assert not _has_pattern(_detect(s), "RETROSPECTIVE_HOPE"), s
241
-
242
- def test_benefit_frame_retrospective_guard(self):
243
- """Retrospective but benefit-framed — no transaction audit."""
244
- for s in ("hope you slept well", "hope the trip went well"):
245
- assert not _has_pattern(_detect(s), "RETROSPECTIVE_HOPE"), s
246
-
247
- def test_self_directed_audit_guard(self):
248
- """SELF_REF owns the audited choice — anxiety, not aggression."""
249
- for s in ("i spent my savings on this hope it was worth it",
250
- "hope my gamble was worth it"):
251
- assert not _has_pattern(_detect(s), "RETROSPECTIVE_HOPE"), s
252
-
253
- def test_elaborated_sincerity_guard(self):
254
- """Supportive elaboration after the audit rescues sincerity."""
255
- for s in ("hope it was worth it you trained two years for this",
256
- "hope youre happy in your new home"):
257
- assert not _has_pattern(_detect(s), "RETROSPECTIVE_HOPE"), s
258
-
259
- def test_paid_off_stays_out(self):
260
- """Benefit-centered 'paid off' is not cost-centered 'worth it'."""
261
- for s in ("hope the gamble paid off", "hope it pays off"):
262
- assert not _has_pattern(_detect(s), "RETROSPECTIVE_HOPE"), s
263
-
264
-
265
- # ── Cross-class invariants ───────────────────────────────────────
266
-
267
- class TestPAInvariants:
268
-
269
- def test_context_dependent_pa_stays_out_of_scope(self):
270
- """Bare context-dependent PA must NOT fire sentence-level patterns.
271
-
272
- ("hope it was worth it" graduated out of this list — its closed
273
- complement is sentence-internal grammar, now RETROSPECTIVE_HOPE.)
274
- """
275
- for s in ("sounds fun",):
276
- matches = _detect(s)
277
- for pat in ("TEMPORAL_GRIEVANCE", "EXCLUSION_CONTRAST",
278
- "IRONIC_DEFERENCE", "FAINT_PRAISE",
279
- "RETROSPECTIVE_HOPE"):
280
- assert not _has_pattern(matches, pat), (s, pat)
281
-
282
- def test_pa_lifts_intent_toward_control(self):
283
- """All five classes push I above 150 — the speaker is fighting."""
284
- for s in ("about time you answered your texts",
285
- "some of us have to be up at six",
286
- "no no youre clearly the expert here",
287
- "bold strategy",
288
- "hope she was worth it"):
289
- vadug, _ = compute_vadug(s)
290
- assert vadug.i > 150, s
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_pendulum.py DELETED
@@ -1,255 +0,0 @@
1
- """Tests for V3 Fixed Physics Layer — pendulum.py.
2
-
3
- Verifies that structural analysis drives VADUG computation correctly.
4
- Tests cover: neutral baseline, positive/negative shifts, negation,
5
- amplification, structure detection passthrough, and safe sentences.
6
- """
7
-
8
- import pytest
9
-
10
- from engine.pendulum import compute_vadug, CENTER
11
- from engine.shared import VADUG
12
-
13
-
14
- # ── Helper ───────────────────────────────────────────────────────
15
-
16
- def _vadug(text: str) -> VADUG:
17
- """Get VADUGWI for text, discard trace."""
18
- result, _ = compute_vadug(text)
19
- return result
20
-
21
-
22
- def _trace(text: str) -> dict:
23
- """Get trace dict for text."""
24
- _, trace = compute_vadug(text)
25
- return trace
26
-
27
-
28
- # ── Neutral input ───────────────────────────────────────────────
29
-
30
- class TestNeutral:
31
-
32
- def test_neutral_v_near_center(self):
33
- """Neutral sentence should have V near 128."""
34
- r = _vadug("the meeting is at three")
35
- assert 118 <= r.v <= 138, f"V={r.v}, expected near 128"
36
-
37
- def test_neutral_all_dimensions_near_center(self):
38
- """Neutral sentence should keep all dimensions near baseline."""
39
- r = _vadug("the meeting is at three")
40
- assert 118 <= r.v <= 138
41
- assert 118 <= r.a <= 138
42
- assert 118 <= r.d <= 138
43
- assert r.u <= 20 # urgency stays low
44
- assert 118 <= r.g <= 138
45
-
46
-
47
- # ── Positive input ──────────────────────────────────────────────
48
-
49
- class TestPositive:
50
-
51
- def test_positive_v_above_140(self):
52
- """Positive sentence should push V above 140."""
53
- r = _vadug("I am very happy")
54
- assert r.v > 140, f"V={r.v}, expected > 140"
55
-
56
- def test_positive_g_above_center(self):
57
- """Happy should feel lighter -- G above center."""
58
- r = _vadug("I am very happy")
59
- assert r.g > 128, f"G={r.g}, expected > 128"
60
-
61
-
62
- # ── Negative input ──────────────────────────────────────────────
63
-
64
- class TestNegative:
65
-
66
- def test_negative_v_below_115(self):
67
- """Negative sentence should push V below 115."""
68
- r = _vadug("I am very sad")
69
- assert r.v < 115, f"V={r.v}, expected < 115"
70
-
71
- def test_negative_g_below_center(self):
72
- """Sad should feel heavier -- G below center."""
73
- r = _vadug("I am very sad")
74
- assert r.g < 128, f"G={r.g}, expected < 128"
75
-
76
-
77
- # ── Negation ────────────────────────────────────────────────────
78
-
79
- class TestNegation:
80
-
81
- def test_not_happy_lower_than_happy(self):
82
- """'not happy' should have lower V than 'happy'."""
83
- v_happy = _vadug("happy").v
84
- v_not_happy = _vadug("not happy").v
85
- assert v_not_happy < v_happy, (
86
- f"not happy V={v_not_happy} should be < happy V={v_happy}"
87
- )
88
-
89
- def test_negation_flips_direction(self):
90
- """Negation should flip V direction on moderate words."""
91
- # Use moderate word to avoid ceiling effects
92
- v_good = _vadug("good").v
93
- v_not_good = _vadug("not good").v
94
- # "not good" should be below center, "good" above
95
- assert v_good > 128
96
- assert v_not_good < 128
97
-
98
-
99
- # ── Amplification ──────────────────────────────────────────────
100
-
101
- class TestAmplification:
102
-
103
- def test_amplifier_increases_displacement(self):
104
- """Amplifier should increase displacement from center on moderate words."""
105
- # Use a moderate word — "good" (dV=50) saturates before amplification helps
106
- v_glad = _vadug("glad").v
107
- v_very_glad = _vadug("very glad").v
108
- dist_glad = abs(v_glad - 128)
109
- dist_very = abs(v_very_glad - 128)
110
- assert dist_very > dist_glad, (
111
- f"very glad dist={dist_very} should be > glad dist={dist_glad}"
112
- )
113
-
114
-
115
- # ── Structure detection passthrough ─────────────────────────────
116
-
117
- class TestStructures:
118
-
119
- def test_farewell_detected(self):
120
- """'I gave my dog to my neighbor' should detect DIVESTITURE (giving away possessions)."""
121
- trace = _trace("I gave my dog to my neighbor")
122
- patterns = [s.pattern for s in trace["structures"]]
123
- assert "DIVESTITURE" in patterns or "FAREWELL" in patterns, f"Expected DIVESTITURE or FAREWELL, got {patterns}"
124
-
125
- def test_method_acquisition_detected(self):
126
- """'just bought a bunch of pills' should detect METHOD_ACQUISITION."""
127
- trace = _trace("just bought a bunch of pills")
128
- patterns = [s.pattern for s in trace["structures"]]
129
- assert "METHOD_ACQUISITION" in patterns, (
130
- f"Expected METHOD_ACQUISITION, got {patterns}"
131
- )
132
-
133
- def test_safe_no_crisis_structures(self):
134
- """'I had a bad day at work' should have no crisis structures."""
135
- trace = _trace("I had a bad day at work")
136
- crisis = {"FAREWELL", "METHOD_ACQUISITION", "BLANKET_APOLOGY",
137
- "SELF_REMOVAL", "SUSPICIOUS_CALM", "EXHAUSTION",
138
- "NO_EXIT", "SELF_NULLIFY"}
139
- found = [s.pattern for s in trace["structures"] if s.pattern in crisis]
140
- assert not found, f"Safe sentence falsely flagged: {found}"
141
-
142
-
143
- # ── VADUGWI object ──────────────────────────────────────────────
144
-
145
- class TestVADUGObject:
146
-
147
- def test_returns_vadug_type(self):
148
- """compute_vadug should return a VADUGWI object."""
149
- result, _ = compute_vadug("hello world")
150
- assert isinstance(result, VADUG)
151
-
152
- def test_all_five_dimensions(self):
153
- """VADUGWI should have all 7 dimensions."""
154
- r = _vadug("I am very happy")
155
- assert hasattr(r, "v")
156
- assert hasattr(r, "a")
157
- assert hasattr(r, "d")
158
- assert hasattr(r, "u")
159
- assert hasattr(r, "g")
160
-
161
- def test_dimensions_in_range(self):
162
- """All dimensions should be 0-255."""
163
- r = _vadug("I am extremely terribly devastatingly sad")
164
- assert 0 <= r.v <= 255
165
- assert 0 <= r.a <= 255
166
- assert 0 <= r.d <= 255
167
- assert 0 <= r.u <= 255
168
- assert 0 <= r.g <= 255
169
-
170
-
171
- # ── Trace dict ──────────────────────────────────────────────────
172
-
173
- class TestTrace:
174
-
175
- def test_trace_has_per_word_entries(self):
176
- """Trace should have one entry per word."""
177
- trace = _trace("I am happy")
178
- assert len(trace["trace"]) == 3
179
-
180
- def test_trace_entry_fields(self):
181
- """Each trace entry should have word, role, coeff, v, a, d, u, g."""
182
- trace = _trace("happy")
183
- entry = trace["trace"][0]
184
- for key in ("word", "role", "coeff", "v", "a", "d", "u", "g"):
185
- assert key in entry, f"Missing key: {key}"
186
-
187
- def test_word_count(self):
188
- """Trace should report correct word count."""
189
- trace = _trace("the quick brown fox")
190
- assert trace["word_count"] == 4
191
-
192
- def test_empty_input(self):
193
- """Empty string should return neutral VADUGWI and empty trace."""
194
- r, trace = compute_vadug("")
195
- assert r.v == 128
196
- assert r.w == 128
197
- assert trace["word_count"] == 0
198
- assert trace["trace"] == []
199
-
200
-
201
- # ── V4: Self-Worth (W) dimension ─────────────────────────────────
202
-
203
- class TestSelfWorth:
204
- """W tracks the user's self-assessment of their own value."""
205
-
206
- def test_default_w_is_neutral(self):
207
- """Default VADUGWI has W=128 (stable self-worth)."""
208
- assert VADUG().w == 128
209
-
210
- def test_neutral_sentence_w_unchanged(self):
211
- """Non-self-referential sentences should not move W."""
212
- r = _vadug("the meeting is at three")
213
- assert r.w == 128
214
-
215
- def test_no_self_ref_w_unchanged(self):
216
- """Sentences without SELF_REF should keep W at 128."""
217
- r = _vadug("she walked her dog")
218
- assert r.w == 128
219
-
220
- def test_self_nullify_drops_w(self):
221
- """'i am worthless' should produce W < 100."""
222
- r = _vadug("i am worthless")
223
- assert r.w < 100, f"W={r.w} should be < 100 for self-nullification"
224
-
225
- def test_self_nothing_drops_w(self):
226
- """'i am nothing' should produce W < 110."""
227
- r = _vadug("i am nothing")
228
- assert r.w < 110, f"W={r.w} should be < 110"
229
-
230
- def test_self_proud_raises_w(self):
231
- """'i am proud of myself' should produce W > 128."""
232
- r = _vadug("i am proud of myself")
233
- assert r.w > 128, f"W={r.w} should be > 128 for self-pride"
234
-
235
- def test_self_deserve_raises_w(self):
236
- """'i deserve to be happy' should produce W > 128."""
237
- r = _vadug("i deserve to be happy")
238
- assert r.w > 128, f"W={r.w} should be > 128"
239
-
240
- def test_told_useless_drops_w(self):
241
- """'he told me i was useless' should drop W below neutral."""
242
- r = _vadug("he told me i was useless")
243
- assert r.w < 128, f"W={r.w} should be < 128"
244
-
245
- def test_w_amplifies_negative_v(self):
246
- """Low W should amplify negative V (self-worth lens effect)."""
247
- # "i am worthless" has low W AND low V
248
- # The V should be lower than it would be without W coefficient
249
- r = _vadug("i am worthless")
250
- assert r.v < 80, f"V={r.v} should be deeply negative with W amplification"
251
-
252
- def test_burden_drops_w(self):
253
- """'im a burden' triggers SELF_NULLIFY which drops W."""
254
- r = _vadug("im a burden to everyone")
255
- assert r.w < 110, f"W={r.w} should be < 110 for self-as-burden"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_proximity.py DELETED
@@ -1,192 +0,0 @@
1
- """Tests for V3 proximity weighting — distance-based influence fields."""
2
-
3
- import pytest
4
-
5
- from engine.word_classifier import WordRole, classify_sentence
6
- from engine.proximity import (
7
- PROXIMITY_DECAY,
8
- compute_proximity_field,
9
- find_role_pairs,
10
- proximity_coefficient,
11
- )
12
-
13
-
14
- # ── Helper ───────────────────────────────────────────────────────
15
-
16
- def _roles(sentence: str):
17
- """Classify a sentence into WordRoles."""
18
- return classify_sentence(sentence.split())
19
-
20
-
21
- # ── compute_proximity_field tests ────────────────────────────────
22
-
23
- class TestProximityField:
24
-
25
- def test_adjacent_words_have_decay_influence(self):
26
- """Adjacent words (distance 1) should have ~0.7 influence."""
27
- roles = _roles("I am happy")
28
- field = compute_proximity_field(roles)
29
- # word 0 -> word 1 = distance 1
30
- assert abs(field[0][1] - PROXIMITY_DECAY) < 1e-9
31
-
32
- def test_distant_words_have_decayed_influence(self):
33
- """Words at distance 4 should have ~0.7^4 = ~0.24 influence."""
34
- roles = _roles("I gave my dog to neighbor")
35
- field = compute_proximity_field(roles)
36
- # word 0 -> word 4 = distance 4
37
- expected = PROXIMITY_DECAY ** 4
38
- assert abs(field[0][4] - expected) < 1e-9
39
-
40
- def test_influence_is_symmetric(self):
41
- """Influence from A->B should equal B->A."""
42
- roles = _roles("very happy today")
43
- field = compute_proximity_field(roles)
44
- assert abs(field[0][2] - field[2][0]) < 1e-9
45
-
46
- def test_no_self_influence(self):
47
- """A word should not appear in its own influence map."""
48
- roles = _roles("I am fine")
49
- field = compute_proximity_field(roles)
50
- for idx, influences in field.items():
51
- assert idx not in influences
52
-
53
- def test_empty_sentence(self):
54
- """Empty input produces empty field."""
55
- field = compute_proximity_field([])
56
- assert field == {}
57
-
58
- def test_single_word(self):
59
- """Single word has no influences."""
60
- roles = _roles("hello")
61
- field = compute_proximity_field(roles)
62
- assert field[0] == {}
63
-
64
- def test_cutoff_applied(self):
65
- """Distant words eventually fall below influence cutoff."""
66
- from engine.proximity import PROXIMITY_DECAY, INFLUENCE_CUTOFF
67
- # Find the distance where influence drops below cutoff
68
- import math
69
- cutoff_dist = int(math.log(INFLUENCE_CUTOFF) / math.log(PROXIMITY_DECAY)) + 1
70
- roles = _roles(" ".join(f"w{i}" for i in range(cutoff_dist + 5)))
71
- field = compute_proximity_field(roles)
72
- # distance cutoff_dist-2 should be included
73
- assert cutoff_dist - 2 in field[0]
74
- # distance cutoff_dist+1 should be excluded
75
- assert cutoff_dist + 1 not in field[0]
76
-
77
-
78
- # ── find_role_pairs tests ────────────────────────────────────────
79
-
80
- class TestFindRolePairs:
81
-
82
- def test_transfer_relation_found(self):
83
- """'I gave my dog to neighbor' should find TRANSFER+RELATION_REF."""
84
- roles = _roles("I gave my dog to neighbor")
85
- # dog is RELATION_REF now (relationship, not possession)
86
- pairs = find_role_pairs(roles, "TRANSFER", "RELATION_REF")
87
- assert len(pairs) > 0
88
- transfer_idxs = [r.position for r in roles if r.role == "TRANSFER"]
89
- relation_idxs = [r.position for r in roles if r.role == "RELATION_REF"]
90
- pair_as = [p[0] for p in pairs]
91
- pair_bs = [p[1] for p in pairs]
92
- assert any(a in transfer_idxs for a in pair_as)
93
- assert any(b in relation_idxs for b in pair_bs)
94
-
95
- def test_acquire_method_found(self):
96
- """'just bought some pills' should find ACQUIRE+METHOD."""
97
- roles = _roles("just bought some pills")
98
- pairs = find_role_pairs(roles, "ACQUIRE", "METHOD")
99
- assert len(pairs) > 0
100
- # "bought" is ACQUIRE, "pills" is METHOD
101
- acquire_idxs = [r.position for r in roles if r.role == "ACQUIRE"]
102
- method_idxs = [r.position for r in roles if r.role == "METHOD"]
103
- pair_as = [p[0] for p in pairs]
104
- pair_bs = [p[1] for p in pairs]
105
- assert any(a in acquire_idxs for a in pair_as)
106
- assert any(b in method_idxs for b in pair_bs)
107
-
108
- def test_sorted_strongest_first(self):
109
- """Pairs should be sorted by proximity strength, strongest first."""
110
- roles = _roles("I gave my dog to neighbor")
111
- pairs = find_role_pairs(roles, "TRANSFER", "POSSESSION")
112
- if len(pairs) > 1:
113
- for i in range(len(pairs) - 1):
114
- assert pairs[i][2] >= pairs[i + 1][2]
115
-
116
- def test_max_distance_respected(self):
117
- """Pairs beyond max_distance should not be returned."""
118
- roles = _roles("gave a b c d e f g dog")
119
- pairs = find_role_pairs(roles, "TRANSFER", "POSSESSION", max_distance=3)
120
- for _, _, strength in pairs:
121
- # All returned pairs must have distance <= 3
122
- assert strength >= PROXIMITY_DECAY ** 3
123
-
124
- def test_no_pairs_returns_empty(self):
125
- """If no matching role pair exists, return empty list."""
126
- roles = _roles("hello world")
127
- pairs = find_role_pairs(roles, "TRANSFER", "METHOD")
128
- assert pairs == []
129
-
130
-
131
- # ── proximity_coefficient tests ──────────────────────────────────
132
-
133
- class TestProximityCoefficient:
134
-
135
- def test_amplifier_boosts(self):
136
- """Nearby AMPLIFIER should boost coefficient above 1.0."""
137
- roles = _roles("very happy")
138
- # "very" = AMPLIFIER at idx 0, target "happy" at idx 1
139
- coeff = proximity_coefficient(roles, 1)
140
- assert coeff > 1.0
141
-
142
- def test_negator_flips(self):
143
- """Nearby NEGATOR should flip coefficient below 0."""
144
- roles = _roles("not happy")
145
- # "not" = NEGATOR at idx 0, target "happy" at idx 1
146
- coeff = proximity_coefficient(roles, 1)
147
- assert coeff < 0.0
148
-
149
- def test_self_ref_increases_magnitude(self):
150
- """SELF_REF nearby should increase |coefficient| vs OTHER_REF."""
151
- roles_self = _roles("I am happy")
152
- roles_other = _roles("they are happy")
153
- # "happy" is the last word in both
154
- coeff_self = proximity_coefficient(roles_self, 2)
155
- coeff_other = proximity_coefficient(roles_other, 2)
156
- assert abs(coeff_self) > abs(coeff_other)
157
-
158
- def test_hedge_dampens(self):
159
- """Nearby HEDGE should dampen coefficient below 1.0."""
160
- roles = _roles("maybe happy")
161
- # "maybe" = HEDGE at idx 0, target "happy" at idx 1
162
- coeff = proximity_coefficient(roles, 1)
163
- assert coeff < 1.0
164
-
165
- def test_no_modifiers_returns_one(self):
166
- """Without modifier roles nearby, coefficient should be 1.0."""
167
- roles = _roles("the dog barked")
168
- # All NEUTRAL/POSSESSION — no AMPLIFIER/NEGATOR/SELF_REF/HEDGE
169
- coeff = proximity_coefficient(roles, 0)
170
- assert coeff == 1.0
171
-
172
- def test_coefficient_capped(self):
173
- """Coefficient should never exceed [-COEFFICIENT_CAP, COEFFICIENT_CAP]."""
174
- from engine.proximity import COEFFICIENT_CAP
175
- roles = _roles("very really extremely totally absolutely happy")
176
- coeff = proximity_coefficient(roles, 5)
177
- assert -COEFFICIENT_CAP <= coeff <= COEFFICIENT_CAP
178
-
179
- def test_empty_roles(self):
180
- """Empty roles list returns 1.0."""
181
- assert proximity_coefficient([], 0) == 1.0
182
-
183
- def test_single_word(self):
184
- """Single word has no modifiers — coefficient is 1.0."""
185
- roles = _roles("happy")
186
- assert proximity_coefficient(roles, 0) == 1.0
187
-
188
- def test_out_of_bounds_index(self):
189
- """Out-of-bounds target index returns 1.0."""
190
- roles = _roles("hello world")
191
- assert proximity_coefficient(roles, 99) == 1.0
192
- assert proximity_coefficient(roles, -1) == 1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_scaffold.py DELETED
@@ -1,30 +0,0 @@
1
- import pytest
2
- import sys, os
3
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
4
-
5
- def test_vadug_import():
6
- from engine.shared import VADUG
7
- v = VADUG(v=100, a=150, d=128, u=50, g=90)
8
- assert v.v == 100
9
- assert v.g == 90
10
-
11
- def test_vocabulary_import():
12
- from engine.vocabulary import VOCABULARY
13
- assert len(VOCABULARY) > 2000
14
- assert "happy" in VOCABULARY
15
- assert "sad" in VOCABULARY
16
-
17
- def test_zones_import():
18
- from engine.zones import ZONES
19
- assert "JOY" in ZONES
20
- assert "CRISIS" in ZONES
21
-
22
- def test_personality_import():
23
- from engine.personality import PersonalityVector
24
- p = PersonalityVector()
25
- assert hasattr(p, "emotional_sensitivity")
26
-
27
- def test_fuzzy_import():
28
- from engine.fuzzy import fuzzy_match
29
- assert fuzzy_match("happyyyy") == "happy"
30
- assert fuzzy_match("tbh") == "honestly"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_slang_register.py DELETED
@@ -1,203 +0,0 @@
1
- """Tests for the Batch 1 slang-register fixes (2026-06-11).
2
-
3
- Covers:
4
- 1. New slang vocabulary (dope, dogwater, washed, masterclass, ...)
5
- 2. 'w' sense disambiguation: WIN vs "with"
6
- 3. "lets go" hype compound + elongation collapse ("LETS GOOOOOO")
7
- 4. "as hell" as pure intensifier (no literal hell charge)
8
- 5. Expletive-as-intensifier rescue after demonstratives ("This shit slaps")
9
- 6. Fuzzy stemmer blocklist ('number' must not stem to 'numb')
10
- 7. Regression guards on regraded words (clean, extra, trap)
11
- """
12
-
13
- from engine.pendulum import compute_vadug
14
-
15
-
16
- def _v(text: str) -> float:
17
- result, _ = compute_vadug(text)
18
- return result.v
19
-
20
-
21
- class TestNewSlangVocab:
22
-
23
- def test_dope_with_elongated_greeting(self):
24
- assert _v("Yoooooo, that's dope!!!!!!!!!!!") >= 140
25
-
26
- def test_dogwater_negative(self):
27
- assert _v("the servers are actually dogwater tonight") <= 123
28
-
29
- def test_washed_negative(self):
30
- assert _v("batista is washed bro") <= 124
31
-
32
- def test_ggs_masterclass(self):
33
- assert _v("ggs that was a masterclass") >= 140
34
-
35
-
36
- class TestWDisambiguation:
37
-
38
- def test_w_as_with_in_food_order(self):
39
- assert 112 <= _v("ordered it w extra cheese") <= 144
40
-
41
- def test_w_as_with_before_name(self):
42
- assert 105 <= _v("don't play w jynxzi") <= 144
43
-
44
- def test_w_as_win_after_massive(self):
45
- assert _v("massive W for the whole team today") >= 138
46
-
47
-
48
- class TestHypeCompounds:
49
-
50
- def test_lets_go_elongated(self):
51
- assert _v("LETS GOOOOOO") >= 140
52
-
53
- def test_clean_as_hell(self):
54
- assert _v("new drop is clean as hell") >= 133
55
-
56
- def test_demonstrative_expletive_rescue_with_trap_genre(self):
57
- assert _v("This shit slaps, we need the trap remix") >= 140
58
-
59
-
60
- class TestStemmerBlocklist:
61
-
62
- def test_number_does_not_stem_to_numb(self):
63
- assert 105 <= _v("she peaked at number three on the charts") <= 144
64
-
65
-
66
- class TestRegressionGuards:
67
-
68
- def test_happy_stays_high(self):
69
- assert _v("I am so happy for you") >= 150
70
-
71
- def test_crisis_stays_low(self):
72
- assert _v("I want to die") <= 60
73
-
74
- def test_literal_hell_stays_negative(self):
75
- assert _v("this is hell") <= 110
76
-
77
- def test_literal_extra_quantity(self):
78
- assert _v("we got extra time to finish") >= 112
79
-
80
- def test_literal_trap_stays_negative(self):
81
- assert _v("he fell into a trap") <= 120
82
-
83
-
84
- # ── Batch 2 (2026-06-11) ────────────────────────────────────────
85
-
86
-
87
- class TestDiscourseNoRescue:
88
-
89
- def test_no_before_self_ref_illness(self):
90
- assert _v("no i'm sick with a sore throat") <= 123
91
-
92
- def test_no_before_self_ref_negation_preserved(self):
93
- assert _v("no i don't want that") <= 110
94
-
95
- def test_no_positive_rescue_still_works(self):
96
- assert _v("no we good") >= 140
97
-
98
-
99
- class TestCookedLiteralGuard:
100
-
101
- def test_cooked_dinner_for_fam(self):
102
- assert 112 <= _v("i cooked dinner for the fam tonight") <= 144
103
-
104
- def test_cooked_on_the_grill(self):
105
- # 111 as of batch 2: "on" is not a literal-object follower, so the
106
- # stored slang charge still applies; held just under neutral by
107
- # W-V coupling. Band guards against regression in either direction.
108
- assert 105 <= _v("I cooked on the grill so yes") <= 144
109
-
110
- def test_home_cooked_meal(self):
111
- assert 112 <= _v("3 pennes and a home cooked meal") <= 144
112
-
113
- def test_im_cooked_bro_still_flips_positive(self):
114
- assert _v("im cooked bro") >= 133
115
-
116
- def test_im_cooked_stays_doomed(self):
117
- assert _v("im cooked") < 110
118
-
119
-
120
- class TestFireSenseSplit:
121
-
122
- def test_fire_hazard_in_kitchen(self):
123
- result, _ = compute_vadug("theres a fire in my kitchen")
124
- assert result.v <= 123
125
- assert result.u >= 25
126
-
127
- def test_fire_praise_its(self):
128
- assert _v("Play bite by night on Roblox it's fire") >= 133
129
-
130
- def test_fire_praise_be(self):
131
- assert _v("game might actually be fire without the broom tho lol") >= 133
132
-
133
- def test_fire_away_not_strongly_positive(self):
134
- assert _v("fire away with your questions") <= 135
135
-
136
- def test_fire_him_not_strongly_positive(self):
137
- assert _v("they had to fire him") <= 145
138
-
139
-
140
- class TestPraiseNounGuard:
141
-
142
- def test_you_the_goat_fr(self):
143
- assert _v("you the goat fr") >= 145
144
-
145
- def test_good_for_you_stays_pa(self):
146
- assert _v("good for you") <= 110
147
-
148
- def test_must_be_nice_stays_pa(self):
149
- assert _v("must be nice") <= 112
150
-
151
-
152
- class TestRegisterCasualInertSkip:
153
-
154
- def test_trailing_ngl_no_drain(self):
155
- assert _v("yo this game gives me hope for the human race man ngl") >= 133
156
-
157
- def test_helluva_combo_ngl(self):
158
- assert _v("Yoo pink and teal, that's one helluva combo ngl") >= 133
159
-
160
- def test_elongated_good_burgers(self):
161
- assert _v("yall i made 2 big burgers today i ate GOOOOOD") >= 133
162
-
163
-
164
- class TestHypeIdioms:
165
-
166
- def test_we_are_so_back(self):
167
- assert _v("WE ARE SO BACK") >= 140
168
-
169
- def test_turned_his_back_unaffected(self):
170
- assert _v("he turned his back") <= 128
171
-
172
- def test_stupid_good_amplifier(self):
173
- assert _v("the chorus on this track is stupid good") >= 140
174
-
175
- def test_stupid_mistake_stays_negative(self):
176
- assert _v("stupid mistake") <= 110
177
-
178
- def test_he_is_stupid_stays_negative(self):
179
- assert _v("he is stupid") <= 115
180
-
181
-
182
- class TestVocabPhaseBatch2:
183
-
184
- def test_lowkey_obsessed_flips_positive(self):
185
- assert _v("honestly lowkey obsessed with this app") >= 133
186
-
187
- def test_obsessed_third_person_stays_nonpositive(self):
188
- assert _v("he is obsessed with her every move") <= 128
189
-
190
- def test_filthy_sports_praise(self):
191
- assert _v("that play was filthy") >= 133
192
-
193
- def test_filthy_bathroom_stays_nonpositive(self):
194
- assert _v("the bathroom was filthy") <= 128
195
-
196
- def test_mid_game_negative(self):
197
- assert _v("mid game tbh, wouldn't recommend") <= 123
198
-
199
- def test_mid_predicate_stays_negative(self):
200
- assert _v("the new album is mid") <= 110
201
-
202
- def test_mid_morning_temporal_neutral(self):
203
- assert 105 <= _v("mid morning meeting") <= 144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_solver.py DELETED
@@ -1,104 +0,0 @@
1
- """Tests for V3 Bidirectional Solver — forward read + backward zone targeting."""
2
-
3
- import pytest
4
-
5
- from engine.solver import (
6
- forward,
7
- state_transition,
8
- solve_for_b_range,
9
- optimal_b_temperature,
10
- )
11
- from engine.shared import VADUG
12
-
13
-
14
- # ── Forward ────────────────────────────────────────────────────────
15
-
16
- class TestForward:
17
- def test_happy_text_high_valence(self):
18
- """'I am happy' should produce V > 140."""
19
- result = forward("I am happy")
20
- assert result.v > 140, f"Expected V > 140, got {result}"
21
-
22
-
23
- # ── State transition ───────────────────────────────────────────────
24
-
25
- class TestStateTransition:
26
- def test_neutral_b_preserves_a_direction(self):
27
- """Neutral B should preserve A's direction — V stays on same side of 128."""
28
- a = VADUG(v=180, a=140, d=150, u=20, g=140)
29
- b = VADUG(v=128, a=128, d=128, u=0, g=128)
30
- c = state_transition(a, b)
31
- # A is positive (180), neutral B should keep C positive
32
- assert c.v > 128, f"Expected C.v > 128, got {c.v}"
33
- # C should be pulled toward neutral but still positive
34
- assert c.v < a.v, f"Expected C.v < A.v ({a.v}), got {c.v}"
35
-
36
- def test_warm_b_lifts_crisis_a(self):
37
- """Warm B should lift a crisis state — C.v > A.v."""
38
- crisis = VADUG(v=60, a=180, d=50, u=200, g=40)
39
- warm = VADUG(v=200, a=100, d=160, u=10, g=180)
40
- c = state_transition(crisis, warm)
41
- assert c.v > crisis.v, f"Expected C.v > {crisis.v}, got {c.v}"
42
- assert c.d > crisis.d, f"Expected C.d > {crisis.d}, got {c.d}"
43
- assert c.g > crisis.g, f"Expected C.g > {crisis.g}, got {c.g}"
44
-
45
- def test_weights_sum_to_one(self):
46
- """Default weights: 60% A + 40% B."""
47
- a = VADUG(v=100, a=100, d=100, u=100, g=100)
48
- b = VADUG(v=200, a=200, d=200, u=200, g=200)
49
- c = state_transition(a, b)
50
- # 100*0.6 + 200*0.4 = 140
51
- assert c.v == 140
52
- assert c.a == 140
53
- assert c.d == 140
54
-
55
-
56
- # ── Backward: zone targeting ──────────────────────────────────────
57
-
58
- class TestSolveForBRange:
59
- def test_neutral_a_to_joy_requires_positive_b(self):
60
- """From neutral A, reaching JOY zone requires positive B (V > 128)."""
61
- neutral = VADUG(v=128, a=128, d=128, u=0, g=128)
62
- ranges = solve_for_b_range(neutral, "JOY", temperature_steps=256)
63
- assert len(ranges) > 0, "Should find valid ranges for JOY from neutral"
64
- # Valid B values should be predominantly positive (above neutral)
65
- # The lowest valid B may dip slightly below 128 due to zone radius,
66
- # but the range center should be well above 128
67
- lowest_start = min(s for s, _ in ranges)
68
- highest_end = max(e for _, e in ranges)
69
- midpoint = (lowest_start + highest_end) // 2
70
- assert midpoint > 128, f"Expected range midpoint > 128, got {midpoint}"
71
-
72
- def test_deep_crisis_to_joy_narrow_or_empty(self):
73
- """Deep crisis A may have empty or narrow range to JOY.
74
-
75
- Can't reach joy from crisis in one step — this is correct behavior.
76
- """
77
- crisis = VADUG(v=30, a=200, d=30, u=230, g=20)
78
- ranges = solve_for_b_range(crisis, "JOY", temperature_steps=256)
79
- # Either empty or very narrow — crisis is far from joy
80
- total_width = sum(end - start + 1 for start, end in ranges)
81
- assert total_width <= 30, (
82
- f"Expected narrow/empty range from deep crisis to JOY, "
83
- f"got total width {total_width}"
84
- )
85
-
86
-
87
- class TestOptimalBTemperature:
88
- def test_returns_value_in_valid_range(self):
89
- """Optimal B should fall within one of the valid ranges."""
90
- neutral = VADUG(v=128, a=128, d=128, u=0, g=128)
91
- opt = optimal_b_temperature(neutral, "JOY")
92
- assert opt is not None, "Should find optimal B for JOY from neutral"
93
- ranges = solve_for_b_range(neutral, "JOY", temperature_steps=256)
94
- in_range = any(start <= opt <= end for start, end in ranges)
95
- assert in_range, f"Optimal B {opt} not in any valid range {ranges}"
96
-
97
- def test_unreachable_returns_none(self):
98
- """If no B can reach the zone, return None."""
99
- # Extremely deep crisis — may not reach NEUTRAL's tiny radius
100
- extreme = VADUG(v=0, a=255, d=0, u=255, g=0)
101
- result = optimal_b_temperature(extreme, "NEUTRAL")
102
- # NEUTRAL has radius 8 — very tight, hard to hit from extreme
103
- # This may or may not be None depending on math, so just check type
104
- assert result is None or isinstance(result, int)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_structures.py DELETED
@@ -1,746 +0,0 @@
1
- """Tests for V3 Structure Detector -- chess-like pattern recognition.
2
-
3
- All test sentences are NOVEL -- things never in any benchmark.
4
- Safe sentences must NOT flag crisis patterns.
5
- """
6
-
7
- import pytest
8
-
9
- from engine.word_classifier import classify_sentence
10
- from engine.structures import StructureDetector, StructureMatch
11
-
12
-
13
- # ── Helper ───────────────────────────────────────────────────────
14
-
15
- def _detect(sentence: str):
16
- """Classify and detect structures in a sentence."""
17
- roles = classify_sentence(sentence.split())
18
- detector = StructureDetector()
19
- return detector.detect_all(roles)
20
-
21
-
22
- def _has_pattern(matches, pattern_name):
23
- """Check if a specific pattern was detected."""
24
- return any(m.pattern == pattern_name for m in matches)
25
-
26
-
27
- def _get_pattern(matches, pattern_name):
28
- """Get the first match of a specific pattern."""
29
- for m in matches:
30
- if m.pattern == pattern_name:
31
- return m
32
- return None
33
-
34
-
35
- # ── FAREWELL ─────────────────────────────────────────────────────
36
-
37
- class TestFarewell:
38
-
39
- def test_gave_dog_to_neighbor(self):
40
- """'I gave my dog to my neighbor' -> DIVESTITURE (giving away possessions)."""
41
- matches = _detect("I gave my dog to my neighbor")
42
- assert _has_pattern(matches, "FAREWELL") or _has_pattern(matches, "DIVESTITURE")
43
-
44
- def test_leaving_keys_with_friend(self):
45
- """'leaving my keys with a friend' -> FAREWELL or DIVESTITURE."""
46
- matches = _detect("leaving my keys with a friend")
47
- assert _has_pattern(matches, "FAREWELL") or _has_pattern(matches, "DIVESTITURE") or _has_pattern(matches, "ABANDONMENT")
48
-
49
- def test_no_farewell_without_ref(self):
50
- """'I gave my dog a treat' -- no person ref, not FAREWELL."""
51
- matches = _detect("I gave my dog a treat")
52
- assert not _has_pattern(matches, "FAREWELL")
53
-
54
-
55
- # ── METHOD_ACQUISITION ───────────────────────────────────────────
56
-
57
- class TestMethodAcquisition:
58
-
59
- def test_bought_pills(self):
60
- """'just bought a bunch of pills' -> METHOD_ACQUISITION."""
61
- matches = _detect("just bought a bunch of pills")
62
- assert _has_pattern(matches, "METHOD_ACQUISITION")
63
-
64
- def test_found_a_gun(self):
65
- """'I found a gun in the closet' -> METHOD_ACQUISITION."""
66
- matches = _detect("I found a gun in the closet")
67
- assert _has_pattern(matches, "METHOD_ACQUISITION")
68
-
69
- def test_bought_groceries_safe(self):
70
- """'just bought some groceries' -- no METHOD, should be safe."""
71
- matches = _detect("just bought some groceries")
72
- assert not _has_pattern(matches, "METHOD_ACQUISITION")
73
-
74
-
75
- # ── FINALITY ─────────────────────────────────────────────────────
76
-
77
- class TestFinality:
78
-
79
- def test_last_time(self):
80
- """'this is the last time' -> FINALITY."""
81
- matches = _detect("this is the last time")
82
- assert _has_pattern(matches, "FINALITY")
83
-
84
- def test_goodbye(self):
85
- """'goodbye everyone' -> FINALITY."""
86
- matches = _detect("goodbye everyone")
87
- assert _has_pattern(matches, "FINALITY")
88
-
89
- def test_finality_with_self_ref(self):
90
- """'I am done with everything' -> FINALITY with SELF_REF boost."""
91
- matches = _detect("I am done with everything")
92
- assert _has_pattern(matches, "FINALITY")
93
- m = _get_pattern(matches, "FINALITY")
94
- assert m.confidence > 0.4 # boosted by SELF_REF
95
-
96
-
97
- # ── BLANKET_APOLOGY ──────────────────────────────────────────────
98
-
99
- class TestBlanketApology:
100
-
101
- def test_sorry_for_everything(self):
102
- """'im sorry for everything' -> BLANKET_APOLOGY."""
103
- matches = _detect("im sorry for everything")
104
- assert _has_pattern(matches, "BLANKET_APOLOGY")
105
-
106
- def test_apologize_to_everyone(self):
107
- """'I apologize to everyone' -> BLANKET_APOLOGY."""
108
- matches = _detect("I apologize to everyone")
109
- assert _has_pattern(matches, "BLANKET_APOLOGY")
110
-
111
- def test_sorry_for_being_late_safe(self):
112
- """'im sorry for being late' -- specific apology, NOT blanket."""
113
- matches = _detect("im sorry for being late")
114
- assert not _has_pattern(matches, "BLANKET_APOLOGY")
115
-
116
-
117
- # ── SELF_REMOVAL ─────────────────────────────────────────────────
118
-
119
- class TestSelfRemoval:
120
-
121
- def test_happier_without_me(self):
122
- """'they would be happier if I wasnt here' -> SELF_REMOVAL."""
123
- matches = _detect("they would be happier if I wasnt here")
124
- assert _has_pattern(matches, "SELF_REMOVAL")
125
-
126
- def test_better_off_without(self):
127
- """'everyone is better off without me' -> SELF_REMOVAL."""
128
- matches = _detect("everyone is better off without me")
129
- assert _has_pattern(matches, "SELF_REMOVAL")
130
-
131
- def test_better_at_cooking_safe(self):
132
- """'she is better at cooking than me' -- comparison, not removal."""
133
- matches = _detect("she is better at cooking than me")
134
- # No conditional word present, should not match
135
- assert not _has_pattern(matches, "SELF_REMOVAL")
136
-
137
-
138
- # ── SUSPICIOUS_CALM ──────────────────────────────────────────────
139
-
140
- class TestSuspiciousCalm:
141
-
142
- def test_finally_at_peace_is_relief(self):
143
- """'I finally feel at peace' = present tense breakthrough, NOT suspicious."""
144
- matches = _detect("I finally feel at peace")
145
- assert not _has_pattern(matches, "SUSPICIOUS_CALM")
146
-
147
- def test_decided_calm_is_suspicious(self):
148
- """'i feel calm now that ive decided' -> SUSPICIOUS_CALM."""
149
- matches = _detect("i feel calm now that ive decided")
150
- assert _has_pattern(matches, "SUSPICIOUS_CALM")
151
-
152
- def test_ready_to_go_is_not_suspicious(self):
153
- """'im ready to go now' = could be waiting for a ride. Needs conversation context."""
154
- matches = _detect("im ready to go now")
155
- assert not _has_pattern(matches, "SUSPICIOUS_CALM")
156
-
157
- def test_peace_without_finally_safe(self):
158
- """'I feel at peace' -- no 'finally', not suspicious."""
159
- matches = _detect("I feel at peace")
160
- assert not _has_pattern(matches, "SUSPICIOUS_CALM")
161
-
162
-
163
- # ── EXHAUSTION ───────────────────────────────────────────────────
164
-
165
- class TestExhaustion:
166
-
167
- def test_cant_take_anymore(self):
168
- """'I cant take this anymore' -> EXHAUSTION."""
169
- matches = _detect("I cant take this anymore")
170
- assert _has_pattern(matches, "EXHAUSTION")
171
-
172
- def test_cant_do_this(self):
173
- """'I cant do this' -> EXHAUSTION (without temporal, lower conf)."""
174
- matches = _detect("I cant do this")
175
- assert _has_pattern(matches, "EXHAUSTION")
176
-
177
- def test_cant_find_keys_safe(self):
178
- """'I cant find my keys' -- 'find' is ACQUIRE not sustain."""
179
- matches = _detect("I cant find my keys")
180
- assert not _has_pattern(matches, "EXHAUSTION")
181
-
182
-
183
- # ── SARCASM_INVERSION ────────────────────────────────────────────
184
-
185
- class TestSarcasmInversion:
186
-
187
- # ── Contradiction feature: Surface-Context Mismatch ─────────
188
- def test_great_another_monday(self):
189
- """'oh great another monday' -> surface-context mismatch."""
190
- matches = _detect("oh great another monday")
191
- assert _has_pattern(matches, "SARCASM_INVERSION")
192
-
193
- def test_great_another_meeting(self):
194
- """'oh great another meeting' -> surface-context mismatch."""
195
- matches = _detect("oh great another meeting")
196
- assert _has_pattern(matches, "SARCASM_INVERSION")
197
-
198
- # ── Contradiction feature: Mock Praise ──────────────────────
199
- def test_nice_work_genius(self):
200
- """'nice work genius' -> mock praise (positive + ironic title)."""
201
- matches = _detect("nice work genius")
202
- assert _has_pattern(matches, "SARCASM_INVERSION")
203
-
204
- # ── Contradiction feature: Dismissive Assent ────────────────
205
- def test_yeah_right(self):
206
- """'yeah right' -> dismissive assent (hollow + echo)."""
207
- matches = _detect("yeah right")
208
- assert _has_pattern(matches, "SARCASM_INVERSION")
209
-
210
- def test_oh_sure_exactly_what_i_needed(self):
211
- """'oh sure thats exactly what i needed' -> dismissive assent."""
212
- matches = _detect("oh sure thats exactly what i needed")
213
- assert _has_pattern(matches, "SARCASM_INVERSION")
214
-
215
- def test_what_a_wonderful_surprise(self):
216
- """'what a wonderful surprise' -> dismissive assent (what-a template)."""
217
- matches = _detect("what a wonderful surprise")
218
- assert _has_pattern(matches, "SARCASM_INVERSION")
219
-
220
- # ── Contradiction feature: Compressed Sarcasm ───────────────
221
- def test_oh_joy(self):
222
- """'oh joy' -> compressed sarcasm (hollow + positive + ultra-short)."""
223
- matches = _detect("oh joy")
224
- assert _has_pattern(matches, "SARCASM_INVERSION")
225
-
226
- def test_oh_how_lovely(self):
227
- """'oh how lovely' -> compressed sarcasm."""
228
- matches = _detect("oh how lovely")
229
- assert _has_pattern(matches, "SARCASM_INVERSION")
230
-
231
- def test_wow_thanks_stacked(self):
232
- """'wow thanks so much for the help' -> stacked positives."""
233
- matches = _detect("wow thanks so much for the help")
234
- assert _has_pattern(matches, "SARCASM_INVERSION")
235
-
236
- # ── Permission Hostility: context-dependent ─────────────────
237
- def test_sure_go_ahead_no_context_is_genuine(self):
238
- """'sure go ahead' without negative context = genuine permission."""
239
- matches = _detect("sure go ahead")
240
- assert not _has_pattern(matches, "SARCASM_INVERSION")
241
-
242
- # ── Safe sentences: no sarcasm ──────────────────────────────
243
- def test_genuine_great_not_sarcasm(self):
244
- """'that was a great wonderful performance' -- genuinely positive."""
245
- matches = _detect("that was a great wonderful performance")
246
- assert not _has_pattern(matches, "SARCASM_INVERSION")
247
-
248
- def test_love_my_mom_not_sarcasm(self):
249
- """'I love my mom' -- RELATION_REF blocks sarcasm."""
250
- matches = _detect("I love my mom")
251
- assert not _has_pattern(matches, "SARCASM_INVERSION")
252
-
253
-
254
- # ── NO_EXIT ──────────────────────────────────────────────────────
255
-
256
- class TestNoExit:
257
-
258
- def test_no_hope(self):
259
- """'there is no hope' -> NO_EXIT."""
260
- matches = _detect("there is no hope")
261
- assert _has_pattern(matches, "NO_EXIT")
262
-
263
- def test_no_point(self):
264
- """'there is no point anymore' -> NO_EXIT."""
265
- matches = _detect("there is no point anymore")
266
- assert _has_pattern(matches, "NO_EXIT")
267
-
268
- def test_no_milk_safe(self):
269
- """'there is no milk' -- 'milk' is not an exit concept."""
270
- matches = _detect("there is no milk")
271
- assert not _has_pattern(matches, "NO_EXIT")
272
-
273
-
274
- # ── SELF_NULLIFY ─────────────────────────────────────────────────
275
-
276
- class TestSelfNullify:
277
-
278
- def test_i_am_nothing(self):
279
- """'I am nothing' -> SELF_NULLIFY."""
280
- matches = _detect("I am nothing")
281
- assert _has_pattern(matches, "SELF_NULLIFY")
282
-
283
- def test_i_am_worthless(self):
284
- """'I am worthless' -> SELF_NULLIFY."""
285
- matches = _detect("I am worthless")
286
- assert _has_pattern(matches, "SELF_NULLIFY")
287
-
288
- def test_i_am_tired_safe(self):
289
- """'I am tired' -- 'tired' is not a null word."""
290
- matches = _detect("I am tired")
291
- assert not _has_pattern(matches, "SELF_NULLIFY")
292
-
293
-
294
- # ── CHOPPER_SPLIT ────────────────────────────────────────────────
295
-
296
- class TestChopperSplit:
297
-
298
- def test_but_splits(self):
299
- """'I was fine but now everything hurts' -> CHOPPER_SPLIT."""
300
- matches = _detect("I was fine but now everything hurts")
301
- assert _has_pattern(matches, "CHOPPER_SPLIT")
302
-
303
- def test_however_splits(self):
304
- """'things were good however it changed' -> CHOPPER_SPLIT."""
305
- matches = _detect("things were good however it changed")
306
- assert _has_pattern(matches, "CHOPPER_SPLIT")
307
-
308
-
309
- # ── SAFE sentences should NOT flag crisis patterns ───────────────
310
-
311
- class TestSafeSentences:
312
- """These everyday sentences must NOT trigger crisis patterns."""
313
-
314
- CRISIS_PATTERNS = {
315
- "FAREWELL", "METHOD_ACQUISITION", "BLANKET_APOLOGY",
316
- "SELF_REMOVAL", "SUSPICIOUS_CALM", "EXHAUSTION",
317
- "NO_EXIT", "SELF_NULLIFY",
318
- }
319
-
320
- def _assert_no_crisis(self, sentence):
321
- matches = _detect(sentence)
322
- crisis_found = [
323
- m.pattern for m in matches if m.pattern in self.CRISIS_PATTERNS
324
- ]
325
- assert not crisis_found, (
326
- f"'{sentence}' falsely flagged: {crisis_found}"
327
- )
328
-
329
- def test_bad_day(self):
330
- self._assert_no_crisis("im having a bad day")
331
-
332
- def test_work_stressful(self):
333
- self._assert_no_crisis("work was stressful")
334
-
335
- def test_mondays_suck(self):
336
- self._assert_no_crisis("mondays suck")
337
-
338
- def test_traffic_was_terrible(self):
339
- self._assert_no_crisis("traffic was terrible today")
340
-
341
- def test_need_coffee(self):
342
- self._assert_no_crisis("I need more coffee")
343
-
344
- def test_forgot_lunch(self):
345
- self._assert_no_crisis("I forgot my lunch at home")
346
-
347
-
348
- # ── StructureMatch dataclass ─────────────────────────────────────
349
-
350
- class TestStructureMatch:
351
-
352
- def test_has_required_fields(self):
353
- m = StructureMatch(
354
- pattern="TEST",
355
- confidence=0.8,
356
- matched_indices=[0, 1],
357
- description="test match",
358
- )
359
- assert m.pattern == "TEST"
360
- assert m.confidence == 0.8
361
- assert m.v_weight == 0.0 # default
362
-
363
- def test_weight_fields(self):
364
- m = StructureMatch(
365
- pattern="TEST",
366
- confidence=0.8,
367
- matched_indices=[0],
368
- description="test",
369
- v_weight=-30.0,
370
- d_weight=-20.0,
371
- u_weight=40.0,
372
- g_weight=50.0,
373
- )
374
- assert m.v_weight == -30.0
375
- assert m.g_weight == 50.0
376
-
377
-
378
- # ── ATMOSPHERIC_GRIEF ───────────────────────────────────────────
379
-
380
- class TestAtmosphericGrief:
381
-
382
- def test_his_chair_still_at_table(self):
383
- """'his chair is still at the table' -> ATMOSPHERIC_GRIEF."""
384
- matches = _detect("his chair is still at the table")
385
- assert _has_pattern(matches, "ATMOSPHERIC_GRIEF")
386
-
387
- def test_found_her_necklace(self):
388
- """'i found her necklace in the drawer' -> ATMOSPHERIC_GRIEF."""
389
- matches = _detect("i found her necklace in the drawer")
390
- assert _has_pattern(matches, "ATMOSPHERIC_GRIEF")
391
-
392
- def test_coffee_mug_hasnt_moved(self):
393
- """'the coffee mug hasnt moved' -> ATMOSPHERIC_GRIEF."""
394
- matches = _detect("the coffee mug hasnt moved")
395
- assert _has_pattern(matches, "ATMOSPHERIC_GRIEF")
396
-
397
- def test_no_trigger_comfortable_chair(self):
398
- """'his chair is comfortable' -- no absence/persistence, NOT atmospheric grief."""
399
- matches = _detect("his chair is comfortable")
400
- assert not _has_pattern(matches, "ATMOSPHERIC_GRIEF")
401
-
402
- def test_no_trigger_active_possessor(self):
403
- """'she sat in her chair' -- person is active, NOT atmospheric grief."""
404
- matches = _detect("she sat in her chair")
405
- assert not _has_pattern(matches, "ATMOSPHERIC_GRIEF")
406
-
407
- def test_grief_score_negative_v(self):
408
- """Atmospheric grief should push V below center (128)."""
409
- matches = _detect("his chair is still at the table")
410
- m = _get_pattern(matches, "ATMOSPHERIC_GRIEF")
411
- assert m is not None
412
- assert m.v_weight < 0, "V weight should be negative (grief)"
413
- assert m.g_weight > 0, "G weight should be positive (heavy)"
414
- assert m.d_weight < 0, "D weight should be negative (helpless)"
415
-
416
-
417
- # ── CONTRADICTION_RESOLVE ──────────────────────────────────────
418
-
419
- class TestContradictionResolve:
420
-
421
- def test_painfully_beautiful(self):
422
- """'painfully beautiful' -> adjective head governs, positive."""
423
- matches = _detect("painfully beautiful")
424
- assert _has_pattern(matches, "CONTRADICTION_RESOLVE")
425
- m = _get_pattern(matches, "CONTRADICTION_RESOLVE")
426
- assert m.v_weight > 0, "Adjective head should make V positive"
427
-
428
- def test_hate_love(self):
429
- """'i hate how much i love you' -> main verb hate dominates."""
430
- matches = _detect("i hate how much i love you")
431
- assert _has_pattern(matches, "CONTRADICTION_RESOLVE")
432
- m = _get_pattern(matches, "CONTRADICTION_RESOLVE")
433
- assert m.v_weight < 0, "Main verb 'hate' should dominate -> negative"
434
-
435
- def test_sweet_revenge(self):
436
- """'sweet revenge' -> noun head governs, negative."""
437
- matches = _detect("sweet revenge")
438
- assert _has_pattern(matches, "CONTRADICTION_RESOLVE")
439
- m = _get_pattern(matches, "CONTRADICTION_RESOLVE")
440
- assert m.v_weight < 0, "Noun head 'revenge' should make V negative"
441
-
442
- def test_hurts_so_good(self):
443
- """'it hurts so good' -> complement overrides verb, positive."""
444
- matches = _detect("it hurts so good")
445
- assert _has_pattern(matches, "CONTRADICTION_RESOLVE")
446
- m = _get_pattern(matches, "CONTRADICTION_RESOLVE")
447
- assert m.v_weight > 0, "Qualifying complement 'so good' should override"
448
-
449
- def test_no_trigger_on_plain_positive(self):
450
- """'really beautiful' -> no contradiction, no trigger."""
451
- matches = _detect("really beautiful")
452
- assert not _has_pattern(matches, "CONTRADICTION_RESOLVE")
453
-
454
- def test_no_trigger_on_plain_negative(self):
455
- """'absolutely terrible' -> no contradiction, no trigger."""
456
- matches = _detect("absolutely terrible")
457
- assert not _has_pattern(matches, "CONTRADICTION_RESOLVE")
458
-
459
-
460
- # ── NUMBERS_CONTEXT ────────────────────────────────────────────
461
-
462
- class TestNumbersContext:
463
-
464
- def test_sleep_deprivation(self):
465
- """'i only slept 3 hours' -> sleep deprivation detected."""
466
- matches = _detect("i only slept 3 hours")
467
- assert _has_pattern(matches, "NUMBERS_CONTEXT")
468
- m = _get_pattern(matches, "NUMBERS_CONTEXT")
469
- assert m.v_weight < 0, "Sleep deprivation should be negative"
470
-
471
- def test_only_one_at_birthday(self):
472
- """'only one person came to my birthday' -> social isolation."""
473
- matches = _detect("only one person came to my birthday")
474
- assert _has_pattern(matches, "NUMBERS_CONTEXT")
475
- m = _get_pattern(matches, "NUMBERS_CONTEXT")
476
- assert m.v_weight < 0
477
-
478
- def test_no_one_at_party(self):
479
- """'no one came to my party' -> social isolation."""
480
- matches = _detect("no one came to my party")
481
- assert _has_pattern(matches, "NUMBERS_CONTEXT")
482
- m = _get_pattern(matches, "NUMBERS_CONTEXT")
483
- assert m.v_weight < 0
484
-
485
- def test_no_trigger_normal_sleep(self):
486
- """'i slept 8 hours' -> no deprivation signal."""
487
- matches = _detect("i slept 8 hours")
488
- assert not _has_pattern(matches, "NUMBERS_CONTEXT")
489
-
490
- def test_no_trigger_everyone_at_party(self):
491
- """'everyone came to my party' -> no isolation."""
492
- matches = _detect("everyone came to my party")
493
- assert not _has_pattern(matches, "NUMBERS_CONTEXT")
494
-
495
-
496
- # ── NEGATED_NEGATIVE_COMPLIMENT ────────────────────────────────
497
-
498
- class TestNegatedNegativeCompliment:
499
-
500
- def test_without_you(self):
501
- """'i couldnt have done it without you' -> positive compliment."""
502
- matches = _detect("i couldnt have done it without you")
503
- assert _has_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT")
504
- m = _get_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT")
505
- assert m.v_weight > 0, "Double negation should be positive"
506
-
507
- def test_reason_didnt_give_up(self):
508
- """'youre the reason i didnt give up' -> positive compliment."""
509
- matches = _detect("youre the reason i didnt give up")
510
- assert _has_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT")
511
- m = _get_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT")
512
- assert m.v_weight > 0
513
-
514
- def test_cant_thank_enough(self):
515
- """'i cant thank you enough' -> positive compliment."""
516
- matches = _detect("i cant thank you enough")
517
- assert _has_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT")
518
- m = _get_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT")
519
- assert m.v_weight > 0
520
-
521
- def test_wouldnt_be_here_without_you(self):
522
- """'i wouldnt be here without you' -> positive compliment."""
523
- matches = _detect("i wouldnt be here without you")
524
- assert _has_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT")
525
-
526
- def test_no_trigger_plain_negation(self):
527
- """'i dont like you' -> not a compliment."""
528
- matches = _detect("i dont like you")
529
- assert not _has_pattern(matches, "NEGATED_NEGATIVE_COMPLIMENT")
530
-
531
-
532
- # ── RECOVERY_SMALL_WIN ─────────────────────────────────────────
533
-
534
- class TestRecoverySmallWin:
535
-
536
- def test_got_out_of_bed_today(self):
537
- """'i got out of bed today' -> recovery milestone."""
538
- matches = _detect("i got out of bed today")
539
- assert _has_pattern(matches, "RECOVERY_SMALL_WIN")
540
- m = _get_pattern(matches, "RECOVERY_SMALL_WIN")
541
- assert m.v_weight > 0, "Small win should be positive"
542
- assert m.w_weight > 0, "Small win should boost self-worth"
543
-
544
- def test_ate_a_full_meal(self):
545
- """'i ate a full meal' -> recovery signal."""
546
- matches = _detect("i ate a full meal")
547
- assert _has_pattern(matches, "RECOVERY_SMALL_WIN")
548
-
549
- def test_finally_took_a_shower(self):
550
- """'i finally took a shower' -> small win with temporal marker."""
551
- matches = _detect("i finally took a shower")
552
- assert _has_pattern(matches, "RECOVERY_SMALL_WIN")
553
- m = _get_pattern(matches, "RECOVERY_SMALL_WIN")
554
- assert m.confidence >= 0.85, "Temporal marker should boost confidence"
555
-
556
- def test_went_outside_first_time(self):
557
- """'i went outside for the first time' -> recovery milestone."""
558
- matches = _detect("i went outside for the first time")
559
- assert _has_pattern(matches, "RECOVERY_SMALL_WIN")
560
-
561
- def test_no_trigger_mundane_no_context(self):
562
- """'the shower is broken' -> no recovery signal without self-ref/temporal."""
563
- matches = _detect("the shower is broken")
564
- assert not _has_pattern(matches, "RECOVERY_SMALL_WIN")
565
-
566
- def test_no_trigger_normal_routine(self):
567
- """'i need some coffee' -> not a recovery win."""
568
- matches = _detect("i need some coffee")
569
- assert not _has_pattern(matches, "RECOVERY_SMALL_WIN")
570
-
571
-
572
- # ── MUNDANE_HYPERBOLE ────────────────────────────────────────────
573
-
574
- class TestMundaneHyperbole:
575
-
576
- def test_oov_token_does_not_trigger(self):
577
- """'i still have his number saved' -- 'number' is OOV; an unknown
578
- token is not evidence of mundane context. MUNDANE_HYPERBOLE must
579
- not fire and cancel the grief reading."""
580
- matches = _detect("i still have his number saved")
581
- assert not _has_pattern(matches, "MUNDANE_HYPERBOLE")
582
- assert _has_pattern(matches, "PERSISTENT_ABSENCE")
583
-
584
- def test_masking_cofire_suppresses_mundane(self):
585
- """'im tired of pretending im okay' -- MASKING (crisis-tier) fires;
586
- MUNDANE_HYPERBOLE must yield. A mundane reading and a crisis
587
- reading cannot both be right."""
588
- matches = _detect("im tired of pretending im okay")
589
- assert _has_pattern(matches, "MASKING")
590
- assert not _has_pattern(matches, "MUNDANE_HYPERBOLE")
591
-
592
- def test_finality_cofire_suppresses_mundane(self):
593
- """'i never got to say goodbye to my brother' -- FINALITY
594
- (crisis-tier) fires; MUNDANE_HYPERBOLE must not cancel it."""
595
- matches = _detect("i never got to say goodbye to my brother")
596
- assert _has_pattern(matches, "FINALITY")
597
- assert not _has_pattern(matches, "MUNDANE_HYPERBOLE")
598
-
599
- def test_genuine_mundane_hyperbole_still_fires(self):
600
- """'this homework is killing me' -- the canonical mundane
601
- hyperbole must still be defused."""
602
- matches = _detect("this homework is killing me")
603
- assert _has_pattern(matches, "MUNDANE_HYPERBOLE")
604
-
605
- def test_traffic_complaint_still_fires(self):
606
- """'i want to die this traffic is insane' -- complaint, not crisis."""
607
- matches = _detect("i want to die this traffic is insane")
608
- assert _has_pattern(matches, "MUNDANE_HYPERBOLE")
609
-
610
-
611
- # ── Governor heartbeat fixes (2026-06-11) ────────────────────────
612
- # Three misfires from a live masked-collapse conversation:
613
- # 1. hedge over-fire: "i guess" alone inverted mild positives into
614
- # PASSIVE_RESIGNATION
615
- # 2. masking miss: "im fine dont worry about it" read POSITIVE
616
- # 3. DIVESTITURE silent on "giving my stuff away" particle order
617
-
618
- class TestHedgeDamping:
619
- """'i guess/suppose' alone damps positivity -- it is NOT resignation."""
620
-
621
- def test_hedge_only_does_not_fire_resignation(self):
622
- """Mildly deflated assessments with a trailing hedge must not
623
- read as passive resignation."""
624
- for text in (
625
- "work was fine today i guess",
626
- "the movie was okay i guess",
627
- "it went alright i suppose",
628
- "dinner was decent i guess",
629
- ):
630
- matches = _detect(text)
631
- assert not _has_pattern(matches, "PASSIVE_RESIGNATION"), \
632
- f"PASSIVE_RESIGNATION over-fired on hedge-only: {text!r}"
633
- assert _has_pattern(matches, "HEDGED_ASSESSMENT"), \
634
- f"HEDGED_ASSESSMENT should damp: {text!r}"
635
-
636
- def test_hedge_only_v_lands_mild_neutral(self):
637
- """Hedged positive must land mild-neutral, not collapse-grade."""
638
- from engine.pendulum import compute_vadug
639
- for text in (
640
- "work was fine today i guess",
641
- "the movie was okay i guess",
642
- "it went alright i suppose",
643
- ):
644
- r, _ = compute_vadug(text)
645
- assert 100 <= r.v <= 135, \
646
- f"V={r.v} should be mild-neutral for hedged positive: {text!r}"
647
-
648
- def test_hedge_with_resignation_evidence_still_fires(self):
649
- """Hedge + genuine resignation signal must still fire."""
650
- for text in (
651
- "i guess i deserved it",
652
- "i suppose youre right",
653
- "i guess nothing matters anymore",
654
- "i guess some people just dont get it",
655
- ):
656
- matches = _detect(text)
657
- assert _has_pattern(matches, "PASSIVE_RESIGNATION"), \
658
- f"PASSIVE_RESIGNATION should fire with evidence: {text!r}"
659
-
660
- def test_explicit_surrender_fires(self):
661
- """First-person surrender verbs are resignation, hedge or not."""
662
- for text in ("whatever happens happens i give up", "i give up", "i quit"):
663
- matches = _detect(text)
664
- assert _has_pattern(matches, "PASSIVE_RESIGNATION"), \
665
- f"PASSIVE_RESIGNATION should fire on surrender: {text!r}"
666
-
667
- def test_negated_surrender_is_perseverance(self):
668
- """Negated give-up is perseverance and must not fire."""
669
- for text in (
670
- "she didnt give up on me",
671
- "i wont give up on this",
672
- "never give up on your dreams",
673
- ):
674
- matches = _detect(text)
675
- assert not _has_pattern(matches, "PASSIVE_RESIGNATION"), \
676
- f"PASSIVE_RESIGNATION misfired on perseverance: {text!r}"
677
-
678
-
679
- class TestMaskingDeflection:
680
- """Self-status claim + dismissive deflection = MASKING, not reassurance."""
681
-
682
- def test_claim_plus_deflection_fires_masking(self):
683
- for text in (
684
- "im fine dont worry about it",
685
- "im okay seriously dont worry",
686
- "its nothing forget about it",
687
- "im good it doesnt matter",
688
- "im fine just drop it",
689
- ):
690
- matches = _detect(text)
691
- assert _has_pattern(matches, "MASKING"), \
692
- f"MASKING should fire on claim+deflection: {text!r}"
693
-
694
- def test_claim_plus_deflection_reads_uneasy(self):
695
- """V must land uneasy (below neutral), W must dip -- the deflection
696
- is masking evidence, not reassurance."""
697
- from engine.pendulum import compute_vadug
698
- r, _ = compute_vadug("im fine dont worry about it")
699
- assert 70 <= r.v <= 115, f"V={r.v} should be uneasy"
700
- assert r.w < 128, f"W={r.w} should dip below neutral"
701
-
702
- def test_helper_reassurance_does_not_fire_masking(self):
703
- """Reassurance directed at someone else has no self-status claim."""
704
- for text in (
705
- "dont worry ill handle it",
706
- "dont worry youll do great",
707
- "forget it ill take care of everything",
708
- ):
709
- matches = _detect(text)
710
- assert not _has_pattern(matches, "MASKING"), \
711
- f"MASKING misfired on helper reassurance: {text!r}"
712
-
713
- def test_negated_emotion_is_not_victimization(self):
714
- """'dont worry' is a negated emotion -- not an act done to the
715
- user. VICTIMIZATION must not fire on it."""
716
- for text in ("im fine dont worry about it", "dont worry ill handle it"):
717
- matches = _detect(text)
718
- assert not _has_pattern(matches, "VICTIMIZATION"), \
719
- f"VICTIMIZATION misfired on negated emotion: {text!r}"
720
-
721
-
722
- class TestDivestitureParticleOrder:
723
- """DIVESTITURE must catch both particle orders and progressive/perfect."""
724
-
725
- def test_divestiture_fires_both_particle_orders(self):
726
- for text in (
727
- "ive been giving my stuff away",
728
- "i gave away my records yesterday",
729
- "been giving my things away lately",
730
- "im giving away all my stuff",
731
- ):
732
- matches = _detect(text)
733
- assert _has_pattern(matches, "DIVESTITURE"), \
734
- f"DIVESTITURE should fire: {text!r}"
735
-
736
- def test_divestiture_benign_does_not_fire(self):
737
- """No first-person possession = no crisis signal."""
738
- for text in (
739
- "giving away free samples",
740
- "the store is giving away prizes",
741
- "giving away the bride",
742
- "they are giving away tickets at the door",
743
- ):
744
- matches = _detect(text)
745
- assert not _has_pattern(matches, "DIVESTITURE"), \
746
- f"DIVESTITURE misfired on benign giveaway: {text!r}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_w_channel.py DELETED
@@ -1,84 +0,0 @@
1
- """Board 2: W (self-worth) attribution routing + I (intent) agency axis.
2
-
3
- W is valence routed through WHO it lands on:
4
- - self-declarative ("i am worthless") -> full attribution, W drops hard
5
- - emotional force targeting self ("he told me") -> strong attribution
6
- - self as actor of negative act (guilt) -> partial attribution, W dips
7
- - atmospheric / other-directed -> no attribution, W stable
8
-
9
- I gains an agency axis on top of the directional (withdraw/connect/control)
10
- logic: volition markers lift I, futility markers sink it.
11
- """
12
-
13
- import pytest
14
-
15
- from engine.pendulum import compute_vadug
16
-
17
-
18
- def _vadug(text):
19
- result, _ = compute_vadug(text)
20
- return result
21
-
22
-
23
- class TestWAttribution:
24
- def test_self_declarative_drops_w_hard(self):
25
- """SELF as subject + emotional predicate = full attribution."""
26
- r = _vadug("i am worthless")
27
- assert r.w < 80, f"W={r.w} should drop hard for self-declaration"
28
-
29
- def test_target_self_drops_w(self):
30
- """Emotional force whose target is self lands on W."""
31
- r = _vadug("he told me im nothing")
32
- assert r.w < 100, f"W={r.w} should drop when self is the target"
33
-
34
- def test_atmospheric_w_stable(self):
35
- """Other-directed/atmospheric forces say nothing about MY worth."""
36
- r = _vadug("the weather is awful")
37
- assert r.w >= 110, f"W={r.w} should stay stable for atmospheric negativity"
38
- r2 = _vadug("they announced layoffs")
39
- assert r2.w >= 110, f"W={r2.w} should stay stable for other-directed news"
40
-
41
- def test_guilt_partial_attribution(self):
42
- """Self harming another dips W, but far less than self-declaration."""
43
- guilt = _vadug("i hurt her badly")
44
- declarative = _vadug("i am worthless")
45
- assert guilt.w < 128, f"W={guilt.w} should dip below neutral for guilt"
46
- assert guilt.w >= 95, f"W={guilt.w} should not collapse for guilt"
47
- assert guilt.w > declarative.w, (
48
- f"guilt W={guilt.w} must stay above self-declarative W={declarative.w}"
49
- )
50
-
51
- def test_masked_collapse_reads_low_w(self):
52
- """Crisis patterns with damaged self-worth push W into the 80-100 band."""
53
- for text in (
54
- "cant do this shit anymore",
55
- "theres no way out",
56
- "i just want it to be over",
57
- "i stopped making plans because whats the point",
58
- ):
59
- r = _vadug(text)
60
- assert r.w <= 100, f"W={r.w} should be <= 100 for: {text!r}"
61
-
62
-
63
- class TestIntentAgency:
64
- def test_agency_raises_intent(self):
65
- """Volition marker + action verb lifts I toward control."""
66
- for text in ("im going to fix this tomorrow", "i need to finish this project"):
67
- r = _vadug(text)
68
- assert r.i >= 160, f"I={r.i} should be >= 160 for agency: {text!r}"
69
-
70
- def test_futility_drops_intent(self):
71
- """Powerlessness/futility markers sink I toward deflect."""
72
- for text in ("whats the point", "i give up", "why bother trying"):
73
- r = _vadug(text)
74
- assert r.i <= 80, f"I={r.i} should be <= 80 for futility: {text!r}"
75
-
76
- def test_agency_travel_not_boosted(self):
77
- """'im going to the store' is travel, not agency."""
78
- r = _vadug("im going to the store")
79
- assert r.i < 160, f"I={r.i} should stay neutral for destinations"
80
-
81
- def test_negated_give_up_not_futility(self):
82
- """'didnt give up' is perseverance — must not sink I."""
83
- r = _vadug("she didnt give up on me")
84
- assert r.i > 64, f"I={r.i} should not read negated give-up as futility"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
engine/tests/test_word_classifier.py DELETED
@@ -1,166 +0,0 @@
1
- """Tests for V3 Layer 1: Word Role Classifier."""
2
-
3
- import pytest
4
- import sys
5
- import os
6
-
7
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
8
-
9
- from engine.word_classifier import classify_sentence, classify_word, WordRole
10
-
11
-
12
- class TestBasicRoles:
13
- def test_self_reference(self):
14
- roles = classify_sentence(["I", "am", "happy"])
15
- assert roles[0].role == "SELF_REF"
16
-
17
- def test_emotional_word(self):
18
- roles = classify_sentence(["I", "am", "happy"])
19
- assert roles[2].role == "EMOTIONAL"
20
- assert roles[2].force is not None
21
- assert roles[2].force[0] > 0 # happy = positive V
22
-
23
- def test_negator(self):
24
- roles = classify_sentence(["I", "am", "not", "happy"])
25
- assert roles[2].role == "NEGATOR"
26
-
27
- def test_amplifier(self):
28
- roles = classify_sentence(["I", "am", "very", "happy"])
29
- assert roles[2].role == "AMPLIFIER"
30
-
31
- def test_transfer(self):
32
- roles = classify_sentence(["I", "gave", "my", "dog", "to", "neighbor"])
33
- assert roles[1].role == "TRANSFER"
34
- assert roles[3].role == "RELATION_REF" # dog is relationship
35
- assert roles[5].role == "RELATION_REF"
36
-
37
- def test_method(self):
38
- roles = classify_sentence(["bought", "a", "bunch", "of", "pills"])
39
- assert roles[0].role == "ACQUIRE"
40
- assert roles[4].role == "METHOD"
41
-
42
- def test_finality(self):
43
- roles = classify_sentence(["this", "is", "the", "last", "time"])
44
- assert roles[3].role == "FINALITY"
45
-
46
-
47
- class TestPositionOverrides:
48
- def test_give_hug_not_transfer(self):
49
- """'give me a hug' — give is still TRANSFER but context matters at structure level."""
50
- roles = classify_sentence(["give", "me", "a", "hug"])
51
- assert roles[0].role == "TRANSFER"
52
- # Structure detector (Task 3) will see TRANSFER + no POSSESSION = not farewell
53
-
54
- def test_fine_after_self(self):
55
- roles = classify_sentence(["im", "fine"])
56
- assert roles[1].role == "PEACE"
57
-
58
- def test_fine_without_self_ref(self):
59
- """'fine' not after SELF_REF should stay in its base role."""
60
- roles = classify_sentence(["that", "is", "fine"])
61
- # "fine" is in PEACE set, so it's PEACE regardless — position override
62
- # only matters if it could be EMOTIONAL vs PEACE
63
- assert roles[2].role == "PEACE"
64
-
65
- def test_chopper(self):
66
- roles = classify_sentence(["I", "love", "you", "but", "im", "leaving"])
67
- assert roles[3].role == "CHOPPER"
68
-
69
- def test_still_is_temporal(self):
70
- """'still' is always TEMPORAL — freshness/persistence marker."""
71
- roles = classify_sentence(["I", "still", "feel", "bad"])
72
- assert roles[1].role == "TEMPORAL"
73
-
74
- def test_never_is_negator(self):
75
- """'never' should be NEGATOR, not TEMPORAL."""
76
- roles = classify_sentence(["I", "never", "said", "that"])
77
- assert roles[1].role == "NEGATOR"
78
-
79
- def test_just_before_acquire_is_temporal(self):
80
- """'just bought' = recently acquired → TEMPORAL."""
81
- roles = classify_sentence(["I", "just", "bought", "a", "car"])
82
- assert roles[1].role == "TEMPORAL"
83
- assert roles[2].role == "ACQUIRE"
84
-
85
- def test_just_is_compressor(self):
86
- """'just' = COMPRESSOR — squeezes magnitude of nearby words."""
87
- roles = classify_sentence(["I", "just", "want", "to", "sleep"])
88
- assert roles[1].role == "COMPRESSOR"
89
-
90
-
91
- class TestNeighbors:
92
- def test_neighbors_filled(self):
93
- roles = classify_sentence(["I", "am", "happy"])
94
- assert roles[0].neighbors == (None, "NEUTRAL") # I: no left, "am" right
95
- assert roles[1].neighbors == ("SELF_REF", "EMOTIONAL") # am: I left, happy right
96
- assert roles[2].neighbors == ("NEUTRAL", None) # happy: am left, no right
97
-
98
- def test_single_word(self):
99
- roles = classify_sentence(["happy"])
100
- assert roles[0].neighbors == (None, None)
101
-
102
- def test_two_words(self):
103
- roles = classify_sentence(["very", "happy"])
104
- assert roles[0].neighbors == (None, "EMOTIONAL")
105
- assert roles[1].neighbors == ("AMPLIFIER", None)
106
-
107
-
108
- class TestAmplifiers:
109
- def test_fucking_is_amplifier(self):
110
- """'fucking' is a 1.6x spice word amplifier."""
111
- roles = classify_sentence(["thats", "fucking", "amazing"])
112
- assert roles[1].role == "AMPLIFIER"
113
-
114
- def test_honestly_is_filler(self):
115
- """'honestly' is a glass breaker — classified as FILLER (processing marker)."""
116
- roles = classify_sentence(["honestly", "this", "sucks"])
117
- assert roles[0].role == "FILLER"
118
-
119
- def test_so_is_amplifier(self):
120
- """'so' before emotional word = AMPLIFIER."""
121
- roles = classify_sentence(["im", "so", "happy"])
122
- assert roles[1].role == "AMPLIFIER"
123
-
124
-
125
- class TestEdgeCases:
126
- def test_empty_sentence(self):
127
- roles = classify_sentence([])
128
- assert roles == []
129
-
130
- def test_all_neutral(self):
131
- roles = classify_sentence(["the", "a", "is"])
132
- for r in roles:
133
- assert r.role == "NEUTRAL"
134
-
135
- def test_emotional_from_vocabulary(self):
136
- """Words not in any role set but in VOCABULARY with |dV| > 15 = EMOTIONAL."""
137
- roles = classify_sentence(["I", "am", "sad"])
138
- # "sad" is not in any role set, but is in VOCABULARY with big dV
139
- assert roles[2].role == "EMOTIONAL"
140
- assert roles[2].force is not None
141
- assert roles[2].force[0] < 0 # sad = negative V
142
-
143
- def test_hedge_words(self):
144
- roles = classify_sentence(["maybe", "I", "should"])
145
- assert roles[0].role == "HEDGE"
146
-
147
- def test_relation_ref(self):
148
- roles = classify_sentence(["my", "mom", "is", "angry"])
149
- assert roles[0].role == "SELF_REF"
150
- assert roles[1].role == "RELATION_REF"
151
-
152
- def test_word_role_dataclass(self):
153
- """WordRole stores all expected fields."""
154
- roles = classify_sentence(["I", "am", "happy"])
155
- r = roles[0]
156
- assert r.word == "i"
157
- assert r.role == "SELF_REF"
158
- assert r.base_role == "SELF_REF"
159
- assert r.position == 0
160
- assert r.force is None # SELF_REF has no force
161
-
162
- def test_punctuation_stripped(self):
163
- """Punctuation should not affect classification."""
164
- roles = classify_sentence(["I'm", "fine!"])
165
- assert roles[0].role == "SELF_REF"
166
- assert roles[1].role == "PEACE"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
  fastapi==0.115.*
2
  uvicorn[standard]==0.30.*
3
- clanker-soul
4
  httpx==0.27.*
5
  pytest==8.*
 
1
  fastapi==0.115.*
2
  uvicorn[standard]==0.30.*
3
+ clanker-soul @ git+https://github.com/deucebucket/clanker-soul.git
4
  httpx==0.27.*
5
  pytest==8.*
scripts/sync_engine.sh CHANGED
@@ -6,4 +6,5 @@ DST="$(cd "$(dirname "$0")/.." && pwd)/engine"
6
  rm -rf "$DST"
7
  cp -r "$SRC" "$DST"
8
  find "$DST" -name '__pycache__' -type d -prune -exec rm -rf {} +
 
9
  echo "vendored engine -> $DST"
 
6
  rm -rf "$DST"
7
  cp -r "$SRC" "$DST"
8
  find "$DST" -name '__pycache__' -type d -prune -exec rm -rf {} +
9
+ rm -rf "$DST/tests"
10
  echo "vendored engine -> $DST"