Skip to content

Commit 93a9a33

Browse files
docs: scrub em dashes, open spec link in a new tab
Two small docs-site cleanups bundled in one PR. Em dashes (—) are a common LLM tell; the README scrub during PR #36 replaced them with commas, colons, semicolons, or sentence splits depending on context. Apply the same pass across every page under docs/. Total: 142 em dashes across 13 files, scrubbed without changing the prose's meaning. ``grep -c "—" docs/**/*.md`` returns zero across the tree. Pattern choice per occurrence: parentheticals (X — Y — Z) become parens or commas; definitions (X — body) become ``X: body``; two-clause sentences (X — Y) become sentence splits or semicolons; appositives become commas. Where the em dash was load- bearing in the prose rhythm, the sentence was rewritten rather than mechanically substituted. Also: the apex "specification" link on the docs landing page now opens in a new tab. attr_list is already enabled in mkdocs.yml, so ``{target="_blank" rel="noopener"}`` after the link does the job inline. uv run mkdocs build --strict clean.
1 parent 9aacc34 commit 93a9a33

13 files changed

Lines changed: 138 additions & 138 deletions

File tree

docs/concepts/checkpointing.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Save state at every node boundary; resume a crashed run from the last
44
saved point on a subsequent `invoke()`. Without a checkpointer, the
5-
engine holds no state across invocations a crash means start-from-entry.
5+
engine holds no state across invocations; a crash means start-from-entry.
66

77
## Wiring a checkpointer
88

@@ -27,7 +27,7 @@ graph = (
2727

2828
The engine writes a record at every `completed` event for outermost-
2929
graph nodes and subgraph-internal nodes. **Fan-out instance internal
30-
events do NOT save** in the shipping version — atomic-restart is the
30+
events do NOT save** in the shipping version. Atomic-restart is the
3131
fan-out contract.
3232

3333
## Saves are synchronous-by-contract
@@ -40,7 +40,7 @@ because the save resolves before the next node runs.
4040

4141
The corollary: slow backends throttle execution. Wrapping a high-
4242
latency persistence layer in a checkpointer makes the whole graph
43-
run at its latency. Plan accordingly async writes inside the
43+
run at its latency. Plan accordingly: async writes inside the
4444
backend (e.g., `asyncio.to_thread` around a sync driver) are fine;
4545
fire-and-forget patterns that return before durability is established
4646
violate the contract.
@@ -94,7 +94,7 @@ Field framing worth getting right:
9494
there.
9595
- **`correlation_id``invocation_id`.** `invocation_id` identifies
9696
*this* graph run uniquely. `correlation_id` is a cross-system
97-
identifier propagated via ContextVar multiple invocations
97+
identifier propagated via ContextVar; multiple invocations
9898
related by a higher-level request can share one `correlation_id`
9999
while each having its own `invocation_id`. See
100100
[Observability](observability.md) for how `correlation_id`
@@ -119,15 +119,15 @@ class Checkpointer(Protocol):
119119
async def delete(self, invocation_id: str) -> None: ...
120120
```
121121

122-
- **`save`** persist the record under `invocation_id`. Durable for
122+
- **`save`**: persist the record under `invocation_id`. Durable for
123123
any backend that documents durability. Synchronous-by-contract per
124124
the section above.
125-
- **`load`** return the *most recent* record for `invocation_id`,
125+
- **`load`**: return the *most recent* record for `invocation_id`,
126126
or `None`. Round-trip-stable with what `save` wrote.
127-
- **`list`** enumerate saved invocations, optionally filtered by
127+
- **`list`**: enumerate saved invocations, optionally filtered by
128128
`CheckpointFilter` (currently a single `correlation_id` field; v1
129129
ships intentionally narrow).
130-
- **`delete`** remove all records for `invocation_id`. No-op if the
130+
- **`delete`**: remove all records for `invocation_id`. No-op if the
131131
invocation has no record (no error).
132132

133133
Backends MUST be safe to share across concurrent invocations; the
@@ -137,10 +137,10 @@ call.
137137

138138
## Two built-in backends
139139

140-
- **`InMemoryCheckpointer`** backed by a dict in process memory.
140+
- **`InMemoryCheckpointer`**: backed by a dict in process memory.
141141
Loses everything on process exit. Useful for tests and short-lived
142142
contexts that want the API surface without disk overhead.
143-
- **`SQLiteCheckpointer`** backed by a SQLite database file.
143+
- **`SQLiteCheckpointer`**: backed by a SQLite database file.
144144
Survives process exit. Reasonable default for any non-trivial use.
145145

146146
Custom backends just implement the four-method Protocol. Targets that
@@ -155,7 +155,7 @@ adapter layer.
155155
cheap; checkpoints are pure overhead.
156156
- **Pipelines whose external side effects can't safely be re-played.**
157157
If node A sends an email, resuming from after A means the email
158-
has already sent fine if your downstream is idempotent, surprising
158+
has already sent; fine if your downstream is idempotent, surprising
159159
if it isn't. Reason explicitly about replay semantics before turning
160160
on resume.
161161

docs/concepts/composition.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ pipeline of reusable sub-pipelines:
77
2. **Subgraphs** encapsulate a sub-pipeline as a single node.
88
3. **Projections** translate state across the subgraph boundary.
99

10-
None of these add new primitives a conditional edge is still one
11-
outgoing edge, a subgraph is still a single node but they change
10+
None of these add new primitives (a conditional edge is still one
11+
outgoing edge, a subgraph is still a single node) but they change
1212
what a graph can express.
1313

1414
## Conditional edges
@@ -52,7 +52,7 @@ it somewhere invisible," state-driven routing gives you:
5252

5353
**Why sync?** Conditional edges are routing decisions, not units of
5454
work. If you want `async def`, the right move is to do the IO in the
55-
producing node and write the decision to a state field exactly what
55+
producing node and write the decision to a state field, exactly what
5656
`classify` does. Keeping edges sync keeps the loop simple to read:
5757
node (async) → merge → edge (sync) → next.
5858

@@ -114,7 +114,7 @@ builder.add_subgraph_node("research", research_subgraph, projection=...)
114114
**Separate state schemas are load-bearing.** The subgraph has its own
115115
`State` subclass, distinct from the parent's. At compile time, the
116116
subgraph's reducer table and field validation are built against its
117-
own schema. Parent fields can't leak in by accident they aren't in
117+
own schema. Parent fields can't leak in by accident; they aren't in
118118
scope on either side of the boundary. **The only way data crosses is
119119
through the projection.**
120120

@@ -153,14 +153,14 @@ If you don't pass a `projection=` argument, you get this. It behaves
153153
asymmetrically:
154154

155155
- **`project_in`: parent state is ignored.** Returns
156-
`subgraph_state_cls()` a fresh instance from the subgraph's
156+
`subgraph_state_cls()`, a fresh instance from the subgraph's
157157
defaults. If the subgraph has a required field, this constructor
158158
fails; the subgraph can't run without an explicit projection.
159159
- **`project_out`: field-name intersection.** Looks at the subgraph's
160160
final state, keeps fields whose names also exist on the parent, and
161161
returns them as a partial update. The parent's reducers then merge.
162162

163-
The asymmetry"closed on the way in, open on the way back" — is by
163+
The asymmetry, "closed on the way in, open on the way back," is by
164164
design. The author opts *in* to sharing data with the subgraph; the
165165
subgraph's observable outputs route back through the parent's reducers
166166
automatically.
@@ -183,17 +183,17 @@ projection = ExplicitMapping[ParentState, SubgraphState](
183183
builder.add_subgraph_node("analyze_a", subgraph, projection=projection)
184184
```
185185

186-
`inputs` and `outputs` are independent pass either, both, or neither.
186+
`inputs` and `outputs` are independent; pass either, both, or neither.
187187

188-
**Asymmetry inputs additive, outputs replacement.** This mirrors the
188+
**Asymmetry: inputs additive, outputs replacement.** This mirrors the
189189
default's asymmetry.
190190

191191
- `inputs` is *additive over no-projection-in*. Subgraph fields named
192192
in `inputs` get the corresponding parent field's value; unnamed
193193
fields get their schema defaults.
194194
- `outputs` *replaces* field-name matching when present. Only pairs
195195
named in `outputs` are merged back. Unnamed subgraph fields are
196-
discarded no slip of extra fields by accident.
196+
discarded, so no slip of extra fields by accident.
197197

198198
**`None` vs `{}` for `outputs`:**
199199

@@ -206,7 +206,7 @@ default's asymmetry.
206206
**Compile-time validation.** `ExplicitMapping.validate` runs at
207207
parent-graph compile and raises `MappingReferencesUndeclaredField` if
208208
any mapping names a field that isn't on the relevant schema.
209-
Refactor-safe if you rename a parent field but forget the mapping,
209+
Refactor-safe: if you rename a parent field but forget the mapping,
210210
construction fails, not runtime.
211211

212212
**The case `ExplicitMapping` uniquely unlocks.** Same subgraph at
@@ -235,13 +235,13 @@ builder.add_subgraph_node(
235235

236236
The two sites address disjoint parent fields, so they cannot collide.
237237
Without explicit mapping, both calls would have to read from and write
238-
to the same parent fields under name matching making "run the same
238+
to the same parent fields under name matching, making "run the same
239239
subgraph twice on different inputs" structurally impossible.
240240

241241
### Custom projection strategies
242242

243-
If you need behavior beyond name-mapping synthesize values, project
244-
conditionally, transform on the way through write a class that
243+
If you need behavior beyond name-mapping (synthesize values, project
244+
conditionally, transform on the way through), write a class that
245245
matches the Protocol:
246246

247247
```python
@@ -268,12 +268,12 @@ A few design points worth sitting with:
268268
- **Unknown fields from `project_out` raise.** Parent's `extra="forbid"`
269269
catches typos at the merge boundary.
270270
- **The `parent_state` argument of `project_out` is for context, not
271-
for writing.** You can read it to decide what to project "only
272-
return the answer if the parent was in a research route" but you
271+
for writing.** You can read it to decide what to project ("only
272+
return the answer if the parent was in a research route") but you
273273
can't mutate it.
274274

275275
`ProjectionStrategy` is a `Protocol`, not a base class. A class fits
276276
the shape or it doesn't; the type checker verifies at use sites. If
277277
you have Java instincts ("where's the `implements` keyword?"), reach
278-
for TypeScript or Go interface instincts instead that's the same
278+
for TypeScript or Go interface instincts instead; that's the same
279279
family.

docs/concepts/fan-out.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ a different input, results merged back deterministically.
66
The "same subgraph at two-or-three call sites" pattern from
77
[`ExplicitMapping`](composition.md#explicitmapping-declarative)
88
handles cases where you know the parent fields up front. Fan-out
9-
handles N call sites where N is determined at runtime "for each
9+
handles N call sites where N is determined at runtime: "for each
1010
item in `state.urls`, run the scraping subgraph; collect the
1111
results."
1212

@@ -15,7 +15,7 @@ results."
1515
A fan-out can dispatch instances driven by a list in state
1616
(`items_field` mode) or by a count resolved from state (`count` mode).
1717

18-
**`items_field` mode** one instance per item in a parent list field:
18+
**`items_field` mode**: one instance per item in a parent list field:
1919

2020
```python
2121
from openarmature.graph import FanOutConfig, FanOutNode
@@ -24,7 +24,7 @@ scrape_all = FanOutNode(
2424
name="scrape_all",
2525
config=FanOutConfig(
2626
subgraph=scrape_subgraph, # CompiledGraph[ScrapeState]
27-
items_field="urls", # parent list field one instance per item
27+
items_field="urls", # parent list field, one instance per item
2828
item_field="url", # subgraph field that receives each item
2929
collect_field="content", # subgraph field whose value is collected
3030
target_field="contents", # parent list field that receives the collection
@@ -36,7 +36,7 @@ scrape_all = FanOutNode(
3636
builder.add_node("scrape_all", scrape_all)
3737
```
3838

39-
**`count` mode** fixed-or-dynamic instance count, no list field:
39+
**`count` mode**: fixed-or-dynamic instance count, no list field:
4040

4141
```python
4242
fan_out = FanOutNode(
@@ -58,7 +58,7 @@ time.
5858

5959
## Per-instance state, inputs and outputs
6060

61-
Each instance gets its own subgraph state distinct from siblings,
61+
Each instance gets its own subgraph state, distinct from siblings,
6262
distinct from the parent. By default the instance receives only:
6363

6464
- the dispatched item in the field named by `item_field` (in
@@ -67,25 +67,25 @@ distinct from the parent. By default the instance receives only:
6767

6868
`inputs` is a `Mapping[subgraph_field, parent_field]`. The subgraph
6969
fields not named in `inputs` (and not `item_field`) take their
70-
schema defaults same closed-by-default-on-the-way-in posture as
70+
schema defaults; same closed-by-default-on-the-way-in posture as
7171
the explicit-projection story for ordinary subgraphs.
7272

7373
On exit, each instance's `collect_field` value becomes one element
7474
of the parent's `target_field` list, in instance-index order. To
7575
collect additional per-instance fields, declare
76-
`extra_outputs: Mapping[parent_field, subgraph_field]` each becomes
76+
`extra_outputs: Mapping[parent_field, subgraph_field]`; each becomes
7777
its own parent list of the same length, instance-index-aligned.
7878

7979
## Error policy
8080

8181
Two values:
8282

83-
- **`"fail_fast"`** (default) the first instance failure cancels
83+
- **`"fail_fast"`** (default): the first instance failure cancels
8484
the in-flight siblings (`asyncio.gather` semantics) and propagates
8585
as a `NodeException` wrapping the failing instance's cause, with
8686
`recoverable_state` set to the parent's pre-fan-out snapshot. Use
8787
this when one bad result invalidates the rest.
88-
- **`"collect"`** instance failures are captured; the fan-out runs
88+
- **`"collect"`**: instance failures are captured; the fan-out runs
8989
to completion. Failed instances contribute nothing to
9090
`target_field`. If you declare `errors_field` on the config, each
9191
failed instance produces a record (`{"fan_out_index": str(idx),
@@ -98,11 +98,11 @@ Choose by whether partial results are useful.
9898
After the fan-out completes, the parent receives a partial update
9999
containing:
100100

101-
- `target_field` list of `collect_field` values, instance-index order.
102-
- Each parent name in `extra_outputs` list of values from the named
101+
- `target_field`: list of `collect_field` values, instance-index order.
102+
- Each parent name in `extra_outputs`: list of values from the named
103103
subgraph field, instance-index order.
104-
- `count_field` (if configured) the instance count.
105-
- `errors_field` (if configured, `"collect"` policy only) per-instance
104+
- `count_field` (if configured): the instance count.
105+
- `errors_field` (if configured, `"collect"` policy only): per-instance
106106
error records.
107107
- `on_empty="noop"` for an empty items_field → all the above with empty
108108
lists; `count_field` set to 0.
@@ -112,9 +112,9 @@ containing:
112112
If `items_field` is set and the parent list is empty (or `count`
113113
resolves to 0):
114114

115-
- `on_empty="raise"` (default) raises `FanOutEmpty` (a runtime
115+
- `on_empty="raise"` (default): raises `FanOutEmpty` (a runtime
116116
error category).
117-
- `on_empty="noop"` emits an empty partial (no instances dispatched,
117+
- `on_empty="noop"`: emits an empty partial (no instances dispatched,
118118
no errors).
119119

120120
## Observability per instance
@@ -124,7 +124,7 @@ The fan-out node's own `started` / `completed` events carry a
124124
`item_count` / `concurrency` / `error_policy` / `parent_node_name`.
125125

126126
Per-instance events have `fan_out_index = N` (0-based) and a
127-
namespace whose final element is the fan-out node's name instances
127+
namespace whose final element is the fan-out node's name; instances
128128
do NOT contribute a separate synthetic namespace element. Backends
129129
disambiguate per-instance spans using `fan_out_index` alongside the
130130
namespace.
@@ -133,7 +133,7 @@ namespace.
133133

134134
A fan-out node's `completed` event triggers a save like any other
135135
outermost-graph or subgraph-internal node. **Per-instance internal
136-
events do NOT save** in the shipping version on resume, the
136+
events do NOT save** in the shipping version; on resume, the
137137
fan-out re-runs end-to-end if it hadn't completed (atomic restart).
138138

139139
A per-instance fan-out resume mode is planned but not yet shipped.
@@ -147,7 +147,7 @@ The signal: N similar pieces of work, N depends on state at runtime
147147
(not at build time), the work is independent enough to run
148148
concurrently. If N is known at build time and small (≤3),
149149
`ExplicitMapping` at multiple subgraph sites is simpler. If the
150-
work isn't independent instance 2 needs instance 1's output
150+
work isn't independent (instance 2 needs instance 1's output),
151151
that's a linear pipeline, not fan-out.
152152

153153
## What fan-out is NOT

0 commit comments

Comments
 (0)