Spec: lightnovelworld Series identity is read from the chapter page, not derived from it (#77) #80
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Spec for #77. Design settled in a grilling session on 2026-08-11; the decision is recorded in ADR-0008 and the vocabulary in
CONTEXT.md. Evidence:docs/research/lightnovelworld-chapter-vs-series-slug.md.Problem Statement
A Reader bookmarks a novel on lightnovelworld and it never shows a New Chapter, ever. The
Series sits in the list with a Latest Chapter frozen at whatever it was on the day it was
added. Nothing in the panel says anything is wrong.
For a smaller group of Readers the failure is louder: the same novel appears in the list
twice. One row was created from the novel's own page, the other from a chapter page,
and only one of them ever updates.
Around 7 to 10% of the novels on this Site are affected (3 of 41 sampled). The Site gives
no hint that these novels are different from any other.
Solution
The userscript stops guessing a Series' address and reads it from the page instead.
Every lightnovelworld chapter page carries a pointer back to the Series it belongs to -
an
All Chapteranchor, plus a microdata breadcrumb saying the same thing. The userscriptreads that pointer and uses the slug in it as the Series identity. Where the pointer is
absent, the page is not a page the script understands and no Bookmark is offered, rather
than a Bookmark being created under an identity that was invented.
Existing Bookmarks created under the old, invented identity are repaired in place the next
time the Reader opens a chapter page of that novel. The Reader sees nothing: no second
row, no lost Progress, no prompt.
Separately, the Poll's chapter scan on this Site stops being scoped to a slug at all, so it
keeps working for a novel whose chapters are published under more than one Chapter Slug.
Because an unscoped scan would otherwise read the visitor comment thread at the foot of
the page, the scan is cut off before that thread begins.
User Stories
CONTEXT.mdand in #79.Implementation Decisions
Identity is discovered, never derived (ADR-0008)
The
lightnovelworldadapter's chapter branch no longer builds a Series address by stringmanipulation of the chapter path. It reads the page's
All Chapteranchor, whosehrefisthe Series address, and takes the slug from it. The primary selector is the
aria-labelattribute on that anchor; it was chosen over three alternatives on measurement (a generic
/novel/href match hits the header navigation index first, a text match false-matches anovel whose title ends in "All Chapter 200", and the JSON-LD breadcrumb's second position
is the chapter rather than the Series). The microdata breadcrumb's second crumb is the
fallback. When neither is present the page resolves to
type: "other".Across 8 chapter pages measured - chapter 1, chapter 1200, the newest chapter, both slugs
of the split novel, two divergent novels, and a novel with a number in its title - both
pointers were present and agreed every time.
The Chapter Slug is not stored
A Chapter Slug is the slug a chapter address is built from. It is not an identity, and a
Series may have several: one sampled novel serves chapters 1-99 under one Chapter Slug and
chapters 100-423 under another, both resolving, both pointing back at the same Series.
Storing it beside the identity was considered and rejected for that reason. The page shape
is carried on the detected page object only so the migration below can recognise a stale
row; nothing persists it.
The Poll's scan is unscoped and truncated
The
lightnovelworldbranch of the backend's chapter scan drops its per-Series slugscoping and matches any chapter-shaped address on the host. This is what fixes the
split-slug novel, which no stored-slug approach can fix.
An unscoped scan would otherwise read the wpdiscuz comment thread the Site server-renders
below the chapter list, whose bodies are HTML and can carry an anchor. The scan therefore
runs against the body truncated at the first occurrence of the comment-thread marker. The
marker was chosen by measurement: it occurs exactly once per page and follows every chapter
anchor on all four Series pages sampled, whereas the two obvious alternatives occur 111 to
143 times per page including in
<head>, before the chapter list.A container-scoped match was considered and rejected. The research note originally proposed
one container; a later probe showed that container is the hidden, empty "Latest Reading"
template and the real list is classless. The backend scans raw HTML with no parser, so
container extraction means a second regex against class names - more fragile than the
truncation, protecting nothing extra.
Fail closed. Marker absent means skip the Series and log. It does not mean scan the
whole page. The log line carries the body length, because the marker sits at roughly 94% of
these documents (measured: byte 646,329 of 685,023 on one page, 1,117,457 of 1,183,036 on
another) and a body cut short by the size cap is indistinguishable from a markup change
without it. The size cap has about 3.5x headroom on this Site, not the 10x its comment
claims; correct that comment with the measured figure.
Client-side migration, opportunistic
A stored row is recognised as stale with no guessing: the chapter page yields both the
Chapter Slug from its own address and the series slug from its pointer. If those differ and
a row exists under the Chapter Slug identity, that row is the stale one.
The rewrite covers four storage sites, all of which must move together: the cached row's
key, identity and address; the retry-queue entry, if one exists under the old key; and the
per-device last-checked map. Then it syncs. Note the queue may hold an entry under the old
key whose replay still succeeds against the old server row, so the rewrite must cover the
queue rather than letting the two keys coexist.
A server-side migration was considered and rejected as impossible: the database holds no
source for the correct slug, and no endpoint on the Site maps one slug to the other.
The repaired row leaves its old shared Series row behind. Nothing deletes it, but the
Poll's due-query is an inner join against Bookmarks, so a Series with no Bookmarks is never
polled again - the orphan is permanently stored and permanently inert. A Reader who never
opens a chapter page of an affected novel does not heal, and continues exactly as today.
Vocabulary
CONTEXT.mdalready carries the settled terms: Series is identified by the canonicalslug the Site publishes, never by title and never by a Chapter Slug; Chapter Slug is a
new term, defined as plural by nature and always discovered; Latest Chapter is the
highest-numbered chapter, not a date and not the Site's own banner.
Testing Decisions
A good test here asserts a value a Reader or the Poll can observe - a resolved identity, a
chapter number, a rewritten row - and would fail if the logic broke. It does not assert
which selector was tried first, that a particular regex was compiled, or that a helper was
called. Three of the four seams already exist and must be reused; only one is new.
Seam 1 - the backend's chapter scan. Table-driven cases against fixture bodies, the
existing prior art in
sites_test.go. Cases: a real Series page body with a real commentblock after the marker, containing a link to a high-numbered chapter of another novel,
asserting the comment cannot win; the split-slug novel, asserting the maximum spans both
Chapter Slugs; a body with no marker, asserting skip rather than whole-page scan.
The current fixture must change. It contains an invented anchor annotated "the last anchor
is another series", and the live survey found no Series page that carries a foreign chapter
anchor - the fixture encodes a page shape the Site does not produce, so its test passes
because of a fiction. Remove it and use real page text. It also uses a novel that is a poor
choice for two independent reasons: its Chapter Slug and series slug coincide, which is why
the defect slipped through in the first place, and its chapter numbering is the anomaly in
#79.
Seam 2 - the userscript's page detection. The existing pure-logic harness in
novel-logic.test.js, which requires the userscript under a four-object browser stub. Thestub's
document.querySelectorcurrently answersmeta[property=...]and plain elementselectors returning text only; it needs to answer an attribute selector with an element
exposing
getAttribute. Extend the stub rather than working around it, per thetesting-the-userscriptskill. Cases: pointer present, identity comes from the pointer andnot the address; pointer absent, breadcrumb present, same result; both absent,
type: "other"; a divergent novel, asserting the two slugs are different and the right one wins.Seam 3 - the row migration. The only new seam. It is written as a pure transform:
cached list, queue and last-checked map in, the same three rewritten out, no storage access
inside. This keeps it inside the existing harness with no new stubbing, and it is the shape
worth testing precisely because the bug hides in one of the four sites being missed. Cases:
all four sites rewritten together; no stale row, nothing changes; queue entry absent, the
other three still move; the row's Progress, Favourite and Lifecycle bucket survive.
Seam 4 - the live canary.
TestSmokeLnwCommentBoundary, an env-gated live testasserting the marker still occurs exactly once and still follows the last chapter anchor.
Prior art and convention:
TestSmokeKaganeImageinsmoke_image_test.go, which skips whenits environment variable is unset. Not part of
go test ./....Not testable here and verified on-device instead, per the skill: the panel's button state,
the toast, and anything reachable only through init.
Out of Scope
a-will-eternalnumbering anomaly. Filed as #79 and closed by decision: Latest Chapter follows the largest number. Do not add a widget or date cross-check while working here.Further Notes
Commands, from the
testing-the-userscriptskill andAGENTS.md:Use the test file path, not the directory form - the directory form fails MODULE_NOT_FOUND
on this machine's Node.
Two facts worth carrying into the work because they cost time to establish. First, these
Series pages are large - up to about 1.18 MB - and sit mostly below the fold of any
truncating fetch, so a probe that reads the first few hundred KB will report the comment
marker as absent when it is present. Two separate investigations reached opposite
conclusions on exactly this before it was resolved by a complete fetch. Second, the old
domain
lightnovelworld.comis shut down and serves a short notice page; only the.netdomain is live.