Phase 0: Repository-Initialisierung nach Bauplan v1.2

- Struktur gemäß §8 (Eigentumsgrenzen), PLAN.md als normative Basis
- Pflichtdokumente: STATUS.md, ERRORS.md, TEST_REPORT.md, CHANGELOG.md, ADRs
- ADR-0001 Python 3.13-Pin, ADR-0002 GStreamer 1.28.6-Pin (Windows),
  ADR-0003 IPC TCP+MessagePack v1
- Kernpakete: hms_protocol, hms_domain, hms_parameter, hms_artnet,
  hms_adaptive, hms_capabilities, hms_plugin_sdk
- Renderer-Spike: D3D11-Primärpfad + Dev-GL-Pfad (§36 Nr. 4-5)
- Control Core: FastAPI REST + WebSocket (§36 Nr. 9)
- Beispielplugins: Passthrough + Gaussian Blur (3 Adaptive-Quality-
  Varianten, HLSL/GLSL/GLES)
- Tools: Art-Net-Emulator, Fixture-Generator (Master32/Layer64-CSV),
  Capability-Probe
- JSON-Schemas: IPC, Plugin, Projekt, Cluster
- 121 Unit-/Integrationstests grün, Ruff grün

Gate 0 bleibt offen: Hardwaremessungen nur auf echter Windows-Referenz-
hardware gültig (§29.7, §33).
This commit is contained in:
HMS MediaEngine Agent
2026-09-11 00:36:59 +02:00
commit 0922cc1d68
133 changed files with 7939 additions and 0 deletions
@@ -0,0 +1,23 @@
"""hms_parameter zentrale Parameter- und Control-Engine (PLAN.md §11)."""
from hms_parameter.engine import (
ControlSource,
MergeMode,
ParameterEngine,
ParameterFrame,
)
from hms_parameter.paths import (
layer_opacity_path,
master_intensity_path,
validate_parameter_path,
)
__all__ = [
"ControlSource",
"MergeMode",
"ParameterEngine",
"ParameterFrame",
"validate_parameter_path",
"layer_opacity_path",
"master_intensity_path",
]
@@ -0,0 +1,156 @@
"""Parameter-Engine: Prioritäten, Übernahme, Frame-Snapshot (PLAN.md §11).
Alle Steuerquellen (Browser, Art-Net, später Timeline/Audio/KI) laufen über
diese Engine; direkte Renderer-Zugriffe sind verboten (§11, §33).
"""
from __future__ import annotations
import enum
import time
from dataclasses import dataclass, field
from hms_parameter.paths import validate_parameter_path
class ControlSource(enum.IntEnum):
"""Steuerquellen in Prioritätsordnung (§11.2)."""
SAFETY = 1 # Not-Aus/Blackout, überstimmt alles
OPERATOR = 2 # expliziter manueller Override
CONSOLE = 3 # freigegebenes Lichtpult (Art-Net)
WEB = 4 # Browser-Livebedienung
TIMELINE = 5 # reserviert
AUDIO = 6 # reserviert (Modulatoren)
AI = 7 # reserviert, niedrigste Priorität
class MergeMode(enum.Enum):
"""Übernahmeverfahren (§11.3)."""
LTP = "ltp" # letzte Änderung gewinnt (Standard)
HTP = "htp" # höchster Wert gewinnt (optional für Intensität)
@dataclass
class _Binding:
value: float
last_change_ns: int
class ParameterFrame:
"""Unveränderlicher Snapshot aller Parameter für genau einen Frame (§11.4)."""
__slots__ = ("_values", "revision", "created_ns")
def __init__(self, values: dict[str, float], revision: int) -> None:
object.__setattr__(self, "_values", dict(values))
object.__setattr__(self, "revision", revision)
object.__setattr__(self, "created_ns", time.monotonic_ns())
def get(self, path: str, default: float = 0.0) -> float:
return self._values.get(path, default)
def as_dict(self) -> dict[str, float]:
return dict(self._values)
def __contains__(self, path: str) -> bool:
return path in self._values
class RevisionConflict(Exception):
"""Erwartete Revision stimmt nicht (optimistische Sperre, §23.2)."""
def __init__(self, current: int, expected: int) -> None:
self.current = current
self.expected = expected
super().__init__(f"revision conflict: current={current}, expected={expected}")
@dataclass
class ParameterEngine:
"""Autoritative Parameter-Instanz des Control Core.
- set_value: Override einer Quelle mit Prioritätsprüfung
- release: Rückgabe an nächstniedrigere Quelle (§11.3)
- snapshot: atomarer Frame-Snapshot (§11.4)
"""
revision: int = 0
default: float = 0.0
merge_mode: MergeMode = MergeMode.LTP
_bindings: dict[str, dict[ControlSource, _Binding]] = field(
default_factory=dict, repr=False
)
_defaults: dict[str, float] = field(default_factory=dict, repr=False)
def set_value(
self,
path: str,
value: float,
source: ControlSource,
expected_revision: int | None = None,
) -> int:
"""Setzt einen Override; gibt die neue Revision zurück."""
if not validate_parameter_path(path):
raise ValueError(f"invalid parameter path: {path!r}")
value = float(value)
if value != value or value in (float("inf"), float("-inf")):
raise ValueError(f"value must be finite, got {value}")
if expected_revision is not None and expected_revision != self.revision:
raise RevisionConflict(self.revision, expected_revision)
per_source = self._bindings.setdefault(path, {})
# Priorität: eine niedrigere Quelle kann eine höhere Quelle nicht
# verdrängen, aber ihre eigene Bindung jederzeit aktualisieren.
existing = per_source.get(source)
now = time.monotonic_ns()
if existing is None:
per_source[source] = _Binding(value, now)
self.revision += 1
elif self.merge_mode is MergeMode.LTP:
# LTP: jede Übernahme aktualisiert Bindung und Revision (§11.3)
per_source[source] = _Binding(value, now)
self.revision += 1
elif value > existing.value:
# HTP: nur ein höherer Wert übernimmt; Maximum bleibt (§11.3)
per_source[source] = _Binding(value, now)
self.revision += 1
return self.revision
def effective_value(self, path: str) -> float:
"""Wirksamer Wert: höchste Priorität gewinnt; sonst Default (§11.1)."""
per_source = self._bindings.get(path)
if not per_source:
return self._defaults.get(path, self.default)
source = min(per_source) # kleinster IntEnum-Wert = höchste Priorität
return per_source[source].value
def current_source(self, path: str) -> ControlSource | None:
per_source = self._bindings.get(path)
if not per_source:
return None
return min(per_source)
def release(self, path: str, source: ControlSource) -> int:
"""Gibt den Override zurück; nächstniedrigere Quelle übernimmt (§11.3)."""
per_source = self._bindings.get(path)
if per_source and source in per_source:
del per_source[source]
if not per_source:
self._bindings.pop(path, None)
self.revision += 1
return self.revision
def snapshot(self) -> ParameterFrame:
"""Atomarer Snapshot aller wirksamen Werte für einen Frame (§11.4)."""
values = {p: self._defaults[p] for p in self._defaults}
for path, per_source in self._bindings.items():
if per_source:
values[path] = per_source[min(per_source)].value
return ParameterFrame(values, self.revision)
def set_default(self, path: str, value: float) -> None:
if not validate_parameter_path(path):
raise ValueError(f"invalid parameter path: {path!r}")
self._defaults[path] = float(value)
@@ -0,0 +1,49 @@
"""Stabile Parameterpfade (PLAN.md §10.2).
Pfade werden niemals aus sichtbaren Namen gebildet; alle Teile sind UUIDs
oder feste Schlüsselwörter.
"""
from __future__ import annotations
import uuid
_ALLOWED_ROOTS = {"composition", "output", "cluster", "master"}
def _is_uuid(value: str) -> bool:
try:
uuid.UUID(value)
return True
except (ValueError, AttributeError):
return False
def validate_parameter_path(path: str) -> bool:
"""True, wenn der Pfad dem Muster §10.2 entspricht."""
if not path or path.startswith("/") or "\\" in path or ".." in path:
return False
parts = path.split("/")
root = parts[0]
if root not in _ALLOWED_ROOTS:
return False
if root == "master":
return len(parts) == 2 and parts[1] != ""
if root in {"composition", "output"}:
if len(parts) < 3:
return False
if not _is_uuid(parts[1]):
return False
return all(p != "" for p in parts[2:])
# cluster: cluster/group/{uuid}/... oder cluster/node/{uuid}/...
if len(parts) >= 3 and parts[1] in {"group", "node"} and _is_uuid(parts[2]):
return all(p != "" for p in parts[3:])
return False
def layer_opacity_path(composition_id: str, layer_id: str) -> str:
return f"composition/{composition_id}/layer/{layer_id}/opacity"
def master_intensity_path() -> str:
return "master/intensity"