fix: tighten album cue extraction so candidates are title-shaped, not sentence fragments (closes task/album-cue-precision)
Source: task/album-cue-precision (plan/steward-linker-design) — album cue regexes over-generated junk candidates (no left anchors), feeding noise to the resolver/LLM and risking fallback mis-links.
- X LP/EP branch: anchor the capture to a 1-4 capitalized/quoted title run (mirrors CAND_RE) so 'I think their new LP' no longer yields the sentence prefix.
- 'the album X' branch: require the name to start with a capital/quote and reject a leading verb/article (is|was|by|the|a|an) so 'the album by Carcass' / 'the album is great' yield no candidate.
- QUOTED_RE: reject prose quotes — a span with mid-string sentence punctuation (. ! ?), over ~6 words, or a lone stopword interjection ('lol') — so prose quotes never become album candidates.
- Preserve position mapping (trim a captured leading quote and shift the index) and dedupe identical spans across the two signals; recall of real titles ('Reign in Blood', 'Human Jerky') unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,43 @@ test("albumMatchesIn skips pure-number and too-short quotes", () => {
|
||||
assert.ok(!names.includes("X")); // length < 2
|
||||
});
|
||||
|
||||
test("albumMatchesIn: 'X LP/EP' branch is anchored, not the sentence prefix", () => {
|
||||
// No left anchor used to yield "I think their new" (the prefix before " LP").
|
||||
const ms = albumMatchesIn("I think their new LP is amazing");
|
||||
assert.ok(!ms.some((m) => m.name === "I think their new"));
|
||||
// Either nothing, or only a title-shaped run — never a sentence fragment.
|
||||
for (const m of ms) assert.ok(/^["“'‘]?[A-Z]/.test(m.name));
|
||||
});
|
||||
|
||||
test("albumMatchesIn: 'the album X' guards a lowercase/verb left edge", () => {
|
||||
// 'the album by Carcass' must not emit 'by Carcass' / 'by' as an album.
|
||||
const a = albumMatchesIn("the album by Carcass").map((m) => m.name);
|
||||
assert.ok(!a.includes("by Carcass"));
|
||||
assert.ok(!a.includes("by"));
|
||||
// 'the album is great…' must emit no album candidate at all.
|
||||
const b = albumMatchesIn("the album is great but I forgot the name");
|
||||
assert.equal(b.length, 0);
|
||||
});
|
||||
|
||||
test("albumMatchesIn: prose quotes are not album candidates", () => {
|
||||
// A quote with sentence-ending punctuation mid-span, or a lone interjection, is not a title.
|
||||
const a = albumMatchesIn('"We hold these truths to be self-evident."').map((m) => m.name);
|
||||
assert.ok(!a.includes("We hold these truths to be self-evident."));
|
||||
assert.equal(albumMatchesIn('"lol"').length, 0);
|
||||
});
|
||||
|
||||
test("albumMatchesIn: recall preserved — real titles still emit", () => {
|
||||
assert.ok(albumMatchesIn("the album Reign in Blood").map((m) => m.name).includes("Reign in Blood"));
|
||||
assert.ok(albumMatchesIn("Reign in Blood LP").map((m) => m.name).includes("Reign in Blood"));
|
||||
assert.ok(albumMatchesIn('"Human Jerky"').map((m) => m.name).includes("Human Jerky"));
|
||||
});
|
||||
|
||||
test("albumMatchesIn: tightened cues still map positions back to the text", () => {
|
||||
for (const text of ["Reign in Blood LP rules", "the album by Carcass", '"Human Jerky" demo']) {
|
||||
for (const m of albumMatchesIn(text)) assert.equal(text.substr(m.index, m.length), m.name);
|
||||
}
|
||||
});
|
||||
|
||||
test("isSafeHttpUrl accepts http/https and rejects javascript/data/garbage/empty", () => {
|
||||
// accepted: the only schemes a resolver link may inject as an href
|
||||
assert.ok(isSafeHttpUrl("https://open.spotify.com/artist/abc"));
|
||||
|
||||
Reference in New Issue
Block a user