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>
59 lines
2.2 KiB
Python
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)
|