5d330c2ff4
A spec, ADR, plan file, or Gitea issue records what was true when it was written and then goes stale silently, so an agent that follows the pointer reads a decision that may already have been reversed. Code is the only source true at read time. Strip every non-code citation from the three AGENTS.md files (ADRs, spec and plan files, docs/research, docs/agents/*, DEPLOY/REDEPLOY, and issue numbers), restating inline any fact the linked doc actually carried: the tea command set and triage label strings move into the root Forge section. The Domain docs subsection goes entirely, as it pointed only at CONTEXT.md and docs/adr/, neither of which exists. Then rewrite the backend and userscript files around derivability, since prose that restates mechanism rots the same way a doc link does. Structure and mechanism now name a symbol and stop; rationale, rejected alternatives and dated measurements stay written out, because code cannot carry them. Record that split as a rule in the root file. Verified by extracting all 118 backticked identifiers and checking each against the Go, JS, SQL, HTML and CSS sources. That caught one claim that was already lying: the old cover text said CoverFetcher was gone, but NewCoverFetcher, TLSCoverFetcher and BrowserCoverFetcher are all live in internal/latest, so the sentence now names only the dead /img/kagane route. Also drop the "Guidance for OpenCode (and Claude Code)" openers, so the files read the same under any harness.
104 lines
5.7 KiB
Markdown
104 lines
5.7 KiB
Markdown
Scope: `userscript/`.
|
|
|
|
Each entry names the code that holds the truth. The prose is only what the code
|
|
cannot tell you: rationale, invariants a refactor would break, and dated
|
|
observations about sites we don't control.
|
|
|
|
### Structure — single IIFE, `manga-bookmark.user.js`
|
|
|
|
Six parts, in file order: site adapters, API client, progress logic, retry
|
|
queue, UI, SPA navigation.
|
|
|
|
**Site adapters** — one per host, `detect(location, document)` returning page
|
|
`type` + IDs.
|
|
|
|
- Identify type and IDs from **URL regex**, which is the most stable surface a
|
|
site exposes; take `title` from **`og:title`** (or the page heading where a
|
|
site ships no og: tags), never CSS classes.
|
|
- **No adapter reads a cover.** The backend acquires, stores and serves every
|
|
Cover from its own origin, the wire `cover` is already an address there, and
|
|
`apiPut` strips any `cover` off an outgoing body.
|
|
|
|
**Progress logic** — auto-upsert `last_chapter` only when
|
|
`chapterNum >= stored last_chapter_num`; unparseable sets the current value.
|
|
Re-reading an old chapter must not regress progress. A manual panel override
|
|
forces any value.
|
|
|
|
**Retry queue** — every write goes through `pushBookmark`/`pushDelete`.
|
|
|
|
- Entries are markers (`{key, op, sendStatus, attempts}`), **never payloads**:
|
|
the body is read from cache at send time, so one entry per key gives ordering
|
|
and coalescing for free.
|
|
- `sendStatus` is **sticky** — while an archive is pending, later writes to that
|
|
key keep carrying the bucket. Without it a successful in-between write
|
|
silently un-archives the series.
|
|
- `refresh()` drains before it fetches and overlays anything still pending, so
|
|
the list never flaps.
|
|
- 400 drops the entry, 401 aborts the pass and keeps the queue, transient
|
|
failures retry to a cap. Latest-chapter writes deliberately stay out of the
|
|
queue.
|
|
|
|
**UI** — rendered inside a **Shadow DOM** root to isolate it from site CSS,
|
|
which is critical on mobile.
|
|
|
|
- The FAB's *hit* area is widened by an invisible `#hit` child. `#fab` must keep
|
|
`touch-action: none` and must **not** regain `overflow: hidden`.
|
|
- `touch-action` is resolved at gesture start, so the strip cannot be both
|
|
browser-scrolled and script-dragged. `makeDraggable` therefore splits by
|
|
intent: a swipe from `#hit` scrolls via `window.scrollBy`, a hold of `ARM_MS`
|
|
arms a reposition drag, and the visible sliver drags with no hold.
|
|
|
|
**SPA navigation** — Asura is Astro and client-routes on comic/chapter pages, so
|
|
`history.pushState`/`replaceState` are patched and `popstate` listened to, and
|
|
`detect()` re-runs on URL change. Demonic uses classic reloads, where the
|
|
initial `document-idle` run suffices.
|
|
|
|
### Live URL shapes
|
|
|
|
Encoded in the adapters; the notes below are the parts a reader of the regex
|
|
would get wrong. **Verified 2026-07-26 unless dated otherwise — sites drift, so
|
|
re-check against a live page before trusting any of it.**
|
|
|
|
- **asurascans.com** — the series slug carries a site-wide build-hash suffix
|
|
(e.g. `-059befe1`) that **rotates on every redeploy**, so `seriesId` must
|
|
strip it (`stripBuildHash` here, `asuraBuildHash` in the backend) while URLs
|
|
keep the full slug — stale-hash URLs 302 to current ones.
|
|
- **demonicscans.org** — slugs may URL-encode punctuation, and the older
|
|
`chaptered.php?manga=<id>&chapter=<n>` form still exists as a redirect, which
|
|
is what series-page chapter-list anchors link through. Encodings (including
|
|
triple-encoded punctuation like `%25252D`) are identical on `/manga/` and
|
|
`/title/` pages, so decode-once seriesIds match (verified 2026-07-28).
|
|
- **comix.to** — only the leading `<id>` is identity; the slug re-renders when a
|
|
series is renamed (`comixSeriesId`). It is an SPA that **never rewrites
|
|
`og:title`**: the server-rendered head keeps whatever document loaded first,
|
|
so on a cold load `og:title` is the homepage's name and after an in-page hop
|
|
it is the *previous* series'. `document.title` is the one thing client routing
|
|
updates, hence titles come from there with the chapter page's `" · Ch.<n>"`
|
|
tail stripped. It publishes no `og:image` either, one of the reasons cover
|
|
acquisition moved to the backend.
|
|
- **kagane.to** — reader URLs carry no chapter number, so the number comes out
|
|
of `og:title`. Two shapes exist, `"<Series> - Chapter <n>[ - Episode <n>]"`
|
|
and `"<Series> - Volume <v> Chapter <n>"`; both must yield a bare series
|
|
title, or the volume tail lands in the bookmark's title. Its covers are
|
|
challenge- and CORP-protected, so nothing outside kagane.to can load one —
|
|
the panel renders the backend's cover address like every other Site.
|
|
- **novelfull.com** (novel script) — no `og:*` tags at all, so the title comes
|
|
from `h3.title` (series) or `a.truyen-title` (chapter).
|
|
- **lightnovelworld.net** (novel script) — chapter paths are flat at the site
|
|
root and their slug is a **Chapter Slug, not an identity**: a Series may
|
|
publish under several. The Series address is read off the page's
|
|
`a[aria-label='All Chapter']` (fallback: the BreadcrumbList's second crumb),
|
|
and a chapter page with no pointer resolves to `other` so no Bookmark is
|
|
offered. The client runs **no latest-chapter scan** for this Site —
|
|
`computeLatestChapter` yields null and `backgroundRefreshLatest` skips it
|
|
before any fetch — because the backend Poll's one-hour cooldown dominates the
|
|
client's four-hour throttle, so a scan would add no freshness while having to
|
|
truncate at the page's wpdiscuz thread, a public write surface.
|
|
|
|
### Second script — `novel-bookmark.user.js`
|
|
|
|
A copy of the manga script with two adapters, `LIBRARY = "novel"` and
|
|
`STORE_PREFIX = "bmgr:novel:"`. No migration loop, because this script has no
|
|
previous installation to carry keys over from. Installed alongside the manga
|
|
script; both write to the same backend, discriminated by `LIBRARY`.
|