diff --git a/README.md b/README.md index 5521779..ff5c013 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ An explicit execution graph: A single execution of a graph: * Produces a complete execution ledger -* Can be replayed byte-for-byte +* Can be replayed semantically (state/artifact hashes and step records) * Can be diffed against other runs --- @@ -135,16 +135,16 @@ A single execution of a graph: Determinant makes the following guarantees: * **Replayability** - Same inputs → same outputs + Same graph/state/config/seed produces the same state and artifact hashes. * **Auditability** - Every step, transition, and artifact is logged + Every step transition, event, and artifact write is logged in `ledger.ndjson`. * **Explainability** - Divergence between runs is attributable to explicit differences + Divergence between runs can be located by comparing manifest and ledger hashes. * **Local-first** - Runs fully offline by default + The runtime executes local Python code only; any network behavior must come from user steps. If you break these guarantees, you are using Determinant incorrectly. @@ -153,20 +153,24 @@ If you break these guarantees, you are using Determinant incorrectly. ## Example (Minimal) ```python -from determinant import State, Step, Graph, RunConfig, run - -class ParseDocs(Step): - def execute(self, state: State, config: dict[str, object], seed: int): - ... - -class ScoreDocs(Step): - def execute(self, state: State, config: dict[str, object], seed: int): - ... - -graph = Graph(steps=[ - ParseDocs(), - ScoreDocs(), -]) +from determinant import State, Step, StepEvent, StepResult, Graph, RunConfig, run + +class AddValue(Step): + def execute(self, state: State, config: dict[str, object], seed: int) -> StepResult: + _ = seed + inc = int(config.get("increment", 1)) + value = int(state.data.get("value", 0)) + inc + return StepResult( + state=State({"value": value}), + events=[StepEvent(event_type="INFO", code="VALUE_UPDATED", message="value updated")], + ) + + +graph = Graph( + graph_id="minimal", + version="v1", + steps=[AddValue()], +) config_data = { "seed": 42, @@ -180,12 +184,16 @@ run_config = RunConfig( ) result = run( graph=graph, - initial_state=State.from_file("input.json"), + initial_state=State({"value": 0}), config=run_config, ) + +print(result.status) # COMPLETED +print(result.ledger_path) # output/runs/example/ledger.ndjson ``` -Running this twice with the same inputs will produce **identical results and ledgers**. +Running this twice with the same inputs will produce the same final state and artifact hashes. +Ledger files include timestamps, so compare semantic fields rather than full file bytes. --- diff --git a/docs/LEDGER_SCHEMA.md b/docs/LEDGER_SCHEMA.md index a338547..d36507d 100644 --- a/docs/LEDGER_SCHEMA.md +++ b/docs/LEDGER_SCHEMA.md @@ -43,22 +43,22 @@ normative structure below: artifacts/ . state/ - _.json + 0000.json + 0001.json + ... meta/ graph.json config.json env.json ``` -**Path requirement:** all ledger and manifest paths are **relative to -`runs/`** (for example `meta/graph.json`, `state/0000_.json`, and +**Path requirement:** all ledger and manifest paths are relative to +`runs/` (for example `meta/graph.json`, `state/0000.json`, and `artifacts/.`). Tooling must not emit or expect absolute paths inside ledger or manifest entries. -**Custom output roots:** configure a different `output_root` via runtime or CLI -settings (for example by setting the run output directory to -`/runs/`), and the runtime will emit runs under the chosen -root. +**Custom output roots:** set `RunConfig.output_dir` to choose ``. +The runtime writes runs under `/runs/`. **Run ID selection:** `run_id` is either provided explicitly or generated by the runtime when omitted. @@ -130,15 +130,13 @@ Example payload (record body fields beyond header): "runtime": {"name": "determinant", "version": "0.1.0"}, "run": { "mode": "execute", - "seed": 42, - "created_by": "cli", - "command": "determinant run graph.json state.json --seed 42" + "seed": 42 }, "inputs": { "graph": {"path": "meta/graph.json", "sha256": "<...>"}, "config": {"path": "meta/config.json", "sha256": "<...>"}, "env": {"path": "meta/env.json", "sha256": "<...>"}, - "initial_state": {"path": "state/0000_.json", "sha256": "<...>"} + "initial_state": {"path": "state/0000.json", "sha256": "<...>"} } } ``` @@ -153,11 +151,11 @@ Declares which step is about to run and the exact state snapshot. "step": { "index": 0, "step_id": "ParseDocs", - "step_version": "codehash:", - "graph_node_id": "n0" + "step_version": "src:", + "graph_node_id": "n0000" }, "state_in": { - "path": "state/0000_.json", + "path": "state/0000.json", "sha256": "<...>" } } @@ -221,9 +219,9 @@ Declares completion and state output. { "type": "STEP_END", "step": {"index": 0, "step_id": "ParseDocs"}, - "status": "OK", + "status": "COMPLETED", "state_out": { - "path": "state/0001_.json", + "path": "state/0001.json", "sha256": "<...>" }, "metrics": {"duration_ms": 12} @@ -240,8 +238,8 @@ Final status + rollups + final state hash. ```json { "type": "RUN_END", - "status": "OK", - "final_state": {"path": "state/00NN_.json", "sha256": "<...>"}, + "status": "COMPLETED", + "final_state": {"path": "state/00NN.json", "sha256": "<...>"}, "rollup": { "steps_ok": 2, "steps_failed": 0, @@ -289,7 +287,7 @@ apply to the `event.data` object itself (not its stringified form). | Record type | Keep (semantic fields) | Ignore for deterministic comparison | | --- | --- | --- | -| `RUN_START` | `runtime.name`, `runtime.version`, `run.mode`, `run.seed`, `inputs.graph.sha256`, `inputs.config.sha256`, `inputs.env.sha256`, `inputs.initial_state.sha256` | `run.created_by`, `run.command`, header fields (`ts_utc`, `hash`, `prev_hash`), any perf metadata | +| `RUN_START` | `runtime.name`, `runtime.version`, `run.mode`, `run.seed`, `inputs.graph.sha256`, `inputs.config.sha256`, `inputs.env.sha256`, `inputs.initial_state.sha256` | header fields (`ts_utc`, `hash`, `prev_hash`), any perf metadata | | `STEP_START` | `step.index`, `step.step_id`, `step.step_version`, `step.graph_node_id`, `state_in.sha256` | header fields (`ts_utc`, `hash`, `prev_hash`), any perf metadata | | `STEP_EVENT` | `step.index`, `step.step_id`, `event.event_type`, `event.code`, `event.data` (compare canonical JSON) | `event.message` (freeform), header fields (`ts_utc`, `hash`, `prev_hash`), any perf metadata | | `ARTIFACT_WRITTEN` | `step.index`, `step.step_id`, `artifact.artifact_id`, `artifact.logical_name`, `artifact.media_type`, `artifact.path`, `artifact.sha256`, `artifact.size_bytes` | header fields (`ts_utc`, `hash`, `prev_hash`), any perf metadata |