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

PropertyValue
Path on disk/home/loca/dev/wrdp/q5vault/audits/qwizz/2026-08-20-feedback-tour.md
Published ashttps://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 statusreleaseEligible: 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 waves Wave A through Wave E, Feedback log and Method & residue (src/lib/mdframes/mdframes.test.ts:39-51,112-114).
  • Sixty-five level-3 sections, one per route, titled <route-id> — <surface>, for example g01 — 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 route carries 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):

![Guest homepage fold: nav, hero heading, tagline, CTA buttons.](/assets/qwizz-tour/2026-08-20/shots/g01-d.png)
<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:

MeasureValue
Sections76 total: 11 at level 2, 65 at level 3, none at level 1
Frames323 total: 320 image, 2 table, 1 code
Image sources315 under /shots/, 5 under /boards/
Frames badged finding65
Section findings139
Assets320 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.

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).

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 of DATA_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.