diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8ec76dc..8468d2f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -19,9 +19,15 @@ workflow are in [AGENTS.md](../AGENTS.md); architecture decisions are in [docs/a - 3D follows [docs/3D-Visualization-Standard.md](../docs/3D-Visualization-Standard.md). three.js is imported only from a `React.lazy` renderer and never reaches the entry bundle; `manualChunks` has no rule for it. - Motion respects `useReducedMotion` (`src/shared/hooks`). +### Playground +- There is exactly one Playground, at `/playground` (`src/features/playground/`). No module gets a playground of its own ([ADR 0004](../docs/adr/0004-playground-traces-share-the-snapshot.md)). +- A trace's steps and a story's beats hold the same snapshot and are drawn by the same 2D and 3D views. A lens chooses which regions are drawn; it never brings its own renderer. +- A trace is never presented as a story: no authored captions, and no garbage-collection sweep, because a run cannot observe one. +- JavaScript first; other languages emit the same snapshot. + ### Code - Icon props are typed `LucideIcon`: R3F's global JSX types make a bare `React.ElementType` reject `className`. -- Light mode only. Tailwind v4 can't see class names built at runtime (`bg-${color}-50`): write classes out literally or map them through a fixed object. +- Light mode only, except the Playground, which is dark ([ADR 0004](../docs/adr/0004-playground-traces-share-the-snapshot.md)). Tailwind v4 can't see class names built at runtime (`bg-${color}-50`): write classes out literally or map them through a fixed object. - Browser support: use Baseline widely available web features directly; a newer feature needs a fallback. The `modern-web-guidance` skill (vendored, read its files, never run its npx CLI) has the patterns. - Tests sit next to the code as `*.test.ts(x)`. diff --git a/AGENTS.md b/AGENTS.md index c3a5aff..884a683 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,9 +27,15 @@ After editing anything in `.agents/`, run `npm run agents:sync` and commit the g - 3D follows [docs/3D-Visualization-Standard.md](docs/3D-Visualization-Standard.md). three.js is imported only from a `React.lazy` renderer and never reaches the entry bundle; `manualChunks` has no rule for it. - Motion respects `useReducedMotion` (`src/shared/hooks`). +### Playground +- There is exactly one Playground, at `/playground` (`src/features/playground/`). No module gets a playground of its own ([ADR 0004](docs/adr/0004-playground-traces-share-the-snapshot.md)). +- A trace's steps and a story's beats hold the same snapshot and are drawn by the same 2D and 3D views. A lens chooses which regions are drawn; it never brings its own renderer. +- A trace is never presented as a story: no authored captions, and no garbage-collection sweep, because a run cannot observe one. +- JavaScript first; other languages emit the same snapshot. + ### Code - Icon props are typed `LucideIcon`: R3F's global JSX types make a bare `React.ElementType` reject `className`. -- Light mode only. Tailwind v4 can't see class names built at runtime (`bg-${color}-50`): write classes out literally or map them through a fixed object. +- Light mode only, except the Playground, which is dark ([ADR 0004](docs/adr/0004-playground-traces-share-the-snapshot.md)). Tailwind v4 can't see class names built at runtime (`bg-${color}-50`): write classes out literally or map them through a fixed object. - Browser support: use Baseline widely available web features directly; a newer feature needs a fallback. The `modern-web-guidance` skill (vendored, read its files, never run its npx CLI) has the patterns. - Tests sit next to the code as `*.test.ts(x)`. diff --git a/CONTEXT.md b/CONTEXT.md index 2572ee9..83150cb 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -8,20 +8,48 @@ A site that teaches programming by showing, step by step, what code and tools do A visualization of one concept, told as a sequence of beats and drawn literally (frames, objects, commits, refs), never as an everyday metaphor. _Avoid_: Animation, demo, walkthrough +**Snapshot**: +The full state at one moment: every entity present, with its values and references. A snapshot is what a view draws. +_Avoid_: State, frame, diff + **Beat**: -One moment of a story: a full snapshot of every entity present, with a title, a caption and a shot. -_Avoid_: Slide, frame, stage +One authored moment of a story: a snapshot with a title, a caption and a shot. +_Avoid_: Slide, frame, stage, step (a step is recorded, not authored) + +**Beat model**: +The pure data behind a story: its beats in order, or for a scenario its paths, each beat holding one snapshot. +_Avoid_: Step model (the old name), script, storyboard + +**Region**: +A fixed area of the scene that holds one kind of entity, such as the stack or the heap, or one cast member's copy of the system. +_Avoid_: Zone, panel, layer + +**Entity**: +One thing drawn in a snapshot, such as a frame, an object or a commit, with an identity that stays the same from one snapshot to the next. +_Avoid_: Node, item, element + +**Cell**: +An entity's fixed position within its region, the same in every view. +_Avoid_: Coordinate, grid position -**Step model**: -The pure data behind a story, one snapshot per beat, that every view renders. -_Avoid_: Script, storyboard +**Slot**: +A named place in an entity that holds either a value or a reference. +_Avoid_: Field, property, variable + +**Reference**: +A link from a slot to another entity. +_Avoid_: Pointer, edge, arrow + +**Focus**: +The entities and slots a beat or step spotlights. An author chooses a beat's focus; a step's focus is whatever that step changed. +_Avoid_: Highlight, selection **View**: -One rendering of a step model: 2D (the default) or 3D. Switching views keeps the beat. +One way of drawing a snapshot: 2D (the default) or 3D. Switching views keeps the beat or step. _Avoid_: Mode, renderer (for the user-facing concept) **Shot**: -The part of the scene a beat is framed on: an overview, one region, or a bridge between regions. +A part of the scene that the author named and framed a beat on. An overview, one region and a bridge between regions are the common kinds. Steps have no shot. _Avoid_: Camera angle, zoom level **Scenario**: @@ -40,10 +68,50 @@ _Avoid_: Branch (overloaded with Git), fork, decision The linear run of beats that follows one option of a choice point. _Avoid_: Branch, route, track +**Timeline**: +The clock times at which a scenario's cast acts, in order. +_Avoid_: Schedule; never the steps of a trace + **Transcript**: -The running log of a scenario: at each beat, who acted, when, the command they ran, and what it printed. -_Avoid_: Console, log, terminal +The running log of a scenario: at each beat, who acted, when, the command they ran or the action they took, and what it printed. +_Avoid_: Log, terminal, console (the Console belongs to the Playground) **Hold view**: The learner's choice to keep their own camera across beats instead of each beat's shot. _Avoid_: Free camera, lock camera + +## Playground + +**Playground**: +The one place where a learner runs their own code and watches what it does to state. There is exactly one; no module has a playground of its own. +_Avoid_: Sandbox, editor, IDE, code lab + +**Trace**: +The record of one run of the learner's code in the Playground: a sequence of steps. A trace is recorded, never authored or approved, so it is not a story. +_Avoid_: Story, timeline, recording, execution + +**Step**: +One moment of a trace: the snapshot recorded when one statement ran, with the line that ran. +_Avoid_: Beat, snapshot (the snapshot is the state a step holds) + +**Console**: +What the learner's code printed during a run, in order. It has no cast and no clock times, so it is not a transcript. +_Avoid_: Transcript, output, log + +**Lens**: +The learner's choice of which part of a snapshot the Playground draws, such as the stack and heap or the stack and queues. A lens is independent of the view: any lens can be drawn in 2D or 3D. +_Avoid_: View, mode, visualization + +**Example**: +Prebuilt code the learner can load into the Playground to run and watch, with a suggested lens and no goal. +_Avoid_: Snippet, template, scenario, demo + +**Exercise**: +A task in the Playground with a goal, checks that pass or fail on the learner's code, and hints. +_Avoid_: Problem, challenge, kata, test + +## Modules + +**Marble diagram tool**: +The RxJS module's interactive diagram: the learner writes a stream as marbles, picks an operator and sees the stream it produces. It does not run the learner's code, so it is not a playground. +_Avoid_: RxJS playground, marble playground diff --git a/docs/RESEARCH/Playground Audit 2026-10.md b/docs/RESEARCH/Playground Audit 2026-10.md new file mode 100644 index 0000000..8e300d7 --- /dev/null +++ b/docs/RESEARCH/Playground Audit 2026-10.md @@ -0,0 +1,51 @@ +# Playground audit, October 2026 + +Findings from running `/playground` in a browser and reading `src/features/playground/` on 2026-10-11 (commit `51a0ecc`). This records what is true today. It is not a plan. + +## State of the 3D work + +- Three visualizations use the step model with a 2D view and a lazy 3D view: JavaScript "Stack ↔ Heap" (#168), Git "What is a branch, really?" (#170) and the Git "Ship a feature" scenario (#173). Shared viewer controls landed in #172. +- `docs/3D-Visualization-Standard.md` and ADR 0001 still say "Prototype" and gate rollout on the owner's sign-off, although Git has shipped two 3D views. +- The three 3D renderers (566, 834 and 1,047 lines) each redefine `PALETTE`, `damp`, `labelStyle`, `SHOTS`, `SCENE_BOUNDS` and `Scene`. The two Git renderers also duplicate `Label`, `CommitBlock`, `commitPos`, `parentArc` and the card constants. +- The canvas and 3D accessibility guides in the `modern-web-guidance` skill (#182) arrived after the 3D views and have not been applied to them. +- The standard's own follow-up is still open: replace the hand-authored Stack ↔ Heap story with traces from the playground's `JsInstrumenter`. + +## What happened when I ran the playground + +| Test | Result | +|---|---| +| Default JS snippet, Event Loop lens | 8 steps. Microtask and task queues are empty on every step. The `setTimeout` and `.then` callbacks are never stepped into. With an empty stack the header reads "Processing Task Queue". | +| Default JS snippet, console | The `setTimeout(…, 1000)` log never appears. | +| "Closures & Scope" example, Heap & Stack lens | 12 steps and no heap objects. At global scope the lens shows "No heap or stack data in current step". `increment()` is never entered and `count` never appears. | +| "Promise Chain" example (TypeScript) | Fails with `Unexpected token ':'`. No timeline. | +| Console output | Strings render as `"Hello, World!"`; the TypeScript error renders as `'`. | +| 1024px-wide viewport | The toolbar clips and the Run button is off screen. | + +Not tested: Python (needs the Pyodide download), the Data Structures lens, the Streams lens, touch input, reduced motion. + +## Causes in the code + +- **Empty heap and queues.** The tracker runtime in `services/instrumentCode.ts` writes `heapObjects: []`, `microtaskQueue: []` and `macrotaskQueue: []` on every step. `instrumentation/PythonInstrumenter.ts` does the same. `HeapStackLens` and `EventLoopLens` read those fields, so they have nothing to draw. +- **No object identity.** `safeClone` deep-copies each value (depth 3, 20 keys) on every step. Two variables that point at one object become two unrelated copies, so a reference cannot be drawn. +- **Functions inside expressions are not traced.** `instrumentNode` only descends through statements. Arrow callbacks, object methods and function expressions get no `enterFunction`/`step` calls. +- **One flat variable list.** `collectScopeVars` captures the variables visible at the current statement. Frames below the top of the stack carry no variables. +- **Results are reported too early.** `SandboxFrame.tsx` posts the result 50 ms after the synchronous code ends, so later timers never reach the console or the timeline. +- **TypeScript is not transpiled.** `handleRun` in `PlaygroundApp.tsx` passes TypeScript source straight to acorn and the sandbox. A comment in `hooks/useSandbox.ts` says the code "should be transpiled before passing to execute". +- **Different vocabularies.** The story step model (`StoryStep`: frames with slots, `refId`, heap objects with stable cells and states, a camera shot) is richer than the playground's `StateSnapshot`. The two also both export a type named `StackFrame` with different shapes. +- **Different themes.** The playground is dark (`data-theme="playground"`); the stories and the 3D palette are light, and AGENTS.md says "Light mode only". +- **Different renderers.** Playground lenses use React Flow or plain DOM; stories use SVG and react-three-fiber. + +## Other playgrounds in the repository + +Reported by an explorer agent from reading the code; nothing here was run in a browser. + +- **`src/components/playground/` (old).** A LeetCode-style shell (problem, tests, submit, score, debug stepper) that never runs the user's code: Run, Test and Submit are `setTimeout` mocks (`Playground.tsx:65-97`) and Submit always returns "accepted" with score 100. Its only real execution path (`utils/instrument.ts`, `new Function` on the main thread, no sandbox or timeout) is unreachable. + - Mounted by `features/bigo/BigOPage.tsx` and `features/datastructures/DataStructuresPage.tsx`, linked from `Sidebar.tsx`, `moduleNavigation.ts` and both modules' Introduction sections, and advertised in the README. + - Worth carrying over: the problem/test/hint schema (`types/playground.ts`), `data/problems.ts` (1,266 lines, imported by nothing), the `ProblemDisplay` and `TestRunner` UI, and the achievement logic in `utils/scoring.ts`. + - Dead: `PerformanceAnalyzer` and `hooks/usePlayground.ts` (never imported, fabricated numbers). + - `utils/instrument.ts` is also used by the JavaScript `CallStack.tsx` and `MemoryHeap.tsx`, so it cannot be deleted with the rest. +- **`src/components/rxjs/playground/`.** Mounted only in the RxJS Visualization Tool section. `MarblePlayground` works but simulates operators by pattern matching (`map`, `filter`, `take` only) rather than running RxJS. `CodePlayground` prints canned output. + +## A limit to keep in mind + +JavaScript cannot observe real garbage collection. A tracer can compute which objects became unreachable from the roots, which is what the Stack ↔ Heap story's tests already do, but it cannot show an actual sweep. diff --git a/docs/RESEARCH/Playground Design Research.md b/docs/RESEARCH/Playground Design Research.md new file mode 100644 index 0000000..79181dc --- /dev/null +++ b/docs/RESEARCH/Playground Design Research.md @@ -0,0 +1,157 @@ +# **Playground Design Research: Tracing, Visualization and Practice** + +Research for turning `/playground` into the "practice after theory" place. Compiled 2026-10-11 from primary sources where they could be reached (tool source code, official docs, the authors' papers). Anything not confirmed from a primary source is tagged **(unverified)**. Observations about this repository come from reading the code, not from running it. + +## **Summary** + +The tools that teach best (Python Tutor, Thonny, Loupe) all do the same three things: they run the program to completion under a hard step limit, record a **full snapshot of frames and heap at every step** with objects named by small stable ids, and let the learner scrub forwards and backwards in the browser. Python Tutor's published trace format is almost the same shape as this project's story step model (`frames` with slots that hold a value or a `refId`, `objects` keyed by id), so the cheapest high-value move is to make the playground tracer emit that shape and let the existing 2D and 3D story renderers act as lenses. No reviewed tool shows heap identity and the microtask/task queues together for arbitrary code: Python Tutor refuses async entirely, and the event-loop tools (Loupe, JS Visualizer 9000) show no heap. The gap is real and reachable with deeper AST instrumentation plus a traced event loop inside the sandbox; an interpreter is not required. The research on learning is consistent that what the learner *does* with a visualization (predicting, answering, changing) matters more than what it draws, and the evidence for 3D is mixed: it helps for structural overview and hurts through occlusion, navigation cost and clutter, which supports the house rule that 3D is optional and only where depth carries meaning. + +## **A. Comparison of tools** + +| Tool | What it shows | How it traces | Limits | What we could borrow | +|---|---|---|---|---| +| **Python Tutor** (Guo) | Frames, heap objects, pointers, closures' parent frames, stdout, per line | Python: runs under `bdb`, records a complete stack+heap snapshot per executed line, sends one JSON trace; stepping is client-side [S1]. JS: Node run with `--expose-debug-as=Debug`, stepping through V8 debug break events; object ids are a hidden `smallObjId_hidden_` property added with `Object.defineProperty` [S3] | 1,000 steps (originally 300) [S1][S3]; JS backend is server-side, ES6 only, Node v6, "very brittle" per its own header [S3][S5]; **no async, timers, promises, DOM** [S4]; closures are inlined as `parent:x` because V8 scopes are not tied to frames [S3]; not for mobile [S4] | Trace format (`REF` ids, `frame_id`, `parent_frame_id_list`, `is_zombie`, event types incl. `instruction_limit_reached`) [S2]; layout heuristics: append new objects at the bottom, never move a rendered object [S1]; "fits on a blackboard" scope [S4] | +| **Loupe** (Roberts) | Call stack, Web APIs, callback queue, render queue | `falafel` rewrites `CallExpression`/`BinaryExpression` with before/after hooks; a busy-wait `delay` slows execution so the animation keeps up [S6] | Live animation, not a recorded trace; no microtask queue component in the source tree [S6]; no heap. Web-worker execution is **(unverified)**: described in a secondary write-up only | Expression-level hook points (before/after a call) rather than statement-only | +| **JS Visualizer 9000** (Hopding) | Call stack, task queue, microtask queue | **Server-side**: Node `worker_threads`, `async_hooks` (`init/before/after/promiseResolve`) for promises, timeouts and microtasks, plus Babel/`falafel` function enter/exit wrapping, inside `vm2` [S7] | Needs a server; `async_hooks` does not exist in browsers; no heap or variables; `vm2`'s own README says sandbox escapes keep being found [S8] | The event vocabulary: `InitPromise`, `ResolvePromise`, `EnqueueMicrotask`, `DequeueMicrotask`, `InitTimeout`, `BeforeTimeout`, `EnterFunction`, `ExitFunction` [S7] | +| **ui.dev JavaScript Visualizer** | Execution contexts, hoisting, scopes, closures | **(unverified)**: no primary description found | **(unverified)** | The creation-phase vs execution-phase framing, if confirmed | +| **Lydia Hallie, "JavaScript Visualized"** | Hand-authored animations of event loop, promises, hoisting | Not traced: made in Keynote and screen-recorded, per the author's own comment [S9] | Fixed examples | Pacing and one-idea-per-frame; proof that authored scenarios teach well alongside free code | +| **Algorithm Visualizer** | Arrays, graphs, logs for algorithms | Author calls tracer APIs (`Array1DTracer.select/patch`, `Tracer.delay()`); `tracers.*` libraries "extract visualizing commands from code" [S10] | Learner must annotate code; only what is annotated is shown | An opt-in `viz.*` API for data-structure lenses where automatic detection is unreliable | +| **VisuAlgo** | Pre-built algorithm animations with pseudo-code | **(unverified)**: about page not reachable | Not arbitrary code **(unverified)** | E-lecture mode and auto-generated quiz questions **(unverified)** | +| **Recursion tree visualizers** (e.g. brpapa) | Call tree of a recursive function | **(unverified)**: tagged `aws-lambda`, so likely server-side | Third-party reports: function must be named `fn`, globals break it **(unverified)** | A call-tree lens derived from enter/exit events we already record | +| **Thonny** | Expression-by-expression evaluation; each call in its own window with locals; optional "name → id → value" heap mode [S11] | Desktop Python IDE; mechanism **(unverified)** | Not web | Two fidelity levels for the same run: simple values first, references on demand | +| **Replay.io** | Time-travel debugging of a recorded browser session | Records non-deterministic inputs at the system-library boundary and re-executes deterministically; determinism is targeted at the JS level [S12] | Needs a custom runtime | The principle only: record once, re-derive any view afterwards | +| **rr** | Record and reverse-execute native processes under gdb | Records kernel inputs and non-deterministic CPU effects; emulates a single core [S13] | Linux, specific CPUs | Reverse "who last wrote this?" queries are possible on a snapshot trace too | +| **Quokka / Wallaby** | Inline values next to code, value explorer | Transpiles, then "adjusts your code" (instrumentation), runs in Node [S14] | Editor plugin; "Time Machine" details **(unverified)** | Inline value annotations in the editor gutter for the current step | +| **StackBlitz WebContainers** | Full Node toolchain in a tab | Browser-based Node runtime [S15]; internals and header requirements **(unverified)** | Commercial terms for non-OSS use; no tracing API documented | Not a fit: heavy, and it does not expose state | +| **Sandpack** (CodeSandbox) | Bundled preview in an iframe | Client talks to a hosted bundler iframe by `dispatch`/`listen` messages [S16] | Depends on a hosted bundler by default; no state tracing | The iframe message protocol pattern we already use | +| **Observable runtime** | Reactive cells | Dataflow: variables recompute when inputs change; observers get `pending/fulfilled/rejected` [S17] | Different execution model from plain JS | "Change an input, everything downstream updates" for parameterised scenarios | +| **JS-Interpreter** (Fraser) | n/a (engine) | Interpreter in JS with `step()`, exposed state stack and scopes, serialisable state [S18] | **ES5 only**, about 200x slower than native, no `let`, no promises [S18] | Reference for how an interpreter exposes scopes | +| **Sval** | n/a (engine) | Acorn-based interpreter, "latest" ES [S19] | No step or hook API documented [S19] | Little without forking | +| **quickjs-emscripten** | n/a (engine) | QuickJS compiled to WASM; host drives the promise job queue with `executePendingJobs()`, interrupt handler, memory limit [S20] | No stepping, debugger or heap-inspection API; about 500 KB to 1.3 MB of WASM [S20] | Host-controlled job queue is the cleanest proof that "we own the event loop" is the right model | +| **Jalangi2** | n/a (analysis framework) | Source instrumentation with analysis callbacks; ships a shadow-memory analysis (`SMemory.js`) [S21] | Targets ES5.1; ES6 untested [S21] | The design: every operation calls a hook, analyses are plug-ins | + +## **B. Techniques for the gaps** + +**B1. Object identity and the heap graph.** Python Tutor's JS backend tags each object once with a hidden non-enumerable id and emits `["REF", id]` wherever it is referenced, so two variables pointing at one object visibly share it [S3]. In the browser a `WeakMap` does the same without touching the object: keys are held weakly, compared by reference, and the feature is Baseline widely available [S22]. The current tracker (`services/instrumentCode.ts`) deep-clones values with `safeClone`, which destroys identity: `a` and `b` aliasing one array become two copies. +*Suggestion:* replace `safeClone` with an encoder that assigns ids from a `WeakMap`, walks from the frame variables (depth and node caps as today), and writes each object once into `heap[id]` with slots that are either a primitive or `{ refId }`. Reachability from frames is then computed, not guessed, which is exactly what `stackHeapStory.ts` asserts in its tests. + +**B2. Closures and scope chains.** Python Tutor models this with `frame_id`, `parent_frame_id_list` and `is_zombie` (a returned frame kept alive by a closure) [S2]; its V8-based JS backend could not tie scopes to frames and fell back to inlining [S3]. Instrumentation can do better than the debugger here because we control function entry. +*Suggestion:* on `enterFunction`, allocate a scope record with an id and the id of the scope where the function was *created* (capture it when the function expression is evaluated, stored in a `WeakMap`). A scope that is still referenced by a live function after its frame pops stays in the snapshot as a "retained scope". This draws the scope chain literally. + +**B3. Microtask and task queues.** The ordering rules are short: after each task, when the stack is empty, the whole microtask queue drains, including microtasks queued while draining, before the next task [S23]. Three ways to observe it: +- *Engine hooks:* `async_hooks` gives exact events but is Node-only [S7]. +- *Patching globals in the sandbox:* wrap `setTimeout`, `queueMicrotask` and `Promise.prototype.then` so each records enqueue, start and end. Native `await` does not go through a patchable `then`; Angular's docs say `async/await` "cannot be patched effectively" by ZoneJS and must be downleveled [S24]. With an AST pass we can instead rewrite `await x` to report suspend and resume around it. +- *Owning the loop:* QuickJS shows the model: nothing asynchronous runs until the host calls `executePendingJobs()` [S20]. +*Suggestion:* inside the sandbox, replace `setTimeout`/`setInterval`/`queueMicrotask` with traced versions backed by our own queues and a virtual clock, and drive them with a small loop (run script, drain microtasks, take next task). Promise reactions are recorded by wrapping `then`/`catch`/`finally` and by the `await` rewrite. Timers then fire instantly and deterministically, and the queue contents at every step are known rather than inferred. Validate the loop against the MDN/HTML ordering examples as unit tests. Whether a wrapped `then` plus `await` rewrite reproduces every native ordering edge case is **(unverified)** and needs a test corpus. + +**B4. Step limits and infinite loops.** Python Tutor stops at a fixed number of executed lines and emits a dedicated `instruction_limit_reached` event with a readable message [S1][S2]. `loop-protect` inserts a time check into every loop and cannot catch runaway recursion [S25]. Pyodide can only be interrupted from another thread through a `SharedArrayBuffer`, which needs a worker and cross-origin isolation headers [S26]. Local observation: `__tracker__.step` currently *returns silently* once `MAX_STEPS` is reached, so an infinite loop keeps spinning without recording, and the in-iframe `setTimeout` watchdog cannot fire while the same thread is busy. +*Suggestion:* make the step budget *throw* a `StepLimitError` (counted on every step, loop iteration and function entry), surface it as a final trace event, and keep the partial trace. As a hard backstop run JS in a Web Worker inside the sandboxed iframe and `terminate()` it on timeout. For Python, count steps in the trace function and raise; treat the `SharedArrayBuffer` interrupt as optional. + +**B5. TypeScript without a heavy compiler.** Type stripping replaces annotations with whitespace so line and column numbers are unchanged and no source map is needed; this is how Node runs `.ts` and how `ts-blank-space` works [S27][S28]. It rejects enums, runtime namespaces, parameter properties and `import =` [S27][S28]; `erasableSyntaxOnly` flags the same constructs [S29]. Sucrase strips types too, also handles enums, and does not type-check [S30]. Local observation: a grep found no transpile step in the playground; `useSandbox.ts` only says TS "should be transpiled before passing to execute", and `acorn` cannot parse annotations. +*Suggestion:* position-preserving stripping is the right fit because the tracer reports original line numbers. Options, cheapest first: (1) ask Monaco's bundled TypeScript worker for emit output, no new dependency, line preservation **(unverified)**; (2) a stripping library (new dependency, owner approval and the `add-dependency` skill apply). Either way, restrict playground TS to erasable syntax and say so in the UI. + +**B6. Python heap under Pyodide.** Python Tutor's Python trace is the reference: full stack and heap per line, heap keyed by object id [S1][S2]. CPython 3.12 added `sys.monitoring` with `LINE`, `PY_START`, `PY_RETURN` and per-location disabling [S31]; the playground pins Pyodide v0.27.7, whose Python version is **(unverified)** (recent Pyodide releases ship 3.13 and 3.14 [S32]). +*Suggestion:* keep `sys.settrace`, and in the trace callback encode `frame.f_locals` with `id(obj)` as the heap key into the *same* snapshot schema as JS, so every lens works for both languages. Follow Python Tutor's choice to draw immutable primitives inline in the frame to avoid arrow clutter [S4]. Consider `sys.monitoring` only if tracing speed becomes a problem. + +## **C. 3D and spatial visualization** + +- **Guided camera, text-led.** Ciechanowski's articles pair one mechanism per figure with prose, sliders that scrub state, drag-to-rotate, colour-coded named parts and a global pause [S33]. Bycroft's LLM visualization is a 3D model of a network "running inference" [S34]; its walkthrough UI could not be read by the fetch tool, so the guided-camera description in the 3D standard is **(unverified)** here. +- **Evidence for 3D.** CodeCity's controlled experiment (41 participants) found a statistically significant gain in correctness and reduction in completion time for program-comprehension tasks [S35]; effect sizes and the baseline tool are **(unverified)**. Against that, Cockburn and McKenzie found spatial-memory performance fell as users gained more freedom to use the third dimension, and users rated those interfaces more cluttered (abstract only) [S36]. Shneiderman argues that disorienting navigation, complex actions and occlusion slow people down, and that good 3D interfaces need overviews, fewer actions and prompt feedback [S37]. A 2026 VR study of software-evolution visualization concluded immersive views do not universally beat traditional ones (search summary only, **unverified**) [S38]. +- **Reading of the evidence (inference).** 3D earns its place for *structure with a meaningful third axis* and loses on precise reading, text and anything occluded. For program state the candidates are: stack above heap with references crossing the gap; scope chain as nested depth; the event loop as lanes (stack, microtasks, tasks) with callbacks moving between them. Flat lists, arrays and variable tables gain nothing. +*Suggestion:* in the playground, offer 3D only on lenses that pass that test (heap/stack and scope chain first), reuse `src/shared/viz3d/` and the shared toolbar, and derive each step's shot automatically from the trace: frame the frame or object that changed, which is the `focus` field of the story step model. Keep 2D the default and keep labels in the DOM. + +## **D. Pedagogy for a practice playground** + +- **Engagement beats graphics.** The meta-study of 24 algorithm-visualization experiments concluded that how learners use the technology matters more than what it shows (abstract and secondary summaries) [S39]. The ITiCSE engagement taxonomy orders activity as no viewing, viewing, responding, changing, constructing, presenting [S40]. Guo's own paper lists "question prompts, mini-quizzes, textual annotations" as the missing active-engagement features [S1]. +- **Predict before run.** PRIMM (Predict, Run, Investigate, Modify, Make) was evaluated with 493 students aged 11 to 14 in 13 schools; the PRIMM group outperformed the control on the post-test [S41]. +- **References are the core misconception.** In a study of 90 first-year students, about one third held non-viable models of value assignment and only 17% held a viable model of reference assignment; visualization combined with *cognitive conflict* (confronting a wrong prediction) worked better than visualization alone [S42]. +- **Async misconceptions.** No peer-reviewed study of novice misconceptions about the JS event loop was found. The commonly cited ones (a zero-delay timer runs immediately; promises are threads; timer delay is exact; promise callbacks and timers run in call order) come from tutorials and are **(unverified)** as research findings. MDN's ordering example is a ready-made prediction exercise [S23]. +- **Explorables and learnable programming.** Victor: show the state, make flow visible and controllable through time, start concrete, and note that live re-execution alone "misses the point" [S43]; explorable explanations let the reader change the author's assumptions [S44]. +- **Parsons problems.** The 2022 ITiCSE review covers the literature; a 2023 follow-up reports evidence for learning common patterns and for distractors (search summaries only, **unverified**) [S45]. +*Suggestion:* add a "Predict" mode to the timeline: before a marked step the view freezes and asks one question whose answer is in the next snapshot ("what does `b[0]` hold now?", "which callback runs next?"), then reveals it. Questions can be generated from the trace: aliasing questions wherever two slots share a `refId`, ordering questions wherever both queues are non-empty. Link lessons to the playground with a URL that carries snippet id, lens and step (the existing `?scenario=` convention), and make each example a short PRIMM ladder: predict, run, investigate with the lens, then a "modify so that…" task checked against the console output or final heap. + +## **E. Layout and accessibility** + +- **Layout precedent.** Python Tutor puts code and the step slider on the left and frames/objects on the right, with every interaction after the trace arrives happening client-side [S1]. It states plainly that it is not for mobile [S4]. +- **Small screens.** Monaco's FAQ answers "No" to mobile browser support [S46]; CodeMirror 6 advertises native selection and editing on phones and screen-reader support [S47]. WCAG 2.2 Reflow requires no two-dimensional scrolling at 320 CSS px, with an exception for content such as diagrams and editing interfaces that need two dimensions [S48]. +- **Canvas and 3D.** The HTML spec requires canvas fallback content that "conveys essentially the same function or purpose" and a one-to-one mapping from interactive regions to focusable fallback elements [S49]. The vendored `modern-web-guidance` guides describe HTML-in-Canvas (`layoutsubtree`) for exposing canvas content, which needs a feature check and a fallback (local files, not Baseline widely available). +*Suggestion:* desktop: editor left, visualization right, timeline as a full-width strip under both, console as a collapsible drawer. Below about 768 px: one pane at a time behind tabs (Code, Visualize, Console) with the timeline pinned at the bottom, and a read-only highlighted code view plus a plain `