feat(field-guide): record-on-read wiring for the card album (FG-4) #97

Merged
manlycucumber merged 1 commit from feat/field-guide-record-on-read into develop 2026-07-11 20:33:39 +00:00
manlycucumber commented 2026-07-11 20:32:37 +00:00 (Migrated from github.com)

FG-4 — Record-on-read wiring

The card album's write path. Reading a creature's Library article now quietly records a card for the active child — the first half of the album feature (the album view is FG-5).

What changed

  • src/lib/fieldguide/RecordOnRead.svelte — a DOM-less component that records a card once, on mount. It is:
    • creatures-only (topic.category === 'creatures');
    • profile-snapshotted at mount — it fires from onMount, not $effect, so activeProfile is not a reactive dependency; switching profiles while an article stays open records nothing new;
    • idempotent (re-reading a creature re-mounts it, but recordOnce is insert-if-absent — no re-timestamp);
    • silent + best-effort — a no-op with no active profile, and it swallows storage failures (private mode / quota / locked-down device) so a missed record never surfaces as an unhandled rejection.
  • Mounted beside <ArticleView> at the three article hosts — the standalone route library/[category]/[topic], LibraryBody, and FieldGuideBody, each inside the {#key topic.path} block. ArticleView stays pure and state-free — the write is a host concern, not the view's.

Records, not rewards

A card stores only { v: 1 } keyed by slug. No count, streak, percentage, or score — title/art join from live gated frontmatter at view time, so an un-approved creature can never leak a title through the album.

Tests

  • Wiring (source-scan) — all three hosts import and mount RecordOnRead inside {#key topic.path}; ArticleView imports no state; the recorder uses onMount, never $effect.
  • Album e2e (tests/e2e/album.spec.ts, reads IndexedDB directly since there's no view yet) — records one card on read; idempotent re-read keeps one card with a frozen timestamp; switching profiles without navigating writes nothing (with a positive control that a subsequent open under the new profile does record); a profileless standalone article records nothing and never crashes.

Verification

Full local gauntlet green: check (430 files, 0 errors) · lint · 168 unit · build (search index unchanged at 2) · all 5 guards (colour/external/content/provenance/offline) · 34 e2e. Hardened after a 6-lens adversarial review (0 findings survived per-finding refutation; three refuted findings were still adopted as cheap defense-in-depth: swallow best-effort write failures, lock the {#key} placement in the wiring test, guard the e2e helper against DB poisoning).

Part of the v0.4.0 Field Guide epic (#92).

## FG-4 — Record-on-read wiring The card album's **write path**. Reading a creature's Library article now quietly records a card for the active child — the first half of the album feature (the album *view* is FG-5). ### What changed - **`src/lib/fieldguide/RecordOnRead.svelte`** — a DOM-less component that records a card **once, on mount**. It is: - **creatures-only** (`topic.category === 'creatures'`); - **profile-snapshotted at mount** — it fires from `onMount`, *not* `$effect`, so `activeProfile` is not a reactive dependency; switching profiles while an article stays open records nothing new; - **idempotent** (re-reading a creature re-mounts it, but `recordOnce` is insert-if-absent — no re-timestamp); - **silent + best-effort** — a no-op with no active profile, and it swallows storage failures (private mode / quota / locked-down device) so a missed record never surfaces as an unhandled rejection. - Mounted beside `<ArticleView>` at the **three article hosts** — the standalone route `library/[category]/[topic]`, `LibraryBody`, and `FieldGuideBody`, each inside the `{#key topic.path}` block. **`ArticleView` stays pure and state-free** — the write is a host concern, not the view's. ### Records, not rewards A card stores only `{ v: 1 }` keyed by slug. No count, streak, percentage, or score — title/art join from live gated frontmatter at view time, so an un-approved creature can never leak a title through the album. ### Tests - **Wiring (source-scan)** — all three hosts import and mount `RecordOnRead` **inside `{#key topic.path}`**; `ArticleView` imports no state; the recorder uses `onMount`, never `$effect`. - **Album e2e** (`tests/e2e/album.spec.ts`, reads IndexedDB directly since there's no view yet) — records one card on read; idempotent re-read keeps one card with a frozen timestamp; **switching profiles without navigating writes nothing** (with a positive control that a subsequent open under the new profile *does* record); a profileless standalone article records nothing and never crashes. ### Verification Full local gauntlet green: `check` (430 files, 0 errors) · lint · **168 unit** · build (search index unchanged at 2) · all **5 guards** (colour/external/content/provenance/offline) · **34 e2e**. Hardened after a **6-lens adversarial review** (0 findings survived per-finding refutation; three refuted findings were still adopted as cheap defense-in-depth: swallow best-effort write failures, lock the `{#key}` placement in the wiring test, guard the e2e helper against DB poisoning). Part of the v0.4.0 Field Guide epic (#92).
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Bosco/bosco!97
No description provided.