Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
29eb134
feat: deliver the round-7 asks
claude Jul 7, 2026
8940a7e
docs: plan forest example (Matz round 8)
claude Jul 7, 2026
f7b7fa5
docs: hill chart example (DHH round 8)
claude Jul 7, 2026
ae2bac8
docs: variance detective example (tenderlove round 8)
claude Jul 7, 2026
d75bb46
docs: three-way plan merge example (fxn round 8)
claude Jul 7, 2026
a592140
docs: adaptive throttle example (ioquatix round 8)
claude Jul 7, 2026
25e06da
docs: journal audit example (Jeremy Evans round 8)
claude Jul 7, 2026
93d94cf
docs: contract semver advisor example (solnic round 8)
claude Jul 7, 2026
f955b7e
docs: dead letter office example (mperham round 8)
claude Jul 7, 2026
ccbafa1
docs: graph-to-specs generator example (sandimetz round 8)
claude Jul 7, 2026
afabbf5
docs: pluggable eval scorers example (ankane round 8)
claude Jul 7, 2026
46ccf60
docs: round 8 index and findings
claude Jul 7, 2026
ddeb24a
feat: deliver the round-8 asks
claude Jul 7, 2026
737cece
docs: failure weather report example (matz round 9)
claude Jul 7, 2026
79df3e7
docs: traffic dial rollout example (dhh round 9)
claude Jul 7, 2026
34d4436
docs: throughput knee sweep example (tenderlove round 9)
claude Jul 7, 2026
e82026a
docs: graph invariants prover example (fxn round 9)
claude Jul 7, 2026
05499b6
docs: fair share example (ioquatix round 9)
claude Jul 7, 2026
644aa62
docs: resize torture test example (jeremyevans round 9)
claude Jul 7, 2026
f1ccce4
docs: contract fixtures generator example (solnic round 9)
claude Jul 7, 2026
1658257
docs: circuit breaker example (mperham round 9)
claude Jul 7, 2026
b612c78
docs: duck agents example (sandimetz round 9)
claude Jul 7, 2026
0dc6423
docs: implementation shootout example (ankane round 9)
claude Jul 7, 2026
6abffb0
docs: round 9 index and findings
claude Jul 7, 2026
34e6d66
feat: deliver the round-9 asks
claude Jul 7, 2026
0414da5
docs: polite form example (matz round 10)
claude Jul 7, 2026
ffd83e1
docs: one-file API example (dhh round 10)
claude Jul 7, 2026
5fc0ab5
docs: contract overhead bench example (tenderlove round 10)
claude Jul 7, 2026
5cec72f
docs: projection agreement prover example (fxn round 10)
claude Jul 7, 2026
ed9d928
docs: cancel drill example (ioquatix round 10)
claude Jul 7, 2026
9bc8c74
docs: relation prober example (jeremyevans round 10)
claude Jul 7, 2026
c2ee998
docs: relation diff example (solnic round 10)
claude Jul 7, 2026
d137694
docs: retry budget example (mperham round 10)
claude Jul 7, 2026
35c99f5
docs: rule shapes example (sandimetz round 10)
claude Jul 7, 2026
eef7646
docs: batch import example (ankane round 10)
claude Jul 7, 2026
b0aad11
docs: round 10 index and findings
claude Jul 7, 2026
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
150 changes: 150 additions & 0 deletions docs/perspectives/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,156 @@ followed:
expressible rules, and an eval-scorer seam for LLM-backed
capabilities.

## Round 8 — structure becomes vocabulary

The round-7 asks shipped as a release (`stats[:roots]`/`stats[:leaves]`,
journal `duration_samples` with `duration_percentile(desc, pct, last:)`,
and `x-agentic-rules` emission in `to_json_schema`), and ten more
experiments followed:

| # | Persona | Built on the round-8 release | Run it | Field notes |
|---|---------|------------------------------|--------|-------------|
| 1 | Matz | Plan forest — the graph drawn as trees, depth as altitude | `examples/plan_forest.rb` | [round-8/01-matz.md](round-8/01-matz.md) |
| 2 | DHH | Hill chart — where the work *is*, from lifecycle hooks | `examples/hill_chart.rb` | [round-8/02-dhh.md](round-8/02-dhh.md) |
| 3 | Aaron Patterson | Variance detective — flaky vs slow, settled by percentiles | `examples/variance_detective.rb` | [round-8/03-tenderlove.md](round-8/03-tenderlove.md) |
| 4 | Xavier Noria | Plan merge — three-way merge with conflicts at seam altitude | `examples/plan_merge.rb` | [round-8/04-fxn.md](round-8/04-fxn.md) |
| 5 | Samuel Williams | Adaptive throttle — AIMD finds the capacity nobody documented | `examples/adaptive_throttle.rb` | [round-8/05-ioquatix.md](round-8/05-ioquatix.md) |
| 6 | Jeremy Evans | Journal audit — five invariants; a tampered journal named precisely | `examples/journal_audit.rb` | [round-8/06-jeremyevans.md](round-8/06-jeremyevans.md) |
| 7 | Piotr Solnica | Contract semver — breaking-or-compatible computed, bump advised | `examples/contract_semver.rb` | [round-8/07-solnic.md](round-8/07-solnic.md) |
| 8 | Mike Perham | Dead letter office — requeue, parked, recovered, by last word | `examples/dead_letter_office.rb` | [round-8/08-mperham.md](round-8/08-mperham.md) |
| 9 | Sandi Metz | Graph to specs — structural roles dictate the test plan | `examples/graph_to_specs.rb` | [round-8/09-sandimetz.md](round-8/09-sandimetz.md) |
| 10 | Andrew Kane | Eval scorers — four ways to say "good enough", one seam | `examples/eval_scorers.rb` | [round-8/10-ankane.md](round-8/10-ankane.md) |

### What round 8 surfaced

1. **Structure became vocabulary**: `roots`/`leaves`/`depth` landed and
were immediately spent three ways — a drawing (forest), a test plan
(graph-to-specs), and merge conflicts named at seam altitude. Metadata
that keeps buying unplanned tools is metadata shaped right.
2. **Durations became distributions**: `duration_samples` turned point
readings into percentiles, and two tools acted on them — the variance
detective separates flaky from slow (p90/p50 ratio), and the adaptive
throttle steers concurrency by p50 drift instead of vibes.
3. **Signal-to-noise as a design goal**: the dead letter office triages
by *most recent* attempt (no paging for ghosts), and the eval scorers
flag exactly one real failure where exact-match flags two. Both argue
the same point: a report is only as good as what its failures mean.
4. **The declarations' blind spot held**: Piotr's semver advisor and
Jeremy's audit both stop at callable rules — predicates stay opaque
to every static tool. Structured rules narrow the gap; they don't
close it.
5. **Next asks**: `RateLimit#resize(n)` so the adaptive throttle can
steer the real limiter instead of simulating one, and journaling
`retryable:` at write time from `failure.retryable?` so triage
survives taxonomy renames. (`eval_scorers.rb` joins the
exit-1-by-design set.)

## Round 9 — the operations round

The round-8 asks shipped as a release (`RateLimit#resize(n)` — live
ceiling changes, growing wakes waiters, shrinking drains — and the
journal recording `retryable:` on `task_failed` at write time from the
failure's own verdict), the two examples that asked were modernized
onto them, and ten more experiments followed:

| # | Persona | Built on the round-9 release | Run it | Field notes |
|---|---------|------------------------------|--------|-------------|
| 1 | Matz | Failure weather — retryable is weather, non-retryable is climate | `examples/failure_weather.rb` | [round-9/01-matz.md](round-9/01-matz.md) |
| 2 | DHH | Traffic dial — a canary rollout as one resized limiter | `examples/traffic_dial.rb` | [round-9/02-dhh.md](round-9/02-dhh.md) |
| 3 | Aaron Patterson | Throughput knee — the ceiling sweep with two honest clocks | `examples/throughput_knee.rb` | [round-9/03-tenderlove.md](round-9/03-tenderlove.md) |
| 4 | Xavier Noria | Graph invariants — seven promises of the reflection API, proved | `examples/graph_invariants.rb` | [round-9/04-fxn.md](round-9/04-fxn.md) |
| 5 | Samuel Williams | Fair share — tenant-fairness composed, shares rebalanced live | `examples/fair_share.rb` | [round-9/05-ioquatix.md](round-9/05-ioquatix.md) |
| 6 | Jeremy Evans | Resize torture — shrink drains, grow wakes, ceilings bind | `examples/resize_torture.rb` | [round-9/06-jeremyevans.md](round-9/06-jeremyevans.md) |
| 7 | Piotr Solnica | Contract fixtures — examples derived from declarations, proved | `examples/contract_fixtures.rb` | [round-9/07-solnic.md](round-9/07-solnic.md) |
| 8 | Mike Perham | Circuit breaker — three strikes for 503s, one for a revoked key | `examples/circuit_breaker.rb` | [round-9/08-mperham.md](round-9/08-mperham.md) |
| 9 | Sandi Metz | Duck agents — five shapes through one seam, one tiny decorator | `examples/duck_agents.rb` | [round-9/09-sandimetz.md](round-9/09-sandimetz.md) |
| 10 | Andrew Kane | Impl shootout — accuracy AND latency on one table | `examples/impl_shootout.rb` | [round-9/10-ankane.md](round-9/10-ankane.md) |

### What round 9 surfaced

1. **resize turned limits into policy objects**: five tools steer one
live limiter — the rollout dial, the ceiling sweep, tenant share
rebalancing, the torture certificate, and the modernized AIMD
throttle. The topology of a limiter graph stays fixed; only the
numbers move at runtime, which is the property that makes it safe.
2. **The write-time verdict became a decision input**: the weather
report (wait vs dig a well), the circuit breaker (three strikes vs
instant trip), and the modernized dead letter office all *act* on
`retryable:` instead of reconstructing it — the error's testimony,
recorded when fresh, drives policy later.
3. **The tools kept correcting their authors** (fifth consecutive
round): Samuel's one-worker tenant couldn't starve, Aaron's
"throughput goes flat" was actually a fall, Xavier's depth
invariant was ill-posed on cycles, Jeremy's harness read its clock
before setting it, Mike's breaker read the wrong journal event, and
Kane's challenger lost a case to a missing stem. Every one was
caught by the example's own output before a user saw it.
4. **Fairness needs unmet demand to be visible**: a FIFO door is fair
to requests, not tenants — starvation only appears when a tenant's
demand exceeds its receipts, which is why quiet outages stay quiet.
5. **Next asks**: relation-typed structured rules (`sum_lte:`,
`requires:`, `mutually_exclusive:`) so generators can satisfy and
advisors can diff the declarable majority of cross-field rules
(Piotr); and a breaker-friendly convention for `retryable: nil` —
"no opinion" should mean retry-with-suspicion, not hopeless (Mike).

## Round 10 — predicates become data

The round-9 asks shipped as a release (`Agentic::RelationRules` —
`sum_lte`/`requires`/`mutually_exclusive` declared as data, enforced
by the validator with derived messages, projected into real draft-07
keywords, and carried in `x-agentic-rules`; plus the retryable-nil
convention on `TaskFailure`: `hopeless?` / `possibly_transient?`),
the two asking examples were modernized, and ten more experiments
followed:

| # | Persona | Built on the round-10 release | Run it | Field notes |
|---|---------|-------------------------------|--------|-------------|
| 1 | Matz | Polite form — every declaration read aloud as a question | `examples/polite_form.rb` | [round-10/01-matz.md](round-10/01-matz.md) |
| 2 | DHH | One-file API — schema, 422s, and 201s derived from one declaration | `examples/one_file_api.rb` | [round-10/02-dhh.md](round-10/02-dhh.md) |
| 3 | Aaron Patterson | Contract overhead — validation priced against the call it guards | `examples/contract_overhead.rb` | [round-10/03-tenderlove.md](round-10/03-tenderlove.md) |
| 4 | Xavier Noria | Projection agreement — both renderings of the law, proved; the nil frontier mapped | `examples/projection_agreement.rb` | [round-10/04-fxn.md](round-10/04-fxn.md) |
| 5 | Samuel Williams | Cancel drill — task cancel is prompt; plan cancel bills you anyway | `examples/cancel_drill.rb` | [round-10/05-ioquatix.md](round-10/05-ioquatix.md) |
| 6 | Jeremy Evans | Relation prober — 13 probes pass; one step off the road draws blood | `examples/relation_prober.rb` | [round-10/06-jeremyevans.md](round-10/06-jeremyevans.md) |
| 7 | Piotr Solnica | Relation diff — the rules join semver; opacity becomes opt-in | `examples/relation_diff.rb` | [round-10/07-solnic.md](round-10/07-solnic.md) |
| 8 | Mike Perham | Retry budget — one fleet-wide wallet; 45 doomed calls become 17 | `examples/retry_budget.rb` | [round-10/08-mperham.md](round-10/08-mperham.md) |
| 9 | Sandi Metz | Rule shapes — one policy, three representations, four consumers | `examples/rule_shapes.rb` | [round-10/09-sandimetz.md](round-10/09-sandimetz.md) |
| 10 | Andrew Kane | Batch import — a reject file with line, field, and rule, at 162us/row | `examples/batch_import.rb` | [round-10/10-ankane.md](round-10/10-ankane.md) |

### What round 10 surfaced

1. **Predicates as data compounded immediately**: within one round,
relation rules were asked as questions (polite form), served as
draft-07 keywords (one-file API), satisfied by the generator,
diffed for semver, audited for consumer count, and used to explain
118 CSV rejections. Six consumers for a feature shipped that
morning — the strongest version yet of "metadata keeps buying
unplanned tools."
2. **Two real defects found by drills**: `cancel_plan` under a joined
reactor is bookkeeping-only — every agent runs and bills while the
status says canceled (Samuel); and a relation rule over an
undeclared field escapes as raw TypeError instead of
ValidationError, turning 422 paths into 500 paths (Jeremy, whose
prober exits 1 by design as the acceptance test).
3. **Agreement-for-different-reasons is a named hazard**: typed
fields guard the nil frontier so both renderings reject
`{express: nil}` — for unrelated reasons; relax the type and the
renderings diverge. Verdict-only tests would call that a pass
(Xavier).
4. **The meter settled the validation debate**: the largest contract
costs 0.14ms against the 800ms call it guards, and rejection
costs 12x the happy path — both numbers now on the table (Aaron,
Kane concurring at 162us/row with rejects included).
5. **Next asks**: make `cancel_plan` stop the scheduler and in-flight
fibers (the cancel drill is the acceptance test); relation rules
must type-check their fields at declaration time or wrap
evaluation failures in ValidationError (the relation prober is
the acceptance test); `RateLimit#try_acquire` for non-blocking
admission so retry budgets can be RateLimits; and align or
document presence semantics (Ruby nil vs JSON null) across the
projection boundary. (`relation_prober.rb` joins the
exit-1-by-design set.)

### What round 6 surfaced

1. **Plans became artifacts**: narratable (tour), serializable with an
Expand Down
61 changes: 61 additions & 0 deletions docs/perspectives/round-10/01-matz.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Round 10 field notes — Matz asks before it's an error

*Built: `examples/polite_form.rb` — a form assistant that turns the
contract's declarations into questions: requireds become requests,
bounds become gentle corrections, relations become follow-ups.*

## What I built and why

An error message is just a question you asked too late. A 422 that
says "express requires customs_code" contains a perfectly good
question — *"since you chose express, may I have your customs
code?"* — wearing armor. I wanted to take the armor off:

```
assistant: may I have your mode? (air, sea, road)
assistant: ah - weight must be less than or equal to 5000. shall we adjust it?
assistant: together weight and volume come to 7000, and 6000 is our
limit - could we lower the volume?
assistant: since you chose express, I'll also need your customs_code
assistant: you've given me api_key and oauth_token - I only need one;
which shall we keep?
```

Six questions, zero errors shown, and *nothing was written twice*:
every line of the conversation is a declaration read aloud in a
kinder register. `required:` became a request, `max:` a correction,
and — this is the part only possible since this morning — the three
relation rules each became their natural follow-up. `requires` is
"then I'll also need"; `sum_lte` is "could we lower it"; and
`mutually_exclusive` is "which shall we keep?"

## Why relations made this possible

Last round the generator could *satisfy* relations silently; this
round the assistant can *discuss* them, and the difference is the
same one: the predicate is data now. A lambda rule could only ever
say pass or fail — you cannot ask a lambda which field to lower, or
what the limit is, or which two things conflict. The relation
declaration carries all three, so the assistant reads the `fields:`,
the `limit:`, and the relation's own shape, and phrases the question
a human clerk would ask. Omotenashi is anticipating the need before
the failure; it turns out anticipation is a data-model feature.

## Notes

- The engine is a ten-line loop: validate, catch, convert the first
violation to a question, repeat. Fixed-point politeness. I enjoyed
that convergence is guaranteed by the same property that makes the
validator honest — every question, answered, strictly shrinks the
violation set.
- The `mutually_exclusive` case is the only one where the assistant
*removes* something rather than requesting it, and it asks
permission first. Deleting a user's input without asking is the
form equivalent of clearing their cart.

## Verdict

The contract now has two voices — the strict one for machines (422s,
schemas) and this one for people — and both read from the same
declarations, so they can never disagree. Kindness that stays
synchronized with correctness: that is my favorite kind of feature.
60 changes: 60 additions & 0 deletions docs/perspectives/round-10/02-dhh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Round 10 field notes — DHH ships the API that isn't there

*Built: `examples/one_file_api.rb` — a complete endpoint derived from
one capability declaration: schema endpoint, 422s with relation
rules explained, output-guarded 201s.*

## What I built and why

Look at a typical API codebase and count the artifacts per endpoint:
a controller, a params validator, a serializer, an OpenAPI YAML that
disagrees with all three, and a test file whose main job is keeping
the other four honest. Five files, one idea. The idea is: *a quote
request has a mode, a weight, and some rules.*

So say that once, and derive the rest:

```
GET /quotes/schema -> 200 (draft-07, 687 bytes)
POST {"mode":"teleport","weight":9000} -> 422 field errors
POST {"weight":4000,"volume":3000} -> 422 "weight + volume
must total at most 6000"
POST {"express":true} -> 422 "express requires
customs_code"
POST (all in order) -> 201 {"price_cents":1800}
```

The app — the actual business — is four lines (`create_quote`). The
"API layer" is a case statement that *reads the declaration*: the
422 renderer never mentions a field name, the schema endpoint is one
method call, and `validate_outputs!` guards the response door too,
so the endpoint can't quietly ship a malformed 201 when someone
refactors the pricing.

## Relations flow to both doors

The round-10 payoff is that the cross-field laws now reach both
audiences without being written twice. The human at the terminal
gets "express requires customs_code" in the 422 — a sentence,
derived. The client generator gets `"dependencies": {"express":
["customs_code"]}` in the schema — draft-07 a stock validator
enforces client-side, before the request is even sent. Same law,
two renderings, one source. That used to require a platform team
with a style guide; now it's a property of the data model.

## Notes

- My first 422 printed each rule violation twice — once flattened
into the `base` field errors, once structured. The renderer now
excludes `base` and keeps the structured form, because a client
that can point at `fields: ["weight", "volume"]` should never have
to parse prose to find out where to put the red border.
- I kept `additionalProperties: true` on display in the schema
rather than hiding unknown-key tolerance. Postel was right and
your API clients are sloppy; design for it in the open.

## Verdict

One declaration, three doors: docs, rejection, and response — all
derived, none drifting. The best code in your app is the code that
isn't there, and this endpoint is mostly made of it.
Loading
Loading