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
6 changes: 3 additions & 3 deletions .claude/skills/implement-a-workflow/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: implement-a-workflow
description: Implement or modify the step bodies of a workflow-workbench GraphSpec. Use whenever asked to implement a step, fill in a strategy, add an arm, or make coherence_check() pass on a design in this repo.
description: Implement or modify the step bodies of a graph-builder-spec GraphSpec. Use whenever asked to implement a step, fill in a strategy, add an arm, or make coherence_check() pass on a design in this repo.
---

# Implementing a workflow
Expand Down Expand Up @@ -34,7 +34,7 @@ does **not** mean the stage is trivial.
## Your acceptance test

```python
from workflow_workbench import blocking
from graph_builder_spec import blocking

findings = spec.coherence_check(strategy)
blocking(findings) # empty == render() will succeed
Expand Down Expand Up @@ -67,7 +67,7 @@ verdict — usually a missing return annotation. Fix the cause rather than filte

```bash
uv run pytest -q
uv run python3 -m workflow_workbench.reference --check
uv run python3 -m graph_builder_spec.reference --check
```

Then say which findings remain and why, including the `NOT CHECKED` ones. **Do not report a
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,50 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### ⛔ Breaking — renamed to `graph-builder-spec`

```python
from workflow_workbench import GraphSpec # before
from graph_builder_spec import GraphSpec # after
```

| | before | after |
|---|---|---|
| distribution | `workflow-workbench` | `graph-builder-spec` |
| import package | `workflow_workbench` | `graph_builder_spec` |
| repository | `borisdev/workflow-workbench` | `borisdev/graph-builder-spec` |
| frontend source | `frontend/workflow-workbench/` | `frontend/graph-builder-spec/` |
| built island | `static/workflow-workbench.{js,css}` | `static/graph-builder-spec.{js,css}` |
| server env vars | `WORKFLOW_WORKBENCH_TOKEN` / `_STORE` | `GRAPH_BUILDER_SPEC_TOKEN` / `_STORE` |

**No alias, no shim.** A missing call site is an `ImportError` at the import, not a silent change
of behaviour — the same choice 0.2.0 made for `NodeSpec(...)` and 0.3.0 for `coherence_check()`.

⚠️ **The env-var rename fails CLOSED, which is why it is safe to do without an alias.** A stale
`WORKFLOW_WORKBENCH_TOKEN` is simply unset under the new name, and `serve()` raises `SystemExit`
rather than binding a non-localhost host unauthenticated — verified, not assumed. The process
refuses to start; it does not start without a token. `GRAPH_BUILDER_SPEC_STORE` falling back to
its default is a cosmetic path change, not an access-control one.

**Why.** The shipped subtitle is *"A declaration layer over Pydantic Graph Builder"*, and
`graph-builder-spec` is that sentence. It reuses **their** noun rather than inventing one
(`domain-language.md` #1 — before inventing a noun, look for one that exists), and `-spec`
matches `GraphSpec` / `StepSpec` / `StrategySpec`, the vocabulary a reader meets in the first
code block. `workflow-workbench` said nothing about what the library wraps.

Rejected: `pydantic-graph-spec` and `pydantic-graph-builder-spec` squat a namespace that belongs
to Pydantic's own packages; `graph-spec` collides with graph databases and GraphQL;
`workflow-spec` keeps a word the code uses and still says nothing about what is wrapped.

⚠️ **Downstream repos track `main`, not a sha**, so this breaks their next `uv sync` until their
dependency name is updated. GitHub redirects the old clone URL; the old **distribution name**
does not exist. `nobsmed-v2` and `ai_computer_use` are updated alongside.

⚠️ **Every `workflow-workbench` / `workflow_workbench` BELOW this entry is the former name**, left
as written. Those entries describe releases that really were published under it, and rewriting
them would make history describe a name that did not exist at the time.


### Added — `check_boundary_types`, the 13th rule

A `GraphSpec`'s declared `input_type` / `output_type` are now compared against the variables the
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# workflow-workbench
# graph-builder-spec

*A declaration layer over Pydantic Graph Builder.*

Expand All @@ -13,7 +13,7 @@ step that composes the greeting and nothing complains: right type, right shape,
normalization stage silently stops mattering. Declaring each value by NAME, not just by type, is
what turns that into a finding.

Workflow Workbench is a [declaration layer](docs/glossary.md#declaration-layer) over Pydantic Graph Builder. You write the workflow's
Graph Builder Spec is a [declaration layer](docs/glossary.md#declaration-layer) over Pydantic Graph Builder. You write the workflow's
shape and its data contracts as **data**, before any step exists — which is what makes that class
of defect findable:

Expand Down Expand Up @@ -98,7 +98,7 @@ so the difference you are looking at is the declaration layer and nothing else:
| | step 1 | step 2 |
|---|---|---|
| **upstream**, unchanged — their [`visualize_graph.py`](https://pydantic.dev/docs/ai/graph/builder/) | `step_a` → `10` | `step_b` → `f'Result: {ctx.inputs}'` |
| **the control** — [`their_hello.py`](examples/ladder/their_hello.py): upstream's shape, a greeting instead of a number, and no workbench in the file | `pick` → `"Hello"` | `compose` → `f"{ctx.inputs}, {ctx.state.name}!"` |
| **the control** — [`their_hello.py`](examples/ladder/their_hello.py): upstream's shape, a greeting instead of a number, and none of this library in the file | `pick` → `"Hello"` | `compose` → `f"{ctx.inputs}, {ctx.state.name}!"` |
| **ours** — [`greeting.py`](examples/greeting.py), the same workflow declared | `normalize` → a clean name | `compose` → `f"Hello, {name}!"` |

Theirs is fine, and that is the point of keeping it: one graph with one implementation per step
Expand Down Expand Up @@ -192,8 +192,8 @@ Python 3.12 or newer, and [uv](https://docs.astral.sh/uv/). No API keys: the exa
string handling and calls no model.

```bash
git clone https://github.com/borisdev/workflow-workbench
cd workflow-workbench
git clone https://github.com/borisdev/graph-builder-spec
cd graph-builder-spec
uv sync --no-dev --extra evals
uv run python3 -m examples.greeting
```
Expand All @@ -214,8 +214,8 @@ Everything it produces goes to the terminal; no files are written. Excerpt:
```

Both mermaid blocks above go past on the way — the specification, then the comparison. A browser
viewer is available as a separate process — `uv run python3 -m workflow_workbench.cli serve`, see
[`serve.py`](workflow_workbench/serve.py) — and nothing in the quickstart needs it.
viewer is available as a separate process — `uv run python3 -m graph_builder_spec.cli serve`, see
[`serve.py`](graph_builder_spec/serve.py) — and nothing in the quickstart needs it.

## The development sequence

Expand Down Expand Up @@ -267,7 +267,7 @@ missed.
</details>

Every one of these exists because it caught something that otherwise **ran and returned a
plausible answer**. Each check's docstring in [`checks.py`](workflow_workbench/checks.py) carries
plausible answer**. Each check's docstring in [`checks.py`](graph_builder_spec/checks.py) carries
the measured case that produced it.

**These are structural checks, not a proof of correctness.** A step that returns its input
Expand Down Expand Up @@ -428,7 +428,7 @@ f.check # 'check_bindings' — which check produced it
f.about # 'compose' — the node; 'source->target' for an edge; '' for the design
f.blocking # True — False only for a `NOT CHECKED — …` stated gap

from workflow_workbench import blocking
from graph_builder_spec import blocking
blocking(spec.coherence_check(unfinished)) # what `render()` refuses on, gaps excluded
```

Expand Down Expand Up @@ -522,7 +522,7 @@ wired, with no hook and no override, so a strategy can change what a node *does*
what the workflow *is*.

Every rule is in the generated table above, and the failure each one exists for is in that
check's docstring in [`checks.py`](workflow_workbench/checks.py) — one place, which is the point.
check's docstring in [`checks.py`](graph_builder_spec/checks.py) — one place, which is the point.
This section used to repeat all of them in a hand-written table and had already drifted to 10 of
12; a test now refuses a second table of check names anywhere outside the generated block.

Expand Down Expand Up @@ -594,7 +594,7 @@ Proposals, not decisions — they are tracked, not quietly pending:
| [`docs/parity.md`](docs/parity.md) | every Pydantic Graph builder feature, declarable or not |
| [`docs/how-it-runs.md`](docs/how-it-runs.md) | their executor from the source, with a probe behind every claim |
| [`examples/greeting.py`](examples/greeting.py) | the walkthrough above; beside it a counter, a fan-out, subgraphs, extraction |
| [`examples/ladder/their_hello.py`](examples/ladder/their_hello.py) | the control — their smallest program's SHAPE, adapted to a greeting, with no workbench in the file |
| [`examples/ladder/their_hello.py`](examples/ladder/their_hello.py) | the control — their smallest program's SHAPE, adapted to a greeting, with none of this library in the file |
| [`examples/contestable.py`](examples/contestable.py) | four judgement-call stages, two strategies, nothing implemented and no score |

Downstream of community requests for
Expand Down Expand Up @@ -629,5 +629,5 @@ uv run python3 -m examples.greeting # the walkthrough above
uv run python3 docs/probe_api.py # the node-identity claims, against the real library
uv run python3 docs/probe_builder_features.py # what the specification can and cannot express
uv run python3 docs/probe_executor.py # every claim in docs/how-it-runs.md
uv run python3 -m workflow_workbench.parity --check # docs/parity.md is generated, not written
uv run python3 -m graph_builder_spec.parity --check # docs/parity.md is generated, not written
```
2 changes: 1 addition & 1 deletion docs/how-it-runs.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ Theirs is an ordered list of markers, so one edge can do both:
run -> ['ADA', 'GRACE']

Ours are separate types and no edge is both. See
[issue #2](https://github.com/borisdev/workflow-workbench/issues/2) for what a path-shaped
[issue #2](https://github.com/borisdev/graph-builder-spec/issues/2) for what a path-shaped
`EdgeSpec` would cost.

**Trigger to fix:** a design wanting `carries=list[Paper], delivers=pmid` on one wire, where the
Expand Down
2 changes: 1 addition & 1 deletion docs/migration-0.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ verification at the foot of this page.

| | |
|---|---|
| `from workflow_workbench import NodeSpec` | still works — `NodeSpec` is still exported |
| `from graph_builder_spec import NodeSpec` | still works — `NodeSpec` is still exported |
| `def f(n: NodeSpec)` in your own code | still type-checks, and now accepts joins and decisions too |
| `JoinSpec`, `DecisionSpec`, `EdgeSpec`, `MapEdgeSpec`, `TransformEdgeSpec`, `VariableSpec`, `StrategySpec`, `SubgraphBinding`, `GraphSpec` | unchanged |
| `GraphSpec.nodes`, `.joins`, `.decisions`, `.edges` | unchanged. `nodes` still holds steps only |
Expand Down
2 changes: 1 addition & 1 deletion docs/migration-0.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,6 @@ The prefix match is still correct and still supported. `blocking(findings)` is t
`render()` uses, if you would rather not spell it out:

```python
from workflow_workbench import blocking
from graph_builder_spec import blocking
blocking(spec.coherence_check(strategy))
```
32 changes: 16 additions & 16 deletions docs/parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ everything is the engine with extra steps.
`docs/probe_builder_features.py` introspects `GraphBuilder` and fails if any public method is
unclassified, so the next thing Pydantic Graph ships turns it red instead of silently widening a
gap this page describes as closed. The rows themselves are generated from
`workflow_workbench/parity.py`; `tests/test_parity.py` fails if this file and that one disagree.
`graph_builder_spec/parity.py`; `tests/test_parity.py` fails if this file and that one disagree.

```bash
python3 -m workflow_workbench.parity # print the table
python3 -m workflow_workbench.parity --check # exit 1 if this file is stale
python3 -m graph_builder_spec.parity # print the table
python3 -m graph_builder_spec.parity --check # exit 1 if this file is stale
```

⚠️ `status` is per FEATURE, and a feature-by-feature table cannot express COMPOSITION. `map` is
Expand All @@ -42,8 +42,8 @@ identically.
<!-- parity:start -->
## Every builder feature, theirs beside ours

<!-- GENERATED from workflow_workbench/parity.py — do not edit by hand. -->
<!-- Regenerate: python3 -m workflow_workbench.parity --write -->
<!-- GENERATED from graph_builder_spec/parity.py — do not edit by hand. -->
<!-- Regenerate: python3 -m graph_builder_spec.parity --write -->

### `step` — **yes**

Expand All @@ -55,7 +55,7 @@ async def double(ctx) -> int:
return ctx.inputs * 2
```

Workflow Workbench:
Graph Builder Spec:

```python
double = StepSpec("double", inputs=(n,), outputs=(n,))
Expand All @@ -74,7 +74,7 @@ g.add(g.edge_from(a).to(b))
g.add_edge(a, b, label='count')
```

Workflow Workbench:
Graph Builder Spec:

```python
EdgeSpec(source=a, target=b, carries=count) # `carries` IS the label
Expand All @@ -88,7 +88,7 @@ Pydantic Graph:
collect = g.join(reduce_sum, initial=0)
```

Workflow Workbench:
Graph Builder Spec:

```python
collect = JoinSpec("collect", reduce_sum, initial=0,
Expand All @@ -107,7 +107,7 @@ Pydantic Graph:
g.edge_from(g.start_node).map().to(square)
```

Workflow Workbench:
Graph Builder Spec:

```python
MapEdgeSpec(source=START, target=square, carries=numbers, delivers=number)
Expand All @@ -125,7 +125,7 @@ d = d.branch(g.match(Urgent).to(escalate))
d = d.branch(g.match(Routine).to(research))
```

Workflow Workbench:
Graph Builder Spec:

```python
route = DecisionSpec("route")
Expand All @@ -146,7 +146,7 @@ async def split(ctx):
yield w
```

Workflow Workbench:
Graph Builder Spec:

```python
split = StepSpec("split", inputs=(text,), outputs=(words,), streams=True)
Expand All @@ -163,7 +163,7 @@ Pydantic Graph:
g.edge_from(a).broadcast(lambda eb: [eb.to(x), eb.to(y)])
```

Workflow Workbench:
Graph Builder Spec:

```python
EdgeSpec(source=a, target=x, carries=v)
Expand All @@ -180,7 +180,7 @@ Pydantic Graph:
g.edge_from(a, b).to(sink)
```

Workflow Workbench:
Graph Builder Spec:

```python
EdgeSpec(source=a, target=sink, carries=v)
Expand All @@ -197,7 +197,7 @@ Pydantic Graph:
d.branch(g.match(int, matches=lambda v: v > 10).to(big))
```

Workflow Workbench:
Graph Builder Spec:

```python
# not declarable. Return a discriminating TYPE from a step instead:
Expand All @@ -215,7 +215,7 @@ Pydantic Graph:
g.edge_from(a).transform(lambda ctx: ctx.inputs.edges).to(b)
```

Workflow Workbench:
Graph Builder Spec:

```python
# fixed — part of the design, like a JoinSpec's reducer:
Expand All @@ -237,7 +237,7 @@ class Increment(BaseNode[S, None, int]):
return DoubleIt(...)
```

Workflow Workbench:
Graph Builder Spec:

```python
# no equivalent for the CLASS. All three things it is used FOR are declarable:
Expand Down
10 changes: 5 additions & 5 deletions docs/probe_builder_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
every check this library exists to provide — `coherence_check()` says so out loud (`NOT CHECKED — ...
overrides build_pydantic_structure()`), and the middle section measures exactly that.

⛔ THE TABLE IS `workflow_workbench/parity.py`, AND IT IS CHECKED AGAINST THE REAL API. It was
⛔ THE TABLE IS `graph_builder_spec/parity.py`, AND IT IS CHECKED AGAINST THE REAL API. It was
hand-written here once, from a grep of method names, and it MISSED FIVE — `stream`, `node`,
`match_node`, `add_mapping_edge`, and the `matches=` predicate on `match` — while reading as a
complete inventory of the gaps. A hand-maintained inventory of someone else's API is wrong the
Expand Down Expand Up @@ -205,7 +205,7 @@ async def sink(ctx) -> int:
print("can a GraphSpec DECLARE it? there is no second route — the declaration is the only wiring.")
print("=" * 92 + "\n")

from workflow_workbench import ( # noqa: E402
from graph_builder_spec import ( # noqa: E402
END, START, EdgeSpec, GraphSpec, StepSpec, StrategySpec, VariableSpec)

n = VariableSpec("n", int)
Expand Down Expand Up @@ -233,12 +233,12 @@ async def dbl(ctx) -> int:
print()


# ── the table lives in workflow_workbench/parity.py — ONE definition ────────────────────────
# ── the table lives in graph_builder_spec/parity.py — ONE definition ────────────────────────
#
# ⛔ Two descriptions of one thing is the drift `.claude/rules/spec-as-code.md` exists to
# prevent, so there is exactly one: `parity.py` is source, `docs/parity.md` is generated from it,
# and this probe reads it rather than keeping its own copy.
from workflow_workbench.parity import FEATURES # noqa: E402
from graph_builder_spec.parity import FEATURES # noqa: E402

ORDER = {"yes": 0, "partial": 1, "refused": 2, "cannot": 3, "plumbing": 4}
print(f"{'GraphBuilder API':<32} {'declarable':>18} what to write instead")
Expand All @@ -256,7 +256,7 @@ async def dbl(ctx) -> int:
print("⚠️ `refused` and `cannot` are NOT the same as missing, and are kept apart on purpose:")
print(" collapsing them into 'no' is how a design decision comes to read as a gap, and how the")
print(" next person 'fixes' it. Full side-by-side examples: docs/parity.md, or")
print(" `python3 -m workflow_workbench.parity`.")
print(" `python3 -m graph_builder_spec.parity`.")


# ── ⛔ and the check that stops this list going stale ────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion examples/contestable.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"""
from __future__ import annotations

from workflow_workbench import (
from graph_builder_spec import (
END, START, EdgeSpec, GraphSpec, StepSpec, StrategySpec, VariableSpec, blocking)

# ── the values that move ────────────────────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion examples/counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from dataclasses import dataclass

from workflow_workbench import END, START, EdgeSpec, GraphSpec, StepSpec, StrategySpec, VariableSpec
from graph_builder_spec import END, START, EdgeSpec, GraphSpec, StepSpec, StrategySpec, VariableSpec


@dataclass
Expand Down
Loading