Skip to content

Improve Literal import fidelity: infer read + carry the added date - #515

Merged
fredrivett merged 6 commits into
mainfrom
fredrivett/literal-import-fidelity
Sep 26, 2026
Merged

fredrivett merged 6 commits into
mainfrom
fredrivett/literal-import-fidelity

Conversation

@fredrivett

@fredrivett fredrivett commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

What & why

Literal→abode mapping fidelity so imported books reflect the real library, plus a dedicated column and guardrail for the "added" date so it's honest and can't silently regress (backend: adapter + writer + timeline read path).

  1. Infer read for reviewed-but-unshelved books. Literal reports some books as NONE (unshelved) while still keeping a rating/review on them (e.g. Wild at Heart — 4.5★ + a review, status NONE). We mapped NONE → null ("Not tracked") while the rating/review imported from a separate query — so they landed as untracked books with an orphaned rating/review. A review means the book was read, so we now infer read when a NONE/unshelved book has a rating or review (and take its finish date from the review). Books already on a shelf keep their real status; a mid-read review still stays reading.

  2. Carry the Literal added date via a dedicated addedAt column. Imported books record Literal's shelf-added date in a new Item.addedAt column, and the dashboard timeline sorts (and the item card's "Saved" label reads) by addedAt — so imports sit at their true place in the timeline instead of piling at import time. addedAt equals createdAt for everything captured in abode (existing rows are backfilled), so only back-dated imports move; createdAt stays the truthful row-creation time. Includes a migration that adds added_at (default now()) and backfills existing rows from created_at.

  3. Guardrail against regressing the sort. The timeline ordering and keyset cursor are centralized in one ITEM_TIMELINE_ORDER_BY constant (+ itemTimelineCursorWhere/itemTimelineCursor helpers) used by both the items API and the dashboard SSR page — previously three hand-copied blocks, any of which could revert to createdAt. An integration test inserts items with createdAt/addedAt deliberately inverted and asserts the listing and cursor keyset follow addedAt, so a regression back to createdAt fails regardless of how the query is written.

Checklist

  • User-facing change → PostHog event, or N/A — N/A (mapping fidelity; existing import funnel events unchanged)
  • New error paths report via captureServerException, or N/A — N/A
  • New behavior is covered by tests — adapter (infer-read from review/rating, addedAt), payload round-trip, writer (addedAt back-dating + now() fallback), query transform, timeline-ordering integration test, updated cursor/fixtures
  • If this fixes a recurring defect, considered a guardrail — yes: centralized sort constant + inverted-date integration test

Notes

  • Applies to future imports only. Because the writer dedupes by ISBN, re-importing skips books you already have, so already-imported books won't be back-filled. To correct an existing import: delete the imported books and re-import (a targeted delete of kind=book + meta.importSource="literal" for the user is the clean way).
  • Migration: adds Item.addedAt and backfills existing rows to created_at; runs on deploy.

🤖 Generated with Claude Code

Two mapping fixes so imported books reflect the real library:

- Infer `read` for a book Literal reports as unshelved (`NONE` → null) that has
  a rating or review — a review means it was read, so it shouldn't import as
  "Not tracked" (with the rating/review orphaned). Shelved books keep their real
  status; an inferred-read book takes its finish date from the review.
- Carry Literal's shelf-added date onto the item's `createdAt`, so imported books
  sit at their true place in the timeline instead of all at import time. Safe:
  no account-scoped logic reads item.createdAt (only sort/sweep ordering).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
abode Ignored Ignored Preview Sep 26, 2026 10:08am UTC

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread app/src/lib/imports/literal/adapter.test.ts
fredrivett and others added 5 commits September 20, 2026 22:30
Strengthen the rating-only inferred-read test to assert finishedAt is taken from
the review's createdAt (distinct from the shelf date), covering the stated
behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
addedAt is the date an item entered the library — equals createdAt for
items captured in abode, but imports back-date it to the source's own
"added" date so imported items slot into their historical place.
createdAt stays the truthful row-creation time.

Migration adds added_at (default now()) then backfills existing rows
from created_at so their timeline position is preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The importer now sets addedAt to the source's added date (Literal's
reading-state createdAt) instead of overloading the item's createdAt.
createdAt reverts to the truthful row-creation time.

Falls back to the column default (now) when the source has no added date.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dashboard now sorts by addedAt (keyset cursor + orderBy in the items
API and SSR first page) and the item card's "Saved" label reads addedAt,
so back-dated imports appear in their historical place. The shared cursor
codec keys on addedAt. Search exposes added_at on its results for the same
card display. For non-import items addedAt equals createdAt, so existing
items are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the library-timeline ordering into a single ITEM_TIMELINE_ORDER_BY
constant plus itemTimelineCursorWhere/itemTimelineCursor helpers in query.ts,
and route both the items API and the dashboard SSR first page through them.
Previously the orderBy and keyset cursor were hand-copied across three places,
any of which could silently revert to createdAt.

Add an integration test that inserts items with createdAt and addedAt
deliberately inverted and asserts the listing (and cursor keyset) follow
addedAt — so a regression back to createdAt fails regardless of how the
query is written.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 14 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread app/src/lib/pagination.ts
@fredrivett
fredrivett merged commit c31fbe1 into main Sep 26, 2026
13 checks passed
@fredrivett
fredrivett deleted the fredrivett/literal-import-fidelity branch September 26, 2026 10:21
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