feat(flows): add the atlas -- a flow compiled to a graph before it runs - #31
Conversation
Pure ast over every file a flow holds, executing nothing, answering with findings rather than raising: ten errors for a flow that cannot run, cannot be answered or cannot end, and eight warnings for a run that may be regretted. surface() reads the flow-facing interfaces themselves -- lifted from the interface test, which now imports it -- and offered() reads the package's own tables, so the checker states the contract it checks rather than keeping a copy to drift. Every rule is the proof of an absence, one function at a time, and the sweep over the builtin and official flows pins the false-positive rate: nothing anywhere, except the one warning rlar has earned -- a loop only its reviewer can end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The second of the two readings: the flow loaded and driven for real, in a subprocess per scenario, by stubs that claim every capability over the real driver base classes -- so the hooks a flow hangs fire as they would, every turn lands at once, and each costs what the scenario says. NEVER_DONE is the reviewer that never says the work is done: a loop with a bound of its own walks to the end of it in milliseconds, and one without is caught by the turn cap or killed by the parent's clock -- the executable proof that a run can end. SILENT answers every turn with nothing, which is every guard tried at once. A refused load comes back as a finding rather than a raise, and the config rules run again on the model the loading actually resolved. The proof's world sleeps for free and works in a scratch directory taken away with the process. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
catalogue() reads what this installed humanize serves at call time: the primitives every backend has, each moment outside EVERYWHERE with the backends whose drivers declare it, the shape a turn can be held to, the tools a flow may offer, and the goal feature -- all off the live enum, the DRIVEN table and the interfaces surface() reads, so a flow written against the catalogue is written against this installation rather than against a snapshot that drifts. briefed() renders it as the one page a compiler -- or a person choosing what to build on -- steers by, split into what every backend serves and what has to be declared on the place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two readings from a command line: the static one over every file the flow holds, which executes nothing, then the flow loaded in a subprocess held to a clock so its live config model is read too. One finding a line with a count under them, --json for a script, --static to keep the flow unloaded, --strict to hold warnings to the bar. Exit 0 with nothing blocking, 1 with any error -- or any warning under --strict -- and 2 for a line to correct or a name no flow answers to. The readings are reached through Hmz().flows.check, so anything else that checks a flow makes the same call and is refused the same way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reference grows a Checking-a-flow section beside Testing-a-flow: the two readings, the rule table with what each code found, the scenarios a proof drives against, and the catalogue. A guide walks the command -- one line, what an error is against a warning, --static for a flow nobody has read, the never-done proof as a library call, --json in a script -- and Writing-a-flow points at it from Check-your-work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s live The aot flow lands what it compiles in the flows of your own, and the two places those are is a fact written in verses.py -- handed through rather than spelled again where it would drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A field that takes at least three answers was answered with none: the fabrication gave every list [], the model refused it, and a flow that bounds its own retries read as one that could not be driven at all. Now a list is answered with as many fabricated elements as its bound asks, and an Annotated element is answered as the type it annotates. Found scanning the official flows: parallel_flame_chase's coordinator takes a plan of at least three lanes. Its own cross-field validator is still past what deterministic fabrication can satisfy -- which the proof reports honestly, as a flow that fails closed after three tries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A shaped answer's Literal field compared against a value the shape does not offer -- review.verdict == "DONE" over Literal["done", "redo"] -- is a guard that never opens, or one that never shuts. Both readings pass, both provings pass, and the flow silently steers by a value no answer will ever hold; found where the shape is declared in the same file and left be where it is not. The rule oh-my-humanize's freeze checker runs on its edge conditions (references undeclared verdict), carried over to answers held to a shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings #29 back onto main after 294528a took it off. The two conflicts were both of the branch's age: `__all__` gained `Board` and `Capability` on either side, and `test_flow_interface` kept the two tests main added while dropping the `_members` helper `checking.surface` now is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An atlas is a flow whose body is read rather than run. `atlas.py` is the vocabulary: the marks (`atlas`, `mind`, `logic`, `sub`) and the prophecy a body compiles to. `prophesying.py` is the reading, pure `ast` over the flow's files, sharing `checking.py`'s parsing and rules and adding the ones an atlas is stricter for -- every one of them decidable, since the subset is chosen so there is nothing to guess. `stepping.py` walks a prophecy a node at a time and picks a stopped run up from the answers it wrote down. Two kinds of ordinary node and one that is a graph: a `mind` is a turn and has one way out, a `logic` is Python and may have several, and an atlas called by an atlas is a supernode. What flows between them is a pydantic model or a plain kind, checked edge by edge before anything runs. `checking.checked` hands an atlas to the stricter reading, so `hmz check` asks one question either way; `driving.declares` hands back the walk rather than the entry point, so every way of running a flow gets the compiling for free. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`prophecy.pkl` beside a flow's entry point is what runs, rather than the atlas compiled again: the compiling is where an atlas is refused, and a repository that has been through it once has an answer worth carrying. The flow's own Python still has to be there -- a prophecy names the functions its nodes are -- so a directory holding a prophecy and nothing else is not a flow. `hmz check` says when a shipped prophecy and the source it came from have drifted apart, since a run walks the shipped one; `--prophecy` prints what an atlas compiles to and `--ship` writes it beside the flow. `Hmz().flows` answers both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Such a flow has no directory of its own -- what is beside it is the other flows -- so writing `prophecy.pkl` there wrote into a path with a `.py` in the middle of it and came back as an OSError. Said as the refusal it is, and the one-file atlas is now covered both ways: it compiles and runs, and it says why it cannot be shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What is set up is the run, and a supernode is a node: one reached as a node was handed no config, so every node inside it that read one read None -- quietly, an hour in. Refused where the supernode is written instead, and the prophecy now carries what the atlas it came from can be set up with, which is what makes the refusal readable across flows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…'s answer A body may end `return draft` three nodes after `draft` was bound, and the walk answered with whatever ran last -- so a supernode handed its caller the wrong value, silently. The way out of a prophecy now carries the name the run answers with, an atlas that says it answers with something is held to saying so on every way out, and a return is a whole name rather than a field of one, a field being a thing a logic node reads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two of them were bugs rather than cleanups, and both were silent. `hmz check official/review:pass` reported `not-an-atlas`: the dispatch to the compiling sat behind a seam that never took a name, so which of a file's atlases was asked for was dropped two lines before it was needed. The choice moves to where both halves of the name are held, and `is_atlas` is the one predicate it turns on. And the bodies an atlas compiles were left out of the ordinary reading by name, so a class beside an atlas with a method of the same name lost every rule -- loop bounds, agent surface, hook moments -- with nothing said. They are left out by identity now. The rest is what the readings were asked for. Reading a flow no longer compiles it: `resumes`, `configures` and `wanted` are answered off the entry point's own annotation, and an atlas that does not compile no longer makes a flow picker say it cannot be picked up -- measured, six compiles for those three questions became none. A node's state was written twice where once would do. A supernode of another flow was read again at every visit, which is new code under a graph already settled. The walk holds what every step of it is against instead of handing nine arguments down four functions, and holds the prophecy's nodes and edges by what a step looks them up by. Reuse, where the reading beside this one already had it: `_annotated` for what names an agent -- which also stops `list[Agent]` reading as one -- `_unquoted` for a quoted annotation, the file's own alias for pydantic's `Field`, and one `shipped()` for the three places that opened `prophecy.pkl` and disagreed about a single-file flow. `canonical` reads the tuples themselves, so a field added to a node later is one the digest sees rather than one it silently drops. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prophecy gained a config and its ways out gained what the run answers with; `reading`, `is_atlas`, `named_as` and `shipped` are the four rules the pass put in one place apiece. All of it was already true of the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The worst was mine and it was a hole: `hmz check --static` promises to execute nothing of the flow it is pointed at, and the prophecy a flowverse ships is a pickle -- so a `prophecy.pkl` naming `os.system` ran it. Reading one is held to the tuples a prophecy is made of and nothing else, which is checked here with a payload that no longer fires. The rest, in the order they would have bitten: - A body that rebinds what the atlas was called with compiled into a graph that reads the original, with the rebinding bound to a name nothing read. - A name bound in one arm of a branch was readable on the path that skipped it, so a node was handed None where the compile is meant to rule that out. - A `sub()` bound under the name its own flow uses ran the sub-prophecy against the caller's module: no error, wrong answer. - A run that had finished replayed every kept answer and did nothing, so every `hmz exec` after the first was a no-op with a new task silently ignored. - `_required` read the root of a dotted name, so `pydantic.Field(...)` made every field optional and the edge checks that turn on it went quiet. - A loop took the node above it for its head without asking whether that node answers the name the test reads, so a loop nothing could end compiled. - A supernode wrote its own key where the run says which node it stopped in, so a `rerun=False` node inside one had its effect twice. - `class Draft(pydantic.BaseModel)` was not read as a model, refusing a legal atlas; the dispatch to the compiling was per-file, so an ordinary flow beside an atlas was reported as one that is not an atlas; a stale prophecy could never be re-shipped, since the finding about it discarded the graph that would have replaced it; and a walrus-guarded answer read as unguarded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An `async def` node compiled clean and bound a bare coroutine as its answer, which the next node was then handed. The walk does not await, and what waits for a model is a turn -- which a mind already is -- so a node that is a coroutine is refused where it is called, and the message the atlas itself is refused with no longer promises otherwise. A run nobody set up was handed None where its config goes, and every read off it came back None: the body of an atlas cannot write `config or Config()`, `config` being a name and that being work. So such a run is handed the model's own defaults, and a config that cannot be built out of them is refused where it is declared -- `unset-config`, since a run may always be started with nothing. And the compiling, which the cleanup pass moved off the readers, went too far the other way: `load` no longer refused a body that will not compile where it was named, and `Runner` carried the refusal into `run`, where a container had been pulled and a cycle opened and the CLI reads a raise as a crash of its own. `readies` is the two places that are about to run one asking for it, so a flow picker still pays nothing and a bad atlas is still refused before anything is chosen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
There are blocking policy/contract issues to resolve (SPEC.md modification restriction per AGENTS.md, downstream humanfia/flowverse update requirement, and a mismatched public return-type annotation in atlas execution wrapper).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces atlases (@atlas), a second flow form whose body is compiled (via pure ast) into a graph “prophecy” and then executed by walking that prophecy node-by-node, enabling resumable runs at node granularity and optional shipping of the compiled prophecy. It also reintroduces and extends the flow validation toolchain (static checking + subprocess proving) via a new hmz check CLI and corresponding SDK APIs.
Changes:
- Add atlas vocabulary + prophecy model, compiler (
prophesying), and runtime walker (stepping) to run compiled graphs and resume deterministically. - Add flow legality checking/proving and expose them through
hmz checkand SDK (Flows.check/prophecy/foretell). - Add extensive tests and documentation for checking flows and writing/running atlases (including prophecy printing/shipping).
File summaries
| File | Description |
|---|---|
| tests/test_stepping.py | New tests covering prophecy walking, resume semantics, supernodes, and shipped prophecies. |
| tests/test_proving.py | New tests validating subprocess proving scenarios, outcomes, and shaped-answer fabrication. |
| tests/test_prophesying.py | New tests for atlas compilation rules, graph structure, shape checking, and stale-prophecy detection. |
| tests/test_flow_interface.py | Switch interface contract checking to use surface() from the checker module. |
| tests/test_cli.py | Register check in CLI command coverage. |
| tests/test_checking.py | New comprehensive tests for static checking rules + sweep tests over shipped/official flows. |
| tests/test_check_command.py | New tests for hmz check exit codes, flags, and JSON output format. |
| tests/test_catalogue.py | New tests for capability catalogue accuracy against live interfaces/backends. |
| src/hmz/SPEC.md | Adds CLI spec for hmz check (but see SPEC-modification policy comment). |
| src/hmz/sdk/SPEC.md | Adds SDK spec for Flows.check/prophecy/foretell (but see SPEC-modification policy comment). |
| src/hmz/sdk/flows.py | Implements SDK methods for checking flows, reading prophecies, and writing shipped prophecies. |
| src/hmz/runner.py | Ensures atlas compilation happens before run setup proceeds (readies(run)). |
| src/hmz/flows/stepping.py | New prophecy-walking runtime for atlas execution + resume semantics + supernodes. |
| src/hmz/flows/checking.py | New static (pure-ast) flow legality checker, surface introspection, and catalogue/briefing helpers. |
| src/hmz/flows/prophesying.py | New atlas compiler producing a canonical Prophecy graph and enforcing atlas-body rules. |
| src/hmz/flows/proving.py | New subprocess-based proving runner with deterministic stub world(s) and bounded execution. |
| src/hmz/flows/driving.py | Integrates atlas compilation/walking into flow loading and adds readies() pre-run compilation. |
| src/hmz/flows/atlas.py | New atlas marks (atlas/mind/logic/sub) and prophecy model + canonicalization + safe-ish pickle reading. |
| src/hmz/flows/init.py | Exposes atlas/checking/prophesying/proving surface; adds reading() and foretold() helpers. |
| src/hmz/cli/check.py | New hmz check implementation with --static/--strict/--json/--prophecy/--ship. |
| src/hmz/cli/init.py | Registers hmz check command. |
| src/hmz/flows/SPEC.md | Adds SPEC coverage for checking/proving/atlas/prophesying/stepping (but see SPEC-modification policy comment). |
| docs/reference/sdk.md | Documents new SDK APIs (check, prophecy, foretell). |
| docs/reference/flows.md | Adds reference sections for checking flows and atlas surface/prophecy/shipping. |
| docs/reference/cli.md | Adds reference docs for hmz check. |
| docs/guide/writing-a-flow.md | Adds guidance on checking flows and introduces atlases conceptually. |
| docs/guide/testing-flows.md | Cross-links to flow checking guide. |
| docs/guide/resuming.md | Adds atlas resume explanation (node-answer-based resume). |
| docs/guide/concepts.md | Adds “Atlas” concept entry. |
| docs/guide/checking-flows.md | New guide page for checking/proving flows and CLI usage. |
| docs/guide/atlas.md | New guide page explaining atlas syntax, compilation rules, prophecy, and shipping. |
| docs/.vitepress/config.mts | Adds navigation links for the new guide pages. |
Review details
- Files reviewed: 32/32 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # `declares` answered reads what it would have read off the entry point itself. | ||
| self.__dict__.update(entry.__dict__) | ||
|
|
||
| def __call__(self, *said: Any) -> Awaitable[None] | None: |
| def _compiled(named: str, read: dict[str, Any], run: Entry) -> Entry: | ||
| """One flow's entry point, or -- for an atlas -- something that runs its prophecy. | ||
|
|
||
| An atlas is a flow whose body is a declaration: what it says is compiled before anything | ||
| runs, and what runs is the prophecy that compiling made. So the entry point itself is | ||
| never called, and what everything else holds is the walk over the prophecy instead -- | ||
| swapped here, where a flow is loaded, so that every way of running one gets both the | ||
| compiling and the walking without knowing there are two kinds of flow. |
| ## `hmz check` | ||
|
|
||
| ```shell | ||
| hmz check [--static] [--strict] [--json] [--prophecy | --ship] <flow> [<flow>...] | ||
| ``` | ||
|
|
||
| Reads a flow for what will not run, before anything runs it. | ||
|
|
||
| - The two readings MUST run in their order: the static one over every file the flow holds, | ||
| which MUST NOT import or execute anything of it -- the flow most worth checking is one | ||
| nobody has read -- and then the flow loaded and its live config model read. The second MUST | ||
| run only in a subprocess with a clock held over it, MUST NOT run where the first found an | ||
| error, and `--static` MUST leave it out altogether: a flow that cannot run is not one to | ||
| run to find out more about. | ||
| - Every finding MUST print one a line -- the file, the line, the severity, the code and what | ||
| is wrong -- with a count under them, and `--json` MUST say the same as one JSON object a | ||
| line for a script to read. Everything wrong MUST be said at once rather than first-failure | ||
| first: a checker is asked so that one reading answers for the whole flow. | ||
| - It MUST exit 0 for flows with nothing blocking -- warnings print and pass -- 1 where any | ||
| error was found, or any warning under `--strict`, and 2 for a line to correct or a name no | ||
| flow answers to, refused as argparse refuses one. | ||
| - What an atlas compiles to MUST be sayable from here, since the line that checks a flow is | ||
| the line that has just read it: `--prophecy` MUST print the canonical prophecy in place of | ||
| the findings, and `--ship` MUST write it into the flow's own directory for every run of it | ||
| from then on to walk. The two MUST NOT be given together, and a name that is not an atlas | ||
| that compiles MUST be said and MUST exit non-zero. | ||
|
|
| @@ -143,6 +148,13 @@ class Flows: | |||
|
|
|||
| The flows there are, and the places they come from. | |||
|
|
|||
| - `check` MUST be the two readings of `hmz.flows` in their order -- the static one, then the | |||
| flow loaded in a subprocess -- and the second MUST NOT run where the first found an error, | |||
| nor say again what the first already said: one call is one answer, whichever way in asked. | |||
| - `prophecy` MUST answer with what an atlas compiles to and with nothing for a flow that is | |||
| not one or does not compile, `check` being where the reasons are said. `foretell` MUST write | |||
| that prophecy into the flow's own directory, which is what every run of it walks from then | |||
| on, and MUST refuse a flow there is none for rather than writing something that is not one. | |||
| @@ -130,11 +141,22 @@ def __getattr__(name: str) -> object: ... | |||
| - What a flow says about itself MUST be the first line of its docstring where the decorator was | |||
| not told one, and for a file that is one flow MUST fall back to the file's own docstring: a | |||
| file that is one flow is documented as that flow. | |||
| - What a reading of a flow is pointed at MUST be worked out in one place, and MUST NOT be | |||
| what runs it: both readings take the whole of a flow -- the directory where there is one, | |||
| so that what the entry point imports beside it is read too, and the file where there is | |||
| not -- while what runs it is the entry point. Two rules for that is two rules to drift. | |||
| - A name MUST resolve to the `__init__.py` of the directory called that, else to the `.py` | |||
| file called that. A path given outright MAY be either, and MUST be taken in both shapes: | |||
| a path with the extension left off is how a single-file flow is written down everywhere a | |||
| name is not, and one shape resolving where the other does not is a flow that is offered and | |||
| cannot be run. | |||
| - A flow's directory MAY hold the prophecy its atlas was already compiled to, beside the | |||
| entry point. Where there is one it MUST be what runs: the compiling is where an atlas is | |||
| refused, and a repository that has been through it has an answer worth carrying rather than | |||
| working out again at every run. What is beside it MUST still be there -- a prophecy names | |||
| the functions its nodes are, and those are in the flow's own Python -- so a directory | |||
| holding a prophecy and no entry point MUST NOT be a flow, the same way one holding neither | |||
| is not. | |||
Both were found by driving `hmz exec` with live turns rather than by reading. Writing the loop's head again at the bottom of the body is the natural Python and the wrong graph: the edge back runs the head anyway, so the copy ran first and had its answer thrown away -- and a node with an effect had it twice a round with nothing said. Observed as one logic node printing its round twice for one turn. `twice-round` refuses it and says to take it out of the body. And one thing wrong in a body was four findings. `for one in [1, 2]:` drew the statement itself, two `unbound-read`s for names it would have bound, and "an atlas with no nodes in it" -- three consequences a reader has to work out are not separate mistakes. What a refused statement would have bound is now spoilt rather than unbound, that carries through a call which was itself refused, and the empty-body finding is only made where nothing else was. One mistake, one finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
50f0a24 to
3657638
Compare
A second kind of flow:
@atlas, whose body is read rather than run. It is anarrower Python, compiled before anything happens into a graph — a prophecy —
of the nodes the run will take and the edges between them.
This also re-lands #29 (checking, proving, the capability catalogue,
hmz check),which
294528ahad taken back off main, and builds on it rather than beside it:the two readings share one parser and one rule set.
What an atlas is
atlas.py— the marks (atlas,mind,logic,sub) and the prophecy abody compiles to. A
mindis one turn by one agent and has exactly one way out;a
logicis Python and may have several, which is what a branch hangs off. Anatlas called by an atlas is a supernode: one node from outside, one graph within.
prophesying.py— the reading, pureast, executing nothing. Every rule isan error and every one is decidable, which is the bargain the narrower Python
makes: what flows along an edge either fits what the far end takes or it does
not. The ordinary reading's warnings still come back over the node bodies.
stepping.py— walks the prophecy a node at a time, writing each answer downper visit, so a run stopped inside a loop is picked up inside that round. The
interrupted node runs again by default;
rerun=Falsesteps past it.Node I/O is a pydantic model or a plain kind, checked edge by edge at compile. An
atlas reaches an atlas by
sub()and reaches an ordinary flow through nothing atall — importing
loadis adynamic-callerror. A flowverse may shipflows/<name>/prophecy.pkl, which is what runs;hmz checksays when it and thesource have drifted.
Notes
hmz check --prophecyprints the canonical graph;--shipwrites it beside theflow.
Hmz().flows.prophecy/.foretellare the same two from the SDK.declaresswaps the entry point only for a markedatlas, and reading a flow compiles nothing, so a picker asking what a flow drives
pays nothing and is refused by nothing.
documented warning).
uv run pre-commit run --all-filesgreen;uv run pytest2067 passed. The oneintermittent failure,
tests/tracing/test_profile.py, reproduces ate079bf2and is a timing assertion sensitive to machine load.
Before merging
/verifyhas not been run — it is disabled for model invocation, so I couldnot clear it.
/simplify(17 findings) and/code-review max --fix(15 defects,including a pickle-execution hole this change had opened in
hmz check --static,which promises to execute nothing) both ran and are green. Please run
/verifybefore merging.
Three
SPEC.mdfiles gained sections for the new modules. AGENTS.md forbidstouching SPEC.md unprompted; the reasoning is that a module with no SPEC has
nothing to adhere to, and #29 set the precedent — but it is a call worth
confirming. No existing clause was altered.
🤖 Generated with Claude Code