From c06c6f86fa2b425015e426dfbd57945cdb5fa014 Mon Sep 17 00:00:00 2001 From: Christian-Manuel Butzke Date: Thu, 2 Jul 2026 04:47:02 +0900 Subject: [PATCH] engine: expose opaque state meta --- src/harel/data/machine.schema.json | 2 ++ src/harel/model.py | 3 +++ tests/test_library_api.py | 42 ++++++++++++++++++++++++++++++ tests/test_model.py | 24 +++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/src/harel/data/machine.schema.json b/src/harel/data/machine.schema.json index 402d51a..36cb479 100644 --- a/src/harel/data/machine.schema.json +++ b/src/harel/data/machine.schema.json @@ -24,6 +24,7 @@ "type": "object", "additionalProperties": { "$ref": "#/$defs/eventDecl" } }, + "meta": { "type": "object" }, "migrations": { "type": "array", "items": { "$ref": "#/$defs/migration" } }, "top": { "$ref": "#/$defs/state", "description": "The outermost state (PSiCC \"top\"); all behavior lives under it." } }, @@ -174,6 +175,7 @@ "additionalProperties": false, "properties": { "type": { "enum": ["simple", "composite", "orthogonal", "final"], "default": "simple" }, + "meta": { "type": "object" }, "esvs": { "type": "object", "additionalProperties": { "$ref": "#/$defs/esv" } }, "entry": { "$ref": "#/$defs/actionList" }, "exit": { "$ref": "#/$defs/actionList" }, diff --git a/src/harel/model.py b/src/harel/model.py index 7a2aac0..5a6045e 100644 --- a/src/harel/model.py +++ b/src/harel/model.py @@ -73,6 +73,7 @@ class State: depth: int order: int # document/DFS order (stable region + declaration ordering) raw: dict[str, Any] + meta: dict[str, Any] = field(default_factory=dict) children: dict[str, State] = field(default_factory=dict) declares_esvs: set[str] = field(default_factory=set) region_index: int | None = None # 0-based region for orthogonal substates @@ -101,6 +102,7 @@ def __init__(self, definition: Definition, top_override: dict[str, Any] | None = self.id = definition.id self.version = definition.version self.format = definition.format + self.meta = dict(definition.raw.get("meta") or {}) self._counter = 0 top_raw = top_override if top_override is not None else definition.top self.top = self._build("top", "top", None, 0, top_raw, None) @@ -128,6 +130,7 @@ def _build( depth=depth, order=order, raw=raw, + meta=dict(raw.get("meta") or {}), region_index=region_index, ) esvs = raw.get("esvs") or {} diff --git a/tests/test_library_api.py b/tests/test_library_api.py index bbb3f1e..1bffd16 100644 --- a/tests/test_library_api.py +++ b/tests/test_library_api.py @@ -27,6 +27,25 @@ push: { transition_to: locked } """ +META_GATE = """\ +id: meta_gate +meta: + host: guardrail +events: + go: {} +top: + meta: + owner: root + initial: { transition_to: a } + states: + a: + meta: + tools: [one, two] + on_events: + go: { transition_to: b } + b: {} +""" + def test_minimum_capability_set_via_public_api() -> None: # 1. load + validate a definition (raises ValidationError if invalid). @@ -91,3 +110,26 @@ def test_public_surface_is_exported() -> None: assert expected <= set(harel.__all__) for name in expected: assert hasattr(harel, name), f"harel.{name} not exported" + + +def test_meta_is_validation_only_model_data_not_runtime_state() -> None: + defs = harel.load_definitions(META_GATE) + assert harel.collect_errors(defs[0].raw) == [] + + host = harel.Host() + host.register_all(defs) + machine = host.machines["meta_gate"] + inst = host.create_root(machine, "m1") + host.run_to_quiescence() + + assert machine.meta == {"host": "guardrail"} + assert machine.top.meta == {"owner": "root"} + assert machine.by_path["top.a"].meta == {"tools": ["one", "two"]} + assert inst.active_leaf_names() == ["a"] + + snap = inst.to_snapshot() + assert "meta" not in snap + + assert host.deliver("m1", "go") is True + host.run_to_quiescence() + assert inst.active_leaf_names() == ["b"] diff --git a/tests/test_model.py b/tests/test_model.py index f0978b3..ca0f63c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -75,3 +75,27 @@ def test_unresolved_reference_raises_at_build() -> None: " states:\n s: { on_events: { go: { transition_to: nowhere } } }\n" ) ) + + +def test_meta_is_exposed_on_machine_and_states() -> None: + machine = Machine( + load_definition( + """\ +id: m +meta: + owner: ui +top: + meta: + role: root + initial: { transition_to: s } + states: + s: + meta: + tools: [search, respond] +""" + ) + ) + + assert machine.meta == {"owner": "ui"} + assert machine.top.meta == {"role": "root"} + assert machine.by_path["top.s"].meta == {"tools": ["search", "respond"]}