8 Commits
Author SHA1 Message Date
paulandClaude Opus 4.8 e06c983b16 feat: optional direct Bandcamp/Apple/YouTube links via Odesli (flag-gated) (closes task/odesli-direct-cross-platform-links)
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>
2026-06-24 02:41:30 +00:00
paulandClaude Opus 4.8 8ac0666a71 fix: scope getRoot to the comment tree so the shadow walk doesn't collect reddit chrome (closes task/getroot-comment-tree-scope)
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>
2026-06-23 16:38:11 +00:00
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
paulandClaude Opus 4.8 1fdc6a9168 docs: sync AGENTS.md service/ layout with resolve-core.js + resolver.test.js (closes task/agents-md-resolve-core-layout)
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>
2026-06-23 08:27:02 +00:00
paulandClaude Opus 4.8 8496e55baf refactor: extract testable resolve-core + cover album routing; check.sh checks .cjs (closes task/resolver-orchestration-testable)
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>
2026-06-23 00:29:59 +00:00
paulandClaude Opus 4.8 2b4babf955 feat: detect & link album mentions, not just artists (closes task/album-detection)
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>
2026-06-22 22:33:32 +00:00
paulandClaude Opus 4.8 ef57761859 feat: spotify-only matching + optional local-LLM gate + google→bandcamp fallback (closes task/drop-musicbrainz-google-fallback, closes task/ollama-classifier)
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>
2026-06-19 12:50:07 +00:00
paulandClaude Opus 4.8 9f9858a050 feat: stewardship doctrine + zero-dep test infra (closes issue/linker-code-stewards-and-test-infra)
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>
2026-06-16 06:36:41 +00:00