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>
6.0 KiB
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-infetch/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: onetype=artist,albumSpotify search per candidate, exact case-insensitive name match against either; each result carrieskind: "artist" | "album".extension/— Firefox MV2 content script (content.js+extract.js+manifest.json+styles.css). No build step.extract.jsis the pure candidate extractor (Title-Case runs + cue phrases for artists; quoted strings + album cue phrases for albums) — node-testable viaextract.test.cjs. Album links render italicised (.rsl-album).- shreddit shadow DOM:
collectTextNodesis shadow-piercing. A TreeWalker can't cross a shadow boundary, so it walks the comment-treeroot's light DOM AND recursively descends into the open shadow roots of elements within that subtree. New reddit'sshreddit-*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.
- shreddit shadow DOM:
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.
- Make a small, reversible change.
- Run the gate:
bash scripts/check.sh(must printALL CHECKS PASSED). - Commit citing the towl ref (e.g.
(task/odesli-direct-links)), pushmain. - Stamp the SHA on the towl item:
stamp_commit { ref, sha, repo: "linker" }. - 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), andresolve-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 inlib.js/resolve-core.js(notresolver.js, which has the server side effect) so it stays testable without opening a port.extension/extract.test.cjscovers 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 — validatecontent.jswithnode --checkand 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 maintainsscripts/check.shand thenode:testsuite. - 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 viacreateElement/textContent, neverinnerHTML; http(s) hrefs only), minimal extension permissions, bounded external API calls (no SSRF), secrets never logged/committed (.envis 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_URLenables it,OLLAMA_MODELdefaultsqwen2.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
" 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.