clanker / engine /force_flow.py
deucebucket's picture
feat: vendor engine + scorer adapter (text -> Score)
6522c75
Raw
History Blame Contribute Delete
16.6 kB
"""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}