diff --git a/.claude/skills/implement-a-workflow/SKILL.md b/.claude/skills/implement-a-workflow/SKILL.md
index 2cc7467..9e3d6dd 100644
--- a/.claude/skills/implement-a-workflow/SKILL.md
+++ b/.claude/skills/implement-a-workflow/SKILL.md
@@ -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
@@ -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
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a30fecc..28fe536 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
index 7827349..7ab1e56 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# workflow-workbench
+# graph-builder-spec
*A declaration layer over Pydantic Graph Builder.*
@@ -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:
@@ -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
@@ -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
```
@@ -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
@@ -267,7 +267,7 @@ missed.
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
@@ -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
```
@@ -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.
@@ -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
@@ -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
```
diff --git a/docs/how-it-runs.md b/docs/how-it-runs.md
index a42488a..0f0111c 100644
--- a/docs/how-it-runs.md
+++ b/docs/how-it-runs.md
@@ -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
diff --git a/docs/migration-0.2.md b/docs/migration-0.2.md
index 913d67a..a570df0 100644
--- a/docs/migration-0.2.md
+++ b/docs/migration-0.2.md
@@ -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 |
diff --git a/docs/migration-0.3.md b/docs/migration-0.3.md
index 1f709eb..903f7ad 100644
--- a/docs/migration-0.3.md
+++ b/docs/migration-0.3.md
@@ -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))
```
diff --git a/docs/parity.md b/docs/parity.md
index a3e2f10..0e7cb48 100644
--- a/docs/parity.md
+++ b/docs/parity.md
@@ -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
@@ -42,8 +42,8 @@ identically.
## Every builder feature, theirs beside ours
-
-
+
+
### `step` — **yes**
@@ -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,))
@@ -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
@@ -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,
@@ -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)
@@ -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")
@@ -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)
@@ -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)
@@ -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)
@@ -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:
@@ -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:
@@ -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:
diff --git a/docs/probe_builder_features.py b/docs/probe_builder_features.py
index 1621355..1e04268 100644
--- a/docs/probe_builder_features.py
+++ b/docs/probe_builder_features.py
@@ -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
@@ -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)
@@ -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")
@@ -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 ────────────────────────────────────────
diff --git a/examples/contestable.py b/examples/contestable.py
index 19a85d9..ec391b2 100644
--- a/examples/contestable.py
+++ b/examples/contestable.py
@@ -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 ────────────────────────────────────────────────────────────────────
diff --git a/examples/counter.py b/examples/counter.py
index e65f5c6..5aee312 100644
--- a/examples/counter.py
+++ b/examples/counter.py
@@ -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
diff --git a/examples/greeting.py b/examples/greeting.py
index 2712b0d..d379fab 100644
--- a/examples/greeting.py
+++ b/examples/greeting.py
@@ -7,7 +7,7 @@
It is a variation of `visualize_graph.py` from