src — reactivity

tiny-test.mjs     test runner (plumbing)
spec.mjs          executable specification — run this
reactivity.js     ← YOU WRITE THIS (mini-vue)
signals.js        ← YOU WRITE THIS TOO, independently
node spec.mjs

Required exports from reactivity.js

reactive(obj)   ref(value)   effect(fn)   computed(fn)   watch(source, cb)   batch(fn)

effect(fn) runs fn immediately. batch(fn) flushes queued effects once at the end.

The two tests that matter

Stage 3 — an effect that never read b must not re-run when b changes. If that fails you have built "re-run everything on any write," which is not dependency tracking. Everything else in this module is downstream of getting stage 3 right.

Stage 5 — the stale-dependency test. An effect reads a or b depending on a flag; after the flag flips, writing the no-longer-read property must not trigger it. Failing here is a dependency leak: effects accumulate dependencies forever and fire on unrelated writes. This is precisely the bug that cleanup-before-re-run exists to prevent, and it is much more convincing after you have watched it happen.

Stage 6 fails if you use a single global "current effect" instead of a stack — the inner effect steals the outer's dependencies.

Then build signals independently

Write signals.js from scratch with a minimal API (signal, computed, effect, batch). Do not refactor the Vue code into it. The point is discovering how much of reactivity.js was essential and how much was Vue-specific. Copy this spec to signals.spec.mjs and adapt the API surface; the behavioural requirements should be identical, which is itself the finding.