Compare commits

..

2 Commits

Author SHA1 Message Date
Julien Calixte
914cf01763 refactor(samples): lay the cobra effect out left-to-right
Move Farmed cobras to the left so the story reads in order (farm engine
-> release bridge -> wild -> cull) and the release bridge no longer
doubles back across the canvas. Positions only; dynamics unchanged.
2026-06-22 23:33:14 +02:00
Julien Calixte
fe17bc8fae feat(samples): add "Success to the successful" competitive-exclusion trap
Two equal researchers and one conserved prize (the field's attention):
a single gap-driven Flow tilts it to whoever is a hair ahead, so a 51-49
near-tie locks in to ~100-0. Escalation's zero-sum twin -- the same
two-Stock all-Reinforcing structure, concentrating instead of exploding.
2026-06-22 23:33:06 +02:00

View File

@@ -21,27 +21,31 @@
* 6. Predator and prey — two coupled Stocks whose interlocking loops oscillate. * 6. Predator and prey — two coupled Stocks whose interlocking loops oscillate.
* 7. Epidemic — a chain of Stocks joined by Stock→Stock Flows: no clouds. * 7. Epidemic — a chain of Stocks joined by Stock→Stock Flows: no clouds.
* *
* Last, four of Donella Meadows' system *traps* — structures that reliably misbehave * Last, five of Donella Meadows' system *traps* — structures that reliably misbehave
* (Thinking in Systems, ch. 5), to contrast the healthy dynamics above — and, paired * (Thinking in Systems, ch. 5), to contrast the healthy dynamics above — and, paired
* with the first, the cure that escapes it: * with the first, the cure that escapes it:
* *
* 8. Tragedy of the commons — two Reinforcing herds overgraze a *renewable* shared * 8. Tragedy of the commons — two Reinforcing herds overgraze a *renewable* shared
* Stock to bare dirt, then starve with it: all ruined. * Stock to bare dirt, then starve with it: all ruined.
* 9. …commons, fixed — the same renewable commons, but stocking is regulated * 9. …commons, fixed — the same renewable commons, but stocking is regulated
* against an agreed reserve: it holds, and the herds end * against an agreed reserve: it holds, and the herds end
* *larger* than the trap's boom leaves alive. * *larger* than the trap's boom leaves alive.
* 10. Escalation — a single Reinforcing loop spanning two Stocks, with * 10. Escalation — a single Reinforcing loop spanning two Stocks, with
* no brake in the structure: an arms race. * no brake in the structure: an arms race.
* 11. Fixes that fail — a fix drains the symptom Stock (B) while its side * 11. Success to the successful — two equal rivals split one conserved prize (the field's
* effect refills it (R): the cure feeds the disease. * attention); a gap-driven Flow tilts it to whoever leads,
* 12. Drift to low performance — a goal that erodes toward actual performance, so the * so a 5149 near-tie locks in to ~1000. Escalation's
* effort it drives never overcomes a steady decay: a * zero-sum twin — it concentrates instead of exploding.
* Reinforcing loop ratchets both downward. * 12. Fixes that fail — a fix drains the symptom Stock (B) while its side
* effect refills it (R): the cure feeds the disease.
* 13. Drift to low performance — a goal that erodes toward actual performance, so the
* effort it drives never overcomes a steady decay: a
* Reinforcing loop ratchets both downward.
* *
* Next, the dynamic the book is named for, and the one the gallery has saved until a * Next, the dynamic the book is named for, and the one the gallery has saved until a
* reader knows every piece it needs: * reader knows every piece it needs:
* *
* 13. Overshoot and collapse — a Reinforcing harvester on a *renewable* Resource with * 14. Overshoot and collapse — a Reinforcing harvester on a *renewable* Resource with
* an extinction threshold (an Allee floor): a fleet * an extinction threshold (an Allee floor): a fleet
* overshoots the renewal rate and pushes the fishery past * overshoots the renewal rate and pushes the fishery past
* the point of no return. The dark twin of "Limits to * the point of no return. The dark twin of "Limits to
@@ -50,14 +54,14 @@
* Last, the language pointed at a live debate — a classic trap (Shifting the burden to * Last, the language pointed at a live debate — a classic trap (Shifting the burden to
* the intervenor, ch. 5) wearing today's clothes: * the intervenor, ch. 5) wearing today's clothes:
* *
* 14. AI deskilling spiral — handing the burden of code quality to AI atrophies the * 15. AI deskilling spiral — handing the burden of code quality to AI atrophies the
* Expertise that holds quality up, so the team leans on AI * Expertise that holds quality up, so the team leans on AI
* harder and Technical debt spirals: addiction, not a fix. * harder and Technical debt spirals: addiction, not a fix.
* *
* And a mechanical coda — the gallery's one *hard* ceiling, to contrast the emergent * And a mechanical coda — the gallery's one *hard* ceiling, to contrast the emergent
* ones (above all "Limits to growth"): * ones (above all "Limits to growth"):
* *
* 15. Bathtub with an overflow — the tap runs flat out (a Constant inflow that never * 16. Bathtub with an overflow — the tap runs flat out (a Constant inflow that never
* reads the level) and a spillway carries whatever rises * reads the level) and a spillway carries whatever rises
* past the brim into a second Stock, the floor. The only * past the brim into a second Stock, the floor. The only
* model on the `overflow` rule — a one-sided Gap, the * model on the `overflow` rule — a one-sided Gap, the
@@ -67,7 +71,7 @@
* Then one last trap, held back until the overflow rule existed to carry it — the * Then one last trap, held back until the overflow rule existed to carry it — the
* language pointed at the most-told cautionary tale in systems thinking: * language pointed at the most-told cautionary tale in systems thinking:
* *
* 16. The cobra effect — a bounty on dead cobras (a Balancing fix) quietly funds a * 17. The cobra effect — a bounty on dead cobras (a Balancing fix) quietly funds a
* cobra *farm* (a Reinforcing engine); breeding outruns what * cobra *farm* (a Reinforcing engine); breeding outruns what
* the bounty can absorb, the farm gluts, and its now-worthless * the bounty can absorb, the farm gluts, and its now-worthless
* surplus spills through an `overflow` gate into the wild — * surplus spills through an `overflow` gate into the wild —
@@ -77,7 +81,7 @@
* *
* And the capstone — every piece at once, at the scale the whole gallery points toward: * And the capstone — every piece at once, at the scale the whole gallery points toward:
* *
* 17. World on a warming planet — the Club of Rome's World3 (Meadows et al., *The Limits * 18. World on a warming planet — the Club of Rome's World3 (Meadows et al., *The Limits
* to Growth*) in miniature, its pollution sector reframed * to Growth*) in miniature, its pollution sector reframed
* as a climate channel: four coupled Stocks where two * as a climate channel: four coupled Stocks where two
* Reinforcing engines (capital, population) overshoot a * Reinforcing engines (capital, population) overshoot a
@@ -943,6 +947,82 @@ function escalation(): Model {
) )
} }
/**
* Success to the successful — Donella Meadows' competitive-exclusion trap (Thinking
* in Systems, ch. 5), known in the sociology of science as the *Matthew effect*
* (Robert Merton, after Matthew 25:29: "unto every one that hath shall be given").
* Two researchers of equal ability compete for one finite thing — the field's
* attention — and the only difference between them is that A begins a single point
* ahead (Renown 51 vs 49 of a fixed 100).
*
* The community's attention is a *conserved* budget: a citation, an invitation, a
* grant that goes to one does not go to the other, so Renown A + Renown B never
* leaves 100. The marginal piece of it drifts to whoever is already better known —
* `attention = factor × (Renown A Renown B)`, a single Stock→Stock Flow draining
* the lesser name into the greater. There are no clouds: nothing enters or leaves
* the system, it only *redistributes*. That one valve carries *two* Reinforcing
* loops — Renown A → [+] → attention → (fills A) → Renown A (more renown wins more
* attention), and Renown B → [] → attention → (drains B) → Renown B (the falling
* name is forgotten ever faster). Neither loop has a surviving ``, so both are
* Reinforcing: nothing brakes the gap, and it can only widen.
*
* A tie is therefore an *unstable* equilibrium — the knife-edge this trap balances
* on. A's one-point lead is enough: attention tilts to A, the gap grows, attention
* tilts harder, and the field locks in. Renown A climbs 51 → 100 and Renown B fades
* 49 → 0 by t≈42 — a near-total monopoly of acclaim, though the two were equally
* able. The cruel lesson Meadows draws: where the prize for winning is more power to
* win, it is *initial conditions*, not merit, that decide the outcome.
*
* Contrast "Escalation", the gallery's other two-Stock all-Reinforcing trap: there
* the cross-loop has no minus and *both* arsenals blow up together (positive-sum,
* unbounded); here the single loop is zero-sum — A rises only as fast as B is drained
* — so the same Reinforcing structure *concentrates* instead of exploding. (The cure
* Meadows prescribes is to break the coupling — level the field, diversify the prize,
* or handicap the leader — none of which the structure here contains: that is the trap.)
*/
function successToTheSuccessful(): Model {
// Two Stocks on one line, the lone attention valve nudged above their midpoint so
// the pipe (Renown B → attention → Renown A) and the two info links (each Stock
// into the valve) read apart instead of overlapping. Hand-placed, not at midpoint.
const renownB = makeStock({ x: -260, y: 80 }, "Renown B")
renownB.initialValue = 49
renownB.description =
"Researcher B's standing — equal in ability to A, and a single point behind at the start. The lesser name the attention drains away from, fading to nothing though B never did anything wrong."
const renownA = makeStock({ x: 260, y: 80 }, "Renown A")
renownA.initialValue = 51
renownA.description =
"Researcher A's standing — one point ahead at the start, and that hair's lead is all it takes. Attention concentrates here until A holds nearly the whole field's acclaim."
// attention = factor × (Renown A Renown B): a Gap whose `+` level is A and ``
// target is B. Source B, target A, so a positive rate drains the lesser name into
// the greater — and since draining B only widens the gap, the rate never reverses.
const attention = makeFlow({ x: 0, y: -120 }, "attention", renownB.id, renownA.id)
attention.rule = { kind: "gap", factor: 0.05 }
attention.description =
"The field's finite attention drifting to the better-known name, ∝ the lead (Renown A Renown B). A conserved transfer: what A gains, B loses — the one valve through which both Reinforcing loops run."
return model(
"Success to the successful",
[renownB, renownA, attention],
[
link(
renownA,
attention,
"+",
"Renown A is the level in the gap: the further ahead A is, the faster attention flows its way. With the inflow it fills, this closes A's Reinforcing loop — the rich getting richer.",
),
link(
renownB,
attention,
"-",
"Renown B is the target the gap is measured against (the input). As B fades the gap only widens, so it is drained faster still — a second Reinforcing loop, the same minus on both the link and the outflow.",
),
],
// From a 5149 near-tie the gap runs away: Renown A climbs 51 → ~100 and Renown B
// fades 49 → ~0 by t≈42, then both hold — a near-total monopoly of acclaim won on a
// one-point head start. The unstable 5050 knife-edge a hair's difference tips.
{ start: 0, stop: 50, dt: 1 },
)
}
/** /**
* Fixes that fail — the archetype where a quick fix relieves a symptom but feeds it * Fixes that fail — the archetype where a quick fix relieves a symptom but feeds it
* through a side effect, so the symptom returns and the fix is reapplied for ever. * through a side effect, so the symptom returns and the fix is reapplied for ever.
@@ -1485,23 +1565,23 @@ function bathtubOverflow(): Model {
* is a Stock the reward built, the brake is the overflow gate, and the damage is permanent. * is a Stock the reward built, the brake is the overflow gate, and the damage is permanent.
*/ */
function cobraEffect(): Model { function cobraEffect(): Model {
// The policy lever (bounty) sits up top, centre, wired into three flows at once: the // Laid out left→right: the farm engine on the left (a Source breeds Farmed cobras,
// intended cull of Wild cobras on the left, and on the right the farm's breeding and // harvest draining up to a Sink), the release bridge in the centre spilling the glut
// cash-out. The release bridge dips below, carrying the farm's glut back across to the // rightward into Wild cobras, and the bounty-driven cull running off to the right. The
// wild, with glut hanging beneath it. Valves are hand-placed, not at midpoints, so // policy lever (bounty) sits up top, wired down into all three flows — cull, breeding,
// every Information Link lands in open space. // cash-out — with glut beneath the bridge it gates. Hand-placed valves keep links clear.
const bounty = makeConverter({ x: 0, y: -300 }, "bounty") const bounty = makeConverter({ x: -80, y: -340 }, "bounty")
bounty.rule = { kind: "constant", value: 1 } bounty.rule = { kind: "constant", value: 1 }
bounty.description = bounty.description =
"The reward paid per dead cobra — held at 1, a normalised policy lever, so every rate here scales with it (a bigger bounty would only run the whole story faster). One lever wired into three flows: the cull, the breeding, and the cash-out." "The reward paid per dead cobra — held at 1, a normalised policy lever, so every rate here scales with it (a bigger bounty would only run the whole story faster). One lever wired into three flows: the cull, the breeding, and the cash-out."
// The intended fix: a bounty-driven cull empties the streets of wild cobras. // The intended fix: a bounty-driven cull empties the streets of wild cobras.
const wild = makeStock({ x: -360, y: -40 }, "Wild cobras") const wild = makeStock({ x: 380, y: -40 }, "Wild cobras")
wild.initialValue = 100 wild.initialValue = 100
wild.description = wild.description =
"The actual problem the bounty targets — hunted down at first, then overrun once the farm's surplus is loosed on it. It has no inflow of its own, so any rebound can only be the farm's doing." "The actual problem the bounty targets — hunted down at first, then overrun once the farm's surplus is loosed on it. It has no inflow of its own, so any rebound can only be the farm's doing."
const cullSink = makeCloud({ x: -680, y: -40 }) const cullSink = makeCloud({ x: 920, y: -40 })
const culling = makeFlow({ x: -520, y: -40 }, "culling", wild.id, cullSink.id) const culling = makeFlow({ x: 660, y: -40 }, "culling", wild.id, cullSink.id)
// culling = 0.16 × Wild × bounty: the Balancing drain, ∝ the stock. Fast enough to // culling = 0.16 × Wild × bounty: the Balancing drain, ∝ the stock. Fast enough to
// crash the wild population by t≈20 — the policy's whole visible success. // crash the wild population by t≈20 — the policy's whole visible success.
culling.rule = { kind: "proportional", factor: 0.16 } culling.rule = { kind: "proportional", factor: 0.16 }
@@ -1509,19 +1589,19 @@ function cobraEffect(): Model {
"Wild cobras killed for the bounty, ∝ Wild cobras × bounty — the intended Balancing fix that empties the streets at first." "Wild cobras killed for the bounty, ∝ Wild cobras × bounty — the intended Balancing fix that empties the streets at first."
// The perverse stock: a farm bred to cash in on the bounty. // The perverse stock: a farm bred to cash in on the bounty.
const farm = makeStock({ x: 360, y: -40 }, "Farmed cobras") const farm = makeStock({ x: -520, y: -40 }, "Farmed cobras")
farm.initialValue = 5 farm.initialValue = 5
farm.description = farm.description =
"Cobras bred to cash in on the bounty — the Stock the reward calls into being. It compounds unseen while the streets look clear, then spills its surplus into the wild." "Cobras bred to cash in on the bounty — the Stock the reward calls into being. It compounds unseen while the streets look clear, then spills its surplus into the wild."
const breedSrc = makeCloud({ x: 680, y: -40 }) const breedSrc = makeCloud({ x: -980, y: -200 })
const breeding = makeFlow({ x: 520, y: -40 }, "breeding", breedSrc.id, farm.id) const breeding = makeFlow({ x: -760, y: -200 }, "breeding", breedSrc.id, farm.id)
// breeding = 0.31 × Farm × bounty: Farm → breeding → Farm, no `` → the Reinforcing // breeding = 0.31 × Farm × bounty: Farm → breeding → Farm, no `` → the Reinforcing
// engine the bounty funds without meaning to. // engine the bounty funds without meaning to.
breeding.rule = { kind: "proportional", factor: 0.31 } breeding.rule = { kind: "proportional", factor: 0.31 }
breeding.description = breeding.description =
"The farm breeding more cobras, ∝ Farmed cobras × bounty — the Reinforcing engine the bounty funds without intending to: more breeding stock, more bred." "The farm breeding more cobras, ∝ Farmed cobras × bounty — the Reinforcing engine the bounty funds without intending to: more breeding stock, more bred."
const harvestSink = makeCloud({ x: 360, y: -260 }) const harvestSink = makeCloud({ x: 600, y: -220 })
const harvest = makeFlow({ x: 360, y: -140 }, "harvest", farm.id, harvestSink.id) const harvest = makeFlow({ x: 400, y: -220 }, "harvest", farm.id, harvestSink.id)
// harvest = 0.10 × Farm × bounty: farmed cobras killed and turned in — the rule beating, // harvest = 0.10 × Farm × bounty: farmed cobras killed and turned in — the rule beating,
// and a Balancing drain on the farm. // and a Balancing drain on the farm.
harvest.rule = { kind: "proportional", factor: 0.1 } harvest.rule = { kind: "proportional", factor: 0.1 }
@@ -1531,11 +1611,11 @@ function cobraEffect(): Model {
// The backfire bridge: once the farm gluts, its worthless surplus is dumped into the // The backfire bridge: once the farm gluts, its worthless surplus is dumped into the
// wild. A one-sided overflow gate (shut below the glut) — no scripted policy reversal // wild. A one-sided overflow gate (shut below the glut) — no scripted policy reversal
// needed; the release falls out of the farm outgrowing its own worth. // needed; the release falls out of the farm outgrowing its own worth.
const glut = makeConverter({ x: 0, y: 300 }, "glut") const glut = makeConverter({ x: -260, y: 260 }, "glut")
glut.rule = { kind: "constant", value: 200 } glut.rule = { kind: "constant", value: 200 }
glut.description = glut.description =
"The farm size past which cobras lose their worth (a fixed 200) — the threshold the spill opens above. A structural stand-in for the day the bounty was scrapped and the snakes became worthless." "The farm size past which cobras lose their worth (a fixed 200) — the threshold the spill opens above. A structural stand-in for the day the bounty was scrapped and the snakes became worthless."
const releases = makeFlow({ x: 0, y: 120 }, "releases", farm.id, wild.id) const releases = makeFlow({ x: -40, y: 60 }, "releases", farm.id, wild.id)
// releases = max(0, 0.6 × (Farm glut)): shut while the farm is worth keeping, then it // releases = max(0, 0.6 × (Farm glut)): shut while the farm is worth keeping, then it
// carries off the surplus. The overflow that overruns the wild — and, draining the farm // carries off the surplus. The overflow that overruns the wild — and, draining the farm
// above the glut, the Balancing brake that caps it. // above the glut, the Balancing brake that caps it.
@@ -1908,6 +1988,12 @@ export const SAMPLES: Sample[] = [
blurb: "An arms race: one Reinforcing loop spanning two Stocks.", blurb: "An arms race: one Reinforcing loop spanning two Stocks.",
build: escalation, build: escalation,
}, },
{
title: "Success to the successful",
blurb:
"Two equal researchers, one a hair ahead: the field's attention locks onto the leader and the rival fades — a winner-take-all trap.",
build: successToTheSuccessful,
},
{ {
title: "Fixes that fail", title: "Fixes that fail",
blurb: "Road building eases congestion (B) but induces the traffic that refills it (R).", blurb: "Road building eases congestion (B) but induces the traffic that refills it (R).",