At 16:50, a photograph is supposed to happen in a small harbor plaza.
The photographer is there. The couple is there. The flowers arrive at 16:30, the arrangement is ready at 16:40, and everything happens as planned.
Then I close a bridge at 16:15.
The courier can no longer take the original route.
The flowers arrive at 16:55.
Setup finishes at 17:05.
The photograph never happens.
That is the entire problem behind Butterfly.

But I wasn't particularly interested in asking:
What happens when I change the world?
Simulation already does that very well.
The question I wanted to ask was different:
What else would have to change so that one exact moment can still happen?
That distinction turned a small 3D experiment into something much stranger.
The photograph is not allowed to move
The easiest way to “fix” the scenario would be to move the photograph to 17:05.
Butterfly is not allowed to do that.
When I select Keep this moment, the photograph becomes a constraint.

It must still happen at 16:50.
In the same place.
With the same people.
With the required flowers ready.
And the bridge must remain closed.
A photograph at 17:05 is not an acceptable approximation.
It is another moment.
That rule became MomentFrame, one of the central ideas in Butterfly.
The system takes something that happened in the original history and freezes the parts that define it.
Then it asks the changed world to find another path back to that exact state.
The world has to earn the solution
Butterfly doesn't switch between prewritten branches.
There is no:
if bridge_closed → flowers_late
The delivery asks the world for a route.
When the bridge becomes unavailable, the valid route changes.
That changes the arrival time.
The new arrival time changes when the flower arrangement can be completed.
The photograph evaluates its requirements again.
And it fails.
The same thing happens when Butterfly searches for a recovery.
The current world contains a bounded set of interventions that are actually permitted. Each candidate is applied to the changed world, the simulation runs again, and the result is checked against the MomentFrame.
In the current scenario, two alternate histories survive.
One activates an already-authored ferry connection.
The other sends the courier earlier, allowing the crossing to happen before the bridge closes.
Both restore the photograph at 16:50.
Neither reopens the bridge.
That last part matters.
The system is not trying to undo the user's change. It is trying to find another history compatible with it.
It is deliberately a very small world
Butterfly currently follows one afternoon in one small harbor town.
That is intentional.
I didn't want to hide the causal structure behind a large generated world and then point at an interesting output.
I wanted the whole chain to remain inspectable.
You can see the bridge.
You can see the courier.
You can see the route.
You can see when the delivery arrives.
You can see when setup completes.
You can see exactly why the photograph disappears.
And when Butterfly proposes a recovery, you can inspect why that works too.
For this experiment, a small world with visible causality was more useful than a large world full of possibilities.
Then the simulator started lying to me
The interesting part of building Butterfly was not getting the first scenario working.
That happened relatively early.
The interesting part was trying to break it afterward.
One of the first problems was almost embarrassing.
The original scenario produced the correct result, but parts of the engine were effectively using stable event IDs as a proxy for temporal ordering.
Nothing looked wrong.
Then I changed the IDs.
The simulation broke.
That meant the result had been correct for the wrong reason.
The fix was not a UI patch. Time had to become explicit in fact resolution.
Another bug was worse.
A fact produced later in the afternoon could leak backward and satisfy a requirement earlier in the timeline.
From the outside, the world could still look plausible.
Internally, causality was broken.
The engine now only accepts producers that can complete at or before the moment being evaluated. Real temporal self-dependencies are treated as cycles rather than quietly inventing an answer.
Those two bugs changed how I approached the rest of the project.
A working scenario was no longer enough.
I started changing assumptions deliberately.
Rename things.
Remove coordinates.
Introduce later transports.
Change route modes.
Move timing boundaries.
If the simulation only works because the original fixture is friendly, I don't consider that a working simulation.
The renderer had its own version of reality
There was another class of bugs that had nothing to do with causal logic.
An actor could be correctly located at a destination according to the simulator while still appearing somewhere else in the 3D scene.
The engine knew one thing.
The renderer showed another.
I eventually separated two concepts that initially looked almost identical:
where something is semantically
and where the renderer knows how to draw it.
That sounds obvious afterward.
It was not obvious while both were represented closely enough that the first scenario appeared to work.
Butterfly kept producing these little cases where the visible result forced me to question the model underneath it.
At one point I had a custom Three.js camera implementation that passed the DOM-level tests.
The screenshot looked terrible.
I removed it.
That gave the project another rule:
A passing test does not overrule visible evidence.
From then on, browser screenshots became part of the development loop instead of something generated at the end.
The recovery is deliberately bounded
Butterfly does not invent an unlimited number of possible histories.
The world already contains the interventions that are allowed to exist.
The recovery engine searches that bounded domain, simulates each candidate, calculates the consequences and rejects anything that doesn't restore the fixed moment.
This also means the engine has to distinguish between very different outcomes.
Maybe a valid solution exists.
Maybe the target was already satisfied.
Maybe every configured intervention has been tested.
Maybe the search budget was reached before the domain was exhausted.
Maybe the available information is insufficient to decide.
Those are not the same answer.
In particular, exhausting the authored intervention set does not prove that no solution could exist in every imaginable version of the world.
It only tells us that the system did not find one inside the domain it was actually allowed to search.
I wanted that boundary to remain explicit.
The world itself is structured data
The simulation is written in TypeScript and the visual world uses Three.js.
Next.js provides the application around it.
Sanity is used as the authored source for the world model: people, places, objects, routes, transport availability, events, requirements, facts, causal relationships and permitted interventions.
Published world data is validated and frozen into a revision before visitors start creating variants.
A visitor changing the bridge schedule is therefore not editing the shared world.
They are creating another history from a known version of it.
That separation became important very quickly because comparing histories is meaningless if the thing being compared is quietly changing underneath you.
Why call it Butterfly?
The obvious reference is the butterfly effect: change something small and consequences propagate outward.
Butterfly starts there.
Close a bridge.
A route changes.
A delivery moves.
A setup moves.
A photograph disappears.
But I became more interested in the reverse problem.
Once consequences have propagated, can we choose one thing we refuse to lose?
And if we do, what does the rest of the world have to become?
That gives Butterfly a slightly different shape from a normal “what if?” simulator.
Most simulations ask:
What happens if the world changes?
Butterfly asks:
What must the world change so that one chosen thing doesn't?
For now, it is one bridge, one courier and one photograph.
That is enough to make the question real.
Try Butterfly:
https://butterfly-mu-orpin.vercel.app
Source code:
https://github.com/Daniele-Cangi/Butterfly
Butterfly is open source under the MIT License.