Use shared item axis context in role routes

This commit is contained in:
2026-06-27 18:18:47 +02:00
parent 867916ee51
commit 6a65f7d35c
11 changed files with 100 additions and 52 deletions
+6 -4
View File
@@ -5,6 +5,11 @@ import re
from string import Formatter
from typing import Any, Callable
try:
from . import item_axis_policy
except ImportError: # Allows local smoke tests with top-level imports.
import item_axis_policy
HARDCORE_POSITION_FAMILY_CHOICES = [
"any",
@@ -847,10 +852,7 @@ def hardcore_source_position_family(subcategory: dict[str, Any], config: dict[st
def hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
text_parts = [str(part or "") for part in parts if str(part or "").strip()]
if isinstance(axis_values, dict):
text_parts.extend(str(value or "") for value in axis_values.values() if str(value or "").strip())
text = " ".join(text_parts).lower()
text = item_axis_policy.context_text(*parts, axis_values=axis_values)
if not text:
return []
keys: list[str] = []
+6 -7
View File
@@ -2,15 +2,14 @@ from __future__ import annotations
from typing import Any
try:
from . import item_axis_policy
except ImportError: # Allows local smoke tests with top-level imports.
import item_axis_policy
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
return " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
def _anal_position_graph(woman: str, man: str, context: str) -> str:
+6 -7
View File
@@ -3,15 +3,14 @@ from __future__ import annotations
import re
from typing import Any
try:
from . import item_axis_policy
except ImportError: # Allows local smoke tests with top-level imports.
import item_axis_policy
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
return " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
def _mentions_ass(text: str) -> bool:
+3 -1
View File
@@ -4,6 +4,7 @@ import random
from typing import Any
try:
from . import item_axis_policy
from .hardcore_role_anal import build_anal_or_double_role_graph
from .hardcore_role_climax import build_climax_role_graph
from .hardcore_role_fallback import (
@@ -23,6 +24,7 @@ try:
from .hardcore_role_outercourse import build_outercourse_role_graph
from .hardcore_role_penetration import build_penetration_role_graph
except ImportError: # Allows local smoke tests with `python -c`.
import item_axis_policy
from hardcore_role_anal import build_anal_or_double_role_graph
from hardcore_role_climax import build_climax_role_graph
from hardcore_role_fallback import (
@@ -85,7 +87,7 @@ def build_hardcore_role_graph(
men = participants["men"]
people = participants["people"]
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
item_text = " ".join((item_axis_values or {}).values()).lower()
item_text = item_axis_policy.context_text(axis_values=item_axis_values)
def any_person(exclude: set[str] | None = None) -> str:
exclude = exclude or set()
+6 -7
View File
@@ -2,15 +2,14 @@ from __future__ import annotations
from typing import Any
try:
from . import item_axis_policy
except ImportError: # Allows local smoke tests with top-level imports.
import item_axis_policy
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
return " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
def build_foreplay_role_graph(
+7 -8
View File
@@ -2,15 +2,14 @@ from __future__ import annotations
from typing import Any
try:
from . import item_axis_policy
except ImportError: # Allows local smoke tests with top-level imports.
import item_axis_policy
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
return " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
def _oral_direction(text: str) -> tuple[bool, bool]:
@@ -54,7 +53,7 @@ def build_oral_role_graph(
item_axis_values: dict[str, Any] | None = None,
pov_labels: list[str] | None = None,
) -> str:
position_text = str((item_axis_values or {}).get("position") or "").lower()
position_text = item_axis_policy.key_text(item_axis_values, "position")
text = _context_text(item_text, item_axis_values)
man_is_pov = man in set(pov_labels or [])
woman_gives, man_gives = _oral_direction(text)
+4 -8
View File
@@ -3,19 +3,15 @@ from __future__ import annotations
from typing import Any
try:
from . import item_axis_policy
from . import outercourse_action_policy as outercourse_policy
except ImportError: # Allows local smoke tests with top-level imports.
import item_axis_policy
import outercourse_action_policy as outercourse_policy
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
return " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
def build_outercourse_role_graph(
@@ -25,7 +21,7 @@ def build_outercourse_role_graph(
item_axis_values: dict[str, Any] | None = None,
pov_labels: list[str] | None = None,
) -> str:
position_text = str((item_axis_values or {}).get("position") or "").lower()
position_text = item_axis_policy.key_text(item_axis_values, "position")
text = _context_text(item_text, item_axis_values)
action_kind = outercourse_policy.infer_outercourse_action_kind(position_text)
if action_kind == outercourse_policy.OUTERCOURSE_GENERIC:
+6 -7
View File
@@ -2,15 +2,14 @@ from __future__ import annotations
from typing import Any
try:
from . import item_axis_policy
except ImportError: # Allows local smoke tests with top-level imports.
import item_axis_policy
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
return " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
def build_penetration_role_graph(
+13
View File
@@ -108,6 +108,19 @@ def action_context_text(axis_values: Any) -> str:
)
def context_text(*parts: Any, axis_values: Any = None) -> str:
text_parts = [clean_text(part) for part in parts if clean_text(part)]
text_parts.extend(axis_value_texts(axis_values))
return " ".join(part.lower() for part in text_parts if part)
def key_text(axis_values: Any, key: str) -> str:
if not isinstance(axis_values, dict):
return ""
values = value_texts(axis_values.get(key))
return values[0].lower() if values else ""
def row_axis_value_texts(
row: dict[str, Any],
*,
+9 -3
View File
@@ -4,6 +4,11 @@ from dataclasses import dataclass
import re
from typing import Any, Callable
try:
from . import item_axis_policy
except ImportError: # Allows local smoke tests with top-level imports.
import item_axis_policy
WOMAN_LOWER_ACCESS_TERMS = (
"penetrat",
@@ -237,7 +242,7 @@ def character_hardcore_clothing_entries(
def hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]:
axis_values = row.get("item_axis_values")
axis_text = " ".join(str(value) for value in axis_values.values()) if isinstance(axis_values, dict) else ""
axis_text = item_axis_policy.context_text(axis_values=axis_values)
role_text = " ".join(
str(part or "")
for part in (
@@ -255,10 +260,11 @@ def hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]:
)
).lower()
full_text = f"{role_text} {detail_text}"
lower_access_text = f"{role_text} {axis_text}"
return {
"woman_lower": any(term in role_text for term in WOMAN_LOWER_ACCESS_TERMS),
"woman_lower": any(term in lower_access_text for term in WOMAN_LOWER_ACCESS_TERMS),
"woman_upper": any(term in full_text for term in WOMAN_UPPER_ACCESS_TERMS),
"man_lower": any(term in role_text for term in MAN_LOWER_ACCESS_TERMS),
"man_lower": any(term in lower_access_text for term in MAN_LOWER_ACCESS_TERMS),
}
+34
View File
@@ -3439,6 +3439,22 @@ def smoke_row_role_graph_policy() -> None:
pov_route.role_graph.startswith("First-person POV from Man A;"),
"Role graph route did not prepend POV role graph directive",
)
structured_axis_route = row_role_graph.resolve_role_graph_route(
rng=random.Random(54),
subcategory={"slug": "oral", "name": "Oral"},
context=context,
item_axis_values={
"position": {"text": "standing oral position"},
"oral_act": ["blowjob", "auto"],
"contact_detail": {"unused": "mouth contact at hip height"},
},
pov_character_labels=[],
is_pose_category=False,
)
_expect(
"kneels in front" in structured_axis_route.source_role_graph,
"Role graph route should read structured/list axis values through item_axis_policy",
)
def smoke_row_assembly_policy() -> None:
@@ -5876,6 +5892,24 @@ def smoke_pair_route_policy() -> None:
partial_common = {**clothing_common, "mode": "partially_removed"}
partial_route = pair_clothing.resolve_hardcore_pair_clothing_result(**partial_common)
_expect(partial_route.requires_body_exposure_scene is True, "Partial lower-access clothing should request scene cleanup")
structured_axis_clothing = pair_clothing.resolve_hardcore_pair_clothing_result(
**{
**clothing_common,
"hard_row": {
"role_graph": "generic adult action",
"item": "generic configured action",
"item_axis_values": {
"position": {"text": "edge-supported penetration"},
"leg_detail": ["thighs open", "auto"],
},
},
"mode": "partially_removed",
}
)
_expect(
structured_axis_clothing.woman_access == "lower",
"Pair clothing should read structured/list axis values for lower-access detection",
)
oral_common = {
**clothing_common,
"hard_row": {