81 Commits

Author SHA1 Message Date
Julien Calixte
0c8a0c294f feat(editor): browse samples in a categorized modal gallery
Replace the cramped Samples dropdown with a modal that lays the gallery
out as a card grid, sectioned by tier, so the growing set stays scannable
and the learning order is visible at once.
2026-06-22 23:47:16 +02:00
Julien Calixte
37e3a1e89f feat(samples): tag each sample with a pedagogical category 2026-06-22 23:47:10 +02:00
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
Julien Calixte
34bc8fa70b style: longer description 2026-06-22 23:19:01 +02:00
Julien Calixte
81425f8f3d style(adr-0004): realign the rate-rules table columns 2026-06-22 23:01:33 +02:00
Julien Calixte
d36f58977e feat(samples): add "The cobra effect" perverse-incentive trap
A bounty on dead cobras (a Balancing cull) funds a Reinforcing cobra
farm; once the farm gluts, the overflow rule spills its worthless
surplus into the wild, overshooting to 4x the starting population.

The engine has no time-triggered events, so the historical bounty
cancellation is modelled emergently: breeding outruns the bounty, the
farm gluts, and the one-sided overflow gate dumps the surplus. Sits
after "Bathtub with an overflow" since it reuses that rule.
2026-06-22 23:00:56 +02:00
Julien Calixte
62f6c5cda1 feat(samples): add "World on a warming planet" capstone
A miniature of the Club of Rome's World3 with its pollution sector
reframed as a climate channel: four coupled Stocks where Reinforcing
capital and population overshoot a finite Resource and the carbon they
burn locks in the warming that finishes them. A qualitative tribute to
the overshoot-and-collapse shape on the existing four rules, not a port
of the equations.
2026-06-21 20:06:35 +02:00
Julien Calixte
9ddff65072 docs(adr-0004): record the overflow rule in the vocabulary
The additive growth the ADR anticipated, now realised: the rule table, why
Overflow is a deliberate threshold exception to "the shape emerges", and why it
is a separate kind rather than a clamp on the (bidirectional) Gap rule.
2026-06-21 18:40:51 +02:00
Julien Calixte
faf8500fa5 feat(samples): add "Bathtub with an overflow" companion
The bathtub given a hard ceiling: a flat-out constant tap and an overflow spillway
that carries whatever rises past the brim into a second Stock, the floor. Water
climbs in a straight line then plateaus just above capacity while the floor fills —
an equilibrium from the spill, not from the inflow easing off. The gallery's one
declared ceiling, a counterpoint to the emergent one in "Limits to growth".
2026-06-21 18:31:39 +02:00
Julien Calixte
f691b99ca5 feat(simulation): add the overflow rule, a one-sided gap for hard ceilings
A fourth Rule kind: overflow = max(0, factor × (level − threshold)). It stays
shut until its `+` level passes its `−` threshold, so an outflow on it spills only
the excess — a spillway / hard ceiling the smooth rules can't draw.

Gap can't stand in for it: it's signed and bidirectional (coffee cooling relies on
that), so below the threshold a gap outflow runs backwards. Clamping at zero is the
whole point, and it can't be applied globally without breaking gap. This is the
additive vocabulary growth ADR-0004 anticipates — a new kind in the union plus a
case in the evaluator — alongside touch-ups to the rule validator and the inspector.
2026-06-21 18:31:39 +02:00
Julien Calixte
46d3279a3c feat(samples): add "Tragedy of the commons, fixed" companion
Rework the trap so its Pasture is a renewable commons (logistic
regrowth) with herd mortality: unregulated breeding overshoots the
renewal rate, grazes the grass to bare dirt, and the herds then starve
with it — resource and users ruined together, not just the grass.

Add the fixed companion sharing that exact renewable Pasture. Stocking
is regulated against an agreed reserve, so the commons holds and the
herds settle at a sustainable size — more cattle than the trap's boom
leaves alive. The only difference between the pair is regulation, not
regrowth, so the comparison is honest.
2026-06-21 16:52:23 +02:00
Julien Calixte
15e9149095 feat(chart): split multi-magnitude tracks across two y-axes
Tracks whose peak values span more than an order of magnitude now plot
on a second right-hand axis, so a small track is no longer flattened
against the baseline by a large one. Also labels the legend's time
column "Time" (was uPlot's default "Value") and gives the toggleable
track rows a pointer cursor so the built-in hide/show is discoverable.
2026-06-21 16:02:10 +02:00
Julien Calixte
889c7361ed refactor(editor): drop duplicate description from gloss card
The Inspector already shows the selected element's description as an
editable field, and it co-renders with the gloss card on selection.
Showing the same text read-only in the gloss card was redundant, so the
card returns to its single job: defining the element's kind.
2026-06-21 15:28:42 +02:00
Julien Calixte
26d3c4493a feat(samples): annotate every sample's nodes and links
Fill in the per-element descriptions across all 13 gallery samples, so
selecting any node or link explains why it is there — a self-guided
tour of each model. The link() helper gains an optional description arg.
2026-06-21 15:19:36 +02:00
Julien Calixte
4a0498ee55 feat(editor): give each node and link a description
A free-text "why this element is here" note on Stocks, Flows,
Converters, and Information Links — editable in the Inspector next to
the unit, shown read-only in the gloss card below the generic kind
definition on selection. The Inspector now also opens for Information
Links, which previously had no editing surface.
2026-06-21 15:19:31 +02:00
Julien Calixte
37bbdf5650 fix(samples): make "Drift to low performance" actually drift down
The model had no downward force on Performance, so its only flow
(improvement) could only pull it up. Performance rose 40 -> 60 while
the Standard fell 80 -> 60, contradicting the "slide downhill" story.

Add a constant decay outflow on Performance and seat it just below the
goal. Now both ratchet down in lockstep (Performance 70 -> 10, Standard
80 -> 20), a fixed gap apart — the genuine eroding-goals trap. Rewrite
the doc-comment, layout note, overview line, and blurb to match.
2026-06-21 13:16:28 +02:00
Julien Calixte
67c16a8d44 fix(samples): lower fishery natural-death rate to 0.01
Drops the Allee threshold to ~30 and lifts carrying capacity to ~1170, so
the stock is hardier and only collapses once heavily overfished. The
overshoot-and-collapse-to-extinction arc holds: Fish drift up, the fleet
overshoots (Boats peak ~700), and the catch drives the stock past the
threshold to a permanent zero by t=150.
2026-06-21 13:12:59 +02:00
Julien Calixte
e7a1bfb5af feat(samples): rework "Overshoot and collapse" as a renewable fishery
The non-renewable Resource (a Stock with no inflow) becomes a renewable
fishery with a true point of no return — an Allee threshold. Spawning
scales with density (~Fish^2) while natural deaths are linear (~Fish), so
below a critical density the deaths win and the stock slides to an
extinction it never recovers from; crowding deaths (~Fish^3) cap a healthy
stock at carrying capacity. A reinvesting fleet (Boats, Reinforcing)
overshoots the renewal rate and drags the fish under the threshold, then
starves and scraps itself.

The negative-positive-negative regrowth curve is the one shape the
proportional rule can't draw alone, so two relay Converters build it
(density to lift spawning to ~Fish^2, crowding for the ~Fish^3 ceiling) —
the Limits-to-growth crowding trick, doubled. At 16 nodes this is the
gallery's largest model and the only one with a Converter feeding a
Converter.

Tuned against the engine: Fish hold near 1000, cross the threshold (~200)
at t~40 as the catch overshoots, then go extinct and stay there; Boats
overshoot to ~450 and collapse back near their start by t=150. No
divergence; loops classify as expected (R: fleet reinvestment, birth
engine; B: natural/crowding/catch drains, scrapping).
2026-06-21 13:04:18 +02:00
Julien Calixte
59e3aafa63 feat(editor): dock the sim panel and refit the canvas to the rest
The results panel floated over the canvas and hid the lower part of the
diagram. Dock it beneath the canvas instead, and refit the view when it
opens or closes so the whole diagram stays framed in the height that
remains. The refit fires on Vue Flow's `dimensions` (post-measure), not
on the toggle, so it frames against the settled size.
2026-06-21 12:41:57 +02:00
Julien Calixte
2ae7cf87d7 fix(chart): disable click-drag-to-zoom on the read-only sim chart 2026-06-20 16:47:10 +02:00
Julien Calixte
69fec5fe2e feat(samples): add "AI deskilling spiral" gallery sample
The thirteenth sample points the language at a live debate: a classic trap,
Shifting the burden to the intervenor (Thinking in Systems, ch. 5), with AI as
the intervenor. Technical debt drives AI reliance, AI churns debt and lets
skills atrophy, and a thinner-skilled team refactors less — so the loop
Technical debt -> AI reliance -> atrophy -> Expertise -> refactoring ->
Technical debt is Reinforcing: addiction, not a fix. A learning Balancing brake
(practice toward a skill ceiling) is tuned to lose.

Maps the brief onto valid roles: Expertise and Technical debt are the stocks;
code quality and lead time read off them (inverse of debt / inverse of
expertise); model price lives in the AI-reliance factor. Buildable on the
existing rule vocabulary. Tuned against the simulator: over t=0..50 Expertise
slides 70 -> ~6 and Technical debt spirals 20 -> ~150, stopped (like
"Escalation") before the loop runs off-chart. The loop detector classifies the
four loops as expected (2 R, 2 B).
2026-06-20 15:28:51 +02:00
Julien Calixte
6a4fe59811 feat(samples): add "Overshoot and collapse" gallery sample
The twelfth sample fills the one classic dynamic the gallery was missing —
overshoot and collapse, the dynamic Thinking in Systems is built around. A
Reinforcing engine (Capital reinvesting its extraction revenue) runs on a
non-renewable Resource, the first Stock in the gallery with no inflow: it
overshoots the limit instead of settling at it, the dark twin of "Limits to
growth".

Buildable with the existing rule vocabulary (proportional only); the
non-negative-stock floor tames the bilinear extraction term as the Resource
runs out. Tuned against the simulator: Capital 5 → ~250 (t≈39) → ~5 by t=150,
Resource 1000 → ~6, no divergence. The loop detector classifies the three
loops as expected (R: investment→Capital→extraction; B: depreciation→Capital;
B: extraction→Resource).
2026-06-20 15:15:42 +02:00
Julien Calixte
3a65bf5d59 fix(editor): keep the stock box fixed so simulating doesn't move its arrows
The in-flow value line grew the card downward, dropping its vertical centre —
and the left/right handles with it, so connected pipes and links shifted. Render
the value as an out-of-flow overlay below the box; the card keeps its size and
the handles stay put.
2026-06-20 15:14:13 +02:00
Julien Calixte
8e1e313b20 refactor(editor): inline the header logo so it tracks the theme primary 2026-06-20 15:05:31 +02:00
Julien Calixte
d570425402 fix(editor): align the playhead with the chart's left gutter
valToPos returns CSS pixels relative to the plot area, but the overlay
sits on the canvas, so the playhead was short by the y-axis width and
started before x=0.
2026-06-20 15:01:21 +02:00
Julien Calixte
2c7ec6a6ec style: recolour the favicon to the #ff9ff3 primary 2026-06-20 14:58:34 +02:00
Julien Calixte
ef0cc2878b fix(editor): stop the t-readout wrapping so playback doesn't reflow
The fixed-width time readout wrapped to two lines for fractional times (dt 0.25
→ 't = 70.25'), growing the panel a row each frame and reflowing the whole
panel — the real flicker. Pin its width and forbid wrapping.
2026-06-20 14:56:42 +02:00
Julien Calixte
f38cf49f25 style: force the primary colour to #ff9ff3 across every theme
The default (no data-theme) resolves to DaisyUI's dark theme, whose
:root:not([data-theme]) selector outranks a custom light theme — so the
previous light-only override never reached buttons in dark mode. Force it with
an !important :root override that wins regardless of theme.
2026-06-20 14:53:41 +02:00
Julien Calixte
d5cda95c3e fix(editor): stop the behaviour chart flickering during playback
The playhead was a canvas line redrawn by redrawing the whole uPlot chart on
every frame, which flickers. Draw it as a DOM overlay that slides over the
canvas instead, so the canvas is only redrawn when the data actually changes.
2026-06-20 14:47:08 +02:00
Julien Calixte
dc30e5f890 style: set the primary theme colour to #2e86de 2026-06-20 14:47:08 +02:00
Julien Calixte
382fdddc68 feat(editor): give each Stock a unit
A Stock can carry a display unit (°C, people, $, …): a new optional field, an
inspector text input, validation + round-trip in io, and a readout beside the
live value on the canvas. Equip the demonstrative samples (Water L, Balance $,
Coffee °C, Population/Epidemic people, Yeast cells). Also bumps the fill gauge
opacity for a more vivid level.
2026-06-20 14:47:08 +02:00
Julien Calixte
ed6f011e69 feat(editor): reset the playhead when a different model loads 2026-06-20 14:39:06 +02:00
Julien Calixte
1d65383bbf fix(editor): make the samples list scrollable so it clears the panel 2026-06-20 14:39:02 +02:00
Julien Calixte
3fc0e23ec7 style(editor): make the stock fill gauge more vivid 2026-06-20 14:37:55 +02:00
Julien Calixte
69435b1315 feat(editor): animate the canvas in step with the simulation
A simulation store owns one run and one playhead, so the chart and the canvas
read off the same instant. While the panel is open, Stocks show a live value and
a fill gauge, Flows and Converters their current value; a play/pause + scrubber
drives the playhead (a wall-clock-paced clock in usePlayback), and a marker
tracks it on the chart.
2026-06-20 14:35:45 +02:00
Julien Calixte
c361b05ec4 feat(editor): chart behaviour over time with uPlot
Draw the simulation's Stock trajectories with uPlot (ADR-0005): a real time
axis, hover-to-read values, and a playhead-marker hook the canvas animation
drives. SimChart owns the imperative uPlot lifecycle behind a small prop API.
2026-06-20 14:35:35 +02:00
Julien Calixte
0c8f89d14e fix(style): load Sono from the api.fonts.coollabs.io host
Switch the body font to Sono and point the @import at
api.fonts.coollabs.io. The bare fonts.coollabs.io host serves the
marketing homepage (HTML), not CSS, so the @font-face rules never
loaded and the font silently fell back to system-ui.
2026-06-20 14:28:58 +02:00
Julien Calixte
f0d207c4d5 feat(model): make limits-to-growth a runnable two-flow logistic
Recast the S-curve as a Reinforcing inflow plus a crowding-driven die-off that
grows with Yeast², using only the existing proportional rule. Yeast now climbs
20 → ~1000 as a true sigmoid, and the detector still classifies it R + B — the
balancing loop stays visible, which a single "logistic" rule would have hidden.
Drops the carrying-capacity converter (a faithful one needs a divide rule).
2026-06-20 14:03:43 +02:00
Julien Calixte
34df540e4a feat(editor): add a simulation panel and an element inspector
- ResultsPanel: a Simulate panel that charts every Stock over time (hand-built
  SVG), with start/stop/dt run controls and a divergence warning
- Inspector: edit a selected Stock's initial value or a Flow/Converter's rule
- store: undoable setInitialValue / setRule / setSimSpec actions
2026-06-20 13:56:19 +02:00
Julien Calixte
40dc9fba43 feat(model): equip the sample gallery with runnable values
Give 10 of 11 samples initial values, rules, and a run window so they simulate
on load: Bathtub (linear), Savings/Escalation/Fixes-that-fail (Reinforcing),
Coffee (Balancing), Population, Epidemic (SIR), Tragedy (overshoot/collapse),
Drift (eroding goal), Predator-prey (damped). Limits to growth is left
diagram-only — its S-curve needs a saturating rule the vocabulary lacks.
2026-06-20 13:56:19 +02:00
Julien Calixte
5b6e830778 feat(model): simulate models from rules over information links
Phase 2 brings Models to life. ADR-0004: behaviour comes from a small fixed
vocabulary of rules (Constant / Proportional / Gap) read over the inbound
Information Links, not free-form formulas — valid by construction.

- types: Rule union, SimSpec, initialValue; replaces the unused equation field
- simulation: forward-Euler engine, dependency-ordered evaluation, algebraic-loop
  detection, non-negative stocks, and a divergence guard
- io: validate and round-trip the new fields (F8)
2026-06-20 13:56:06 +02:00
Julien Calixte
964e621f0e fix(editor): match the flow arrowhead weight to its line
Vue Flow's open arrow renders at 0.625x the edge stroke (its 20-unit
viewBox is squeezed into a 12.5 marker box), so the chevron looked thin
against the 2.5px pipe. A marker strokeWidth of 20/12.5 cancels the
downscale, drawing the head at the line's weight at any width.
2026-06-20 12:43:27 +02:00
Julien Calixte
528fc97c7f refactor(model): polish the coffee and limits-to-growth samples
Rename the coffee outflow "heat loss" to "cooling" so the balancing loop
reads more naturally, and stack carrying capacity above the source in
limits to growth so its link to crowding is a clean horizontal hop.
2026-06-20 11:51:49 +02:00
Julien Calixte
21a459256a feat(editor): dock flow arrows square at any pipe angle
Swap the filled-triangle arrowhead for an open arrow on flow pipes and
information links, and route flow pipes through a custom cubic edge whose
horizontal flatten arm scales with the vertical drop, not just the
horizontal gap. Vue Flow's default arm collapses toward zero when the
valve sits nearly above the stock, so a steep pipe stayed flat only in
its last pixels and the arrowhead detached from the visible line. The
pipe now keeps a real horizontal run into the stock's left edge at any
approach angle.
2026-06-20 11:51:34 +02:00
Julien Calixte
df03012c8b style(model): align every sample layout to the 20px grid
Snap all sample coordinates to multiples of 20 so they sit on the
editor grid and read cleanly. Computed midpoint() valves had to land
on grid points too, so a few endpoints were nudged (e.g. escalation
arsenals 300->280, limits-to-growth source -300->-280). Population is
re-laid as a grid cascade so its links no longer overlap the pipes.
2026-06-20 11:19:37 +02:00
Julien Calixte
d7d0966adc feat(editor): snap dragged nodes to the 20px grid
Match Vue Flow's snap grid to the background gap so nodes line up
on the dots they already render against.
2026-06-20 11:19:37 +02:00
Julien Calixte
27b7a8da5e feat(model): add fixes-that-fail and drift traps to the gallery
Two more Meadows system traps: Fixes that fail (a balancing
road-building fix whose induced-driving side effect reinforces the
congestion it targets) and Drift to low performance (an eroding goal
that spirals actual performance and the standard downward).

Also re-lays-out the coupled-stock samples for readability: predator
and prey and escalation now use aligned two-row layouts, and fixes
that fail stacks its flow valves in one column so the backfire link
reads as a clean vertical.
2026-06-20 11:09:04 +02:00
Julien Calixte
26dd245a94 feat(model): extend the sample gallery with five more models
Add a second tier past the four-model primer: Limits to growth, Predator
and prey, and Epidemic each introduce one structure the primer never
shows, plus two of Donella Meadows' system traps — Tragedy of the
commons and Escalation — to contrast healthy dynamics.
2026-06-20 10:40:55 +02:00
Julien Calixte
169dd043c3 feat(editor): explain refused loop-closure drags
Dragging a Flow back onto a Stock it already touches is refused (an
Information Link never targets a Stock), but that gesture is almost
always someone trying to "close the loop" by hand. The Flow's own pipe
already is the link back, so surface a self-dismissing hint saying so
instead of letting the drop die silently (F4: guide, don't nag).
2026-06-20 10:40:49 +02:00