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>
5.7 KiB
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
titlefromog: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
coveris already an address there, andapiPutstrips anycoveroff 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. sendStatusis 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
#hitchild.#fabmust keeptouch-action: noneand must not regainoverflow: hidden. touch-actionis resolved at gesture start, so the strip cannot be both browser-scrolled and script-dragged.makeDraggabletherefore splits by intent: a swipe from#hitscrolls viawindow.scrollBy, a hold ofARM_MSarms 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, soseriesIdmust strip it (stripBuildHashhere,asuraBuildHashin 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 rewritesog:title: the server-rendered head keeps whatever document loaded first, so on a cold loadog:titleis the homepage's name and after an in-page hop it is the previous series'.document.titleis the one thing client routing updates, hence titles come from there with the chapter page's" · Ch.<n>"tail stripped. It publishes noog:imageeither, 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 fromh3.title(series) ora.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 tootherso no Bookmark is offered. The client runs no latest-chapter scan for this Site —computeLatestChapteryields null andbackgroundRefreshLatestskips 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.