Source: task/getroot-comment-tree-scope (plan/steward-linker-design) — getRoot's union querySelector resolved to <main> on new reddit, so the shadow-piercing walk over-collected reddit chrome. - getRoot now tries shreddit-comment-tree, then .commentarea, then main/body sequentially (narrow-first) instead of one union querySelector, so the comment tree wins when present. - collectInto takes an allowShadow flag: a narrow comment root pierces every open shadow host; a broad fallback root descends only into comment-component hosts (tagName SHREDDIT-COMMENT*), so chrome shadow roots (nav/sidebar/composer) are never walked. - README + AGENTS shadow-DOM notes updated to describe the two-layer scoping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.5 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 chosenroot'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). Scope is two-layer:getRoottries the narrow comment containers first (shreddit-comment-tree, then old reddit's.commentarea) before the broadmain/body, so on a new-reddit comment page the root is the comment tree and the walk stays within it; and whengetRootfalls back to a broad root (no comment tree present) the recursion descends only into comment-component shadow hosts (tagNamestartingSHREDDIT-COMMENT), so reddit's chrome shadow roots (nav, sidebar, composer) are never walked. (A single unionquerySelectorcould not do this — it returns the first match in document order, and<main>is an ancestor of the comment tree, so it would always win.) It 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.