Files
kunail0804-ComfyUI-Scene-Co…/compiler/common/config.py
T
d1bebdb571 feat(compiler): add typed, injectable configuration system (#5) (#49)
Implement the compiler configuration per MASTER_SPEC §23 in
compiler/common/config.py, with a strict configuration schema.

- Frozen dataclasses per section (analyzer, resolver, validator, prompt_builder,
  debug) plus a top-level `schema` version; every default matches §23.2.
- Config.from_json validates against schemas/json/configuration.schema.json and
  raises ConfigError carrying a Fatal SC0014 message on invalid input; every
  section/field is optional and falls back to defaults.
- load_config(path) reads and validates a JSON file.
- Deterministic by construction: additionalProperties:false rejects unknown keys
  (including any random seed); temperature has minimum 0.
- Injected into stages; no global/singleton config state.
- Ships config/default_config.json (the §23.2 example) as the canonical default.

Tests: defaults match §23.2, full/empty/partial load, file load, and
invalid-config rejection (wrong type, unknown field, forbidden seed, negative
temperature) producing SC0014.

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

175 lines
5.6 KiB
Python

"""Typed, injectable compiler configuration (MASTER_SPEC §23).
Configuration changes behaviour, never architecture, and MUST stay deterministic:
there is no random-seed option and unknown keys are rejected. Every section and
field is optional; omitted values fall back to the §23.2 defaults.
Configuration is loaded once and injected into stages — this module intentionally
exposes no global/singleton config. Invalid configuration is a Fatal condition
(the compiler cannot run) and raises :class:`ConfigError` carrying an ``SC0014``
message.
"""
from __future__ import annotations
import json
from collections.abc import Mapping
from dataclasses import dataclass, fields
from pathlib import Path
from typing import Any
from compiler.common.result import Message, Severity
from schemas.validation import validate_document
_CONFIG_SCHEMA = "configuration"
class ConfigError(Exception):
"""Raised when configuration is invalid; carries the Fatal SC0014 message."""
def __init__(self, message: Message) -> None:
super().__init__(message.description)
self.message = message
def _section_from_json(section_cls: type, data: Mapping[str, Any] | None) -> Any:
"""Build a config section, filling missing fields with the section's defaults."""
values = data or {}
known = {f.name for f in fields(section_cls)}
return section_cls(**{name: values[name] for name in known if name in values})
@dataclass(frozen=True)
class AnalyzerConfig:
"""Scene Analyzer options (§23.2). temperature 0 maximizes repeatability."""
backend: str = "ollama"
model: str = "llama3"
temperature: float = 0.0
max_retries: int = 3
timeout: int = 60
def to_json(self) -> dict[str, Any]:
return {
"backend": self.backend,
"model": self.model,
"temperature": self.temperature,
"max_retries": self.max_retries,
"timeout": self.timeout,
}
@dataclass(frozen=True)
class ResolverConfig:
"""Resolver options (§23.2)."""
knowledge_base: str = "knowledge_base/"
strict_mode: bool = True
allow_aliases: bool = True
expansion_enabled: bool = True
max_expansion_depth: int = 8
def to_json(self) -> dict[str, Any]:
return {
"knowledge_base": self.knowledge_base,
"strict_mode": self.strict_mode,
"allow_aliases": self.allow_aliases,
"expansion_enabled": self.expansion_enabled,
"max_expansion_depth": self.max_expansion_depth,
}
@dataclass(frozen=True)
class ValidatorConfig:
"""Scene Validator options (§23.2)."""
allow_unknown_fields: bool = False
def to_json(self) -> dict[str, Any]:
return {"allow_unknown_fields": self.allow_unknown_fields}
@dataclass(frozen=True)
class PromptBuilderConfig:
"""Prompt Builder options (§23.2)."""
target: str = "easy_illustrious"
separator: str = ","
trim_empty_outputs: bool = True
remove_duplicate_tags: bool = True
def to_json(self) -> dict[str, Any]:
return {
"target": self.target,
"separator": self.separator,
"trim_empty_outputs": self.trim_empty_outputs,
"remove_duplicate_tags": self.remove_duplicate_tags,
}
@dataclass(frozen=True)
class DebugConfig:
"""Debug/logging options (§23.2)."""
enabled: bool = False
level: str = "basic"
def to_json(self) -> dict[str, Any]:
return {"enabled": self.enabled, "level": self.level}
@dataclass(frozen=True)
class Config:
"""The complete compiler configuration (§23.2), immutable once produced."""
schema: str = "1.0"
analyzer: AnalyzerConfig = AnalyzerConfig()
resolver: ResolverConfig = ResolverConfig()
validator: ValidatorConfig = ValidatorConfig()
prompt_builder: PromptBuilderConfig = PromptBuilderConfig()
debug: DebugConfig = DebugConfig()
@classmethod
def from_json(cls, data: Mapping[str, Any]) -> Config:
"""Validate and build a Config; raise ConfigError (SC0014) if invalid."""
issues = validate_document(data, _CONFIG_SCHEMA)
if issues:
details = "; ".join(f"{i.path or '<root>'}: {i.message}" for i in issues)
raise ConfigError(
Message(
code="SC0014",
severity=Severity.FATAL,
title="Invalid configuration",
description=f"Configuration failed schema validation: {details}",
context={"issues": [i.message for i in issues]},
)
)
return cls(
schema=data.get("schema", "1.0"),
analyzer=_section_from_json(AnalyzerConfig, data.get("analyzer")),
resolver=_section_from_json(ResolverConfig, data.get("resolver")),
validator=_section_from_json(ValidatorConfig, data.get("validator")),
prompt_builder=_section_from_json(PromptBuilderConfig, data.get("prompt_builder")),
debug=_section_from_json(DebugConfig, data.get("debug")),
)
def to_json(self) -> dict[str, Any]:
return {
"schema": self.schema,
"analyzer": self.analyzer.to_json(),
"resolver": self.resolver.to_json(),
"validator": self.validator.to_json(),
"prompt_builder": self.prompt_builder.to_json(),
"debug": self.debug.to_json(),
}
def load_config(path: str | Path) -> Config:
"""Load and validate configuration from a JSON file.
Raises:
ConfigError: If the file contents fail schema validation (SC0014).
"""
data = json.loads(Path(path).read_text(encoding="utf-8"))
return Config.from_json(data)