Files
kunail0804-ComfyUI-Scene-Co…/compiler/splitter/category_splitter.py
T
499a2490ca feat(splitter): implement the Category Splitter (#13) (#57)
Add compiler/splitter/category_splitter.py: split_into_categories(tags,
logger=None) -> CompilerResult (data = CategoryMap), per MASTER_SPEC §18.

- Each Resolved Tag is placed in the category recorded on it by the Resolver
  (its Knowledge Base Entry category); tags are never modified, generated, or
  removed (original objects kept verbatim).
- Tags within a category preserve Resolver order; category ordering follows the
  shared canonical 19-category order; empty categories are omitted (§18.4).
- Defensive: a tag whose category is outside the 19 stops with SC0008.

Tests: assignment, within-category ordering, canonical category ordering,
empty-category omission, tag identity preserved, empty input, and SC0008.

Co-authored-by: kunail0804 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:00:54 +02:00

59 lines
2.2 KiB
Python

"""The Category Splitter (MASTER_SPEC §18).
Organizes Resolved Tags into the 19 canonical categories, preserving order and
without modifying, generating, or removing any tag. Each tag is placed in the
category recorded on it by the Resolver (which is the category of its Knowledge
Base Entry).
Category ordering is deterministic (the shared canonical order); tags within a
category keep Resolver order; empty categories are omitted from the Category Map
(§18.4).
"""
from __future__ import annotations
from types import MappingProxyType
from compiler.common.categories import CANONICAL_CATEGORIES, is_valid_category
from compiler.common.log import StructuredLogger
from compiler.common.message_codes import message
from compiler.common.result import CompilerResult
from schemas.models import CategoryMap, ResolvedTag
def split_into_categories(
tags: tuple[ResolvedTag, ...],
logger: StructuredLogger | None = None,
) -> CompilerResult:
"""Group Resolved Tags into a Category Map.
Returns:
A CompilerResult whose ``data`` is the :class:`CategoryMap` on success,
or ``None`` with an SC0008 error if any tag carries a category outside
the 19 canonical categories.
"""
buckets: dict[str, list[ResolvedTag]] = {}
for resolved in tags:
if not is_valid_category(resolved.category):
return CompilerResult().add_error(
message(
"SC0008",
f"Tag '{resolved.tag}' has invalid category '{resolved.category}'.",
tag=resolved.tag,
category=resolved.category,
)
)
buckets.setdefault(resolved.category, []).append(resolved)
# Deterministic category order; empty categories omitted. Original ResolvedTag
# objects are kept verbatim (tags are never modified).
ordered = {
category: tuple(buckets[category])
for category in CANONICAL_CATEGORIES
if category in buckets
}
category_map = CategoryMap(categories=MappingProxyType(ordered))
if logger is not None:
logger.basic("categories_split", categories=len(ordered), tags=len(tags))
return CompilerResult(data=category_map)