Source: task/odesli-direct-cross-platform-links (plan/improvement-backlog) — bandcamp was only a Google search; Odesli gives direct cross-platform links for Spotify-resolved names.
- service/odesli.js: makeOdesli({fetch,baseUrl}) -> getLinks(spotifyUrl); pure parseOdesli core; bounded AbortSignal.timeout; non-200/timeout/garbage -> {} (graceful).
- resolver.js: opt-in ODESLI / ODESLI_URL flag (default OFF). When ON, enrich each Spotify-resolved result with additive bandcamp/appleMusic/youtube fields (isHttpUrl-validated), cached per Spotify url. OFF -> no call, byte-identical behavior. /health + startup log surface the flag.
- extension: a tiny additive "bc" link to a direct Bandcamp page when result.bandcamp is present (reuses isSafeHttpUrl); primary artist/album/spotify/google rendering unchanged. styles.css: bandcamp-teal secondary link.
- odesli.test.js + odesli.fixture.json: parseOdesli unit-tested against a real captured Odesli response (Blood Incantation album -> direct bandcamp; absent apple/youtube omitted; junk -> {}); getLinks 200/non-200/throw/parse-error paths.
- docs: .env.example, README, AGENTS.md note the opt-in flag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
Source: task/agents-md-resolve-core-layout (plan/steward-linker-quality) — layout inventory omitted resolve-core.js / resolver.test.js after the orchestration was extracted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Source: task/resolver-orchestration-testable (plan/steward-linker-quality) — new album orchestration in resolver.js had zero node:test coverage.
- Extract the pure album/artist orchestration into service/resolve-core.js
(resolveAll/resolveOne/pickDirect/cacheKey) taking injected search/classify
fns; resolver.js becomes a thin server wiring the real Spotify search +
ollama classify. No behavior change.
- Add service/resolver.test.js: self-titled artist+album tie-break, LLM-album
-> Google fallback, kind-aware cache separation, plus gate/drop/error paths.
- scripts/check.sh: node --check now also matches *.cjs (extract.test.cjs).
- AGENTS.md Testing: note resolve-core coverage + the extract.test.cjs extractor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Source: task/album-detection (objective/linker-improvements) — project brief asks for artist OR album linking; MVP linked artists only.
- service/lib.js: pickAlbum (the exact-name gate, named for the album path) + linkResult now carries kind:"artist"|"album" (additive; defaults to artist so older clients keep working).
- service/resolver.js: one Spotify type=artist,album search per candidate, exact-match against either; returns {url,kind}; cache key includes the kind hint. resolveOne/resolveAll thread the LLM kind hint through.
- service/llm.js: parseClassification now returns Map(name -> "artist"|"album") so album-kind candidates route to the album path (.has() still works like the old Set).
- extension/extract.js: albumMatchesIn over two precise signals (quoted strings + album cue phrases "the album X", "X LP/EP"), folded into allMatchesIn.
- extension/content.js + styles.css: album links render italicised (.rsl-album) with an album tooltip; artist behavior unchanged.
- Tests: new pure-logic coverage in lib.test.js / llm.test.js / extract.test.cjs (album shaping, Map-kind routing, album candidate extraction). scripts/check.sh green.
- README.md + AGENTS.md: document album linking.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Source: human-feedback note 01KVDYV3A35B57N15CRBQDXPCH (project/reddit-spotify-linker,
2026-06-18) — "remove the musicbrainz integration and just use spotify"; a google search
for "<name> bandcamp" fallback; and "would a local llm be better able to categorise these
... include instructions for running something suitable via ollama (16GB M2)".
- Remove MusicBrainz entirely (viaMusicBrainz + url-rels + rate-gate; lib relUrls/mbResult).
Spotify is now the only music API; it both gates by exact-name match and supplies the
direct artist link.
- Optional ollama LLM gate (OLLAMA_URL enables it, OLLAMA_MODEL default qwen2.5:3b) called
over plain HTTP -> stays zero npm-dep. It classifies which candidates are real
artists/albums (the matching-quality lever). Pure prompt/parse logic in new llm.js
(unit-tested in llm.test.js); the fetch + resolveAll gate wiring live in resolver.js.
- A confirmed name links to its direct Spotify page when available, else a Google search for
"<name> bandcamp". New "google" primary platform rendered by the extension (styles.css
blue accent + platform-aware link title in content.js).
- README: drop MusicBrainz; document the two gates + a runnable local-LLM setup for a 16GB
MacBook M2. AGENTS.md: flip the old "no ML/NER" standing decision per the human override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stand up code stewards + testing for the linker, adapted to its JS/Node stack:
- service/lib.js: extract the resolver's pure logic (norm + exact-name match + link
builders) so it is unit-testable without the server side effect; resolver.js imports it.
- service/lib.test.js: node:test coverage of the precision lever (7 tests).
- scripts/check.sh: zero-dep gate (node --check + JSON validation + node --test).
- AGENTS.md: repo doctrine — plain-repo push flow (NOT towl's PR pipeline), the three
stewards (quality/design/security), the test gate, standing decisions (MV2 stays; no ML).
Source: issue/linker-code-stewards-and-test-infra (inbox human-feedback 2026-06-16 —
"should have a set of code stewards ... ensure a testing infra gets setup and maintained").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>