fix: converge album cue extraction — weak-cue lowercase limited to one token, multi-word lowercase only after a strong cue, digit titles capture full continuation (closes task/album-cue-lower-run-converge)

Source: task/album-cue-lower-run-converge (plan/steward-linker-design) — the 16d8f74 LOWER_RUN both leaked lowercase prose and truncated digit-led titles; this converges the precision/recall balance.

- Weak cue (album|record|ep|lp): admit a capital/digit-led run OR a SINGLE lowercase token only (LOWER_ONE, guarded by a negative lookahead so a second lowercase word reads as prose and is rejected) — closes the 'makes me cry' / 'changed everything' prose leak.
- Strong cue (called|titled|named): the ONLY place a multi-word lowercase run (LOWER_RUN) is admitted — 'an album called good kid maad city' → 'good kid maad city'.
- New DIGIT_RUN (digit lead absorbs trailing lowercase tokens, bounded 1-4 words, tried before TITLE_RUN) fixes the digit truncation — '36 chambers'/'1989 deluxe'/'808s and heartbreak' capture in full.
- Position-mapping invariant enforced in the album push helper; bounded regexes (ReDoS test on a multi-KB pathological input); comprehensive BOTH-direction tests (precision: non-stop-set lowercase leads emit nothing; recall: digit continuation, single token, strong cue).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
paul
2026-06-23 10:31:39 +00:00
co-authored by Claude Opus 4.8
parent 1fdc6a9168
commit b0f7116493
2 changed files with 130 additions and 18 deletions
+92 -3
View File
@@ -112,10 +112,12 @@ test("albumMatchesIn: recall — number-led titles via an explicit album cue", (
assert.ok(albumMatchesIn("the album 36 Chambers").map((m) => m.name).includes("36 Chambers"));
});
test("albumMatchesIn: recall — lowercase-styled titles after a cue", () => {
// good kid maad city is fully lowercase — a stylized title, not prose; the cue admits it.
test("albumMatchesIn: recall — a lowercase-styled MULTI-word title needs a STRONG cue", () => {
// good kid maad city is fully lowercase. The WEAK cue (album|record|ep|lp) can't tell a stylized
// title from prose, so it emits NOTHING; a STRONG explicit cue (called|titled|named) licenses it.
assert.equal(albumMatchesIn("the album good kid maad city").length, 0); // weak cue → nothing
assert.ok(
albumMatchesIn("the album good kid maad city").map((m) => m.name).includes("good kid maad city")
albumMatchesIn("an album called good kid maad city").map((m) => m.name).includes("good kid maad city")
);
});
@@ -173,6 +175,93 @@ test("albumMatchesIn: every rebalance candidate maps back to the source text", (
}
});
// --- Convergence: the prior LOWER_RUN both leaked lowercase prose and truncated digit-led titles.
// The stable design: weak cue (album|record|ep|lp) admits a capital/digit-led run OR a SINGLE
// lowercase token only; a MULTI-word lowercase title needs a STRONG cue (called|titled|named); a
// digit lead absorbs its full lowercase continuation. These assert BOTH directions — the precision
// floor (album path emits NOTHING on non-stop-set lowercase leads, the bug the old test missed) AND
// the recovered recall (digit continuation, single-token, strong-cue). ---
test("albumMatchesIn: precision — weak-cue lowercase leads OUTSIDE the stop-set emit nothing", () => {
// The shipped leak: LOWER_RUN was guarded only by a 6-word stop-set, so any OTHER lowercase lead
// ('changed', 'dropped', 'makes', 'really', 'sounds') after a weak cue leaked a prose fragment.
for (const text of [
"the album i bought yesterday",
"this record changed everything for me",
"their ep dropped on bandcamp last night",
"the album makes me cry",
"the album really grew on me",
"the album sounds great",
]) {
assert.equal(albumMatchesIn(text).length, 0, `expected no album candidate from: ${text}`);
}
// The old stop-set precision cases must still hold too.
assert.equal(albumMatchesIn("the album by Carcass").length, 0); // verb/article left edge
assert.equal(albumMatchesIn("I think their new LP is amazing").length, 0); // lowercase filler before LP
assert.equal(albumMatchesIn('"We hold these truths to be self-evident."').length, 0); // prose quote
});
test("albumMatchesIn: recall — a digit-led title captures its full lowercase continuation", () => {
// The shipped bug: TITLE_RUN's [A-Z0-9] lead matched the bare digit and stopped, so '36 chambers'
// truncated to '36'. The digit-led run now absorbs trailing lowercase tokens (bounded to 4 words).
assert.ok(albumMatchesIn("the album 36 chambers").map((m) => m.name).includes("36 chambers"));
assert.ok(albumMatchesIn("the album 1989 deluxe").map((m) => m.name).includes("1989 deluxe"));
assert.ok(
albumMatchesIn("the album 808s and heartbreak").map((m) => m.name).includes("808s and heartbreak")
);
// Prior digit recall cases (bare number, "X LP", capital-led mixed) still pass.
assert.ok(albumMatchesIn("the album 1989").map((m) => m.name).includes("1989"));
assert.ok(albumMatchesIn("1989 LP").map((m) => m.name).includes("1989"));
assert.ok(albumMatchesIn("the album 36 Chambers").map((m) => m.name).includes("36 Chambers"));
});
test("albumMatchesIn: recall — weak cue admits a SINGLE lowercase token (whole-title only)", () => {
// 'untitled' is the entire title — a lone lowercase token survives; a second lowercase word would
// be prose and is rejected (covered by the precision test above).
assert.ok(albumMatchesIn("the album untitled").map((m) => m.name).includes("untitled"));
assert.ok(albumMatchesIn("the album untitled.").map((m) => m.name).includes("untitled"));
});
test("albumMatchesIn: recall — a MULTI-word lowercase title needs a STRONG cue (called/titled/named)", () => {
// The ONLY place a bare multi-word lowercase run is admitted: an explicit naming verb.
assert.ok(
albumMatchesIn("an album called good kid maad city").map((m) => m.name).includes("good kid maad city")
);
assert.ok(albumMatchesIn("the record titled good kid maad city").map((m) => m.name).includes("good kid maad city"));
assert.ok(albumMatchesIn("the album named good kid maad city").map((m) => m.name).includes("good kid maad city"));
// The same multi-word lowercase run WITHOUT a strong cue emits nothing (the precision invariant).
assert.equal(albumMatchesIn("the album good kid maad city").length, 0);
});
test("albumMatchesIn: convergence cases all map back to the source text", () => {
// Position-mapping invariant across the new precision/recall cases (digit continuation, single
// token, strong cue) — text.substr(index, length) === name.
const texts = [
"the album 36 chambers",
"the album 1989 deluxe",
"the album 808s and heartbreak",
"the album untitled",
"the album untitled.",
"an album called good kid maad city",
"the record titled good kid maad city",
"the album Reign in Blood", // capital-led unchanged
"Reign in Blood LP",
"the album Money Store dropped",
];
for (const text of texts) {
for (const m of albumMatchesIn(text)) assert.equal(text.substr(m.index, m.length), m.name);
}
});
test("albumMatchesIn: no ReDoS — a pathological multi-KB input returns promptly", () => {
// Bounded runs ({0,3}, single linear lookahead) must stay linear. A worst-case repeat of the
// cue + many tokens must not blow up; assert it completes well under a generous wall-clock bound.
const evil = "the album " + "a ".repeat(20000) + "!".repeat(20000);
const t0 = Date.now();
albumMatchesIn(evil);
assert.ok(Date.now() - t0 < 1000, "albumMatchesIn took too long on a pathological input");
});
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"));