docs: make every AGENTS.md cite code, not docs or issues (#113)

Every `AGENTS.md` now cites code and nothing else.

## Why

Two rot mechanisms, same symptom — an agent confidently follows a stale statement:

1. **Non-code citations.** A spec, ADR, plan file, or issue records what was true when it was written. Nothing updates it when the decision reverses.
2. **Prose restating mechanism.** The code changes, the paragraph doesn't, and the next reader trusts the paragraph.

Code is the only source true at read time.

## What changed

**All three files:** removed every ADR ref, spec/plan pointer (`docs/superpowers/specs/*`, `plans/*`, `docs/research/*`), `DEPLOY.md`/`REDEPLOY.md`, `docs/agents/*`, and issue number. Facts those links carried are restated inline — the `tea` command set and the five triage label strings now live in the root Forge section. `### Domain docs` is deleted: it pointed only at `CONTEXT.md` and `docs/adr/`, neither of which exists.

**`backend/` and `userscript/`:** rewritten around derivability.

| Class | In code? | Treatment |
|---|---|---|
| Structure — packages, routes, env vars, columns | yes | name the symbol, nothing else |
| Mechanism — what a function does | yes | symbol + one line |
| Rationale — why, what a "simplify" breaks | **no** | written out |
| Measurement — observation against a service we don't control | **no** | written out, dated |

`backend/AGENTS.md` 20578 → 15512 bytes, `userscript/AGENTS.md` 7129 → 5912. Root grows 16905 → 19292: the cost of inlining the `docs/agents/*` facts plus the new rule.

**Rule** recorded in root as `## Writing an AGENTS.md`. Sole non-code exception is a sibling `AGENTS.md`. Closing clause: every symbol named must exist, since a dead pointer is a bug rather than a stale sentence.

**Harness-agnostic:** dropped the `Guidance for OpenCode (and Claude Code)` openers for plain scope lines.

## Verification

Applied the new rule to itself — extracted all 118 backticked identifiers across the three files and checked each against every `.go`, `.js`, `.sql`, `.html` and `.css` source. Zero repo symbols missing; the 8 non-matches are external (`GM_setValue`, `navigator.webdriver`, `HeadlessChrome`, `curl`, …).

That check caught a claim that was **already lying** on `main`: the cover section said `CoverFetcher` was gone, but `NewCoverFetcher`, `TLSCoverFetcher` and `BrowserCoverFetcher` are all live in `internal/latest`. Now names only the genuinely dead `/img/kagane/{id}` route. Exactly the failure the rule exists to prevent.

No code touched — documentation only, nothing to test.

Reviewed-on: #113
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #113.
This commit is contained in:
2026-08-17 13:43:49 +07:00
committed by sulthan
parent 550b258c59
commit 766aa8f00d
3 changed files with 433 additions and 394 deletions
+94 -84
View File
@@ -1,93 +1,103 @@
Guidance for OpenCode (and Claude Code) working under `userscript/`. See root `AGENTS.md` for the project-wide architecture diagram, hard constraints, and design system.
Scope: `userscript/`.
### Userscript structure (single IIFE, `manga-bookmark.user.js`)
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.
1. **Site adapters** — one per host, `detect(location, document)` return page `type` + IDs. Identify type/IDs from **URL regex** (most stable); pull `title` from **`og:title`** (or the page heading where a site ships no og: tags), not CSS classes. **No adapter reads a cover**: the backend acquires, stores and serves every Cover from its own origin (ADR-0007), the wire's `cover` is already an address on our origin, and `apiPut` strips any `cover` off an outgoing body.
2. **API client** — `apiGet/apiPut/apiDelete` with bearer header; `localStorage` key `bmgr:manga:cache` for instant render + offline fallback.
3. **Progress logic** — auto-upsert `last_chapter` only when `chapterNum >= stored last_chapter_num` (re-reading old chapters must not regress progress; unparseable -> set current). Manual panel override forces any value.
4. **Retry queue** — every write go through `pushBookmark`/`pushDelete`, so
failed mutation park in `localStorage` (`bmgr:manga:queue`) and replayed on
next navigation, reconnect, or `refresh()`. Entries are markers
(`{key, op, sendStatus, attempts}`), never payloads — body read from
cache at send time, so one entry per key give ordering and coalescing for
free. `sendStatus` is **sticky**: while archive pending, later writes to
that key keep carrying bucket, which stop successful
in-between write from silently un-archiving series. `refresh()` drains
before it fetches and overlays anything still pending, so list never
flaps. 400 drops entry, 401 abort pass and keep queue, and
transient failures retry to cap of 10. Latest-chapter writes deliberately
stay out of queue. See
`docs/superpowers/specs/2026-07-27-offline-retry-queue-design.md`.
5. **UI** — rendered inside **Shadow DOM** root to isolate from site CSS
(critical on mobile). Three tabs (All / Favourites / Archived) and row of
link chips to web UI and both manga sites; `WEB_BASE` sits in CONFIG
block next to `API_BASE`. FAB is `7 × 44` edge tab whose *hit* area
widened to `28 × 72` by invisible `#hit` child; `#fab` must keep
`touch-action: none` and must **not** regain `overflow: hidden`. Since
`touch-action` resolved at gesture start, strip can't be both
browser-scrolled and script-dragged, so `makeDraggable` splits by intent: swipe
from `#hit` scrolls via `window.scrollBy`, hold of `ARM_MS` arms
reposition drag, visible sliver drags with no hold. See
`docs/superpowers/specs/2026-07-28-edge-tab-hitbox-design.md`.
6. **SPA navigation** — Asura is Astro, client-routed on comic/chapter pages: patch `history.pushState`/`replaceState` + listen `popstate`, re-run `detect()` on URL change so auto-update fire without reload. Demonic uses classic reloads (initial `document-idle` run suffice).
### Structure — single IIFE, `manga-bookmark.user.js`
### Live URL shapes (verified 2026-07-26, may drift — re-check against live pages before trust)
Six parts, in file order: site adapters, API client, progress logic, retry
queue, UI, SPA navigation.
- **asurascans.com**: series `/comics/<slug>` (slug carries trailing
site-wide build-hash suffix, e.g. `-059befe1`, that **rotates on every
redeploy**), chapter `/comics/<slug>/chapter/<n>`. `seriesId` must strip
hash (`/-[0-9a-f]{8}$/`, `stripBuildHash` in userscript,
`asuraBuildHash` in backend); URLs keep full slug — stale-hash
URLs 302 to current ones. Astro-rendered; chapter links present in raw
server HTML.
- **demonicscans.org**: series `/manga/<slug>` (slug may URL-encode punctuation, e.g. `%2527` for `'`), chapter `/title/<slug>/chapter/<n>/<page>` (older `chaptered.php?manga=<id>&chapter=<n>` form still exists as redirect, what series-page chapter-list anchors link through).
Encodings (incl. triple-encoded punctuation like `%25252D`) identical
on /manga/ and /title/ pages, so decode-once seriesIds match — verified
2026-07-28.
- **comix.to**: series `/title/<id>-<slug>`, chapter
`/title/<id>-<slug>/<uploadId>-chapter-<n>`. Only the leading `<id>` is
identity — the slug re-renders when a series is renamed (`comixSeriesId`).
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
"Comix — Read Comics online for free" and after an in-page hop it is the
*previous* series' name. `document.title` is the one thing client routing does
update, so titles come from there, with the chapter page's `" · Ch.<n>"` tail
stripped. It publishes no `og:image` either, which is one of the reasons cover
**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**: series `/series/<uuid>`, reader
`/series/<uuid>/reader/<bookUuid>`. Reader URLs carry no chapter number, so
the number comes out of `og:title`. Two shapes exist: `"<Series> - Chapter
<n>[ - Episode <n>]"` and, for volume-numbered series, `"<Series> - Volume <v>
Chapter <n>"` with no episode name — 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 directly; the panel renders the backend's own cover address like every
other Site. Behind a Cloudflare JS challenge, so the backend polls it
through the headless browser.
- **novelfull.com** (novel script): series `/<slug>.html`, chapter
`/<slug>/chapter-<n>[-<title-slug>].html`. No `og:*` tags at all — title from
`h3.title` (series) or `a.truyen-title` (chapter); the script reads no cover.
Behind a Cloudflare JS challenge no TLS fingerprint
clears, so the backend polls it through the headless browser.
- **lightnovelworld.net** (novel script): series `/novel/<slug>/`, chapter
`/<slug>-chapter-<n>/` — flat, at the site root. The chapter path's slug is a
Chapter Slug, not an identity: the Series address is read off the page's
- **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 Series may publish under several Chapter Slugs. A chapter page with no
pointer resolves to `other`, so no Bookmark is offered. `h1.entry-title` is
the clean title on a series page and `<Title> Chapter <n>` on a chapter page.
Its series page lists every chapter with an
absolute href, so the backend polls it with the plain TLS client.
The client performs no latest-chapter scan for this Site: the Poll's
one-hour cooldown dominates the client's four-hour throttle, so a scan
would add no freshness, and the page's wpdiscuz thread is a public write
surface a scan would have to truncate at. `computeLatestChapter` yields
null here and `backgroundRefreshLatest` skips the Site before any fetch.
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`
### 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 (this script has no previous
installation to carry keys over from). Installed alongside the manga script;
both write to the same backend with the same `LIBRARY` column discriminating
them.
`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`.