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

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-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 " 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.