Files
5e146d5a55 feat!: v1.1 — Resolver emits a flat prompt, drop categories (#85)
* feat!: collapse pipeline — Resolver emits a flat prompt, drop categories (v1.1)

BREAKING CHANGE: the Category Splitter and Prompt Builder stages and nodes are
removed. The Resolver is now the translator from Scene JSON to prompt: it resolves
concepts to Knowledge Base tags and joins them into one flat comma-separated
prompt string, in resolution order (which naturally leads with character concepts).

Resolver node outputs are now (prompt, warnings, errors, json) — the json being
the traceable resolved tags so a translation problem is inspectable. Node count
drops from 8 to 6. The CategoryMap/PromptOutput models and their schemas are
removed; the `category` field remains on entries and resolved tags for
traceability only. Debug Viewer simplified to scene/warnings/errors.

Goldens, example scenes/workflow, integration/performance/repo-layout/release
tests, benchmark script, and the docs (README + wiki) are updated to the flat
prompt. Version bumped to 1.1.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build: drop removed splitter/builder packages from pyproject

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

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

163 lines
5.2 KiB
Python

"""End-to-end compiler pipeline tests (issue #27, MASTER_SPEC §26.2).
Exercises Scene JSON -> Validator -> Resolver -> flat prompt against the shipped
Knowledge Base. The Analyzer is bypassed (fixed Scene JSON inputs), so no test
requires a running Ollama.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from compiler.common.config import Config
from compiler.common.knowledge_base import load_knowledge_base
from compiler.resolver.illustrious_resolver import resolve_scene, tags_to_prompt
from compiler.validator.scene_validator import validate_scene
KB_DIR = Path(__file__).resolve().parent.parent.parent / "knowledge_base"
@pytest.fixture(scope="module")
def knowledge_base():
return load_knowledge_base(KB_DIR)
def character(id_: int, **fields) -> dict:
base = {
"id": id_,
"identity": [],
"appearance": [],
"clothing": [],
"accessories": [],
"pose": [],
"expression": [],
"actions": [],
}
base.update(fields)
return base
def scene(characters, interactions=(), **sections) -> dict:
return {
"characters": list(characters),
"interactions": list(interactions),
"objects": list(sections.get("objects", [])),
"environment": list(sections.get("environment", [])),
"camera": list(sections.get("camera", [])),
"lighting": list(sections.get("lighting", [])),
"metadata": {"schema_version": "1.0"},
}
class Compiled:
"""The outputs of a full compilation run."""
def __init__(self, prompt: str, warning_codes: list[str]) -> None:
self.prompt = prompt
self.warning_codes = warning_codes
def compile_scene(scene_json: dict, knowledge_base, config: Config) -> Compiled:
"""Run the full no-LLM pipeline and return the flat prompt + warnings."""
validated = validate_scene(scene_json, config)
assert validated.success, [m.code for m in validated.errors]
resolved = resolve_scene(validated.data, knowledge_base, config)
assert resolved.success, [m.code for m in resolved.errors]
prompt = tags_to_prompt(resolved.data, config.prompt_builder.separator)
warning_codes = [m.code for m in (*validated.warnings, *resolved.warnings)]
return Compiled(prompt, warning_codes)
# --- reference scenes ------------------------------------------------------
def test_single_character(knowledge_base) -> None:
doc = scene(
[
character(
0,
identity=["girl"],
appearance=["blonde hair", "blue eyes"],
clothing=["dress"],
expression=["smile"],
)
],
environment=["classroom"],
lighting=["sunset"],
)
result = compile_scene(doc, knowledge_base, Config())
# Flat prompt in resolution order: character concepts, then scene sections.
assert result.prompt == "1girl,blonde hair,blue eyes,dress,smile,classroom,sunset"
assert "SC0001" not in result.warning_codes # all core concepts are known
def test_two_characters(knowledge_base) -> None:
doc = scene(
[
character(0, identity=["girl"], appearance=["long hair"]),
character(1, identity=["man"], appearance=["short hair"]),
]
)
result = compile_scene(doc, knowledge_base, Config())
assert result.prompt == "1girl,long hair,1boy,short hair"
def test_character_interaction(knowledge_base) -> None:
doc = scene(
[character(0, identity=["girl"]), character(1, identity=["boy"])],
interactions=[{"participants": [0, 1], "concept": "hug"}],
)
result = compile_scene(doc, knowledge_base, Config())
assert result.prompt == "1girl,1boy,hug"
def test_indoor_scene(knowledge_base) -> None:
doc = scene(
[character(0, identity=["girl"], pose=["sitting"])],
objects=["chair", "table"],
environment=["bedroom"],
)
result = compile_scene(doc, knowledge_base, Config())
assert result.prompt == "1girl,sitting,chair,table,bedroom"
def test_outdoor_scene(knowledge_base) -> None:
doc = scene(
[character(0, identity=["girl"], actions=["walking"])],
environment=["forest"],
camera=["wide shot"],
)
result = compile_scene(doc, knowledge_base, Config())
assert result.prompt == "1girl,walking,forest,wide shot"
# --- determinism -----------------------------------------------------------
def test_pipeline_is_deterministic(knowledge_base) -> None:
doc = scene(
[
character(
0,
identity=["girl"],
appearance=["blonde hair"],
clothing=["school uniform"],
)
],
environment=["classroom"],
)
first = compile_scene(doc, knowledge_base, Config())
second = compile_scene(doc, knowledge_base, Config())
assert first.prompt == second.prompt
def test_expansion_flows_end_to_end(knowledge_base) -> None:
# school_uniform expands to blazer + pleated_skirt in the shipped KB.
doc = scene([character(0, identity=["girl"], clothing=["school uniform"])])
result = compile_scene(doc, knowledge_base, Config())
assert result.prompt == "1girl,school uniform,blazer,pleated skirt"