Software Design Document: Practice Thy Algorithms (Browser Edition)
Table of Contents
- Introduction and Overview
- Goals and Non-Goals
- System Architecture
- Data Design
- Component Design
- Interface Design
- User Interface Design
- Assumptions and Dependencies
- Alternatives Considered
- Milestones and Rollout
- Future Work
- Glossary
- References
1. Introduction and Overview
1.1 Purpose
Practice Thy Algorithms is currently a multi-language (JavaScript, Python,
Ruby) algorithm-practice repository. Solving problems requires cloning the repo
and installing each language's toolchain locally (npm install, a Python
interpreter, bundle install).
This document describes a rewrite that lets users solve and verify the same
problems entirely in the browser — no clone, no local language installs, no
backend server. The application is a static site hosted on GitHub Pages,
modeled on the sibling project
build-your-own-alu.
1.2 Scope
Version 1.0 includes:
- In-browser problem browsing and selection (the existing 22 problems).
- In-browser code editor with syntax highlighting.
- In-browser execution and test verification for JavaScript and Ruby.
- Per-user progress/solution persistence (client-side).
- Static deployment to GitHub Pages via GitHub Actions.
Stretch goal: Python support.
Out of scope (v1.0): see §2.2 Non-Goals.
1.3 Background and Context
The existing repository already provides the most important asset for this
rewrite: a centralized problem-definition system. Shared/problems.json is
the single source of truth for problem statements, function signatures (per
language), parameters, return types, and test cases — including comparison
metadata (exact, unordered_array, set_equality). Today, language-specific
generators in Shared/generators/ convert this JSON into Jest / unittest /
RSpec test files that run on the local CLI.
The browser rewrite reuses problems.json directly and replaces the "generate
test files and run a local test runner" workflow with "run user code and test
cases in a sandboxed in-browser language runtime."
1.4 Primary Features
- Browse a catalog of algorithm problems with academic-style problem statements.
- Choose a language (JS / Ruby; Python as stretch) per problem.
- Write a solution in an editor pre-seeded with the correct function signature.
- Run the bundled test cases and see pass/fail feedback instantly, client-side.
- Resume previous work (solutions persist locally).
1.5 Target Audience
The primary user is the solo self-learner — an individual practicing
algorithms (e.g., for interview prep) on their own. This implies a
single-user, local-only model: no accounts, no authentication, no sharing,
and no social/leaderboard features. This reinforces the static, client-only
architecture (§3) — there is no per-user server state
to manage.
2. Goals and Non-Goals
2.1 Goals
- G1. Zero-install: a learner can solve a problem with only a web browser.
- G2. Reuse
Shared/problems.json as the single source of truth — no
divergence between problem definitions and what runs.
- G3. Support JavaScript and Ruby execution and verification in v1.0.
- G4. Deploy as a fully static site on GitHub Pages (no backend, no hosting
cost, no server maintenance).
- G5. Reuse the proven engineering patterns and deployment pipeline of the
sibling build-your-own-alu project.
2.2 Non-Goals
- Server-side execution, accounts/auth, sharing, or social/leaderboard features
— everything stays client-side and single-user (see §1.5).
- Arbitrary/freeform code execution beyond the defined problem set.
- A mobile-first or native app experience (best-effort responsive is sufficient).
- Maintaining the legacy local CLI workflow. The browser app deprecates and
replaces the CLI test-runner workflow (npm test / rspec / unittest); the
README and local setup are updated/archived at cutover (see
§10).
- Expanding or changing the problem set in v1.0. v1.0 is a strict port of the
existing 22 problems — no additions, removals, reordering, or statement
rewrites. Content curation is a post-v1.0 follow-up.
3. System Architecture
3.1 High-Level Architecture
A single-page, fully static web application. All computation — including running
user code and executing test cases — happens in the browser. The only "backend"
is GitHub Pages serving static assets.
┌─────────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Problem │ │ Code Editor │ │ Results / Test │ │
│ │ Catalog UI │ │ (CodeMirror 6)│ │ Feedback Panel │ │
│ └──────┬───────┘ └──────┬────────┘ └─────────▲─────────┘ │
│ │ │ │ │
│ ▼ ▼ │ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ App Core (state, routing) │ │
│ │ - loads problems.json │ │
│ │ - localStorage persistence │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ │ dispatch run │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Execution Layer (Web Workers) │ │
│ │ ┌────────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ JS Runner │ │ Ruby Runner │ │ Python Runner │ │ │
│ │ │ (native) │ │ (ruby.wasm) │ │ (Pyodide)* │ │ │
│ │ └────────────┘ └──────────────┘ └───────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼ (static hosting only)
┌──────────────┐
│ GitHub Pages │
└──────────────┘
* Python runner is a stretch goal.
3.2 Technology Stack
| Concern |
Choice |
Notes |
| Build tool / dev server |
Vite |
As in build-your-own-alu. |
| UI framework |
React |
Component model and declarative state for the run lifecycle, results panel, and hint reveals. |
| Code editor |
CodeMirror 6 |
Framework-agnostic; wrapped in a React component. @codemirror/lang-javascript for JS, @codemirror/legacy-modes (Ruby) for Ruby; switched via a CodeMirror Compartment. |
| App language |
TypeScript |
Type safety for the LanguageRunner contracts and JS↔WASM marshalling. |
| Unit testing (of the app) |
Vitest |
As in build-your-own-alu. |
| E2E testing |
Playwright |
Minimal smoke suite (load app, run a JS solution → pass, run a Ruby solution → pass) to cover the WASM-in-Worker path unit tests can't. |
| JS user-code execution |
Native eval/Function in a Web Worker |
No runtime download. |
| Ruby user-code execution |
ruby.wasm (@ruby/wasm-wasi + @ruby/3.4-wasm-wasi, the ruby+stdlib.wasm build → CRuby 3.4) |
Lazy-loaded (~30 MB), self-hosted via Vite ?url; +stdlib needed for json/set/stringio. |
| Python user-code execution (stretch) |
Pyodide (CPython → WASM) |
Lazy-loaded. |
| Persistence |
localStorage |
— |
| Hosting / CI |
GitHub Pages + GitHub Actions |
Static deploy. |
3.3 Execution Model and Sandboxing
- User code runs inside Web Workers to keep the UI responsive and to isolate
long-running or infinite-loop solutions (the worker can be terminated on
timeout).
- Each language runner loads its runtime lazily — the multi-MB Ruby/Python WASM
payloads are fetched only when that language is first selected.
- A per-run timeout/watchdog terminates runaway user code.
Payload budget: lazy-load, no hard size cap. The initial page stays light —
the JavaScript path needs no runtime download and loads fast. The Ruby (and
later Python) WASM runtimes are fetched only when that language is first
selected, behind a loading indicator, accepting whatever size the upstream
runtime is. (As built, the Ruby ruby+stdlib.wasm binary is ~30 MB, self-hosted
as a hashed Vite asset emitted under the Pages base path — no CDN dependency.)
Optimizing WASM payload size is not a v1.0 concern; it can be revisited if
first-Ruby-run latency proves painful.
3.4 Deployment Architecture
- A push to
master triggers a GitHub Actions workflow: install dependencies →
vite build → publish dist/ to GitHub Pages.
- The base path is configured for project pages
(jaysonvirissimo.github.io/practice-thy-algorithms/).
- Fully static; no environment secrets or runtime config required.
4. Data Design
4.1 Source of Truth: problems.json
The existing Shared/problems.json schema is reused largely as-is. Per problem:
{
"two_sum": {
"title": "Two Sum",
"description": "Implement a function that locates a pair of elements ...",
"complexity": "O(n)",
"parameters": [ { "name": "...", "type": "...", "description": "..." } ],
"returnType": { "javascript": "...", "ruby": "...", "python": "..." },
"functionSignatures": { "javascript": "function twoSum(nums, target)",
"ruby": "def two_sum(nums, target)",
"python": "def two_sum(nums, target)" },
"hints": [ "Approach hint 1 (language-agnostic) ...", "Approach hint 2 ..." ],
"testCases": [
{ "input": { "nums": [2,7,11,15], "target": 9 },
"expected": [0,1],
"description": "...",
"comparison": { "mode": "exact|unordered_array|set_equality",
"type": "deep_equality" } }
]
}
}
hints is a new optional, top-level (language-agnostic) field added for the
browser app; see §5.6.
4.2 How the Browser Consumes Problem Data
problems.json is imported/bundled at build time (or fetched at runtime as a
static asset) and rendered into the catalog and editor.
- Function signatures seed the editor's starting code per language.
- Test cases drive the in-browser test harness (see §5.3).
- Comparison metadata (
exact, unordered_array, set_equality) is honored by
the in-runtime comparison logic instead of by generated test files.
4.3 User State and Persistence
State is local-only, with no portability. All state lives in localStorage;
there is no export/import, no URL sharing, and no cross-device sync (sync would
require a backend, and sharing is out of scope for the solo-learner audience —
see §1.5). Data is tied to a single browser profile.
Persisted state:
- Latest solution code per
(problem, language).
- Pass/fail / completion status per problem.
- Hint-reveal progress per problem (how many hints have been shown).
- UI preferences (theme; Vim-keybindings on/off toggle — see §7.3).
4.4 Known Data-Layer Gotchas (from current repo)
- Function-name inconsistencies: JSON signatures sometimes disagree with
what tests expect (documented in CLAUDE.md). The browser harness must invoke
the function name actually defined by functionSignatures, consistently across
languages.
- Ordering-sensitive results: handled by the
comparison.mode metadata.
5. Component Design
5.1 Problem Catalog
- Input:
problems.json.
- Output: a browsable/filterable list; selecting a problem loads it into the
workspace.
- Notes: shows per-language availability (all 22 problems exist in
JS/Ruby/Python today).
5.2 Editor Workspace
- Input: selected problem and language; seeded with the function signature.
- Output: the user's source code; emits "run" events.
- Built on: CodeMirror 6 (wrapped as a React component) with the appropriate
language mode.
- Features: optional Vim keybindings (persisted toggle) and a "reset to
signature" action; no autocomplete or syntax-guide panel in v1.0 (see
§7.3).
5.3 Test Harness
The core new component. Strategy: assert in runtime. Assertions run natively
inside the target language's VM, which then serializes a structured result back
to the host. This compares results using each language's own equality/type
semantics — critical for the linked-list problems (Reverse Linked List, Remove
Nth From End, Detect Cycle) where marshalling to JS would be lossy — and lets the
existing per-language comparison matchers (exact, unordered_array,
set_equality) port over largely intact. (Note: the problem set contains no tree
problems, so ListNode is the only non-primitive marshalled type in v1.0.)
As implemented (M1–M2): the two runtimes realize this differently.
- JavaScript — the harness is real worker code, not a generated string: a
pure, synchronous runHarness(userCode, problem) evaluates the user function
via new Function(...) and asserts using shared TypeScript comparators. Because
it is worker-agnostic, the exact same function is unit-tested on the main thread
under Vitest (workers don't run under jsdom).
- Ruby — a static
prelude.rb (ListNode, array↔list, comparators) is eval'd
once at VM boot, and a driver.rb (__ptap_run) is eval'd per run. The
metadata (function name, arg names, test cases) is passed in as base64-encoded
JSON embedded in an eval string and decoded + JSON.parsed in-VM — the
ruby.wasm JS bridge cannot pass a raw JS string as a Ruby argument, and base64
is injection-safe. (Python/Pyodide would mirror the Ruby shape.)
For a (problem, language, userCode) tuple:
- Compose a runnable program in the target language: user code plus a generated
harness. The harness receives the problem's test cases as a JSON string
and parses it inside the VM (JSON.parse in Ruby / json.loads in
Python) — inputs travel as data, never as code-generated literals, avoiding
quoting/injection issues and using one mechanism across all languages.
- For each test case the harness builds the
input, calls the problem's
function, and asserts the return against expected using comparison.mode.
- Execute it in the language's runner (worker).
- The harness serializes per-case outcomes (pass/fail, actual, expected, error)
plus timing into a JSON string and returns it as the eval's return value
across the runtime's JS bridge (ruby.wasm RbValue / Pyodide runPython
return) — not via stdout. This keeps the result protocol cleanly separated
from the user's own stdout.
- The host deserializes that JSON into the
TestRunResult contract
(§6.1) for rendering.
User stdout is captured separately from the result protocol and surfaced in
the results panel as debug output (see §5.5),
so learners can puts/print while debugging without corrupting the harness
result.
Comparison-mode parity. The three comparison modes (exact,
unordered_array, set_equality) are implemented natively in each language. To
keep the implementations from diverging, a single shared fixture
comparison-vectors.json holds { mode, a, b, equal } vectors that every
language's comparator must satisfy; it is exercised in CI so a divergence fails
the build. As built, the JS comparators run directly under Vitest, and the Ruby
comparators are exercised by booting ruby.wasm (Node, via the pure-JS
browser_wasi_shim) inside a Vitest test and running every vector through the
real in-VM __ptap_compare.
5.4 Language Runners
- JS Runner: native execution in a Worker; cheapest path, no download. Spawns
a fresh worker per run so an infinite loop can be hard-terminated (3 s
timeout); spawning is cheap.
- Ruby Runner:
ruby.wasm VM in a Worker; lazy-loaded WASM. Marshals inputs
(JSON) into Ruby values and results back out. Because VM boot is multi-MB and
takes seconds, it uses a persistent worker loaded once via init() and
reused across runs (10 s execution timeout); on timeout the worker is terminated
and the VM transparently re-booted on the next run.
- Python Runner (stretch): Pyodide VM; same lazy-load, persistent-worker, and
marshalling pattern as Ruby.
5.5 Results and Feedback Panel
- Renders per-test pass/fail, diffs of expected vs. actual, runtime errors, and
overall completion state; persists status to localStorage.
- Also displays the user's captured stdout (debug
puts/print output),
kept separate from the harness result protocol (see §5.3).
5.6 Hints
v1.0 includes progressive hints (not full guided walkthroughs). Hint content
is authored in problems.json as a hints field per problem, keeping the JSON
the single source of truth.
Hints are language-agnostic: a single set per problem, shared across
JavaScript, Ruby, and Python. They describe the approach/algorithm (e.g., "use
a hash map to remember complements") rather than language-specific syntax, so
the same hint sequence applies regardless of the selected language. hints is a
top-level field on the problem, not keyed by language (unlike functionSignatures
and returnType).
- Input: the selected problem's
hints array (independent of language).
- Output: progressively revealable hints (one at a time) in the problem pane.
- Persistence: the number of hints revealed is tracked in
localStorage
(see §4.3).
The field is optional and the UI degrades gracefully when it is absent, so
authoring hints for all 22 problems can trail the engine work.
6. Interface Design
6.1 Internal Interfaces
A common LanguageRunner contract keeps the app core language-agnostic:
interface LanguageRunner {
init(): Promise<void> // load runtime (lazy)
run(userCode: string,
problem: Problem): Promise<TestRunResult> // execute + verify
dispose(): void // tear down worker / free memory
}
TestRunResult = {
protocolVersion: number, // harness-protocol version (currently 1)
passed: boolean,
cases: Array<{ description, passed, expected, actual, error? }>,
durationMs: number,
runtimeError?: string,
stdout?: string // user debug output, captured separately
}
The in-VM harness asserts natively, then emits this exact JSON, identical across
JS/Ruby/Python:
{
"protocolVersion": 1,
"passed": false,
"durationMs": 12.3,
"runtimeError": null,
"stdout": "",
"cases": [
{ "description": "...", "passed": true,
"expected": <json>, "actual": <json>, "error": null }
]
}
expected/actual are display-only canonical serializations — the
pass/fail verdict is decided in-VM (so pointer structures like
ListNode/TreeNode are compared with native semantics), and these fields
exist only so the results panel can render a diff. Pointer structures
serialize to their array representation for display.
error is per-case (assertion detail or an exception raised while running
that case); runtimeError is reserved for load/parse/compile failures that
prevent any case from running.
protocolVersion lets the three emitters be version-guarded against drift.
6.2 External Interfaces
- None at runtime beyond fetching static assets (app bundle and WASM modules)
from GitHub Pages. No external APIs, no auth.
6.3 Data Contracts
- The
problems.json schema (see §4.1) is
the contract between content and app. Schema changes must remain
backward-compatible with the browser app.
7. User Interface Design
7.1 Layout
- Problem list / navigation (sidebar or top).
- Problem statement pane (title, description, complexity target, examples).
- Code editor pane (with language selector).
- Run button plus results/feedback pane.
The layout is desktop-first: optimized for desktop, not broken on tablet,
with no dedicated mobile layout. A code editor plus results panel needs
horizontal space, and the solo-learner audience is desktop-centric.
7.2 Key Flows
- Land → browse problems → pick one.
- Pick language → editor seeds the signature.
- Write solution → Run → see per-case feedback.
- Pass → problem marked complete (persisted); move on.
7.3 Visual and UX Details
Visual direction: a fresh, themed design — not bound to alu's aesthetics.
The UI adopts a medieval illuminated-manuscript / "holy book" aesthetic to
match the archaic project name Practice Thy Algorithms. Motifs: parchment/vellum
textures, blackletter display typography (paired with a legible body/monospace
face for code), illuminated drop-caps, marginalia-style annotations, and
ornamental borders.
Typography and palette ("Scriptorium"). All faces are OFL-licensed and
Google-Fonts hosted (GitHub-Pages friendly):
| Role |
Face |
| Display (headings only) |
UnifrakturMaguntia (blackletter) |
| Body |
EB Garamond (serif) |
| Code / editor |
JetBrains Mono |
| Palette role |
Hex |
| Parchment (background) |
#f4ecd8 |
| Ink (text) |
#2b2117 |
| Gold (illumination accent) |
#b8860b |
| Rubric (emphasis / errors) |
#8b2e2e |
UnifrakturMaguntia is ornate, so it is restricted to short display headings;
body copy uses EB Garamond for readability.
The code editor and results panel must stay highly legible — theming must not
compromise readability of code or pass/fail feedback. The CodeMirror editor
theme should harmonize with the manuscript palette while keeping strong syntax
contrast.
Editor features:
- Optional Vim keybindings — a toggle (CodeMirror extension); preference
persisted in localStorage (see §4.3).
- Reset to signature — a button that restores the editor to the starting
function signature for the current (problem, language), discarding the
in-progress attempt.
- No autocomplete in v1.0.
- No syntax-guide panel in v1.0.
8. Assumptions and Dependencies
8.1 Assumptions
- Users run a modern, WASM-capable, evergreen browser.
- The problem set and its test cases are correct and authoritative as defined in
problems.json today.
- Static client-side execution is acceptable — i.e., users could inspect or
bypass the test cases (acceptable for a practice tool).
8.2 Dependencies
- ruby.wasm (
@ruby/wasm-wasi + @ruby/3.4-wasm-wasi, ruby+stdlib.wasm) —
CRuby 3.4 compiled to WASM. Known limits: WASI Preview 1 (no sockets/networking,
no threads) — fine for self-contained algorithm problems. Booted via the
browser entry (pure-JS browser_wasi_shim); no COOP/COEP isolation needed,
so it runs on plain GitHub Pages.
- Pyodide (stretch) — CPython compiled to WASM.
- Vite, React, CodeMirror 6, Vitest, Playwright.
- GitHub Pages + GitHub Actions for hosting and CI.
8.3 Constraints
- No backend and no persistent server-side state.
- WASM payload sizes affect first-load UX for Ruby/Python (mitigated by lazy
loading).
9. Alternatives Considered
| Decision |
Options |
Chosen |
Rationale |
| Ruby in browser |
ruby.wasm (CRuby→WASM) vs. Opal (Ruby→JS transpile) vs. server-side |
ruby.wasm |
Runs real CRuby semantics client-side; matches the try.ruby-lang.org direction; no server. Opal diverges from real Ruby; a server breaks the static-hosting goal. |
| Test harness location |
Assert inside the WASM runtime vs. bridge values to JS and compare in JS |
Assert in runtime |
Native equality/type semantics and correct handling of ListNode/TreeNode returns; reuses existing per-language matchers. Trade-off: comparison logic maintained in three languages. |
| Reuse generators |
Keep generating CLI test files vs. run test cases live in the browser |
Run live in browser |
Avoids a build step; problems.json stays the single source of truth. |
| App language |
JavaScript vs. TypeScript |
TypeScript |
Type safety for runner contracts and JS↔WASM marshalling. |
| UI framework |
React vs. Preact / Svelte / Solid vs. near-vanilla |
React |
Familiar component model and declarative state for moderate interactivity. |
| Problem set (v1.0) |
Strict port vs. revisit content |
Strict port |
The existing 22 problems' passing tests are the regression baseline for trusting the in-browser runner; content curation deferred. |
| Python engine |
Pyodide vs. skip for v1.0 |
Defer (stretch) |
Keeps v1.0 scope tight. |
10. Milestones and Rollout
- M0 — Scaffold ✅ (done): Vite + React + TypeScript app and GitHub Pages CI
deploying a minimal themed page (proves the deploy pipeline).
- M1 — JS MVP ✅ (done): catalog, editor, JS runner, and in-runtime test
harness across all 22 problems.
- M2 — Ruby ✅ (done): ruby.wasm runner integrated and verified across all 22
problems, with comparator parity asserted in CI.
- M3 — Polish (next): persistence (localStorage of code/language/results),
feedback UX, and hints (seed hint sequences for 2–3 problems to exercise the UI;
the rest trail into v1.x). Vim-toggle persistence also lands here.
- M4 — Stretch: Python via Pyodide (same persistent-worker + in-VM harness
pattern as Ruby).
- M5 — Cutover: retire/archive the legacy CLI workflow and rewrite the README
around the browser app.
The M0→M5 ordering is firm; no calendar target dates are committed (solo,
spare-time project). Milestones advance in order as time allows. Current status:
M0–M2 complete and deployed; M3 is next.
11. Future Work
Items intentionally deferred beyond v1.0:
- Hint authoring for the full set of 22 problems (content task; the engine
degrades gracefully without it — see §5.6).
- WASM payload optimization if first-Ruby-run latency proves painful
(out of scope for v1.0 — see §3.3).
- Python support via Pyodide (M4 stretch).
- Content curation of the problem set (deferred post-v1.0 — see
§2.2).
12. Glossary
- ALU — Arithmetic Logic Unit; subject of the sibling reference project.
- CRuby (MRI) — the reference C implementation of Ruby.
- CodeMirror 6 — extensible in-browser code editor.
- GitHub Pages — static site hosting served from a GitHub repo.
- Pyodide — CPython compiled to WebAssembly for in-browser Python.
- ruby.wasm — WebAssembly ports of CRuby; runs Ruby in the browser.
- SPA — Single-Page Application.
- WASI — WebAssembly System Interface (Preview 1 in current ruby.wasm).
- WASM (WebAssembly) — portable binary instruction format runnable in browsers.
- Web Worker — background browser thread for isolating/parallelizing work.
13. References
Software Design Document: Practice Thy Algorithms (Browser Edition)
Table of Contents
1. Introduction and Overview
1.1 Purpose
Practice Thy Algorithms is currently a multi-language (JavaScript, Python,
Ruby) algorithm-practice repository. Solving problems requires cloning the repo
and installing each language's toolchain locally (
npm install, a Pythoninterpreter,
bundle install).This document describes a rewrite that lets users solve and verify the same
problems entirely in the browser — no clone, no local language installs, no
backend server. The application is a static site hosted on GitHub Pages,
modeled on the sibling project
build-your-own-alu.
1.2 Scope
Version 1.0 includes:
Stretch goal: Python support.
Out of scope (v1.0): see §2.2 Non-Goals.
1.3 Background and Context
The existing repository already provides the most important asset for this
rewrite: a centralized problem-definition system.
Shared/problems.jsonisthe single source of truth for problem statements, function signatures (per
language), parameters, return types, and test cases — including comparison
metadata (
exact,unordered_array,set_equality). Today, language-specificgenerators in
Shared/generators/convert this JSON into Jest / unittest /RSpec test files that run on the local CLI.
The browser rewrite reuses
problems.jsondirectly and replaces the "generatetest files and run a local test runner" workflow with "run user code and test
cases in a sandboxed in-browser language runtime."
1.4 Primary Features
1.5 Target Audience
The primary user is the solo self-learner — an individual practicing
algorithms (e.g., for interview prep) on their own. This implies a
single-user, local-only model: no accounts, no authentication, no sharing,
and no social/leaderboard features. This reinforces the static, client-only
architecture (§3) — there is no per-user server state
to manage.
2. Goals and Non-Goals
2.1 Goals
Shared/problems.jsonas the single source of truth — nodivergence between problem definitions and what runs.
cost, no server maintenance).
sibling build-your-own-alu project.
2.2 Non-Goals
— everything stays client-side and single-user (see §1.5).
replaces the CLI test-runner workflow (
npm test/rspec/unittest); theREADME and local setup are updated/archived at cutover (see
§10).
existing 22 problems — no additions, removals, reordering, or statement
rewrites. Content curation is a post-v1.0 follow-up.
3. System Architecture
3.1 High-Level Architecture
A single-page, fully static web application. All computation — including running
user code and executing test cases — happens in the browser. The only "backend"
is GitHub Pages serving static assets.
3.2 Technology Stack
@codemirror/lang-javascriptfor JS,@codemirror/legacy-modes(Ruby) for Ruby; switched via a CodeMirrorCompartment.LanguageRunnercontracts and JS↔WASM marshalling.eval/Functionin a Web Worker@ruby/wasm-wasi+@ruby/3.4-wasm-wasi, theruby+stdlib.wasmbuild → CRuby 3.4)?url;+stdlibneeded forjson/set/stringio.3.3 Execution Model and Sandboxing
long-running or infinite-loop solutions (the worker can be terminated on
timeout).
payloads are fetched only when that language is first selected.
Payload budget: lazy-load, no hard size cap. The initial page stays light —
the JavaScript path needs no runtime download and loads fast. The Ruby (and
later Python) WASM runtimes are fetched only when that language is first
selected, behind a loading indicator, accepting whatever size the upstream
runtime is. (As built, the Ruby
ruby+stdlib.wasmbinary is ~30 MB, self-hostedas a hashed Vite asset emitted under the Pages base path — no CDN dependency.)
Optimizing WASM payload size is not a v1.0 concern; it can be revisited if
first-Ruby-run latency proves painful.
3.4 Deployment Architecture
mastertriggers a GitHub Actions workflow: install dependencies →vite build→ publishdist/to GitHub Pages.(
jaysonvirissimo.github.io/practice-thy-algorithms/).4. Data Design
4.1 Source of Truth:
problems.jsonThe existing
Shared/problems.jsonschema is reused largely as-is. Per problem:{ "two_sum": { "title": "Two Sum", "description": "Implement a function that locates a pair of elements ...", "complexity": "O(n)", "parameters": [ { "name": "...", "type": "...", "description": "..." } ], "returnType": { "javascript": "...", "ruby": "...", "python": "..." }, "functionSignatures": { "javascript": "function twoSum(nums, target)", "ruby": "def two_sum(nums, target)", "python": "def two_sum(nums, target)" }, "hints": [ "Approach hint 1 (language-agnostic) ...", "Approach hint 2 ..." ], "testCases": [ { "input": { "nums": [2,7,11,15], "target": 9 }, "expected": [0,1], "description": "...", "comparison": { "mode": "exact|unordered_array|set_equality", "type": "deep_equality" } } ] } }hintsis a new optional, top-level (language-agnostic) field added for thebrowser app; see §5.6.
4.2 How the Browser Consumes Problem Data
problems.jsonis imported/bundled at build time (or fetched at runtime as astatic asset) and rendered into the catalog and editor.
exact,unordered_array,set_equality) is honored bythe in-runtime comparison logic instead of by generated test files.
4.3 User State and Persistence
State is local-only, with no portability. All state lives in
localStorage;there is no export/import, no URL sharing, and no cross-device sync (sync would
require a backend, and sharing is out of scope for the solo-learner audience —
see §1.5). Data is tied to a single browser profile.
Persisted state:
(problem, language).4.4 Known Data-Layer Gotchas (from current repo)
what tests expect (documented in
CLAUDE.md). The browser harness must invokethe function name actually defined by
functionSignatures, consistently acrosslanguages.
comparison.modemetadata.5. Component Design
5.1 Problem Catalog
problems.json.workspace.
JS/Ruby/Python today).
5.2 Editor Workspace
language mode.
signature" action; no autocomplete or syntax-guide panel in v1.0 (see
§7.3).
5.3 Test Harness
The core new component. Strategy: assert in runtime. Assertions run natively
inside the target language's VM, which then serializes a structured result back
to the host. This compares results using each language's own equality/type
semantics — critical for the linked-list problems (Reverse Linked List, Remove
Nth From End, Detect Cycle) where marshalling to JS would be lossy — and lets the
existing per-language comparison matchers (
exact,unordered_array,set_equality) port over largely intact. (Note: the problem set contains no treeproblems, so
ListNodeis the only non-primitive marshalled type in v1.0.)As implemented (M1–M2): the two runtimes realize this differently.
pure, synchronous
runHarness(userCode, problem)evaluates the user functionvia
new Function(...)and asserts using shared TypeScript comparators. Becauseit is worker-agnostic, the exact same function is unit-tested on the main thread
under Vitest (workers don't run under jsdom).
prelude.rb(ListNode, array↔list, comparators) is eval'donce at VM boot, and a
driver.rb(__ptap_run) is eval'd per run. Themetadata (function name, arg names, test cases) is passed in as base64-encoded
JSON embedded in an
evalstring and decoded +JSON.parsed in-VM — theruby.wasm JS bridge cannot pass a raw JS string as a Ruby argument, and base64
is injection-safe. (Python/Pyodide would mirror the Ruby shape.)
For a
(problem, language, userCode)tuple:harness. The harness receives the problem's test cases as a JSON string
and parses it inside the VM (
JSON.parsein Ruby /json.loadsinPython) — inputs travel as data, never as code-generated literals, avoiding
quoting/injection issues and using one mechanism across all languages.
input, calls the problem'sfunction, and asserts the return against
expectedusingcomparison.mode.plus timing into a JSON string and returns it as the eval's return value
across the runtime's JS bridge (ruby.wasm
RbValue/ PyodiderunPythonreturn) — not via stdout. This keeps the result protocol cleanly separated
from the user's own stdout.
TestRunResultcontract(§6.1) for rendering.
User stdout is captured separately from the result protocol and surfaced in
the results panel as debug output (see §5.5),
so learners can
puts/printwhile debugging without corrupting the harnessresult.
Comparison-mode parity. The three comparison modes (
exact,unordered_array,set_equality) are implemented natively in each language. Tokeep the implementations from diverging, a single shared fixture
comparison-vectors.jsonholds{ mode, a, b, equal }vectors that everylanguage's comparator must satisfy; it is exercised in CI so a divergence fails
the build. As built, the JS comparators run directly under Vitest, and the Ruby
comparators are exercised by booting
ruby.wasm(Node, via the pure-JSbrowser_wasi_shim) inside a Vitest test and running every vector through thereal in-VM
__ptap_compare.5.4 Language Runners
a fresh worker per run so an infinite loop can be hard-terminated (3 s
timeout); spawning is cheap.
ruby.wasmVM in a Worker; lazy-loaded WASM. Marshals inputs(JSON) into Ruby values and results back out. Because VM boot is multi-MB and
takes seconds, it uses a persistent worker loaded once via
init()andreused across runs (10 s execution timeout); on timeout the worker is terminated
and the VM transparently re-booted on the next run.
marshalling pattern as Ruby.
5.5 Results and Feedback Panel
overall completion state; persists status to
localStorage.puts/printoutput),kept separate from the harness result protocol (see §5.3).
5.6 Hints
v1.0 includes progressive hints (not full guided walkthroughs). Hint content
is authored in
problems.jsonas ahintsfield per problem, keeping the JSONthe single source of truth.
Hints are language-agnostic: a single set per problem, shared across
JavaScript, Ruby, and Python. They describe the approach/algorithm (e.g., "use
a hash map to remember complements") rather than language-specific syntax, so
the same hint sequence applies regardless of the selected language.
hintsis atop-level field on the problem, not keyed by language (unlike
functionSignaturesand
returnType).hintsarray (independent of language).localStorage(see §4.3).
The field is optional and the UI degrades gracefully when it is absent, so
authoring hints for all 22 problems can trail the engine work.
6. Interface Design
6.1 Internal Interfaces
A common
LanguageRunnercontract keeps the app core language-agnostic:The in-VM harness asserts natively, then emits this exact JSON, identical across
JS/Ruby/Python:
{ "protocolVersion": 1, "passed": false, "durationMs": 12.3, "runtimeError": null, "stdout": "", "cases": [ { "description": "...", "passed": true, "expected": <json>, "actual": <json>, "error": null } ] }expected/actualare display-only canonical serializations — thepass/fail verdict is decided in-VM (so pointer structures like
ListNode/TreeNodeare compared with native semantics), and these fieldsexist only so the results panel can render a diff. Pointer structures
serialize to their array representation for display.
erroris per-case (assertion detail or an exception raised while runningthat case);
runtimeErroris reserved for load/parse/compile failures thatprevent any case from running.
protocolVersionlets the three emitters be version-guarded against drift.6.2 External Interfaces
from GitHub Pages. No external APIs, no auth.
6.3 Data Contracts
problems.jsonschema (see §4.1) isthe contract between content and app. Schema changes must remain
backward-compatible with the browser app.
7. User Interface Design
7.1 Layout
The layout is desktop-first: optimized for desktop, not broken on tablet,
with no dedicated mobile layout. A code editor plus results panel needs
horizontal space, and the solo-learner audience is desktop-centric.
7.2 Key Flows
7.3 Visual and UX Details
Visual direction: a fresh, themed design — not bound to alu's aesthetics.
The UI adopts a medieval illuminated-manuscript / "holy book" aesthetic to
match the archaic project name Practice Thy Algorithms. Motifs: parchment/vellum
textures, blackletter display typography (paired with a legible body/monospace
face for code), illuminated drop-caps, marginalia-style annotations, and
ornamental borders.
Typography and palette ("Scriptorium"). All faces are OFL-licensed and
Google-Fonts hosted (GitHub-Pages friendly):
#f4ecd8#2b2117#b8860b#8b2e2eUnifrakturMaguntia is ornate, so it is restricted to short display headings;
body copy uses EB Garamond for readability.
The code editor and results panel must stay highly legible — theming must not
compromise readability of code or pass/fail feedback. The CodeMirror editor
theme should harmonize with the manuscript palette while keeping strong syntax
contrast.
Editor features:
persisted in
localStorage(see §4.3).function signature for the current
(problem, language), discarding thein-progress attempt.
8. Assumptions and Dependencies
8.1 Assumptions
problems.jsontoday.bypass the test cases (acceptable for a practice tool).
8.2 Dependencies
@ruby/wasm-wasi+@ruby/3.4-wasm-wasi,ruby+stdlib.wasm) —CRuby 3.4 compiled to WASM. Known limits: WASI Preview 1 (no sockets/networking,
no threads) — fine for self-contained algorithm problems. Booted via the
browserentry (pure-JSbrowser_wasi_shim); no COOP/COEP isolation needed,so it runs on plain GitHub Pages.
8.3 Constraints
loading).
9. Alternatives Considered
ListNode/TreeNodereturns; reuses existing per-language matchers. Trade-off: comparison logic maintained in three languages.problems.jsonstays the single source of truth.10. Milestones and Rollout
deploying a minimal themed page (proves the deploy pipeline).
harness across all 22 problems.
problems, with comparator parity asserted in CI.
feedback UX, and hints (seed hint sequences for 2–3 problems to exercise the UI;
the rest trail into v1.x). Vim-toggle persistence also lands here.
pattern as Ruby).
around the browser app.
The M0→M5 ordering is firm; no calendar target dates are committed (solo,
spare-time project). Milestones advance in order as time allows. Current status:
M0–M2 complete and deployed; M3 is next.
11. Future Work
Items intentionally deferred beyond v1.0:
degrades gracefully without it — see §5.6).
(out of scope for v1.0 — see §3.3).
§2.2).
12. Glossary
13. References
https://www.atlassian.com/work-management/knowledge-sharing/documentation/software-design-document
https://github.com/jaysonvirissimo/build-your-own-alu
Shared/problems.jsonandCLAUDE.mdin this repo.