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:
+38
-15
@@ -26,10 +26,13 @@
|
||||
// 1. A quoted string ("Reek of Putrefaction" or 'Scum') — a strong "this is a title" cue,
|
||||
// but only when it reads like a title (short, no sentence-ending punctuation mid-span),
|
||||
// so prose quotes ("We hold these truths…", "lol") don't become candidates.
|
||||
// 2. An album cue phrase ("the album Scum", "Scum LP/EP") naming a title-shaped run: the
|
||||
// capture starts at a capitalized/quoted token (so a cue followed by a verb/article —
|
||||
// "the album by Carcass", "the album is great" — yields no candidate) and the LP/EP
|
||||
// branch is anchored to 1-4 capitalized words instead of grabbing the sentence prefix.
|
||||
// 2. An album cue phrase ("the album Scum", "Scum LP/EP") naming a title-shaped run. The WEAK
|
||||
// cue (album|record|ep|lp) can't separate a stylized lowercase title from prose, so after it
|
||||
// only a capital/digit-led run OR a SINGLE lowercase token is admitted (a multi-word
|
||||
// lowercase run after a weak cue is rejected — it leaks prose). A multi-word lowercase title
|
||||
// is admitted ONLY after a STRONG explicit cue ("album called good kid maad city"). A cue
|
||||
// followed by a verb/article ("the album by Carcass", "the album is great") yields no
|
||||
// candidate, and the LP/EP branch is anchored to a 1-4-word title, not the sentence prefix.
|
||||
// Group 1 of each holds the candidate; the `d` flag gives its position for in-place wrapping.
|
||||
const QUOTED_RE = /["“]([^"”]{2,80})["”]|['‘]([^'’]{2,80})['’]/gd;
|
||||
// A single word inside a title run (Title-Case/ALL-CAPS — or, for a digit-led run, a number).
|
||||
@@ -40,18 +43,34 @@
|
||||
// is excluded. The lead also admits a DIGIT, so number-led titles ("1989 LP", "36 Chambers")
|
||||
// survive; it does NOT admit a bare lowercase lead (keeps "their new LP" from leaking "new").
|
||||
const TITLE_RUN = `["“'‘]?[A-Z0-9]${WORD}(?:[ ]+(?:${TOKEN}|of|the|and|&|in|on|to|for|a|an)){0,3}["”'’]?`;
|
||||
// LOWER_RUN — a fully lowercase-styled title (lead + inner words all lowercase/digit), bounded to
|
||||
// 1-4 words, for stylized titles after an explicit album cue ("good kid maad city", "untitled").
|
||||
// A verb/article lead ("the album by Carcass", "the album is great") is captured here but then
|
||||
// dropped by ALBUM_LEAD_STOP below, so this run never weakens the precision guard.
|
||||
// DIGIT_RUN — a DIGIT-led run whose inner words may be lowercase ("36 chambers", "808s and
|
||||
// heartbreak", "1989 deluxe"). The leading digit IS the precision signal a title was meant (a
|
||||
// bare lowercase lead is not), so unlike TITLE_RUN this run absorbs trailing lowercase tokens —
|
||||
// bounded to 1-4 words so it never grabs the sentence tail. Tried BEFORE TITLE_RUN in the
|
||||
// alternation so a digit lead captures its full continuation, not just the bare number.
|
||||
const DIGIT_RUN = `["“'‘]?[0-9]${WORD}(?:[ ]+[A-Za-z0-9'’.&\\-]+){0,3}["”'’]?`;
|
||||
// LOWER_RUN — a fully lowercase-styled MULTI-word title (lead + inner words all lowercase/digit),
|
||||
// bounded to 1-4 words. The weak cue (album|record|ep|lp) cannot tell a stylized lowercase title
|
||||
// ("good kid maad city") from prose ("makes me cry") — the lowercase-prose space dwarfs the rare
|
||||
// real case — so a multi-word lowercase run is admitted ONLY after a STRONG explicit cue
|
||||
// (called|titled|named). After the weak cue, only a single lowercase token (LOWER_ONE) is allowed.
|
||||
const LOWER_RUN = `[a-z0-9]${WORD}(?:[ ]+[a-z0-9]${WORD}){0,3}`;
|
||||
// LOWER_ONE — a SINGLE lowercase token admitted after the WEAK cue ("the album untitled"). The
|
||||
// negative lookahead `(?![ ]+[a-z])` requires it to be the WHOLE title: a second lowercase word
|
||||
// means prose ("makes me cry", "changed everything"), which is rejected outright — the lever that
|
||||
// closes the LOWER_RUN precision leak while keeping a one-word stylized title like "untitled".
|
||||
const LOWER_ONE = `[a-z]${WORD}(?![ ]+[a-z])`;
|
||||
const ALBUM_CUE_RE = new RegExp(
|
||||
// "the album X" — bounded to a 1-4-token run (TITLE_RUN for a capital/digit-led title, LOWER_RUN
|
||||
// for a lowercase-styled one), so "the album Money Store dropped" yields "Money Store", not the
|
||||
// sentence tail. A verb/article lead is dropped by ALBUM_LEAD_STOP below.
|
||||
`\\b(?:the )?(?:album|record|ep|lp)\\s+(?:called\\s+|titled\\s+|named\\s+)?(${TITLE_RUN}|${LOWER_RUN})\\b` +
|
||||
// "X LP/EP" — anchored title run (no left-anchor-less prefix grab).
|
||||
`|\\b(${TITLE_RUN})\\s+(?:LP|EP)\\b`,
|
||||
// STRONG cue: "album called/titled/named X" — the explicit naming verb licenses a multi-word
|
||||
// lowercase title (LOWER_RUN), the ONLY place a bare lowercase run is admitted. Digit- and
|
||||
// capital-led runs work here too. DIGIT_RUN precedes TITLE_RUN so a digit lead absorbs its
|
||||
// lowercase continuation instead of [A-Z0-9] matching the bare number and stopping.
|
||||
`\\b(?:the )?(?:album|record|ep|lp)\\s+(?:called|titled|named)\\s+(${DIGIT_RUN}|${TITLE_RUN}|${LOWER_RUN})\\b` +
|
||||
// WEAK cue: "the album X" — a capital/digit-led title (DIGIT_RUN|TITLE_RUN) or at most a
|
||||
// SINGLE lowercase token (LOWER_ONE). No multi-word lowercase run here: it would leak prose.
|
||||
`|\\b(?:the )?(?:album|record|ep|lp)\\s+(${DIGIT_RUN}|${TITLE_RUN}|${LOWER_ONE})\\b` +
|
||||
// "X LP/EP" — anchored title run (no left-anchor-less prefix grab); digit-led too.
|
||||
`|\\b(${DIGIT_RUN}|${TITLE_RUN})\\s+(?:LP|EP)\\b`,
|
||||
"gd"
|
||||
);
|
||||
// First word of an album-cue capture that, despite a leading capital, is not a title.
|
||||
@@ -144,6 +163,10 @@
|
||||
const out = [];
|
||||
const seen = new Set(); // dedupe identical (index,length) spans across the two signals
|
||||
const push = (name, index, length) => {
|
||||
// Position-mapping invariant: the wrapper links a candidate via text.substr(index, length),
|
||||
// so the emitted span must reproduce the name exactly. Drop any candidate that violates it
|
||||
// (a defensive guard over the quote/cue index-shift arithmetic) rather than mis-link the DOM.
|
||||
if (text.substr(index, length) !== name) return;
|
||||
const key = index + ":" + length;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
@@ -170,7 +193,7 @@
|
||||
}
|
||||
ALBUM_CUE_RE.lastIndex = 0;
|
||||
while ((m = ALBUM_CUE_RE.exec(text))) {
|
||||
const gi = m.indices[1] || m.indices[2]; // "album X" group or "X LP/EP" group
|
||||
const gi = m.indices[1] || m.indices[2] || m.indices[3]; // strong-cue / weak-cue / "X LP/EP" group
|
||||
if (!gi) continue;
|
||||
// Trim a leading opening-quote the title run may have captured, shifting the index so
|
||||
// the emitted span still maps back to the source text; then strip trailing punct/quotes.
|
||||
|
||||
Reference in New Issue
Block a user