The first document imported into TNDM is a screenshot gallery, which makes it a good worked example: it exercises every frame type the parser produces and it has a real feedback loop to close. Nothing about the deck is gallery-specific — see md-parser for the rules that apply to any document.
The source document
| Property | Value |
|---|---|
| Path on disk | /home/loca/dev/wrdp/q5vault/audits/qwizz/2026-08-20-feedback-tour.md |
| Published as | https://wiki.wrdp.loca.zone/audits/qwizz/2026-08-20-feedback-tour (HTTP 200, checked 2026-08-20) |
| Deck title | 🎞️ QuizWizz UI/UX Feedback Tour — Complete Shot Gallery, taken from frontmatter title |
| Release status | releaseEligible: false (.../2026-08-20-feedback-tour.md:7) |
It is the record of a capture run against the WRDP WordPress bench: every QuizWizz surface, persona
and state reachable on 2026-08-20, captured as 315 screenshots plus 5 contact-sheet boards, grouped
by capture wave and route (.../2026-08-20-feedback-tour.md:10). The document is bench-local
evidence at releaseEligible: false, and importing it into TNDM does not change that: reviewing a
frame here is not a release decision.
Its structure is what the parser turns into a deck.
- No level-1 heading anywhere, so the deck title comes from frontmatter
(
src/lib/mdframes/mdframes.test.ts:101-103). - Eleven level-2 sections:
Tour yamlsum,Coverage,Blocking defects found during capture,Boards, the five capture wavesWave AthroughWave E,Feedback logandMethod & residue(src/lib/mdframes/mdframes.test.ts:39-51,112-114). - Sixty-five level-3 sections, one per route, titled
<route-id> — <surface>, for exampleg01 — landing/front page(.../2026-08-20-feedback-tour.md:88). - Each route section holds the route’s URL in backticks, then one image per shot immediately
followed by a
<sub>metadata line, then its callouts (.../2026-08-20-feedback-tour.md:88-105). - Two callout types:
> [!warning] Findings in this routecarries the QA findings, and> [!note] Feedback — <route-id>is the placeholder where tour notes are written (.../2026-08-20-feedback-tour.md:1495-1499).
A real image and caption pair, verbatim (.../2026-08-20-feedback-tour.md:92-93):

<sub>g01-d.png · desktop 1280x900 · fold</sub>The <sub> line becomes the frame’s sub, and it is split on · into tags, so that shot arrives in
the deck as tags: ["g01-d.png", "desktop 1280x900", "fold"] — filename first, then viewport, then
crop kind (src/lib/mdframes/index.ts:440-450, verified against the live deck). A trailing
⚠️ finding chip in the same line adds the finding badge that the frame card renders as its
verdict chip (src/lib/mdframes/index.ts:451-457, example at
.../2026-08-20-feedback-tour.md:124).
The coverage table splits the 315 shots across the five waves: Wave A 141, Wave B 13, Wave C 130,
Wave D 8, Wave E 23, over 65 routes, with 250 clean and 65 finding verdicts
(.../2026-08-20-feedback-tour.md:36-43). Five shots are in the shotlist but deliberately not on
disk, each with its blocking reason spelled out — two dead preview tabs, one notice with no mutation
control, one unreachable import notice, and one state that does not exist
(.../2026-08-20-feedback-tour.md:45-51).
The import call
curl -sX POST https://tndm.loca.zone/api/decks/import \
-H 'content-type: application/json' \
-d '{"md_path":"/home/loca/dev/wrdp/q5vault/audits/qwizz/2026-08-20-feedback-tour.md","copy_from":"/home/loca/dev/wrdp/q5vault/assets/qwizz-tour/2026-08-20","id":"qwizz-2026-08-20"}'copy_from rather than base_url on purpose. The gallery is published from a Quartz build, and the
wiki host serves a frozen dist-<epoch> directory that is only replaced by an explicit production
build, so a deck pointed at those URLs would show whatever that build happened to contain. Copying
makes the deck self-contained: 315 shots and 5 boards were copied into
/home/loca/tndm/decks/qwizz-2026-08-20/assets/{shots,boards} — 320 files, byte-for-byte the same
count as the source directory, with zero missing — and every image src was rewritten to the deck’s
own route, for example /api/decks/qwizz-2026-08-20/assets/shots/g01-d.png
(src/lib/server/importer.ts:227-237; asset counts re-counted on disk 2026-08-20).
The resulting deck
Measured from GET https://tndm.loca.zone/api/decks/qwizz-2026-08-20 on 2026-08-20:
| Measure | Value |
|---|---|
| Sections | 76 total: 11 at level 2, 65 at level 3, none at level 1 |
| Frames | 323 total: 320 image, 2 table, 1 code |
| Image sources | 315 under /shots/, 5 under /boards/ |
Frames badged finding | 65 |
| Section findings | 139 |
| Assets | 320 copied, 0 missing |
The two table frames are the Coverage table and the blocking-defects table; the single code frame
is the Tour yamlsum fenced block, tagged yaml from the fence language
(src/lib/mdframes/index.ts:386-388, src/lib/mdframes/mdframes.test.ts:93-98). Paragraph prose
produces no frames unless the parser is asked for them, which is why 1618 lines of document collapse
to 323 reviewable frames rather than several hundred more.
Why the deck shows 320 images and the fixture asserts 321
The parser’s contract is pinned to a frozen copy of this document at
src/lib/mdframes/fixtures/qwizz-tour.md — md5 a15fb4f0ffb6d07e8256402563193d15, 1619 lines,
re-checked 2026-08-20. Its assertions are 76 sections, 65 at level 3, 321 image frames, 316 under
/shots/ and 5 under /boards/, 66 frames badged finding, and a non-empty sub on all 321
(src/lib/mdframes/mdframes.test.ts:54-84).
The live deck is exactly one image, one shot and one badge lower, for one reason: after the fixture
was frozen, the source document withdrew v01-d-p02-player-intro.png. The fixture still carries the
frame, with its finding chip, at src/lib/mdframes/fixtures/qwizz-tour.md:1480-1481. In the
live document that frame is gone from the route section and listed instead as
blocked(no-such-state): a fresh load with cleared storage mounts straight into question 1, so the
intro state it claimed to show does not exist
(/home/loca/dev/wrdp/q5vault/audits/qwizz/2026-08-20-feedback-tour.md:51, withdrawal recorded at
.../2026-08-20-feedback-tour.md:1493-1494).
That divergence is the fixture working as intended, not drift to be fixed. The fixture is a snapshot
with measured ground truth; if a run disagrees with a constant in the test file, the parser is wrong,
never the constant (src/lib/mdframes/mdframes.test.ts:1-10). Deck numbers are re-measured per
import; fixture numbers are never re-baselined. md-parser carries the full contract.
How the gallery becomes rail and sections
The rail mirrors the document’s heading tree. Every level-2 heading opens a collapsible group and
every level-3 heading under it becomes a row, so the five capture waves become five groups with
their route sections underneath (src/lib/components/Rail.svelte:19-35). The four non-wave level-2
sections come along as groups too: Tour yamlsum holds the single code frame, Coverage and
Blocking defects found during capture hold one table frame each, and Boards holds the five board
images (src/lib/mdframes/mdframes.test.ts:116-122). A wave heading holds no frames itself — its
frames live in the route sections below it — so the rail’s group badge is the sum of the group,
header plus rows (src/lib/components/Rail.svelte:43-47). Feedback log and Method & residue
hold no frames at all, so they carry no badge and drop out of the progress ring, which counts only
sections holding at least one frame (src/routes/s/[sessionId]/+page.svelte:76-78; measured on the
live deck, where seven sections hold zero frames).
Ids are derived, not authored: a section id is the slug of its heading — g01 — landing/front page
becomes g01--landingfront-page — and a frame id is <sectionId>/<seq>, so the first shot of that
route is g01--landingfront-page/1. Frame ids therefore contain a slash, which is why the app
resolves cards by element id rather than by CSS selector
(src/routes/s/[sessionId]/+page.svelte:112-115).
One honest caveat about the findings count. Both callout types collapse into the section’s
findings[], so the 139 section findings are a mix of real QA findings and the
_(awaiting tour notes)_ placeholders that every route section still carries. Route g01, for
example, arrives with findings: ["(awaiting tour notes)"] and no real finding at all. The rail’s
warning triangle and the section’s findings badge count both, so read the finding count per frame —
the finding badge on frames, of which there are 65 — as the QA signal, and treat section findings
as callout text (src/lib/mdframes/index.ts:288-335, live deck section g01--landingfront-page).
Closing the loop back into the gallery
The review is exported as markdown and merged into the source document’s own feedback log.
GET https://tndm.loca.zone/api/sessions/<sessionId>/export.md returns
text/markdown; charset=utf-8 with a tandem-<sessionId>.md filename
(src/routes/api/sessions/[id]/export.md/+server.ts:12-17). The document opens with a fenced YAML
header carrying deck, title, session, generated, duration, events, a by_kind histogram,
frames_touched and sections_done, then a ## Tandem audit <sessionId> — <date> heading, then one
bullet per decision (src/lib/server/export.ts:108-133). Five of the nine event kinds earn a
bullet — note, task, verdict, speech and scribble — because nav and status are
transport chatter and say is the agent talking out loud
(src/lib/server/export.ts:17-27). Each bullet leads with its kind and its frame or section target,
then the verdict, the text and the tags, all squeezed onto one line
(src/lib/server/export.ts:49-70).
The merge target is the gallery’s own log. ## Feedback log sits at
/home/loca/dev/wrdp/q5vault/audits/qwizz/2026-08-20-feedback-tour.md:1601, and as of 2026-08-20 it
holds exactly one bullet, the publication entry - 2026-08-20 — gallery published, tour ready for the first pass. (.../2026-08-20-feedback-tour.md:1605). Accepted notes and applied decisions are
appended there, dated, one bullet each; the document’s own instruction is that notes are written
into the per-route Feedback — <route-id> callout first and promoted to the log once accepted or
rejected (.../2026-08-20-feedback-tour.md:12). Nothing from a TNDM session has been merged yet.
Two mechanical details matter when merging:
- Scribble bullets render as a relative link
[scribble](scribbles/<file>)(src/lib/server/export.ts:56-59), while the gallery addresses its images as/assets/qwizz-tour/2026-08-20/<subdir>/<file>. The PNGs have to be copied out ofDATA_DIR/sessions/<id>/scribbles/into the gallery’s asset tree and the link rewritten to that path, or it will not resolve in the published wiki. - Frame targets in bullets are deck ids like
g02--builder-craft/17, not filenames. The filename lives in the frame’s first tag, so a bullet meant for human readers usually wants the shot filename spliced in from the deck.
Re-importing the deck with the same id after a merge refreshes the frames and captions from the
updated document and leaves existing sessions untouched
(src/lib/server/importer.ts:255-261), so the deck can be brought back in line with the gallery
without losing the review that produced the change.
Next
- quickstart — the review loop, keys, voice and collab in operator order.
- evidence — what was measured on this deck and what remains unverified.
- md-parser — the parsing rules and the frozen-fixture contract.