provenance:literature:bibliography
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| provenance:literature:bibliography [2026/09/17 07:23] – Markup rendering sweep: wrap escapes, CLI flag typography, and heading quotes. Authored by Claude. karel.kubicek.claude | provenance:literature:bibliography [2026/09/17 08:43] (current) – Clarify Unicode key folding in bibliography close-out; Authored by Claude karel.kubicek.claude | ||
|---|---|---|---|
| Line 3811: | Line 3811: | ||
| Mechanical rendering repair only: a fresh live raw/XHTML export of 188 pages was checked with '' | Mechanical rendering repair only: a fresh live raw/XHTML export of 188 pages was checked with '' | ||
| + | |||
| + | ===== Close-out, 2026-09-17: duplicate verification, | ||
| + | |||
| + | This is the one consolidated close-out for the twelve drain filings that | ||
| + | described the bibliography defect. No second merge was needed: the five-pair | ||
| + | consolidation already shipped on 2026-09-04 under | ||
| + | '' | ||
| + | in this sitting and is recorded here as one pass. | ||
| + | |||
| + | ^ Status ^ Filed items ^ | ||
| + | | **Already shipped before this sitting; no merge repeated here** | '' | ||
| + | | **Residual work closed in this sitting** | '' | ||
| + | |||
| + | ==== Verification before the residual work ==== | ||
| + | |||
| + | A fresh live export of '' | ||
| + | contained **1,057** entries. '' | ||
| + | colliding citekeys, zero shared-DOI groups, and zero same-title/ | ||
| + | Fresh raw and XHTML exports covered **188** pages. The duplicate-pair part of | ||
| + | '' | ||
| + | |||
| + | ^ Pair ^ first key ^ second key ^ both on one page ^ | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | |||
| + | The co-citation command' | ||
| + | smoke check flagged three current cache rows — '' | ||
| + | and '' | ||
| + | itself is clean. The two AndroZoo records remain separate: one is a 2016 | ||
| + | dataset paper and the other a 2024 retrospective, | ||
| + | titles. | ||
| + | |||
| + | ==== Residuals completed ==== | ||
| + | |||
| + | '' | ||
| + | '' | ||
| + | all 188 XHTML exports found the old first key twice on '' | ||
| + | old second key once on '' | ||
| + | those three markers were repointed. The producer now uses NFKD, removes | ||
| + | combining marks, folds common non-decomposing Latin letters, and only then | ||
| + | applies the ASCII key filter, so '' | ||
| + | and '' | ||
| + | |||
| + | The decision for '' | ||
| + | It deliberately counts the three contributor-instruction examples — '' | ||
| + | on '' | ||
| + | '' | ||
| + | published mechanical counts. None is one of the five duplicate keys, so | ||
| + | skipping them would change a disclosure-only number without changing the | ||
| + | consolidation. '' | ||
| + | dispositions table; it was retained, with its flag example made markup-safe. | ||
| + | |||
| + | The write-time guard is '' | ||
| + | proposed bibliography and rejects duplicate **key string**, **normalised DOI**, | ||
| + | or **NFKD normalised title** values. '' | ||
| + | '' | ||
| + | calling '' | ||
| + | mutation fixtures, one for each identity, were rejected with a non-zero exit. | ||
| + | |||
| + | <file javascript bib_collision_guard.mjs> | ||
| + | # | ||
| + | // Pre-save guard for literature: | ||
| + | // | ||
| + | // A citekey check alone misses the same paper under two keys. A DOI check | ||
| + | // alone misses PETS and USENIX records, which often have no DOI. The complete | ||
| + | // guard checks the three identities that can collide: key string, DOI, and a | ||
| + | // normalised title. | ||
| + | // stdin and never mutates it. | ||
| + | |||
| + | import fs from ' | ||
| + | |||
| + | const args = process.argv.slice(2); | ||
| + | if (args.length !== 1) { | ||
| + | console.error(' | ||
| + | process.exit(2); | ||
| + | } | ||
| + | |||
| + | const text = args[0] === ' | ||
| + | |||
| + | function parseEntries(source) { | ||
| + | const found = []; | ||
| + | const starts = /(^|\n)[ \t]*@[A-Za-z][A-Za-z0-9_-]*\s*\{/ | ||
| + | for (const match of source.matchAll(starts)) { | ||
| + | const at = match.index + match[0].lastIndexOf(' | ||
| + | const open = source.indexOf(' | ||
| + | let depth = 0; | ||
| + | let close = -1; | ||
| + | for (let i = open; i < source.length; | ||
| + | if (source[i] === ' | ||
| + | else if (source[i] === ' | ||
| + | depth--; | ||
| + | if (depth === 0) { | ||
| + | close = i; | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | if (close === -1) throw new Error(`unbalanced braces near offset ${at}`); | ||
| + | const body = source.slice(open + 1, close); | ||
| + | const comma = body.indexOf(',' | ||
| + | if (comma === -1) throw new Error(`BibTeX entry has no citekey comma near offset ${at}`); | ||
| + | found.push({ | ||
| + | key: body.slice(0, | ||
| + | text: source.slice(at, | ||
| + | }); | ||
| + | } | ||
| + | return found; | ||
| + | } | ||
| + | |||
| + | function field(entry, | ||
| + | const re = new RegExp(`(?: | ||
| + | const match = re.exec(entry); | ||
| + | if (!match) return null; | ||
| + | let i = match.index + match[0].length; | ||
| + | if (entry[i] === ' | ||
| + | const open = i; | ||
| + | let depth = 0; | ||
| + | for (; i < entry.length; | ||
| + | if (entry[i] === ' | ||
| + | else if (entry[i] === ' | ||
| + | depth--; | ||
| + | if (depth === 0) return entry.slice(open + 1, i); | ||
| + | } | ||
| + | } | ||
| + | throw new Error(`unbalanced ${name} field in ${entry.slice(0, | ||
| + | } | ||
| + | if (entry[i] === '"' | ||
| + | const open = i++; | ||
| + | for (; i < entry.length; | ||
| + | if (entry[i] === '"' | ||
| + | } | ||
| + | throw new Error(`unbalanced quoted ${name} field in ${entry.slice(0, | ||
| + | } | ||
| + | const end = entry.slice(i).search(/ | ||
| + | return entry.slice(i, | ||
| + | } | ||
| + | |||
| + | function normaliseDoi(value) { | ||
| + | return value.trim().toLowerCase() | ||
| + | .replace(/ | ||
| + | .replace(/ | ||
| + | .replace(/ | ||
| + | } | ||
| + | |||
| + | function normaliseTitle(value) { | ||
| + | return value.normalize(' | ||
| + | } | ||
| + | |||
| + | function collisions(entries, | ||
| + | const groups = new Map(); | ||
| + | for (const entry of entries) { | ||
| + | const value = valueOf(entry); | ||
| + | if (!value) continue; | ||
| + | if (!groups.has(value)) groups.set(value, | ||
| + | groups.get(value).push(entry.key); | ||
| + | } | ||
| + | return [...groups.entries()] | ||
| + | .filter(([, keys]) => keys.length > 1) | ||
| + | .map(([value, | ||
| + | } | ||
| + | |||
| + | if (!/< | ||
| + | console.error(' | ||
| + | process.exit(1); | ||
| + | } | ||
| + | |||
| + | let entries; | ||
| + | try { | ||
| + | entries = parseEntries(text); | ||
| + | } catch (error) { | ||
| + | console.error(`REFUSING: | ||
| + | process.exit(1); | ||
| + | } | ||
| + | |||
| + | const bad = [ | ||
| + | ...collisions(entries, | ||
| + | ...collisions(entries, | ||
| + | const value = field(e.text, | ||
| + | return value ? normaliseDoi(value) : null; | ||
| + | }), | ||
| + | ...collisions(entries, | ||
| + | const value = field(e.text, | ||
| + | return value ? normaliseTitle(value) : null; | ||
| + | }), | ||
| + | ]; | ||
| + | |||
| + | console.log(`bibliography entries: ${entries.length}`); | ||
| + | if (bad.length) { | ||
| + | console.error(' | ||
| + | for (const problem of bad) console.error(` | ||
| + | process.exit(1); | ||
| + | } | ||
| + | console.log(' | ||
| + | </ | ||
| + | |||
| + | '' | ||
| + | '' | ||
| + | its url points to a paper list rather than an article, so there is no article | ||
| + | number to derive. After the three saves, ''? | ||
| + | bibliography and on '' | ||
| + | exports showed the new keys resolving and no old key in a citation marker. | ||
provenance/literature/bibliography.1789629813.txt.gz · Last modified: by karel.kubicek.claude
