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: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: release

# Tag-triggered release: build sdist+wheel and publish to PyPI via Trusted
# Publishing (OIDC) — no API token stored. Configure a PyPI Trusted Publisher for
# project `harel-python` (repo fruwehq/harel-python, workflow release.yml,
# project `determa-state` (repo fruwehq/determa-state-python, workflow release.yml,
# environment `pypi`) before the first tag-driven release.
on:
push:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ jobs:
- name: Ruff
run: ruff check .
- name: Mypy
run: mypy src/harel
run: mypy src/determa
- name: Unit tests
run: pytest -q

# Conformance: the language-agnostic suite from fruwehq/harel-conformance, run
# Conformance: the language-agnostic suite from fruwehq/determa-state-conformance, run
# black-box against this implementation. Separate job: it downloads an external
# repo, so a failure here means "diverges from the spec suite", not "our code broke".
conformance:
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ venv/
.ruff_cache/
.coverage
.DS_Store
.harel/
.determa/
.cache/
40 changes: 20 additions & 20 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Contributing to harel-python
# Contributing to Determa State (Python)

**harel-python** is the Python reference implementation of the
[harel](https://github.com/fruwehq/harel) statechart engine. It is correct **iff** it
passes the language-agnostic [conformance suite](https://github.com/fruwehq/harel-conformance).
The prose specification lives in [`fruwehq/harel`](https://github.com/fruwehq/harel)
**determa-state** is the Python reference implementation of the
[Determa State](https://github.com/fruwehq/determa-state-spec) statechart engine. It is correct **iff** it
passes the language-agnostic [conformance suite](https://github.com/fruwehq/determa-state-conformance).
The prose specification lives in [`fruwehq/determa-state-spec`](https://github.com/fruwehq/determa-state-spec)
(`SPEC.md`, the JSON Schema, and examples); the executable correctness target lives in
[`fruwehq/harel-conformance`](https://github.com/fruwehq/harel-conformance).
[`fruwehq/determa-state-conformance`](https://github.com/fruwehq/determa-state-conformance).

## Dev setup

Expand All @@ -15,7 +15,7 @@ source .venv/bin/activate # or `.venv\Scripts\activate` on Windows
pip install -e '.[dev]'
```

Python ≥ 3.11. The package is import-named `harel`, distribution-named `harel-python`.
Python ≥ 3.11. The package is import-named `determa.state`, distribution-named `determa-state`.

## The gate

Expand Down Expand Up @@ -45,15 +45,15 @@ make conformance # == pytest conformance
```

The suite is **not** a submodule. `conformance/conftest.py` clones
`fruwehq/harel-conformance` at the release tag matching this package's version (falling
`fruwehq/determa-state-conformance` at the release tag matching this package's version (falling
back to `main` while the tag does not yet exist) into a gitignored `.cache/` directory and
reuses it. To force a refresh, delete `.cache/`.

- **Offline / local edits:** point the tests at a local checkout with
`HAREL_CONFORMANCE_DIR=/path/to/harel-conformance` (and `HAREL_SPEC_DIR=/path/to/harel`
`DETERMA_CONFORMANCE_DIR=/path/to/determa-state-conformance` (and `DETERMA_SPEC_DIR=/path/to/determa-state-spec`
for the schema-parity test). If the suite cannot be obtained and no override is set, the
conformance tests **skip** rather than error.
- **Black-box CLI conformance** runs the implementation's `harel` (via `python -m harel`)
- **Black-box CLI conformance** runs the implementation's `determa-state` (via `python -m determa.state`)
as a **subprocess** against `conformance/run_cli.py`, so packaging/entry-point regressions
are caught (SPEC §13.6).

Expand All @@ -65,29 +65,29 @@ reuses it. To force a refresh, delete `.cache/`.
4. **No AI/assistant attribution anywhere** — not in commits, PR bodies, comments, or
docs (no `Co-Authored-By:`, no "Generated with…"). Commits and PRs read as the
author's own work.
5. One issue → one PR. A behavior change usually pairs with a `harel` spec edit and a
`harel-conformance` case; link them from the PR.
5. One issue → one PR. A behavior change usually pairs with a `determa-state-spec` edit and a
`determa-state-conformance` case; link them from the PR.

## Versioning

The version source of truth is **`pyproject.toml`** (`version = …`); the package derives
`harel.__version__` from the installed distribution metadata (no second copy to keep in
sync). The package version **is** the implemented harel spec version.
`determa.state.__version__` from the installed distribution metadata (no second copy to keep in
sync). The package version **is** the implemented Determa State spec version.

> harel, harel-conformance, and harel-python share one synchronized SemVer version
> determa-state-spec, determa-state-conformance, and determa-state share one synchronized SemVer version
> (currently pre-1.0 `0.0.x`). A release tags all three `vX.Y.Z` in lockstep; an
> implementation declares "implements harel spec vX.Y.Z" and pins the conformance suite
> implementation declares "implements Determa State spec vX.Y.Z" and pins the conformance suite
> at that tag.

### Releasing `vX.Y.Z` (lockstep)
1. Land all spec / conformance / implementation changes on the three `main` branches.
2. Bump the version in **`pyproject.toml`** (here) and the `VERSION` files in `harel` and
`harel-conformance`.
3. Tag `vX.Y.Z` on **harel** and **harel-conformance** (`gh api -X POST
2. Bump the version in **`pyproject.toml`** (here) and the `VERSION` files in `determa-state-spec` and
`determa-state-conformance`.
3. Tag `vX.Y.Z` on **determa-state-spec** and **determa-state-conformance** (`gh api -X POST
repos/fruwehq/<repo>/git/refs -f ref=refs/tags/vX.Y.Z -f sha=$(gh api
repos/fruwehq/<repo>/commits/main --jq .sha)`), so this package pins the matching
conformance tag instead of falling back to `main`.
4. Tag **harel-python** `vX.Y.Z` only to publish to PyPI — it triggers `release.yml`
4. Tag **determa-state** `vX.Y.Z` only to publish to PyPI — it triggers `release.yml`
(Trusted Publishing).

## License
Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
test:
pytest -q

# Conformance — the language-agnostic suite from fruwehq/harel-conformance, run
# Conformance — the language-agnostic suite from fruwehq/determa-state-conformance, run
# black-box against this implementation. Downloads the suite (pinned to the release
# tag matching this package's version) into .cache/ on first run.
# Offline / against a local checkout: HAREL_CONFORMANCE_DIR=/path/to/harel-conformance make conformance
# Offline / against a local checkout: DETERMA_CONFORMANCE_DIR=/path/to/determa-state-conformance make conformance
conformance:
pytest conformance -q

# Refresh the bundled JSON Schema from fruwehq/harel at the matching version tag
# (or HAREL_SPEC_DIR=/path/to/harel). The schema-drift test guards that they match.
# Refresh the bundled JSON Schema from fruwehq/determa-state-spec at the matching version tag
# (or DETERMA_SPEC_DIR=/path/to/determa-state-spec). The schema-drift test guards that they match.
sync-schema:
python scripts/sync_schema.py

Expand Down
55 changes: 28 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
# harel-python
# determa-state

Reference implementation (**Python**) of the [**harel**](https://github.com/fruwehq/harel)
Reference implementation (**Python**) of the [**Determa State**](https://github.com/fruwehq/determa-state-spec)
statechart engine.

The normative `SPEC.md`, the JSON Schema for machine YAML, and the cross-language
**conformance suite** live in the spec repo. This repository implements that spec in
Python and is correct **iff it passes the conformance suite**.

Implements the **harel spec v0.0.3** (early alpha; all fruwehq harel repos share one
[synchronized version](https://github.com/fruwehq/harel)).
Implements the **Determa State spec v0.0.5** (early alpha; all Determa State repos share one
[synchronized version](https://github.com/fruwehq/determa-state-spec)).

Status: **passing the full conformance suite** — all 30 engine cases
(`conformance/01`–`30`) plus `conformance/cli/01`–`03`. Implements YAML 1.2 loading
Status: **passing the full conformance suite** — all 31 engine cases
(`conformance/01`–`31`) plus `conformance/cli/01`–`03`. Implements YAML 1.2 loading
+ validation, the full statechart semantics (RTC dispatch, hierarchy, orthogonal
regions + `done`, shallow/deep history, choice pseudostates, submachine states, esvs, CEL guards,
structured actions,
active objects + bus, defer, timers, faults), static contracts, snapshot
round-trip + safe-point migration, Mermaid `export`, and the §13 CLI. Built up
the build order in [issue #3][issue].

[issue]: https://github.com/fruwehq/harel-python/issues/3
[issue]: https://github.com/fruwehq/determa-state-python/issues/3

## Conformance suite

The cross-language **conformance suite** is the single source of truth for correctness;
this repository is correct **iff it passes it**. The suite lives in
[`fruwehq/harel-conformance`](https://github.com/fruwehq/harel-conformance); the test
harness **fetches it at the matching release tag** (`v0.0.1`) into a gitignored
[`fruwehq/determa-state-conformance`](https://github.com/fruwehq/determa-state-conformance); the test
harness **fetches it at the matching release tag** (`v0.0.5`) into a gitignored
`.cache/` — no git submodule. The normative `SPEC.md` and JSON Schema live in
[`fruwehq/harel`](https://github.com/fruwehq/harel); the schema-drift test fetches the
[`fruwehq/determa-state-spec`](https://github.com/fruwehq/determa-state-spec); the schema-drift test fetches the
schema at the same tag.

For **offline** work, point the harness at a local checkout:
```
export HAREL_CONFORMANCE_DIR=/path/to/harel-conformance # the suite
export HAREL_SPEC_DIR=/path/to/harel # the schema (optional)
export DETERMA_CONFORMANCE_DIR=/path/to/determa-state-conformance # the suite
export DETERMA_SPEC_DIR=/path/to/determa-state-spec # the schema (optional)
```

## Scope (per the spec)
Expand All @@ -56,16 +56,16 @@ export HAREL_SPEC_DIR=/path/to/harel # the schema (option
- A test harness that runs the upstream conformance cases against this engine.

## Use as a library
The CLI (`harel …`) is a thin wrapper over a programmatic API; an engine can be
The CLI (`determa-state …`) is a thin wrapper over a programmatic API; an engine can be
embedded in a host program **without** the CLI or the file-backed store (SPEC §2):

```python
import harel
import determa.state as ds

defs = harel.load_definitions(open("gate.yaml").read())
harel.validate(defs[0].raw) # raises ValidationError if invalid
defs = ds.load_definitions(open("gate.yaml").read())
ds.validate(defs[0].raw) # raises ValidationError if invalid

host = harel.Host()
host = ds.Host()
host.register_all(defs)
inst = host.create_root(host.machines["gate"], "g1", external={"fare": 50})
host.run_to_quiescence()
Expand All @@ -74,15 +74,15 @@ host.deliver("g1", "coin", {"amount": 100}) # typed event; False if rejected
host.run_to_quiescence()
assert inst.active_leaf_names() == ["unlocked"]
assert inst.resolved_esvs()["fare"] == 50
assert inst.status is harel.Status.ACTIVE
assert inst.status is ds.Status.ACTIVE

host.advance("30s") # virtual clock
snaps = host.snapshot_all() # persist / round-trip (§8)
host.restore_all(snaps)
```

The public surface is everything exported from the top-level `harel` package
(`harel.__all__`): `Host`, `Instance`, `Definition`, `Machine`, `Status`, `Event`,
The public surface is everything exported from the `determa.state` package
(`determa.state.__all__`): `Host`, `Instance`, `Definition`, `Machine`, `Status`, `Event`,
`load_definitions` / `load_definition`, `validate` / `collect_errors`, and the
error types. See [`tests/test_library_api.py`](tests/test_library_api.py).

Expand All @@ -93,22 +93,23 @@ faulted }`. Built-ins: `JsonlObserver(stream)` (a drop-in transition log) and
`CollectingObserver` (records to a list).

```python
import sys, harel
host = harel.Host(observer=harel.JsonlObserver(sys.stdout)) # one JSON line per step
import sys
import determa.state as ds
host = ds.Host(observer=ds.JsonlObserver(sys.stdout)) # one JSON line per step
```

The Observer is *domain* observability (what the machine did). For *operational*
diagnostics the engine also emits **standard-library logging** under the `harel` logger
diagnostics the engine also emits **standard-library logging** under the `determa.state` logger
(dispatch/transition at `DEBUG`, faults/dead-letter at `WARNING`). It is silent by
default (a `NullHandler` is attached); enable it from the host app:

```python
import logging
logging.basicConfig(level=logging.DEBUG) # or logging.getLogger("harel").setLevel(...)
logging.basicConfig(level=logging.DEBUG) # or logging.getLogger("determa.state").setLevel(...)
```

## Layout
- `src/harel/` — the package.
- `src/determa/state/` — the package.
- `tests/` — the implementation's own **unit tests** (hermetic, offline).
- `conformance/` — the harness that runs the external **conformance suite** black-box
against this implementation (kept separate from the unit tests).
Expand All @@ -122,8 +123,8 @@ make check # ruff + mypy + unit tests (hermetic, offline) — the PR gate
make conformance # download & run the language-agnostic conformance suite
```
Equivalently: `pytest` runs the unit tests only; `pytest conformance` runs the
conformance suite (it fetches `harel-conformance` into `.cache/` on first run — set
`HAREL_CONFORMANCE_DIR` to use a local checkout offline). The two are **separate**:
conformance suite (it fetches `determa-state-conformance` into `.cache/` on first run — set
`DETERMA_CONFORMANCE_DIR` to use a local checkout offline). The two are **separate**:
unit tests never touch the network; conformance is opt-in.

## License
Expand Down
14 changes: 7 additions & 7 deletions conformance/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Fetch the language-agnostic conformance suite before test collection.

The suite lives in ``fruwehq/harel-conformance`` (no git submodule). It is cloned at the
The suite lives in ``fruwehq/determa-state-conformance`` (no git submodule). It is cloned at the
release tag matching this package's version (falling back to ``main`` while the tag does
not yet exist) into a gitignored ``.cache/`` directory and reused. Override with a local
checkout via ``HAREL_CONFORMANCE_DIR`` for offline work. If the suite cannot be obtained
checkout via ``DETERMA_CONFORMANCE_DIR`` for offline work. If the suite cannot be obtained
(offline, no override), the conformance tests skip rather than error.
"""

Expand All @@ -13,22 +13,22 @@
import subprocess
from pathlib import Path

import harel
import determa.state as ds

_ROOT = Path(__file__).resolve().parent.parent
_CACHE = _ROOT / ".cache" / "harel-conformance"
_REPO = "https://github.com/fruwehq/harel-conformance.git"
_CACHE = _ROOT / ".cache" / "determa-state-conformance"
_REPO = "https://github.com/fruwehq/determa-state-conformance.git"


def _ensure_conformance() -> None:
if os.environ.get("HAREL_CONFORMANCE_DIR"):
if os.environ.get("DETERMA_CONFORMANCE_DIR"):
return # caller provides a local checkout
if (_CACHE / ".git").exists():
return # already fetched; reuse (force a refresh by deleting .cache/)
_CACHE.parent.mkdir(parents=True, exist_ok=True)
# Prefer the release tag matching our version; fall back to main (tags may not exist
# yet pre-release). Network/tooling failure leaves the suite absent -> tests skip.
for ref in (f"v{harel.__version__}", "main"):
for ref in (f"v{ds.__version__}", "main"):
try:
subprocess.run(
["git", "clone", "--depth", "1", "--branch", ref, _REPO, str(_CACHE)],
Expand Down
24 changes: 12 additions & 12 deletions conformance/harness.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Conformance-suite harness helpers.

The language-agnostic suite (SPEC §9) lives in ``fruwehq/harel-conformance`` and is
The language-agnostic suite (SPEC §9) lives in ``fruwehq/determa-state-conformance`` and is
fetched at the matching release tag by ``conftest.py`` (no git submodule). These helpers
locate the fetched suite, enumerate cases, and run engine cases against this
implementation (create the root as id ``root``, per step ``send``/``advance``, run all
Expand All @@ -17,19 +17,19 @@
from types import ModuleType
from typing import Any

from harel import Host
from determa.state import Host

REPO_ROOT = Path(__file__).resolve().parent.parent


def conformance_root() -> Path:
"""Root of the fetched ``harel-conformance`` checkout.
"""Root of the fetched ``determa-state-conformance`` checkout.

``HAREL_CONFORMANCE_DIR`` overrides with a local checkout (offline/dev); otherwise
``DETERMA_CONFORMANCE_DIR`` overrides with a local checkout (offline/dev); otherwise
the cache populated by ``conftest.py`` is used.
"""
env = os.environ.get("HAREL_CONFORMANCE_DIR")
return Path(env) if env else REPO_ROOT / ".cache" / "harel-conformance"
env = os.environ.get("DETERMA_CONFORMANCE_DIR")
return Path(env) if env else REPO_ROOT / ".cache" / "determa-state-conformance"


CONFORMANCE_DIR = conformance_root() / "conformance"
Expand Down Expand Up @@ -129,15 +129,15 @@ def cli_cases() -> list[Path]:
def run_cli_case(case_dir: Path) -> None:
"""Run a CLI case **black-box** via the spec repo's reference runner (§13.6).

Invokes this package as a subprocess (``python -m harel``), so packaging and
Invokes this package as a subprocess (``python -m determa.state``), so packaging and
entry-point regressions are caught — not an in-process import. Delegating to the
suite's ``conformance/run_cli.py`` also avoids harness drift.
"""
runner = _load_cli_runner()
rc = runner.main(
[
"--cmd",
f"{sys.executable} -m harel",
f"{sys.executable} -m determa.state",
"--conformance-dir",
str(CONFORMANCE_DIR / "cli"),
case_dir.name,
Expand All @@ -148,7 +148,7 @@ def run_cli_case(case_dir: Path) -> None:

def _load_cli_runner() -> ModuleType:
path = CONFORMANCE_DIR / "run_cli.py"
spec = importlib.util.spec_from_file_location("harel_cli_runner", path)
spec = importlib.util.spec_from_file_location("determa_cli_runner", path)
assert spec is not None and spec.loader is not None, f"runner not found: {path}"
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
Expand All @@ -158,14 +158,14 @@ def _load_cli_runner() -> ModuleType:
# --- engine case runner -----------------------------------------------------
def run_engine_case(case: EngineCase) -> None:
"""Execute one engine conformance case, asserting every ``expect`` (SPEC §9)."""
from harel import collect_errors, load_definition, load_definitions
from harel.contracts import load_contract, validate_contracts
from determa.state import collect_errors, load_definition, load_definitions
from determa.state.contracts import load_contract, validate_contracts

test = _load_yaml(case.test_file)
assert case.machine_files, f"{case.name}: no machine files"

if "static" in test:
from harel import ValidationError
from determa.state import ValidationError

expected = bool(test["static"]["valid"])
try:
Expand Down
Loading
Loading