Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f6ba188
feat: deliver the round-10 asks
claude Jul 7, 2026
17d92d0
docs: allocation audit example (ko1 round 11)
claude Jul 7, 2026
faa37d4
docs: threads drill example (headius round 11)
claude Jul 7, 2026
6c037b2
docs: capacity planner example (nateberkopec round 11)
claude Jul 7, 2026
b05d47b
docs: plan flog example (zenspider round 11)
claude Jul 7, 2026
051a562
docs: confident pipeline example (avdi round 11)
claude Jul 7, 2026
f56c2ac
docs: plan kata example (kytrinyx round 11)
claude Jul 7, 2026
d920355
docs: contract cop example (bbatsov round 11)
claude Jul 7, 2026
6bdc4f2
docs: telemetry bus example (josevalim round 11)
claude Jul 7, 2026
743d25e
docs: ports and adapters example (jodosha round 11)
claude Jul 7, 2026
3f9a4d5
docs: tenant shards example (eileencodes round 11)
claude Jul 7, 2026
6f984d4
docs: round 11 index and findings
claude Jul 7, 2026
b4df517
feat: deliver the round-11 asks
claude Jul 7, 2026
c1fd6aa
docs: api surface census example (wycats round 12)
claude Jul 7, 2026
bb92005
docs: onboarding trail example (sarahmei round 12)
claude Jul 7, 2026
49bac3c
docs: require cost report example (schneems round 12)
claude Jul 7, 2026
29805ef
docs: event prof example (palkan round 12)
claude Jul 7, 2026
5a09ad7
docs: hostile inputs probe example (flavorjones round 12)
claude Jul 7, 2026
3f810de
docs: honest doubles example (searls round 12)
claude Jul 7, 2026
ede203a
docs: plan DSL example (rkh round 12)
claude Jul 7, 2026
b6f83a9
docs: self-correcting output example (obie round 12)
claude Jul 7, 2026
6cf2f47
docs: gentle deprecations example (rafaelfranca round 12)
claude Jul 7, 2026
f243a7a
docs: write path profile example (byroot round 12)
claude Jul 7, 2026
995074b
docs: round 12 index and findings
claude Jul 7, 2026
c3a635e
fix: bring RateLimit's in-flight counters under the window mutex
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
104 changes: 104 additions & 0 deletions docs/perspectives/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,110 @@ followed:
projection boundary. (`relation_prober.rb` joins the
exit-1-by-design set.)

## Round 11 — a new cast takes the bench

The round-10 asks shipped as a release: `cancel_plan` is now prompt
(bookkeeping first, then fiber stops - never the reactor handle, never
the calling fiber), relation declarations fail fast at validator
construction (undeclared fields and sum_lte-over-strings refuse to
boot), `RateLimit#try_acquire` gives non-blocking admission, and
relations over untyped fields stay out of draft-07 keywords. The two
exit-1-by-design probers flipped to green acceptance tests, the retry
budget's wallet became a real windowed RateLimit — and then **ten new
prolific Rubyists** took over the experiments:

| # | Persona | Built with the gem | Run it | Field notes |
|---|---------|--------------------|--------|-------------|
| 1 | Koichi Sasada (ko1) | Allocation audit — exact object counts per operation, via GC.stat | `examples/allocation_audit.rb` | [round-11/01-ko1.md](round-11/01-ko1.md) |
| 2 | Charles Nutter (headius) | Threads drill — real parallelism vs everything shared; a load-order bug caught | `examples/threads_drill.rb` | [round-11/02-headius.md](round-11/02-headius.md) |
| 3 | Nate Berkopec | Capacity planner — Little's Law over journal percentiles | `examples/capacity_planner.rb` | [round-11/03-nateberkopec.md](round-11/03-nateberkopec.md) |
| 4 | Ryan Davis (zenspider) | Plan flog — a pain score per plan; idiom free, coupling priced | `examples/plan_flog.rb` | [round-11/04-zenspider.md](round-11/04-zenspider.md) |
| 5 | Avdi Grimm | Confident pipeline — ten timid conditionals vs one barricade | `examples/confident_pipeline.rb` | [round-11/05-avdi.md](round-11/05-avdi.md) |
| 6 | Katrina Owen | Plan kata — red/green/refactor with graph assertions written first | `examples/plan_kata.rb` | [round-11/06-kytrinyx.md](round-11/06-kytrinyx.md) |
| 7 | Bozhidar Batsov | Contract cop — seven named cops, mechanical autocorrect only | `examples/contract_cop.rb` | [round-11/07-bbatsov.md](round-11/07-bbatsov.md) |
| 8 | José Valim | Telemetry bus — :telemetry on the hooks; runtime attach/detach, crash isolation | `examples/telemetry_bus.rb` | [round-11/08-josevalim.md](round-11/08-josevalim.md) |
| 9 | Luca Guidi | Ports and adapters — the domain survives the migration, with a purity scan | `examples/ports_and_adapters.rb` | [round-11/09-jodosha.md](round-11/09-jodosha.md) |
| 10 | Eileen Uchitelle | Tenant shards — N journals, N limits, one ignorant control plane | `examples/tenant_shards.rb` | [round-11/10-eileencodes.md](round-11/10-eileencodes.md) |

### What round 11 surfaced

1. **Fresh eyes found a fresh class of bug immediately**: headius's
bare-journal drill caught `Time#iso8601` used without requiring
"time" — a load-order bug nine rounds of fiber-world examples never
tripped, fixed in this round's release. New perspectives audit
different assumptions.
2. **The runtime got audited from below**: exact allocation counts
(37 objects per happy validation, 11x on rejection, zero GC runs
per plan), real-thread verdicts (journal and registry hold on real
locks; the windowed limiter coasts on the GVL), and Little's Law
turning the journal into a capacity plan whose binding constraint
was outside the meeting.
3. **The teaching seat is real**: the kata (assertions before tasks),
the flog score (calibrated so idiom is free), the confident/timid
contrast (nil-tolerance launders errors), and the cop (autocorrect
only what has one right answer) are all *pedagogy tools* built on
the same reflection surfaces the ops tools use.
4. **The architecture seat approves the seams**: the duck-typed
`agent:` seam let a pure domain walk in without signing tenancy
(ports-and-adapters with a purity scan), and ten lines bridged the
hooks to a :telemetry-style bus with crash isolation - frameworks
orchestrate, domains decide, handlers come and go.
5. **Next asks**: a Mutex around the windowed limiter's stamp
bookkeeping so the answer is the same on every Ruby (headius);
`remove_task`/rewire so refactoring a plan doesn't mean demolition
(Katrina, with the kata as acceptance test); and a multiprocess
journal drill to certify the flock claim (headius, follow-up).

## Round 12 — a third cast, and the bill arrives itemized

The round-11 asks shipped as a release: the windowed limiter's stamp
bookkeeping holds a real Mutex (the threads drill now certifies
instead of observes), `remove_task`/`rewire_task` make plan
refactoring surgical (the kata refactors in place), and the process
drill certifies the flock claim across four forked writers. Then a
third cast of ten prolific Rubyists took the bench:

| # | Persona | Built with the gem | Run it | Field notes |
|---|---------|--------------------|--------|-------------|
| 1 | Yehuda Katz (wycats) | API surface census — 112 methods, 58 earning rent, 54 on loan | `examples/api_surface.rb` | [round-12/01-wycats.md](round-12/01-wycats.md) |
| 2 | Sarah Mei | Onboarding trail — a house tour computed from who-mentions-whom | `examples/onboarding_trail.rb` | [round-12/02-sarahmei.md](round-12/02-sarahmei.md) |
| 3 | Richard Schneeman | Require cost report — the bill lands at first touch, not at require | `examples/require_cost.rb` | [round-12/03-schneems.md](round-12/03-schneems.md) |
| 4 | Vladimir Dementyev | EventProf for plans — task-seconds by tag; llm owns 78% | `examples/event_prof.rb` | [round-12/04-palkan.md](round-12/04-palkan.md) |
| 5 | Mike Dalessio | Hostile inputs — a torn tail denies ALL recovery; exit 1 until | `examples/hostile_inputs.rb` | [round-12/05-flavorjones.md](round-12/05-flavorjones.md) |
| 6 | Justin Searls | Honest doubles — fakes show their papers at load time | `examples/honest_doubles.rb` | [round-12/06-searls.md](round-12/06-searls.md) |
| 7 | Konstantin Haase | Plan DSL — thirty lines of Sinatra over the public API | `examples/plan_dsl.rb` | [round-12/07-rkh.md](round-12/07-rkh.md) |
| 8 | Obie Fernandez | Self-correcting output — violations become the correction prompt | `examples/self_correcting_output.rb` | [round-12/08-obie.md](round-12/08-obie.md) |
| 9 | Rafael França | Gentle deprecations — warn once per site, tally, strict on schedule | `examples/gentle_deprecations.rb` | [round-12/09-rafaelfranca.md](round-12/09-rafaelfranca.md) |
| 10 | Jean Boussier (byroot) | Write path profile — JSON acquitted at 0.4%; the fsync IS the product | `examples/write_path_profile.rb` | [round-12/10-byroot.md](round-12/10-byroot.md) |

### What round 12 surfaced

1. **The stewardship seat spoke**: the census split 112 public
methods into earned API and accidental API; the deprecation shim
choreographed a rename across three releases; the DSL proved sugar
can stay entirely outside the engine. Three different answers to
"how does this gem grow old gracefully."
2. **One real defect, found where parsers meet reality**: a torn
journal tail — the exact artifact of the crash the journal exists
for — crashes replay in the wrong error class and denies all
recovery (`hostile_inputs.rb` exits 1 as the acceptance test).
3. **Costs arrived itemized**: require costs land at first constant
touch, not at require (Zeitwerk's deferral, priced); the journal
write is 99.6% fsync and 0.4% JSON, with group commit named as a
different promise rather than a faster one; plan time profiles by
tag with the barriers indicted, not the budget.
4. **The AI-application seat matured**: the self-correcting output
loop showed the contract doubling as a correction-prompt
generator, and honest doubles put load-time verification at the
LLM boundary — patterns, not vibes.
5. **Next asks**: tolerant journal replay — salvage whole lines,
report (don't raise on) a torn or mis-encoded tail
(`hostile_inputs.rb` is the acceptance test); an optional
`fsync_every: n` group-commit mode with its durability trade
named in the constructor (byroot); and consider a strict-shapes
replay mode for audit tools vs the tolerant recovery default
(flavorjones).

### What round 6 surfaced

1. **Plans became artifacts**: narratable (tour), serializable with an
Expand Down
69 changes: 69 additions & 0 deletions docs/perspectives/round-11/01-ko1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Round 11 field notes — Koichi Sasada audits the promissory notes

*Built: `examples/allocation_audit.rb` — exact per-operation object
counts via `GC.stat(:total_allocated_objects)`, plus where the GC
actually fires during a plan.*

## What I built and why

I think about programs the way the VM sees them: not as logic, but
as a stream of allocations arriving at the GC's door. Every object
is a promissory note — cheap to write, collected on schedule, and
the schedule does not consult your latency SLO. So my first question
for any framework is not "is it fast?" but "what does it *allocate*,
per operation, exactly?" Not sampled. Counted:

```
Task.new 18 objects
validator: happy path 37 objects
validator: rejection 440 objects
graph snapshot, isolated 96 objects
to_json_schema 29 objects
full 10-task plan 1661 objects, 0 GC runs
```

`total_allocated_objects` is a monotonic counter the VM maintains
anyway — the audit is free of observer distortion, which sampling
profilers never quite are. Warm-up matters: the first validation
compiles a dry-schema; charging that to the per-call price would be
accounting fraud.

## Reading the numbers like a VM person

- **37 objects per happy validation** is the number to multiply by
your request rate. At 1000 rps that's 37,000 notes a second, a
couple of young-gen pages — fine, *and now known to be fine*,
which is different from assumed.
- **Rejection costs 11x the happy path.** Exceptions carry
backtraces, messages, violation hashes. Error paths are allocation
paths — the classic mistake is capacity-planning the happy path
and then meeting a hostile client who makes you allocate 440
objects per garbage request.
- **The graph snapshot's 96 objects are a purchase, not a leak.**
That's the dup+freeze immutability every graph tool since round 5
leans on. When you see allocation in a profile, ask what it
*bought* before you delete it — half of "optimization" is
accidentally refunding a purchase the design depended on.
- **Zero GC runs in a 10-task plan** means plan overhead lives
comfortably inside a young generation. The framework will never be
why your agent process pauses; the 800ms network calls will hide
every pause the VM takes anyway.

## Notes

- I subtracted the orchestrator-construction cost from the graph row
by measuring the build alone. Attribution is the whole game in
allocation work; a hot method blamed for its callee's allocations
sends someone optimizing the wrong function for a week.
- What I'd measure next (round-12 thought, not quite an ask): object
*lifetimes*, not just counts — journal entries are written and
dropped (good, dies young), but `@results` accumulates for the
plan's life (fine for 10 tasks, interesting at 10,000).

## Verdict

The framework's allocation profile is boring, and boring is the
correct aesthetic for infrastructure: small numbers on hot paths,
honest purchases where immutability is sold, error paths priced at
11x and now labeled as such. Allocation isn't evil; unbudgeted
allocation is. There's a budget now.
72 changes: 72 additions & 0 deletions docs/perspectives/round-11/02-headius.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Round 11 field notes — Charles Nutter brings real threads

*Built: `examples/threads_drill.rb` — the journal, the registry, and
the windowed limiter hammered by actual Ruby threads, judged by the
standard of a VM with no GVL to hide behind.*

## What I built and why

Fibers are polite: they yield when asked and never interrupt a
two-step dance. Threads are not, and on JRuby they run *actually in
parallel* — every "works fine in production" claim earned under
MRI's GVL gets re-examined on my VM, usually at 2am. So I drill
everything this gem calls shared:

```
journal, 8 threads x 150 events: 1200/1200 lines, 0 torn
registry, 8 threads x 50 each: 0 registrations lost
windowed try_acquire, ceiling 50: admitted 50/50 (observed)
```

The journal and the registry hold, and they hold for the right
reason: they *paid* — a Mutex, flock, fsync. Those survive real
parallelism because they're real locks, not scheduling luck.

## The drill drew blood before the threads even started

First run, drill 1 crashed in all eight threads at once:
`undefined method 'iso8601' for an instance of Time`. The journal
calls `Time#iso8601` — a method from the `time` stdlib — without
requiring it. Every previous example worked because something else
(async, loaded when an orchestrator or limiter was touched) had
required it first. My drill used the journal *bare*, and the
load-order debt came due.

This is the classic works-on-my-boot bug, and it's exactly the
species JRuby users hit constantly: different load orders, different
lazy-loading, same gem, sudden NoMethodError on a stdlib method.
The fix is one line (`require "time"` where it's used) and it's in
this round's release — but the lesson is the durable part: **every
file must require what it uses.** Transitive requires are a loan
from a dependency's internals, and dependencies refinance without
telling you.

## Luck wearing a lab coat

Drill 3 is the honest one. The windowed limiter's `try_acquire`
reads `@stamps.size`, then appends — check-then-act, no mutex. Eight
threads, 1600 attempts, ceiling 50: admitted exactly 50. So it's
fine? No — it's *unobserved*. MRI's GVL makes the two steps nearly
atomic by scheduling accident; JRuby runs them genuinely
concurrently, and unsynchronized size-check-then-append is precisely
the shape that over-admits there. The drill prints both possible
outcomes honestly and files the round-12 ask: a Mutex around the
stamp bookkeeping, so the answer is the same on every Ruby. A lock
you only need on some VMs is a lock you need.

## Notes

- The journal's flock+fsync means it would hold across *processes*,
not just threads — the strongest claim in the gem, and the drill
only tested the weaker half. A multiprocess drill is the natural
sequel.
- Note what I did NOT ask for: making the orchestrator thread-safe.
It's fiber-architected and says so; the drill only holds
*explicitly shared* structures to the parallel standard.

## Verdict

Two structures paid for real locks and passed a real-parallelism
drill; one is coasting on the GVL and now has that in writing; and
the drill's warm-up caught a load-order bug that fiber-world never
would have. Bring threads to your gem before your users' VM does.
69 changes: 69 additions & 0 deletions docs/perspectives/round-11/03-nateberkopec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Round 11 field notes — Nate Berkopec does the capacity math

*Built: `examples/capacity_planner.rb` — Little's Law over the
journal's duration percentiles, then the plan checked against every
configured limit. The binding constraint wasn't the one the meeting
was about.*

## What I built and why

"How many workers do we need?" is the most expensive question teams
answer by feeling. The feeling-based answers cluster at two poles:
over-provision 4x (pay the cloud bill forever) or size to the demo
(page the on-call forever). Meanwhile the actual answer has been
math since 1961: **L = λW**. Concurrency needed equals arrival rate
times service time. The journal already stores W — per-task duration
samples across thirty runs — so the planner only needs λ, your peak
target:

```
task p50 p95 lanes (p50/p95)
fetch:ticket 83ms 158ms 1 / 1
classify 364ms 751ms 1 / 2
draft:reply 993ms 2387ms 2 / 5
total at p95: 8 lanes for 120 tickets/min
```

**Plan for p95, not p50.** Capacity sized to the median queues every
time latency has a bad day, and in this journal latency has a bad
day one run in eight — which is what real latency looks like
(log-normal-ish, long right tail), not what the demo looked like.
The gap between the p50 plan (4 lanes) and the p95 plan (8) is
exactly the gap between "works" and "works during the incident."

## The constraint that wasn't in the meeting

Then the part that actually saves the quarter: check the computed
plan against *every* limit in the system, not just the one under
debate. The meeting was about `concurrency_limit: 8` — which, the
math says, *holds*. The provider quota of 90/min against 120/min
arrivals is the real story, and it's not a "slowdown": λ/μ > 1 has
**no steady state**. The queue grows without bound until something
breaks, and the something is usually memory, at 3am, wearing the
disguise of an unrelated alert. Utilization greater than one isn't
a performance problem; it's an arithmetic problem, and no amount of
worker tuning fixes arithmetic.

This is the recurring shape of capacity incidents: everyone tunes
the limit they own, and the binding constraint belongs to a vendor
contract nobody re-read. The planner's job is to put all the limits
in one table with one verdict column.

## Notes

- The journal made this a twenty-line tool. Percentile baselines
across runs (`duration_percentile`, round 8) were built for
regression-hunting; capacity planning is the same data asked a
business question. Good telemetry keeps being reusable like this.
- The planner deliberately reports lanes *per task*: draft:reply
needs 5 of the 8, so if you shard workers by task type, that's
your split — and if draft:reply gets slower next release, Aaron's
perf tools and this planner will disagree with the cloud bill in
the same direction, which is how you know to act.

## Verdict

A journal plus Little's Law is a capacity plan; a dashboard plus a
feeling is a postmortem. The math took thirty lines, found the
binding constraint outside the meeting's agenda, and turned "how
many workers?" into a question with a receipt.
Loading
Loading