Spaces:
Running
Running
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 +6 -0
- Dockerfile +11 -0
- README.md +16 -0
- engine/tests/__init__.py +0 -0
- engine/tests/test_battleship.py +0 -133
- engine/tests/test_crisis.py +0 -80
- engine/tests/test_neutralizer.py +0 -79
- engine/tests/test_novel.py +0 -107
- engine/tests/test_pa_structures.py +0 -290
- engine/tests/test_pendulum.py +0 -255
- engine/tests/test_proximity.py +0 -192
- engine/tests/test_scaffold.py +0 -30
- engine/tests/test_slang_register.py +0 -203
- engine/tests/test_solver.py +0 -104
- engine/tests/test_structures.py +0 -746
- engine/tests/test_w_channel.py +0 -84
- engine/tests/test_word_classifier.py +0 -166
- requirements.txt +1 -1
- scripts/sync_engine.sh +1 -0
.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"
|