Run cd ~/Projects/latered/app && npm install && npm run dev -- --port 5174, then openhttp://localhost:5174. Expect: a landing/funnel page, NOT the create-profile form
yet — tagline "Put it off. Properly.", the lede "A to-do list that believes in
tomorrow...", three proof cards ("Tomorrow, in one tap", "The Guilt Meter keeps score",
"Every task has a page"), and the footnote "And yes — the day ends at 4 am here, like
it does everywhere else."
latered
Derived from: commit 4c144fc (master), 2026-08-07, against a local npm run dev
Flows
check each step and final checkFirst visit: the pitch, then claim your desk
Proves the funnel entry a brand-new visitor actually sees, and the identity gate under it — which is one step longer than the project's own DOGFOOD.md currently documents.
Persona / precondition: a browser with no latered IndexedDB database yet (fresh profile, private window, or a deleted DB) — first-ever visit.
Click Claim your desk (the landing page's button). Expect: the actual
create-profile card replaces it: heading "Claim your desk.", the line "One profile, on
this device only. A passcode locks the app; your items never leave your machine.", and
three fields — "What should we call you?", "Passcode" (placeholder "At least 4
characters"), "Same passcode, once more" (placeholder "Repeat it").
Leave the name blank, type any passcode and matching confirm, click Start putting
things off. Expect: the error "A name to greet you by — any name works." and no
profile is created.
Fill a name, type a 3-character passcode (with matching confirm), submit. Expect:
"Give the passcode at least 4 characters."
Fill a name, a valid 4+ character passcode, and a DIFFERENT value in the confirm field,
submit. Expect: "The passcodes do not match. Try them again."
Fill all three correctly (matching passcodes, 4+ chars) and submit. Expect: you land
straight on the Today view — no email verification step, because there is no server at
all (the passcode only gates this browser's local storage).
Final check: reload the page in the SAME tab. Expect: you are not asked to sign in
again — the app opens straight to Today (the unlock lives in sessionStorage, which
survives a same-tab reload; F9's final check covers what happens when the tab is closed).
First-login product tour (9 stops)
Proves the guided tour a first-time user gets automatically, and that it never repeats uninvited.
Persona / precondition: F1 just completed for the first time this session — the tour should be opening on its own right now.
Observe what happens immediately after F1's last step. Expect: the tour opens by
itself, spotlighting the brand mark with title "Meet latered." and body starting "A
to-do list that respects the fine art of not doing...".
Click through all 9 stops with Next. Expect: exactly these 9 titles in order —
"Meet latered.", "Today is the whole game.", "Add something to neglect.", "The Guilt
Meter keeps score.", "The day ends at 4 am.", "All items, one collection.", "Every task
has a page.", "Pro mode, when you want more.", "Now neglect responsibly." — the middle
stops jump you across routes (Today → All items → Settings) and the last brings you
back to Today.
Start the tour again (Settings, see step 5) and press Esc partway through.
Expect: it closes immediately, regardless of which of the 9 stops you're on.
Reload the page. Expect: the tour does NOT auto-open a second time — you land
straight on Today (it only auto-runs "on the FIRST login, and for anyone who never
finished it", per the source comment).
Go to Settings, find "The tour" card ("Nine stops through everything the desk does
— the same walk that ran on your first login."), click Replay the tour. Expect:
the same 9-stop tour reopens on demand.
Final check: finish or Esc out of a replay and confirm you land back on whichever
route you started the replay from, fully interactive (no leftover overlay).
Today: the signature push
Proves the one-tap "push to tomorrow" primitive and its undo — the commission's headline mechanic (SPEC §2.1).
Persona / precondition: signed in, on Today, with at least one open item at Guilt 0 / Pushes 0 (a freshly created item, or run F5's "Set up a demo room" first if Today is empty).
Note the item's row meta line, which reads "Guilt {n} · Pushes {n}". Expect: a
freshly created item reads "Guilt 0 · Pushes 0".
Click → Tomorrow on that item. Expect: the row animates out (skip judging the
animation itself if your OS has "reduce motion" on — see F10 step 3), a toast appears
at the bottom reading exactly "{title}" is tomorrow's problem now. with an Undo
action, and the row disappears from Today.
Click Undo in the toast before it expires. Expect: the item reappears on Today
at Guilt 0 / Pushes 0 again — Undo fully reverts the push, it does not just re-add the
row.
Push the SAME item again and this time let the toast run its full ~6 seconds without
touching it. Expect: the toast disappears on its own and the push stands — because
step 3's Undo reverted pushCount to 0, this is push #1 again, so Guilt should read 6
(not 12) and Pushes should read 1.
Click Done on a different open item. Expect: it animates out of Today; opening
All items with "Show done" toggled on (F7) shows it struck through with Guilt reset to
0.
Final check: open All items and confirm the item pushed in step 4 shows a scheduled
date of tomorrow — it is deferred, not deleted; it will reappear on Today once the day
rolls over.
Multi-day push and the guilt math
Proves Layer B of the engine — "Later…" — and that distance costs strictly more than a single tap, against the documented formula in SPEC §2.3.
Persona / precondition: Today, one open item at Guilt 0 / Pushes 0 (the math below only lines up starting from a fresh item).
Click Later… on the item. Expect: a panel opens labeled "Push further (costs
escalate)" with +3 days, Next week, a date field, and Cancel.
Click +3 days. Expect: the toast reads "{title}" pushed 3 days out.; per the
formula round(perPush × (1 + 0.5×(days-1))) with this being push #1
(perPush = 6), the guilt added is round(6 × (1 + 0.5×2)) = round(6×2) = 12 — the row
should now read "Guilt 12 · Pushes 1".
Click Later… again, then Next week (+7 days, now push #2 so perPush =). Expect: toast
6+6×1 = 12"{title}" pushed 7 days out.; guilt deltaround(12 × (1+0.5×6)) = round(12×4) = 48, landing at Guilt 60 (12+48) — check the
row's "Guilt 60" reading.
With guilt at 60 (tier "guilty", the documented 50–74 band), open the Guilt Meter panel
on Today's right rail. Expect: the meter's fill is visibly pulsing — the
"breathing" animation (CSS class ui-guilt-meter--breathing) is wired to apply at BOTHguilty and shame, not only at shame — unless your OS has reduced motion on.
Open Later… and pick a specific date via the date field instead of a preset.
Expect: the toast reads "{title}" moved to {date}. and the same escalating
formula applies, using the number of days between today's schedule and your picked
date as deltaDays.
Click Cancel in an open Later… panel. Expect: the panel closes with no push
applied — Guilt and Pushes stay exactly as they were.
Final check: the item's tier badge text matches its guilt band — 0-24 "calm", 25-49
"nagging", 50-74 "guilty", 75-100 "shame" — read the badge's TEXT, not just its color
(color-alone-conveys-meaning was a fixed accessibility finding perdocs/qa/FINDINGS.md).
Inline create and the empty-day path
Proves item creation never uses a browser popup, and that an empty Today offers a real way forward — both were filed and fixed accessibility/UX issues (FINDINGS.md O1).
Persona / precondition: Today, any state.
Click + New item in the toolbar. Expect: an inline input opens at the top of
the list — NOT a window.prompt() dialog — placeholder "What are you putting off?",
with Add it (disabled while empty) and Cancel.
Press Enter with the field empty. Expect: nothing destructive happens — no
blank item is created.
Type a title and press Enter. Expect: the item appears in the Today list
immediately, and the input stays open and focused for another entry rather than
closing.
Type a second title, then press Escape instead of Enter. Expect: the create row
closes WITHOUT adding that second item.
Push or complete every item currently on Today until the list is empty. Expect: the
empty state appears — heading "Nothing scheduled for today", the line "Either you are
thriving, or everything is already tomorrow's problem.", and two buttons: Add
something to neglect later and Set up a demo room.
Click Set up a demo room. Expect: a populated set of demo items appears on
Today in one click, no confirmation dialog and no server round-trip — this is explicit,
user-triggered seeding, per the source comment "never injected into a non-empty day
silently".
Final check: refresh the page after step 6. Expect: the demo items are still
there — they were written through the same repository as any manually created item, not
held only in memory.
The unified item editor
Proves the note-taking depth behind every task — one page holding both the task face and the memo face, per SPEC §4.
Persona / precondition: any item open on the editor surface (click its title from Today or All items).
In the toolbar, click B, Highlight, H, the checkbox glyph, Callout,
Code, and Table in turn — each inserts its own Markdown snippet at the cursor.
Expect: the live preview pane (toggle: Hide preview / Show preview) renders
each as it lands: bold text, a highlighted span, a heading, a checkbox list item, a
titled callout box, a fenced code block, and a real <table> — not raw Markdown
syntax.
Type [[ followed by a few letters of another item's title. Expect: a suggestion
list of matching items appears; picking one inserts [[Full Title]], and the preview
turns it into a clickable wikilink.
Click that wikilink in the preview. Expect: the editor switches to the target item —
or, if you typed a title with no match, a new stub item is created and opened
("unresolved links create stub items", SPEC §4.2).
Type # followed by a few letters. Expect: a tag-suggestion list appears; the
finished tag shows up as a colored chip both in the preview and in the editor footer's
tag row.
Click Markdown, Plaintext, and JSON in the editor footer in turn.
Expect: three separate file downloads ({title}.md, {title}.txt,{title}.json), each containing your content in that format.
Click Print / PDF in the header. Expect: a new window opens with a print-ready
rendering of the item and the browser's native print dialog opens automatically — this
is a real OS print boundary, not an in-app PDF generator (per the source comment
"browsers and Tauri own the native PDF dialog").
Final check: navigate to a different item and back. Expect: every formatting
element from steps 1-4 is still exactly there — the editor is not losing your draft on
navigation.
All items: the real list
Proves the second surface — search-and-browse over everything, task or memo, done or not, with no row cap.
Persona / precondition: several items exist (a mix of open/done, some tagged, ideally including work from F6).
Click All items in the sidebar. Expect: heading "Tasks, memos, and everything
between.", a badge with the total item count, and EVERY open item listed — no 6-row cap
like Today's guilt-sorted slice.
Type a word from one item's memo body into "Search titles, memos, #tags…". Expect:
the list narrows to matching items (fuzzy match on title + body text).
Clear the search and instead type a scoped query, tag:work is:open (swap work for a
real tag you've used). Expect: only open items carrying that tag (or a sub-tag likework/anything) remain — this exercises the documented scoped-operator search (SPEC
§4.4: tag:, is:, due:, guilt:), not plain text matching.
Search for guilt:>=50. Expect: only items with guilt 50 or higher appear.
Clear the search and click the Show done (N) toggle. Expect: completed items
appear, struck through, and the button's own label flips to "Hiding nothing"; click it
again and they disappear, the label reverting to "Show done (N)".
Search for gibberish, e.g. zzzqqqxx. Expect: "Nothing matches that search." with
the hint "Try fewer words, or a #tag." — not a blank white area.
Final check: click any row's title. Expect: it opens on the editor surface to the
right, the same as clicking a Today row.
Pro mode: additive, never destructive
Proves G8 — the advanced tier only ever ADDS surfaces; switching it off never removes, hides destructively, or replaces base data.
Persona / precondition: Settings, General mode, at least one item carrying a wikilink and a tag from F6.
On Settings, under "Advanced mode", confirm the sidebar has no Pro entry while in
General. Expect: only Today / All items / Settings show.
Click Pro. Expect: the sidebar gains a fourth entry, "Pro" (hint "Backlinks,
properties, graph & more"); the Settings card's Pro button becomes the accent
(pressed) one.
Open the Pro section. Expect: a tab strip with exactly five tabs — Backlinks,
Properties, Graph, Guilt analytics, Export — opening on Backlinks by
default.
With focus on the tab strip, press the Right arrow key repeatedly. Expect:
focus AND the active tab both advance across all five (roving-tabindex — only the
active tab sits in the page's Tab order); Home/End jump to the first/last tab.
Visit Graph. Expect: a node/edge diagram reflecting your wikilinks, with anaria-label reading "Graph of {n} items and {n} links". Visit Export and click
Export vault (N). Expect: a bulk download of your items as an
Obsidian-compatible vault (one .md file per item).
Switch back to General in Settings. Expect: the Pro nav entry disappears
immediately, but every item you touched in Pro (tags, wikilinks, titles) is unchanged
in Today/All items — General "hides pro-only surfaces" per SPEC §3, it does not deletepro data.
Final check: flip back to Pro once more. Expect: any backlinks/graph data tied
to your items from step 5 is still there — the additive data survived the round-trip.
Appearance and the lock
Proves the theme system is genuinely live (not just painted-on buttons) and that the local passcode gate actually gates.
Persona / precondition: signed in, Settings visible.
Under "Appearance", click Dark. Expect: the whole app switches instantly to a
warm brown-black paper look (never a cold navy/slate dark theme), and the choice
survives a reload.
Click System, then flip your OS or browser's emulated light/dark setting.
Expect: the app's theme follows it live, with no manual reload needed.
Click Light. Expect: the third option also applies immediately.
Click Lock the desk in the sidebar footer. Expect: you land on "Welcome back,
{your name}." with a single Passcode field — NOT the "Claim your desk" create form,
since your profile already exists.
Type a WRONG passcode, submit. Expect: the error "That passcode does not open this
workspace." — the field clears and refocuses, with no hint about the real passcode.
Type the correct passcode, submit. Expect: you're back exactly where you left off
(same route, same items) — locking never touched Today's data.
Final check: close the browser TAB entirely (not just reload) and reopenlocalhost:5174. Expect: you land on "Welcome back" again, locked — the unlock lives
in sessionStorage, which a closed tab clears, unlike F1's final check (a same-tab
reload).
Keyboard-only and reduced motion
Proves the accessibility pass recorded in docs/qa/FINDINGS.md actually holds in the running app — and re-exposes the one gap that pass left open on purpose.
Persona / precondition: Today, at least 2 open items; a way to enable OS/browser "reduce motion".
Using only Tab / Shift+Tab / Enter / Space, reach an item's title, then Tab to
→ Tomorrow and press Enter. Expect: the push fires exactly as a mouse click
would, with a visible focus ring at every stop — real <button> elements throughout,
per FINDINGS.md's keyboard-navigation table.
Tab to Later…, press Enter to open the panel, then keep pressing Tab past the
date field and its submit control. Expect (documented, currently failing —
FINDINGS.md O3, filed as "minor", not fixed): ideally focus stays trapped inside the
open panel until it closes. The shipped behavior is that focus is free to leave the
panel while it remains open on screen — confirm which one you actually see, and record
a FAIL only if you'd consider this newly severe, since it is a known, accepted gap.
Enable OS-level "reduce motion", then push an item to tomorrow. Expect: the push
applies INSTANTLY with no slide animation, and the Guilt Meter's breathing pulse (F4
step 4) also stops animating — nothing conveys information through motion alone.
Inspect the Guilt Meter with a screen reader or the accessibility-tree devtools panel.
Expect: role="progressbar" with aria-valuetext reading like "{n} percent,
{tier}" — the tier name is announced, not conveyed by color alone.
Final check: tab all the way through the Pro tab strip (F8) using only arrow keys plus
Home/End — confirm the roving-tabindex pattern holds (only the active tab is a Tab stop).
Restart survival
Proves this is genuinely local-first: everything written through the repository outlives the process, not just the React tree in memory.
Persona / precondition: a live session with at least 3 items in assorted states (open, done, pushed).
Note the exact Guilt/Pushes numbers on 2-3 items, and which are marked done.
Stop the dev server (Ctrl-C in the terminal running npm run dev), start it again, and
reload the browser tab. Expect: every item, its guilt/push numbers, its done state,
and your theme/mode settings are byte-for-byte the same as step 1 — nothing lived only
in memory.
This flow could not be exercised live while authoring this document — no Node.js/npm
were available in that sandbox. Treat this as the flow with the least prior confidence
and run it carefully.
Final check: open DevTools → Application → IndexedDB → latered → items, and
confirm the row count matches what All items shows on screen.
Operational checks: the three shells' config
Proves the packaging surfaces beyond the browser — latered ships a Tauri desktop config and a Capacitor mobile config alongside the PWA — are at least structurally sound.
Persona / precondition: a terminal in ~/Projects/latered/app.
Run npm run cap:config-check. Expect: Capacitor config: valid JSON — this exact
check (JSON.parse on capacitor/capacitor.config.json) was independently re-run in
this authoring session via Python's json.load, which succeeded; the npm/Node form
itself was not re-executed here (no Node.js in the authoring sandbox).
Run npm run tauri:config-check. Expect: Tauri config: valid JSON — same caveat:
[sensitive content omitted]
In the running web app, open DevTools → Application → Manifest. Expect: name
"latered", short name "latered", display: standalone, and two icons (icon.svg,icon-512.png) — matching public/manifest.webmanifest verbatim; all six brand/icon
files it and the service worker reference exist on disk (icon.svg, icon-512.png,wolf-mark.svg, empty-desk.svg, desk-bg.jpg, logo-lockup.png).
Check DevTools → Application → Service Workers. Expect: a worker namedlatered-shell-v4 (or newer) is active; reload while online and you should see fresh
content, not a stale cached shell — the worker's own source comment says documents are
served "NETWORK-FIRST... so a new deploy is never held hostage by an old cache".
Final check: run npm test. Expect: all suites pass. The last recorded run
(docs/qa/FINDINGS.md, pre-dating this document) reported "20 files, 94 tests" passing —
a materially lower pass count here means something regressed since that audit and should
be filed, not waved through.
Not covered and why
- Live execution of any flow above: this document was authored in a sandbox with no
Node.js, npm, or browser — every Expect: is sourced from reading the exact strings and
logic in the shipped source at commit 4c144fc, not from watching the app run.
- Tauri desktop shell and Capacitor mobile shell as RUNNING apps (native menus, mobile
gestures, install flow): only their JSON configs were checked (F12); launching the
actual native shells needs platform toolchains (Xcode/Android Studio, Rust) not
available here.
- GitHub/cloud sync: SPEC §1.6 explicitly marks sync "out of scope for this unit" and
src/storage/sync/ exists only as a scaffold — nothing user-facing to test yet.
- The literal 4 am rollover crossing real midnight: time-dependent, cannot be forced
on demand; walk it passively by leaving a tab open past 4 am local time and watching the
date line and the daily-rollover job fire on their own.
- Attachment/image embedding end-to-end (the editor's Attach button, wired to
AttachmentRepository): present in source but not walked step-by-step here; add a flow
once file-size/type limits are documented.
- Multi-day date-picker edge cases (leap years, DST boundaries) in the push engine:
src/core/engine/dates.ts carries its own unit tests, not re-verified in this pass.
Results
| Flow | Verdict | Failures → tickets |
|---|---|---|
| F1 landing + claim your desk | ||
| F2 first-login tour | ||
| F3 the signature push | ||
| F4 multi-day push + guilt math | ||
| F5 inline create + empty day | ||
| F6 the unified editor | ||
| F7 all items | ||
| F8 pro mode | ||
| F9 appearance + lock | ||
| F10 keyboard + reduced motion | ||
| F11 restart survival | ||
| F12 operational config checks |