Files
linker/AGENTS.md
T
paulandClaude Opus 4.8 071cc3f85a fix: pierce new-reddit (shreddit) shadow DOM when collecting comment text (closes task/handle-shreddit-shadow-dom)
Source: task/handle-shreddit-shadow-dom (plan/steward-linker-design) — the TreeWalker only walked light DOM, so new-reddit comment bodies inside shadow roots were missed.

- collectTextNodes now walks the comment-tree root's light DOM AND recursively descends into the open shadow roots of elements within that subtree, so comment text is reached whether reddit slots it (light DOM) or encapsulates it (shadow DOM).
- Scoped to getRoot()'s comment tree (never the whole document's shadow roots); bounded by a depth cap; a no-op on old reddit (no shadow roots).
- Slotted light-DOM nodes are not double-collected (a shadow walker sees the shadow tree's own nodes, not a <slot>'s assigned light nodes); a Set de-dupes by node identity belt-and-braces.
- All existing guards preserved (inSkippable / rsl-link skip). README + AGENTS document the support.
- Limitation: CLOSED shadow roots are unreachable from a content script and stay unlinked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:28:31 +00:00

98 lines
6.0 KiB
Markdown

# Agent rules for the `linker` repo (reddit → spotify/bandcamp)
A Firefox extension + a tiny Node resolver that turn artist/band names in reddit
comments into Spotify/Bandcamp links. This file is the repo-local doctrine; the work
is tracked in the **towl** work-item store under project **`reddit-spotify-linker`**.
## Stack & layout
- `service/` — zero-dependency Node resolver (Node 18+, built-in `fetch`/`http`).
`resolver.js` = the thin HTTP server that wires the real Spotify search + ollama
classify into resolve-core; `resolve-core.js` = the pure resolve orchestration
(`resolveAll`/`resolveOne`/`pickDirect`/`cacheKey`, taking injected search/classify
fns — unit-testable without network); `lib.js` = pure, side-effect-free logic (the
exact-name match precision lever + link shaping) so it's unit-testable; `llm.js` =
pure ollama prompt/parse helpers (the optional local-LLM gate, HTTP-only — no
in-process model runtime); `lib.test.js` + `llm.test.js` + `resolver.test.js` =
`node:test`. The only music API is Spotify (MusicBrainz removed). The resolver confirms **both artists
and albums**: one `type=artist,album` Spotify search per candidate, exact
case-insensitive name match against either; each result carries
`kind: "artist" | "album"`.
- `extension/` — Firefox **MV2** content script (`content.js` + `extract.js` +
`manifest.json` + `styles.css`). No build step. `extract.js` is the pure candidate
extractor (Title-Case runs + cue phrases for artists; quoted strings + album cue
phrases for albums) — node-testable via `extract.test.cjs`. Album links render
italicised (`.rsl-album`).
- **shreddit shadow DOM:** `collectTextNodes` is shadow-piercing. A TreeWalker can't
cross a shadow boundary, so it walks the comment-tree `root`'s light DOM AND
recursively descends into the **open** shadow roots of elements within that subtree.
New reddit's `shreddit-*` components may encapsulate comment bodies in a shadow root;
this reaches them whether reddit slots (light DOM) or encapsulates (shadow DOM), with
no need to know which. It is scoped to the comment-tree root (never the whole
document's shadow roots, which would pull in reddit chrome) and is a no-op on old
reddit (no shadow roots). Slotted light-DOM nodes are not double-collected (a shadow
walker sees the shadow tree's own nodes, not a `<slot>`'s assigned light nodes; a
Set de-dupes belt-and-braces). **Closed** shadow roots are unreachable from a content
script — a documented limitation.
- `scripts/check.sh` — the local check gate.
## How work lands (IMPORTANT — this is NOT the towl repo)
`linker` is hosted at **git.yapplesauce.com/paul/linker**, a **plain repo**: there is
**no PR pipeline and no branch protection**. You **push to `main` directly** (via the
user's PAT). Do **not** use `towl-orchestrate` / worktree+PR here.
1. Make a small, reversible change.
2. Run the gate: `bash scripts/check.sh` (must print `ALL CHECKS PASSED`).
3. Commit citing the towl ref (e.g. `(task/odesli-direct-links)`), push `main`.
4. Stamp the SHA on the towl item: `stamp_commit { ref, sha, repo: "linker" }`.
5. Close the towl item.
**NEVER touch the `towl` or `stickball` repos from work scoped to this project.**
## Testing (zero-dep)
`node --test` over `service/*.test.js` exercises `lib.js` (norm + the exact-name match
+ link builders), `llm.js` (the classifier prompt/parse), and `resolve-core.js` (the
album/artist orchestration — kindHint tie-break, kind-aware cache key, allowFallback
gating — driven with injected fake search/classify fns, no network). Keep pure logic in
`lib.js`/`resolve-core.js` (not `resolver.js`, which has the server side effect) so it stays
testable without opening a port. `extension/extract.test.cjs` covers the pure extractor
(artist/cue/album candidates) and must be extended with each detection change. **Add/extend a
test with every logic change.** The extension's in-browser behavior is **not** headlessly
testable in this environment — validate `content.js` with `node --check` and a
documented manual check on a real reddit thread (`about:debugging` → load temporary
add-on).
## Code stewards (towl plans under objective/linker-stewardship)
Standing, language-appropriate stewards (adapted from towl's SCOUT→FILE doctrine; run
them like any other steward — e.g. via a `/loop`). Each files findings into towl and
fixes small things directly.
- **quality** (`plan/steward-linker-quality`) — correctness + regressions; **owns and
maintains `scripts/check.sh` and the `node:test` suite**.
- **design** (`plan/steward-linker-design`) — detection precision/recall, the
injected-link UX, README accuracy, reddit-DOM drift.
- **security** (`plan/steward-linker-security`) — DOM injection (build nodes via
`createElement`/`textContent`, **never `innerHTML`**; http(s) hrefs only), minimal
extension permissions, bounded external API calls (no SSRF), secrets never
logged/committed (`.env` is gitignored).
## Standing decisions
- Firefox **MV2** stays — Mozilla has no deprecation timeline; do not migrate to MV3
speculatively.
- **Optional local LLM is the recommended better-matching path** (human override,
feedback 2026-06-18 — supersedes the old "no ML/NER" rule). The resolver may call
ollama over HTTP (`OLLAMA_URL` enables it, `OLLAMA_MODEL` defaults `qwen2.5:3b`)
to classify which candidates are real artists/albums. It stays **zero-NPM-dep**:
HTTP only, no in-process model runtime. The heuristic + Spotify exact-match is the
zero-dep default when no LLM is configured.
- Detection precision lever (zero-dep default): a candidate links **only** when
Spotify returns an **artist or album** whose name matches it case-insensitively. A
confirmed name links to its Spotify artist/album page if available, else a
"<name> bandcamp" Google fallback. Album candidates are over-generated from precise
signals only (quoted strings + album cue phrases) because album free-text is noisier
than artist names — the Spotify exact-match (and/or LLM `kind=album`) is the filter.