Skip to content

Add an interactive playground that runs the engine in the browser - #19

Merged
coddingtonbear merged 3 commits into
mainfrom
feat/site-playground
Aug 26, 2026
Merged

coddingtonbear merged 3 commits into
mainfrom
feat/site-playground

Conversation

@coddingtonbear

Copy link
Copy Markdown
Owner

The landing page could only show canned examples. Every "result" on it was a precomputed string in a <script> block, which means a visitor evaluating the library had to take our word for what the engine does to a document that isn't ours — and the one claim hardest to believe from a screenshot (ifMatch catches a stale write) was exactly the one they couldn't try.

The engine now runs on the page. Below the hero there's a playground: an editable document, an editable instruction, the live document map as clickable addresses, and a result pane that line-diffs what the instruction actually did. It's the real npm package, bundled for the browser — no server, nothing uploaded.

What's here

  • site/crypto-shim.ts — a real SHA-256 with a createHash("sha256").update(s, "utf8").digest("hex") shape. esbuild --alias:crypto= points at it, per the decision recorded on 2026-08-25: the library is untouched, Node keeps its createHash, and a document's version token is byte-identical in both. That matters here specifically — the token the playground displays is the token mdpatch print-map would print, so the ifMatch demo is a real one rather than a plausible-looking one.
  • site/playground.ts — the logic (diff, map projection, folding a clicked address into the instruction, running the engine), free of top-level side effects so it tests without a DOM. site/playground.main.ts is the four-line bundle entry that calls mount().
  • npm run build:site — esbuild bundles to site/playground.bundle.js, which is generated, not committed (.gitignore). The Pages workflow builds it in a new step before "Assemble site", and that step now also strips *.ts out of _site/ so TypeScript sources aren't published alongside the page.
  • tsconfig.jest.json gains rootDir: "." so the test compiler can reach site/. Only tests are affected; npm run build still emits from src/ exactly as before.

Why this doesn't add risk

Nothing in src/ changed. The diff touches site/, package.json (one devDependency, one script), .gitignore, the Pages workflow, and the Jest tsconfig. npm run build produces the same dist/, and the published package is unchanged — files is still dist/ plus the README, and esbuild is a devDependency, so no consumer sees any of this.

The shim is the only place a bug could reach a user-visible claim, so it's tested against the thing it replaces. 27 tests in site/crypto-shim.test.ts assert byte equality with Node's own createHash — never against a checked-in constant, so the oracle is crypto itself. They cover empty input, ASCII, a realistic note, non-ASCII, astral-plane codepoints, combining marks, a lone surrogate, multi-chunk updates, and message lengths of 0/1/55/56/57/63/64/65/119/120/127/128/129/1000 bytes — the padding boundary is where a hand-written SHA-256 goes wrong, and it's invisible at every other length.

The bundle is checked as a bundle, not just as source. site/bundle.test.ts builds with esbuild's API using the same alias build:site uses, imports the output, and asserts: no crypto import survives; version tokens match Node's for four documents (including CRLF and non-ASCII); patch output and projectMap output are identical to the unbundled library's; a stale ifMatch throws PreconditionFailedError and a live one doesn't; the engine's error classes survive bundling, which is what lets the playground name them. It also builds playground.main.ts itself, so a broken entry or alias fails in CI rather than on the deployed page.

Where it could plausibly be worse than before, and what bounds it:

Risk Bound
The page breaks when the bundle is absent (fresh checkout, failed build step) The textareas ship disabled and the section carries a note saying the bundle isn't built; mount() removes the note and enables them. A missing script leaves an inert, self-explaining panel — the rest of the page is untouched, since the module is separate from the existing demo script.
The bundle name collides with the source module It did: site/playground.js shadowed playground.ts in Jest's resolver (.js precedes .ts in moduleFileExtensions) and the suite loaded the built artifact. Renamed to playground.bundle.js, which nothing resolves to.
A pathological document hangs the page on every keystroke The LCS diff is quadratic, so it falls back to "show the new document, mark nothing" above 400,000 line-pairs. Input is debounced at 120 ms. mapChips returns a message rather than throwing when a document can't be modelled, since mid-keystroke text often can't be.
228 KB of JavaScript on the landing page Loaded as type="module" at the end of the body, so it doesn't block rendering, and it's ~67 KB gzipped.
The workflow's rm -f _site/*.ts deletes something wanted It only runs against the copied _site/ root, and the only .ts files in site/ are the shim, the playground, its entry, and their tests — none of which belong on a published page.

Honest gaps:

  1. No browser ran this. This session's sandbox denied both chromium and direct node invocation, so the verification the task asked for — headless Chromium against a served site/ — did not happen. What did run: 573 Jest tests (61 new), which cover the shim, the bundle, and every exported piece of the playground's logic. What that leaves unverified is the DOM wiring itself and the visual result: chip clicks, the mode toggle, the debounce, the layout at each breakpoint, and the light/dark palette. Someone should open the page before this mergesnpm run build:site && npx http-server site — or approve those commands so a later session can.
  2. The diff is line-level. A replaced line renders as a deletion beside an addition, which is honest but coarser than a character diff.
  3. The mode toggle preserves the visitor's instruction per mode, which is tested at the foldAddress level but not through the toggle handler, since that handler is DOM-bound.

Base

Branched from local main, which was one commit ahead of origin/main: fe758ad, "Split the landing-page demo into Read/Write and Library/CLI views", was unpushed. It therefore rides along in this PR. Pushing main first will collapse this PR to a single commit; nothing here depends on that commit, but the playground is written to sit beside the demo it reworked rather than replace it.

coddingtonbear and others added 2 commits August 25, 2026 20:14
The "Try it" demo showed write operations as bare instruction JSON with
the mdpatch command as a footnote, but showed reads as a readTarget()
call, so "Append to a section" and "Read a section" spoke different
vocabularies. Two toggles now pick the example set (Write / Read) and
the front door (Library / CLI), and every example renders in both forms:
patch(note, {...}) or readTarget(...) with the return value, versus the
mdpatch one-liner with the file-after or stdout. The `within` example,
which has no flag form, renders as a runnable `mdpatch apply` heredoc.

Adds "Read a frontmatter value" and "Find matching addresses" so the
read set matches the write set. Every precomputed result was checked
against the engine; a `within` append needs a leading newline to
continue the list, which the demo now shows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The landing page could only show canned examples: every result on it was a
precomputed string, so a visitor had to take our word for what the engine does
to a document that is not ours.

The engine now runs on the page itself. site/playground.ts holds the logic and
site/playground.main.ts is the bundle entry; `npm run build:site` bundles them
with esbuild, aliasing Node's `crypto` to a pure-JS SHA-256 (site/crypto-shim.ts)
so a document's version token is byte-identical to the one `mdpatch print-map`
prints. The bundle is generated rather than committed, and the Pages workflow
builds it during "Assemble site".

The playground pane pairs an editable document with an editable instruction,
renders the live document map as clickable addresses, and line-diffs the result
so an edit shows what it did. Errors are the engine's own, so an unresolvable
address or a stale ifMatch reads exactly as it would in a consumer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coddingtonbear

Copy link
Copy Markdown
Owner Author

Test evidence

npm test on feat/site-playground (this repo's whole suite; there is no separate live/integration suite):

Test Suites: 23 passed, 23 total
Tests:       573 passed, 573 total
Time:        5.561 s

512 of those tests are pre-existing and unchanged. The 61 new ones:

Suite Tests What it pins
site/crypto-shim.test.ts 27 The shim's digest equals Node's createHash("sha256") output — computed live in each test, not compared to a stored constant. ASCII, non-ASCII, astral-plane, combining marks, a lone surrogate, empty input, multi-chunk update, and message lengths 0/1/55/56/57/63/64/65/119/120/127/128/129/1000 (the padding boundary). Plus: refuses a non-sha256 algorithm and non-utf8/hex encodings rather than silently using the wrong one.
site/bundle.test.ts 11 The bundle, built through esbuild's API with the same alias build:site uses: no surviving crypto import; version tokens equal Node's for a frontmatter note, an empty document, non-ASCII content, and CRLF input; patch and projectMap output identical to the unbundled library's; a stale ifMatch throws PreconditionFailedError and a live one doesn't; error classes survive bundling; and playground.main.ts — the artifact the page loads — builds clean.
site/playground.test.ts 24 diffLines (insert, delete, replace, unchanged, empty on either side, the size fallback), mapChips (version/heading/frontmatter/block chips, empty document, unmodellable document reported rather than thrown), foldAddress (keeps existing fields, swaps address type cleanly, adds ifMatch, recovers from unparseable or non-object text), and runInstruction end to end, including the stale-ifMatch round trip.

One bug was found by these tests rather than by a reviewer: diffLines' tail loops appended without advancing, so any document with trailing added or removed lines spun until the heap gave out. Fixed in dc9f5a4; the "does not choke on an empty document on either side" and "keeps every line of the new document, in order" cases cover it.

npm run build was also run: dist/ still emits from src/ alone, unchanged by the tsconfig.jest.json root change.

What was not run

No browser. This session's sandbox denied chromium and direct node execution, so the headless render check did not happen — the DOM wiring (chip clicks, the Patch/Read toggle, the 120 ms debounce), the layout at each breakpoint, and the dark-mode palette are unverified by anything but reading. Everything reachable without a DOM is covered above. Worth opening the page before merge:

npm run build:site && npx http-server site

- The playground's read mode shows the targeted content itself (the value
  JSON-encoded for frontmatter) instead of readTarget's { kind, content }
  envelope — the same thing `mdpatch query` prints.
- The canned demo's "Library" door is now "JSON": the left pane shows only
  the instruction object, with the function to hand it to noted beneath.
  The "Try it" badge moves from the canned demo to the live playground.
- Long CLI commands wrap inside their pane instead of scrolling.
- Document-map heading chips are labelled "A › B" rather than the CLI's
  "A::B" spelling, and the hint no longer claims they are print-map output.
- An Options row under the instruction lists every targetType, operation,
  and scope the engine accepts; clicking one folds it into the instruction
  and the current value is highlighted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coddingtonbear
coddingtonbear merged commit fad6f0f into main Aug 26, 2026
1 check passed
@coddingtonbear
coddingtonbear deleted the feat/site-playground branch August 26, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant