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>
98 lines
6.0 KiB
Markdown
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.
|