Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/harel/data/machine.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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." }
},
Expand Down Expand Up @@ -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" },
Expand Down
3 changes: 3 additions & 0 deletions src/harel/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {}
Expand Down
42 changes: 42 additions & 0 deletions tests/test_library_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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"]
24 changes: 24 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}
Loading