File size: 16,595 Bytes
ecca2a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
"""V4 Layer 1.5: Force Flow Resolver — WHO does WHAT to WHOM.

Every sentence has a force flow: an actor pushes force toward a target.
The direction determines how the impact lands.

  "I love my dog"     → Self --love(+)--> pet      (giving affection)
  "He hit me"         → Other --hit(-)--> Self      (victimization)
  "I hit him"         → Self --hit(-)--> Other      (aggression, D stays)
  "The medicine stopped working" → thing --stopped--> function (purpose severed)

The resolver identifies Subject-Verb-Object triples from word roles
and computes a force flow direction that modifies how the physics
loop applies word forces.

Force flow affects:
  - D (dominance): actor keeps/gains D, target loses D
  - W (self-worth): self-directed negative = W drops harder
  - V (valence): direction amplifies or dampens based on who is affected
"""

from dataclasses import dataclass
from typing import List, Optional

from .word_classifier import WordRole


@dataclass
class ForceFlow:
    """Resolved force flow for a sentence."""
    actor_idx: int = -1          # position of the actor (subject)
    actor_role: str = ""         # SELF_REF, OTHER_REF, RELATION_REF, or NEUTRAL (thing)
    force_idx: int = -1          # position of the main force word (verb)
    force_valence: int = 0       # dV of the force word
    target_idx: int = -1         # position of the target (object)
    target_role: str = ""        # SELF_REF, OTHER_REF, RELATION_REF, or NEUTRAL
    negated: bool = False        # is the force negated? (stopped, not, didn't)

    @property
    def self_is_actor(self) -> bool:
        return self.actor_role == "SELF_REF"

    @property
    def self_is_target(self) -> bool:
        return self.target_role == "SELF_REF"

    @property
    def other_acts_on_self(self) -> bool:
        """Other/relation acts on self — victimization pattern."""
        return (self.actor_role in ("OTHER_REF", "RELATION_REF")
                and self.target_role == "SELF_REF")

    @property
    def self_acts_on_self(self) -> bool:
        """Self acts on self — self-directed force."""
        return self.actor_role == "SELF_REF" and self.target_role == "SELF_REF"

    @property
    def effective_valence(self) -> int:
        """Force valence after negation."""
        if self.negated:
            return -self.force_valence
        return self.force_valence


# Roles that can be actors or targets (entities, not operators)
_ENTITY_ROLES = {"SELF_REF", "OTHER_REF", "RELATION_REF"}
_NEGATOR_ROLES = {"NEGATOR"}


def resolve_force_flow(roles: List[WordRole]) -> Optional[ForceFlow]:
    """Resolve the primary force flow (SVO) from classified word roles.

    Scanning strategy:
      1. Find the strongest force word (highest |dV| in vocabulary)
      2. Look LEFT for the nearest entity → actor (subject)
      3. Look RIGHT for the nearest entity → target (object)
      4. Check for negators between actor and force

    Returns ForceFlow or None if no clear SVO found.
    """
    if len(roles) < 1:
        return None

    # Find the strongest emotional force word
    # Check both role-assigned force AND vocabulary lookup (same as pendulum)
    from .vocabulary import VOCABULARY
    best_force_idx = -1
    best_force_strength = 0

    for i, wr in enumerate(roles):
        force = wr.force or VOCABULARY.get(wr.word)
        if force is not None:
            strength = abs(force[0])  # |dV|
            if strength > best_force_strength:
                best_force_strength = strength
                best_force_idx = i

    if best_force_idx == -1 or best_force_strength < 10:
        return None  # no meaningful force word

    # Use the resolved force for valence
    force_word = roles[best_force_idx]
    _resolved_force = force_word.force or VOCABULARY.get(force_word.word)

    force_word = roles[best_force_idx]

    # Look LEFT for actor (nearest entity before the force word)
    actor_idx = -1
    actor_role = ""
    for j in range(best_force_idx - 1, -1, -1):
        if roles[j].role in _ENTITY_ROLES:
            actor_idx = j
            actor_role = roles[j].role
            break

    # Look RIGHT for target (nearest entity after the force word)
    target_idx = -1
    target_role = ""
    for j in range(best_force_idx + 1, len(roles)):
        if roles[j].role in _ENTITY_ROLES:
            target_idx = j
            target_role = roles[j].role
            break

    # If no explicit target but actor is SELF_REF and force is self-directed
    # (e.g., "i am stupid"), self is both actor and target
    if target_idx == -1 and actor_role == "SELF_REF":
        target_idx = actor_idx
        target_role = "SELF_REF"

    # If no explicit target but actor is OTHER/RELATION AND the force is
    # strong enough, the implied target is SELF. The user is the default
    # gravitational center. "he lied" = he lied to ME. "he proposed" = to ME.
    # But "she laughed" alone is ambiguous -- don't imply target on weak forces.
    if (target_idx == -1
            and actor_role in ("OTHER_REF", "RELATION_REF")
            and best_force_strength >= 25):
        target_role = "SELF_REF"  # implied, no index

    # IMPERATIVE detection: strong force word + no actor + no target + short sentence
    # = bare command aimed at the listener. "Shut up" = USER → OTHER.
    # "Get out" = USER → OTHER. The speaker is the actor, the listener is the target.
    #
    # ALSO: command tokens (getout, shutup, fuckoff) with possessive SELF_REF
    # ("get out of MY way", "shut MY door") = SELF is authority, not target.
    # The possessive "my" after a command = ownership, not victimhood.
    _COMMAND_TOKENS = {"shutup", "getout", "fuckoff", "backoff", "pissoff"}
    force_word_text = roles[best_force_idx].word if best_force_idx >= 0 else ""
    is_command_token = force_word_text in _COMMAND_TOKENS

    if is_command_token:
        # Command token always = SELF commands OTHER, regardless of possessives
        actor_role = "SELF_REF"
        target_role = "OTHER_REF"
    elif (actor_idx == -1 and target_idx == -1
            and best_force_strength >= 25 and len(roles) <= 6):
        actor_role = "SELF_REF"
        target_role = "OTHER_REF"
    elif (actor_role == "" and target_role == ""
            and best_force_strength >= 30):
        actor_role = "SELF_REF"
        target_role = "OTHER_REF"

    # If no actor or target resolved (even implied), give up
    if actor_role == "" and target_role == "":
        return None

    # Check for negation: either a NEGATOR between actor and force,
    # OR the actor itself is a negating word (nobody, nothing, no one → resolved to "nobody")
    # "nobody hurt me" = negated actor + negative verb = positive outcome
    # "nobody loves me" = negated actor + positive verb = negative outcome
    _NEGATING_ACTORS = {"nobody", "nothing", "none", "noone"}
    # Search for negators between actor and force, AND before actor
    # "it WASNT my fault" = wasnt is before "my" (actor) but negates the whole predicate
    search_start = max(0, (actor_idx - 2) if actor_idx >= 0 else 0)
    negated = any(
        roles[j].role in _NEGATOR_ROLES
        for j in range(search_start, best_force_idx)
    )
    # Actor itself is a negation word
    if actor_idx >= 0 and roles[actor_idx].word in _NEGATING_ACTORS:
        negated = True

    return ForceFlow(
        actor_idx=actor_idx,
        actor_role=actor_role,
        force_idx=best_force_idx,
        force_valence=_resolved_force[0] if _resolved_force else 0,
        target_idx=target_idx,
        target_role=target_role,
        negated=negated,
    )


def compute_intent(flow: Optional[ForceFlow], roles=None) -> int:
    """Compute Intent (I) dimension from force flow.

    Intent = WHERE is the force aimed and WHY.
      0   = WITHDRAW (retreating, cutting ties, pulling away)
      64  = DEFLECT (avoiding, redirecting, not engaging)
      128 = NEUTRAL (informational, no directional intent)
      192 = CONNECT (reaching toward, building, repairing)
      255 = CONTROL (dominating, directing, commanding)

    The intent is determined by:
    1. The force valence (positive = connect/heal, negative = poison/attack)
    2. The direction (who is acting on whom)
    3. Structural cues (PULL_AWAY = withdraw, POWER = control)
    """
    # No resolved flow = neutral directional intent, but the agency axis
    # below can still move I from phrase-level volition/futility markers.
    ev = flow.effective_valence if flow is not None else 0
    intent = 128  # start neutral

    # Positive force directed at OTHER/RELATION = CONNECT (healing potion → other)
    if ev > 0 and flow.self_is_actor and not flow.self_is_target:
        intent = 160 + min(ev // 4, 60)  # 160-220

    # Self-directed force: accountability, self-attack, self-affirm, or deflection.
    # Must check BEFORE generic positive-self, because negated accountability
    # (ev positive after flip) would otherwise read as self-affirmation.
    elif flow is not None and flow.self_acts_on_self:
        _ACCOUNTABILITY_WORDS = {"wrong", "sorry", "apologize", "fault", "mistake",
                                "responsibility", "owe", "messed", "screwed"}
        force_word = roles[flow.force_idx].word if roles and 0 <= flow.force_idx < len(roles) else ""
        raw_fv = flow.force_valence  # before negation

        if force_word in _ACCOUNTABILITY_WORDS and not flow.negated and raw_fv < 0:
            # "I was wrong" = accepting the hit = CONNECT/REPAIR intent
            intent = 170 + min(abs(raw_fv) // 5, 40)  # 170-210 = connect
        elif force_word in _ACCOUNTABILITY_WORDS and flow.negated:
            # "It wasn't my fault" = DEFLECTING the hit = DEFLECT
            intent = 80 + min(abs(raw_fv) // 5, 30)  # 80-110 = deflect
        elif raw_fv < 0 and not flow.negated:
            # "I hate myself" = self-destruction = WITHDRAW
            intent = 40 - min(abs(raw_fv) // 6, 30)  # 40-10 = withdraw
        elif raw_fv > 0:
            # "I am proud of myself" = self-affirm = CONNECT
            intent = 150 + min(abs(raw_fv) // 6, 40)  # 150-190 = connect

    # Negative force from OTHER onto SELF = being attacked (DEFLECT/WITHDRAW)
    elif ev < 0 and flow.other_acts_on_self:
        intent = 80 - min(abs(ev) // 4, 60)  # 80-20 = deflect→withdraw

    # Negative force SELF → OTHER: could be ATTACK/CONTROL or SELF-ASSESSMENT
    # "im a burden to everyone" = self-assessment (withdraw), not attack
    # "i hate you" = attack (control)
    # Distinguish: if the force word is self-descriptive (burden, problem, waste)
    # it's self-assessment/withdraw, not control
    elif ev < 0 and flow.self_is_actor and not flow.self_is_target:
        # Check if this is self-assessment (self describing self negatively TO others)
        _SELF_ASSESSMENT = {"burden", "problem", "waste", "mistake", "obstacle",
                           "nuisance", "hindrance", "liability", "deadweight",
                           "nothing", "worthless", "useless", "failure", "trash",
                           "garbage", "broken", "pathetic", "stupid", "weak"}
        force_word = roles[flow.force_idx].word if roles and 0 <= flow.force_idx < len(roles) else ""
        if force_word in _SELF_ASSESSMENT:
            intent = 40 - min(abs(ev) // 6, 30)  # withdraw -- self-assessment, not attack
        else:
            intent = 200 + min(abs(ev) // 4, 55)  # control -- attacking other

    # Positive force from OTHER = receiving (not self-initiated)
    elif ev > 0 and flow.other_acts_on_self:
        intent = 155 + min(ev // 5, 50)  # 155-205 = connect (receiving)

    # ── AGENCY AXIS (Board 2) ──────────────────────────────────────
    # The directional logic above answers WHERE the force aims; this
    # answers whether the speaker still owns their own motion.
    # Volition/planning markers ("i want to <verb>", "im going to <verb>")
    # lift I toward CONTROL territory. Powerlessness/futility markers
    # ("whats the point", "i give up") sink I toward DEFLECT.
    futility_hit = False
    if roles:
        words = [r.word for r in roles]

        _FUTILITY_PHRASES = (
            ("whats", "the", "point"), ("what", "is", "the", "point"),
            ("whats", "even", "the", "point"),
            ("why", "bother"), ("why", "even", "bother"), ("why", "i", "bother"),
            ("why", "even", "try"),
            ("i", "give", "up"), ("give", "up"),
            ("no", "point"), ("cant", "do", "this"),
        )
        _FUTILITY_NEGATORS = {"didnt", "dont", "never", "wont", "not", "cant", "couldnt"}
        for phrase in _FUTILITY_PHRASES:
            plen = len(phrase)
            for start in range(len(words) - plen + 1):
                if tuple(words[start:start + plen]) == phrase:
                    # "didnt give up" = perseverance, not futility
                    if start > 0 and words[start - 1] in _FUTILITY_NEGATORS:
                        continue
                    futility_hit = True
                    break
            if futility_hit:
                break

        if futility_hit:
            intent = min(intent, 64)  # powerlessness — sink toward deflect
        elif 90 <= intent <= 160:
            # Only lift out of the neutral band; never override a strong
            # directional reading (self-destruction withdraw, attack control).
            _AGENCY_PHRASES = (
                ("i", "want", "to"), ("im", "going", "to"),
                ("i", "am", "going", "to"), ("im", "gonna",),
                ("i", "will"), ("let", "me"), ("i", "need", "to"),
            )
            # The marker must bind to an action, not a destination/object:
            # "im going to fix this" = agency; "im going to the store" = travel.
            _NON_ACTION = {
                "the", "a", "an", "my", "your", "his", "her", "their", "our",
                "it", "them", "me", "him", "us", "this", "that", "be",
            }
            for phrase in _AGENCY_PHRASES:
                plen = len(phrase)
                for start in range(len(words) - plen + 1):
                    if tuple(words[start:start + plen]) == phrase:
                        nxt = start + plen
                        if nxt < len(words) and words[nxt] not in _NON_ACTION:
                            intent = max(intent, 168)  # agency — owns the motion
                            break
                else:
                    continue
                break

    # Check for structural withdraw cues
    if roles:
        has_pull_away = any(r.role == "PULL_AWAY" for r in roles)
        has_finality = any(r.role == "FINALITY" for r in roles)
        if has_pull_away or has_finality:
            intent = min(intent, 80)  # cap at deflect -- pulling away

    return max(0, min(255, intent))


def compute_flow_modifiers(flow: Optional[ForceFlow]) -> dict:
    """Compute VADUGWI modifiers from force flow direction.

    Returns dict with keys: v_mod, d_mod, w_mod (multipliers, 1.0 = no change).
    """
    if flow is None:
        return {"v_mod": 1.0, "d_mod": 1.0, "w_mod": 1.0}

    v_mod = 1.0
    d_mod = 1.0
    w_mod = 1.0

    ev = flow.effective_valence

    if flow.other_acts_on_self:
        # Other → negative → Self = victimization: amplify negative V, drop D
        if ev < 0:
            v_mod = 1.3   # negative hits harder when you're the target
            d_mod = 0.8   # D drops — you're being acted upon
            w_mod = 1.2   # self-worth takes a hit from being targeted
        # Other → positive → Self = receiving love/support
        elif ev > 0:
            v_mod = 1.1   # positive slightly amplified (being loved)
            w_mod = 0.9   # self-worth slightly boosted (dampens W loss)

    elif flow.self_acts_on_self:
        # Self → negative → Self = self-attack: W drops hard
        if ev < 0:
            w_mod = 1.5   # self-directed negative hits W 50% harder
            d_mod = 0.85  # D drops — attacking yourself
        # Self → positive → Self = self-affirmation
        elif ev > 0:
            w_mod = 0.7   # W boosted (dampens W loss, amplifies W gain)

    elif flow.self_is_actor and not flow.self_is_target:
        # Self → action → Other = agency: D preserved
        if ev < 0:
            d_mod = 1.1   # you have power (you're the one acting)
        elif ev > 0:
            d_mod = 1.05  # slight D boost for positive agency

    return {"v_mod": v_mod, "d_mod": d_mod, "w_mod": w_mod}