Audit category selector identities
This commit is contained in:
@@ -561,9 +561,9 @@ Improve later:
|
||||
|
||||
- keep `tools/prompt_map_audit.py` passing; it now checks referenced
|
||||
expression/composition/scene pools, item-template axes, object-template
|
||||
metadata values for both string and object templates, registered formatter
|
||||
policy coverage for route families, and critical route documentation plus
|
||||
expected smoke coverage.
|
||||
metadata values for both string and object templates, category/subcategory
|
||||
identity uniqueness, registered formatter policy coverage for route
|
||||
families, and critical route documentation plus expected smoke coverage.
|
||||
|
||||
### Node / UI Path
|
||||
|
||||
|
||||
@@ -902,6 +902,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`.
|
||||
- category identity validation so custom category names/slugs do not collide
|
||||
with built-in selectors, category identities stay unique, and exact
|
||||
subcategory selectors cannot become ambiguous.
|
||||
- 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.
|
||||
@@ -920,9 +923,9 @@ The script does not import ComfyUI. It parses the repo and prints:
|
||||
|
||||
Use its output to spot doc drift after adding a new node or pool. If a new node
|
||||
or pool appears there but not in this map, update the relevant route table. The
|
||||
script exits nonzero when JSON pool references, item template axes, critical
|
||||
route docs, critical route smoke registrations, or registered node display
|
||||
names do not resolve.
|
||||
script exits nonzero when JSON pool references, item template axes, category
|
||||
identities, critical route docs, critical route smoke registrations, or
|
||||
registered node display names do not resolve.
|
||||
|
||||
## Behavioral Smoke Helper
|
||||
|
||||
|
||||
@@ -118,6 +118,10 @@ AUDIT_DOC_SNIPPETS: tuple[tuple[str, str], ...] = (
|
||||
"docs/prompt-pool-routing-map.md",
|
||||
"multi-seed route sweeps",
|
||||
),
|
||||
(
|
||||
"docs/prompt-pool-routing-map.md",
|
||||
"category identity validation",
|
||||
),
|
||||
(
|
||||
"docs/prompt-pool-routing-map.md",
|
||||
"node documentation validation",
|
||||
@@ -141,6 +145,15 @@ ALLOWED_PROMPT_ROW_READS: set[tuple[str, str]] = {
|
||||
|
||||
ROUTE_POLICY_ACTION_EXCLUSIONS = {"default"}
|
||||
ROUTE_POLICY_POSITION_EXCLUSIONS = {"any"}
|
||||
RESERVED_CATEGORY_CHOICES = {
|
||||
"auto_weighted",
|
||||
"auto_full",
|
||||
"woman",
|
||||
"man",
|
||||
"couple",
|
||||
"group_or_layout",
|
||||
"custom_random",
|
||||
}
|
||||
NODE_DOC_PATHS = (
|
||||
"docs/prompt-pool-routing-map.md",
|
||||
"README.md",
|
||||
@@ -418,6 +431,117 @@ def _json_reference_errors(paths: list[Path]) -> list[tuple[str, str, str]]:
|
||||
return errors
|
||||
|
||||
|
||||
def _identity_key(value: Any) -> str:
|
||||
return re.sub(r"\s+", " ", str(value or "").strip()).lower()
|
||||
|
||||
|
||||
def _identity_location(category: dict[str, Any], subcategory: dict[str, Any] | None = None) -> str:
|
||||
category_slug = str(category.get("slug") or category.get("name") or "category").strip()
|
||||
location = f"categories.{category_slug}"
|
||||
if subcategory is not None:
|
||||
sub_slug = str(subcategory.get("slug") or subcategory.get("name") or "subcategory").strip()
|
||||
location = f"{location}.subcategories.{sub_slug}"
|
||||
return location
|
||||
|
||||
|
||||
def _add_duplicate_identity_error(
|
||||
seen: dict[str, str],
|
||||
*,
|
||||
key: str,
|
||||
location: str,
|
||||
label: str,
|
||||
errors: list[tuple[str, str, str]],
|
||||
) -> None:
|
||||
if not key:
|
||||
return
|
||||
previous = seen.get(key)
|
||||
if previous is not None:
|
||||
errors.append(("(category library)", location, f"duplicate {label}: {key!r} also used at {previous}"))
|
||||
return
|
||||
seen[key] = location
|
||||
|
||||
|
||||
def _category_identity_errors(paths: list[Path]) -> list[tuple[str, str, str]]:
|
||||
# Use the normalized library because that is the selector surface exposed by
|
||||
# category/preset nodes and exact subcategory routing.
|
||||
_ = paths
|
||||
try:
|
||||
categories = category_policy.load_category_library()
|
||||
except Exception as exc:
|
||||
return [("(category library)", "categories", f"cannot load categories: {exc}")]
|
||||
|
||||
errors: list[tuple[str, str, str]] = []
|
||||
category_names: dict[str, str] = {}
|
||||
category_slugs: dict[str, str] = {}
|
||||
exact_selectors: dict[str, str] = {}
|
||||
exact_slug_selectors: dict[str, str] = {}
|
||||
|
||||
for category in categories:
|
||||
location = _identity_location(category)
|
||||
name = _identity_key(category.get("name"))
|
||||
slug = _identity_key(category.get("slug"))
|
||||
if not name:
|
||||
errors.append(("(category library)", f"{location}.name", "missing normalized category name"))
|
||||
if not slug:
|
||||
errors.append(("(category library)", f"{location}.slug", "missing normalized category slug"))
|
||||
if name in RESERVED_CATEGORY_CHOICES or slug in RESERVED_CATEGORY_CHOICES:
|
||||
errors.append(
|
||||
(
|
||||
"(category library)",
|
||||
location,
|
||||
"category identity collides with reserved built-in selector",
|
||||
)
|
||||
)
|
||||
_add_duplicate_identity_error(category_names, key=name, location=location, label="category name", errors=errors)
|
||||
_add_duplicate_identity_error(category_slugs, key=slug, location=location, label="category slug", errors=errors)
|
||||
|
||||
subcategory_names: dict[str, str] = {}
|
||||
subcategory_slugs: dict[str, str] = {}
|
||||
for subcategory in category.get("subcategories") or []:
|
||||
if not isinstance(subcategory, dict):
|
||||
continue
|
||||
sub_location = _identity_location(category, subcategory)
|
||||
sub_name = _identity_key(subcategory.get("name"))
|
||||
sub_slug = _identity_key(subcategory.get("slug"))
|
||||
if not sub_name:
|
||||
errors.append(("(category library)", f"{sub_location}.name", "missing normalized subcategory name"))
|
||||
if not sub_slug:
|
||||
errors.append(("(category library)", f"{sub_location}.slug", "missing normalized subcategory slug"))
|
||||
_add_duplicate_identity_error(
|
||||
subcategory_names,
|
||||
key=sub_name,
|
||||
location=sub_location,
|
||||
label=f"subcategory name in {category.get('name')}",
|
||||
errors=errors,
|
||||
)
|
||||
_add_duplicate_identity_error(
|
||||
subcategory_slugs,
|
||||
key=sub_slug,
|
||||
location=sub_location,
|
||||
label=f"subcategory slug in {category.get('name')}",
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
exact_selector = _identity_key(category_policy.exact_subcategory_selector(category, subcategory))
|
||||
slug_selector = f"{slug} / {sub_slug}" if slug and sub_slug else ""
|
||||
_add_duplicate_identity_error(
|
||||
exact_selectors,
|
||||
key=exact_selector,
|
||||
location=sub_location,
|
||||
label="exact subcategory selector",
|
||||
errors=errors,
|
||||
)
|
||||
_add_duplicate_identity_error(
|
||||
exact_slug_selectors,
|
||||
key=slug_selector,
|
||||
location=sub_location,
|
||||
label="category/subcategory slug selector",
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -1064,6 +1188,13 @@ def main() -> int:
|
||||
return 1
|
||||
print("OK: all JSON pool references and item template axes resolve.")
|
||||
|
||||
print("\n# Category Identity Validation")
|
||||
category_identity_errors = _category_identity_errors(category_paths)
|
||||
if category_identity_errors:
|
||||
print_table(("Source", "Path", "Issue"), category_identity_errors)
|
||||
return 1
|
||||
print("OK: category and subcategory identities are unique and selector-safe.")
|
||||
|
||||
print("\n# Hardcore Template Metadata Validation")
|
||||
hardcore_metadata_errors = _hardcore_template_metadata_errors(category_paths)
|
||||
if hardcore_metadata_errors:
|
||||
|
||||
Reference in New Issue
Block a user