Inherit hardcore template metadata

This commit is contained in:
2026-06-27 14:56:08 +02:00
parent 29e5e65e5f
commit a8d69083cd
7 changed files with 244 additions and 6 deletions
+48
View File
@@ -250,6 +250,19 @@ def _template_axis_errors(path: str, node: dict[str, Any]) -> list[tuple[str, st
return errors
def _container_template_metadata_errors(path: str, node: dict[str, Any]) -> list[tuple[str, str]]:
if "item_template_metadata" not in node:
return []
metadata = node.get("item_template_metadata")
if not isinstance(metadata, dict):
return [(f"{path}.item_template_metadata", "item_template_metadata must be an object")]
normalized = template_metadata_policy.template_metadata(metadata)
return [
(f"{path}.item_template_metadata", issue)
for issue in template_metadata_policy.template_metadata_errors(normalized)
]
def _walk_json_references(
value: Any,
*,
@@ -261,6 +274,10 @@ def _walk_json_references(
) -> None:
if isinstance(value, dict):
errors.extend((file_name, item_path, issue) for item_path, issue in _template_axis_errors(path, value))
errors.extend(
(file_name, item_path, issue)
for item_path, issue in _container_template_metadata_errors(path, value)
)
for key, child in value.items():
if at_root and key in POOL_DEFINITION_KEYS and isinstance(child, dict):
for pool_name, pool_values in child.items():
@@ -318,6 +335,30 @@ def _json_reference_errors(paths: list[Path]) -> list[tuple[str, str, str]]:
return errors
def _hardcore_template_metadata_errors(paths: list[Path]) -> list[tuple[str, str, str]]:
errors: list[tuple[str, str, str]] = []
for path in paths:
data = _load_category_json(path)
for category in data.get("categories") or []:
if str(category.get("slug") or "") != "hardcore_sexual_poses":
continue
for subcategory in category.get("subcategories") or []:
templates = subcategory.get("item_templates")
if not isinstance(templates, list) or not templates:
continue
sub_path = f"categories.{category.get('slug')}.subcategories.{subcategory.get('slug')}"
metadata = subcategory.get("item_template_metadata")
if not isinstance(metadata, dict):
errors.append((path.name, sub_path, "missing item_template_metadata default block"))
continue
normalized = template_metadata_policy.template_metadata(metadata)
if not template_metadata_policy.template_action_family(normalized):
errors.append((path.name, f"{sub_path}.item_template_metadata", "missing normalized action_family"))
if not template_metadata_policy.template_position_family(normalized):
errors.append((path.name, f"{sub_path}.item_template_metadata", "missing normalized position_family"))
return errors
def _smoke_case_names(path: Path) -> set[str]:
if not path.exists():
return set()
@@ -445,6 +486,13 @@ def main() -> int:
return 1
print("OK: all JSON pool references and item template axes resolve.")
print("\n# Hardcore Template Metadata Validation")
hardcore_metadata_errors = _hardcore_template_metadata_errors(category_paths)
if hardcore_metadata_errors:
print_table(("File", "Path", "Issue"), hardcore_metadata_errors)
return 1
print("OK: hardcore template subcategories define explicit route metadata defaults.")
print("\n# Routing Documentation Validation")
routing_doc_errors = _routing_doc_errors()
if routing_doc_errors:
+77
View File
@@ -3936,6 +3936,19 @@ def smoke_hardcore_position_config_policy() -> None:
"generic contact",
)
_expect(source_action_family == "outercourse", "Source action-family fallback should accept hyphenated source aliases")
default_action_route = row_route_metadata.resolve_action_position_route(
is_pose_category=True,
subcategory={"slug": "anal_double_penetration"},
hardcore_position_config=None,
item_template_metadata={"action_family": "default", "position_family": "anal"},
item_text="toy-assisted double penetration with front-and-back contact",
source_role_graph="one partner between two bodies",
source_composition="",
pose="",
item_axis_values={"double_act": "toy-assisted double penetration"},
)
_expect(default_action_route.get("position_family") == "anal", "Default-action metadata should preserve position family")
_expect(default_action_route.get("action_family") == "toy_double", "Default-action metadata should still allow action inference")
item_text, item_name, axis_values, template_metadata = pb._compose_item(
random.Random(42),
{},
@@ -3974,6 +3987,70 @@ def smoke_hardcore_position_config_policy() -> None:
_expect(formatter_hints.get("krea") == ["keep mouth contact readable"], "Template metadata route lost Krea formatter hint")
_expect(formatter_hints.get("sdxl") == ["oral contact", "kneeling oral"], "Template metadata route lost SDXL formatter hints")
_expect(formatter_hints.get("caption") == ["oral contact caption detail"], "Template metadata route lost caption formatter hint")
inherited_text, _inherited_name, inherited_axis_values, inherited_metadata = pb._compose_item(
random.Random(42),
{},
{
"name": "Inherited metadata route",
"item_template_metadata": {
"action_family": "foreplay",
"position_family": "manual",
"position_keys": ["kneeling"],
"formatter_hint": {"caption": "inherited caption cue"},
},
"item_templates": ["{act} in {position}"],
"item_axes": {
"act": ["hand stimulation"],
"position": ["kneeling manual position"],
},
},
"Inherited metadata route",
women_count=1,
men_count=1,
)
_expect(inherited_text == "hand stimulation in kneeling manual position", "Inherited template metadata changed item text")
_expect(inherited_axis_values == {"act": "hand stimulation", "position": "kneeling manual position"}, "Inherited template metadata lost axis values")
_expect(inherited_metadata.get("action_family") == "foreplay", "String template did not inherit action family")
_expect(inherited_metadata.get("position_family") == "manual", "String template did not inherit position family")
_expect(pb._template_position_keys(inherited_metadata) == ["kneeling"], "String template did not inherit position keys")
_expect(
route_metadata.row_formatter_hints({"item_template_metadata": inherited_metadata}, "caption") == ["inherited caption cue"],
"String template did not inherit formatter hints",
)
override_text, _override_name, _override_axis_values, override_metadata = pb._compose_item(
random.Random(42),
{},
{
"name": "Override metadata route",
"item_template_metadata": {
"action_family": "foreplay",
"position_family": "manual",
"formatter_hint": {"all": "inherited shared cue"},
},
"item_templates": [
{
"template": "{act} in {position}",
"action_family": "oral",
"formatter_hint": {"krea2": "override krea cue"},
}
],
"item_axes": {
"act": ["mouth contact"],
"position": ["kneeling oral position"],
},
},
"Override metadata route",
women_count=1,
men_count=1,
)
_expect(override_text == "mouth contact in kneeling oral position", "Override template metadata changed item text")
_expect(override_metadata.get("action_family") == "oral", "Template object did not override inherited action family")
_expect(override_metadata.get("position_family") == "manual", "Template object should keep inherited position family when absent")
_expect(
route_metadata.row_formatter_hints({"item_template_metadata": override_metadata}, "krea")
== ["inherited shared cue", "override krea cue"],
"Template metadata did not merge inherited and template formatter hints",
)
route_row = {
"action_family": "penetrative",
"position_family": "Oral",