diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 0876d3f..7c54604 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -888,6 +888,9 @@ The script does not import ComfyUI. It parses the repo and prints: - JSON reference validation for every `scene_pools`, `expression_pools`, and `composition_pools` reference; - item template validation so `{placeholder}` names resolve to `item_axes`. +- effective category route coverage so each normalized category path has + usable item, scene, expression, composition, and hardcore route metadata + before runtime fallbacks can hide a gap. - route documentation validation so critical route modules are listed in this map and the architecture plan, and registered in `SMOKE_CASES` by their expected smoke cases. diff --git a/tools/prompt_map_audit.py b/tools/prompt_map_audit.py index 1347f85..1ae1764 100644 --- a/tools/prompt_map_audit.py +++ b/tools/prompt_map_audit.py @@ -22,6 +22,7 @@ if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) import category_template_metadata as template_metadata_policy # noqa: E402 +import category_library as category_policy # noqa: E402 import caption_naturalizer # noqa: E402 import krea_formatter # noqa: E402 import prompt_builder as pb # noqa: E402 @@ -380,6 +381,138 @@ def _hardcore_template_metadata_errors(paths: list[Path]) -> list[tuple[str, str return errors +def _item_candidates_for_coverage(subcategory: dict[str, Any]) -> list[Any]: + items = subcategory.get("items") + if isinstance(items, list) and items: + return items + return [subcategory.get("name") or subcategory.get("slug") or ""] + + +def _configured_pool_errors( + *, + category: dict[str, Any], + subcategory: dict[str, Any], + item: Any, + path: str, + pool_label: str, + direct_key: str, + pool_key: str, + pool_library: dict[str, list[Any]], + inherit_key: str, +) -> list[tuple[str, str, str]]: + try: + entries = category_policy.configured_pool( + category, + subcategory, + item, + direct_key, + pool_key, + pool_library, + inherit_key, + ) + except Exception as exc: + return [("(category library)", f"{path}.{pool_label}", f"cannot resolve effective pool: {exc}")] + if not entries: + return [("(category library)", f"{path}.{pool_label}", "missing effective configured entries")] + return [] + + +def _effective_category_coverage_errors(paths: list[Path]) -> list[tuple[str, str, str]]: + # `paths` is accepted to keep this check grouped with the other category + # validations. The effective route check uses the normalized loader because + # generation also consumes normalized category objects. + _ = paths + categories = category_policy.load_category_library() + scene_pools = category_policy.load_scene_pool_library() + expression_pools = category_policy.load_expression_pool_library() + composition_pools = category_policy.load_composition_pool_library() + errors: list[tuple[str, str, str]] = [] + + if not categories: + return [("(category library)", "categories", "no categories loaded")] + + for category in categories: + category_slug = str(category.get("slug") or category.get("name") or "category") + category_path = f"categories.{category_slug}" + subject_type = str(category.get("subject_type") or "").strip() + if not subject_type: + errors.append(("(category library)", f"{category_path}.subject_type", "missing subject_type")) + + subcategories = category.get("subcategories") + if not isinstance(subcategories, list) or not subcategories: + errors.append(("(category library)", f"{category_path}.subcategories", "missing subcategories")) + continue + + for subcategory in subcategories: + if not isinstance(subcategory, dict): + errors.append(("(category library)", f"{category_path}.subcategories", "subcategory must be an object")) + continue + sub_slug = str(subcategory.get("slug") or subcategory.get("name") or "subcategory") + sub_path = f"{category_path}.subcategories.{sub_slug}" + effective_subject = str(subcategory.get("subject_type") or subject_type).strip() + if not effective_subject: + errors.append(("(category library)", f"{sub_path}.subject_type", "missing effective subject_type")) + + has_items = isinstance(subcategory.get("items"), list) and bool(subcategory.get("items")) + has_templates = isinstance(subcategory.get("item_templates"), list) and bool(subcategory.get("item_templates")) + if not has_items and not has_templates: + errors.append(("(category library)", f"{sub_path}.items", "missing items or item_templates")) + + for item_index, item in enumerate(_item_candidates_for_coverage(subcategory)): + item_path = f"{sub_path}.items[{item_index}]" + errors.extend( + _configured_pool_errors( + category=category, + subcategory=subcategory, + item=item, + path=item_path, + pool_label="scenes", + direct_key="scenes", + pool_key="scene_pools", + pool_library=scene_pools, + inherit_key="inherit_scenes", + ) + ) + errors.extend( + _configured_pool_errors( + category=category, + subcategory=subcategory, + item=item, + path=item_path, + pool_label="expressions", + direct_key="expressions", + pool_key="expression_pools", + pool_library=expression_pools, + inherit_key="inherit_expressions", + ) + ) + errors.extend( + _configured_pool_errors( + category=category, + subcategory=subcategory, + item=item, + path=item_path, + pool_label="compositions", + direct_key="compositions", + pool_key="composition_pools", + pool_library=composition_pools, + inherit_key="inherit_compositions", + ) + ) + + if category_slug == "hardcore_sexual_poses" and has_templates: + metadata = subcategory.get("item_template_metadata") + if not isinstance(metadata, dict): + errors.append(("(category library)", f"{sub_path}.item_template_metadata", "missing route metadata")) + continue + normalized = template_metadata_policy.template_metadata(metadata) + if not template_metadata_policy.template_action_family(normalized): + errors.append(("(category library)", f"{sub_path}.item_template_metadata", "missing normalized action_family")) + if not template_metadata_policy.template_position_family(normalized): + errors.append(("(category library)", 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() @@ -743,6 +876,13 @@ def main() -> int: return 1 print("OK: hardcore template subcategories define explicit route metadata defaults.") + print("\n# Effective Category Route Coverage Validation") + category_coverage_errors = _effective_category_coverage_errors(category_paths) + if category_coverage_errors: + print_table(("Source", "Path", "Issue"), category_coverage_errors) + return 1 + print("OK: category routes define effective item, scene, expression, composition, and route metadata coverage.") + print("\n# Routing Documentation Validation") routing_doc_errors = _routing_doc_errors() if routing_doc_errors: