{"record":{"id":"10717ddb72ec0d58","repo":"koala73/worldmonitor","slug":"source-card-anchor-collision-json-stringify-seen-get-anchor","errorCode":null,"errorMessage":"Source card anchor collision: ${JSON.stringify(seen.get(anchor))} and ${JSON.stringify(provider.provider)} both map to #${anchor}","messagePattern":"Source card anchor collision: (.+?) and (.+?) both map to #(.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/crawlable-sources-page.mjs","lineNumber":776,"sourceCode":" * so the import would close a cycle. A local slug helper is also what the other\n * generators in scripts/ do.\n */\nexport function sourceCardAnchors(sourceCatalog) {\n  const slugBase = (key) => String(key ?? '')\n    .normalize('NFKD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/&/g, ' and ')\n    .replace(/[^a-zA-Z0-9]+/g, '-')\n    .replace(/^-+|-+$/g, '')\n    .toLowerCase() || 'source';\n\n  const anchors = new Map();\n  const seen = new Map();\n  for (const provider of sourceCatalog) {\n    const key = String(provider.provider ?? '');\n    const anchor = `provider-${slugBase(key)}-${createHash('sha1').update(key).digest('hex').slice(0, 16)}`;\n    if (seen.has(anchor) && seen.get(anchor) !== provider.provider) {\n      throw new Error(\n        `Source card anchor collision: ${JSON.stringify(seen.get(anchor))} and ${JSON.stringify(provider.provider)} both map to #${anchor}`,\n      );\n    }\n    seen.set(anchor, provider.provider);\n    anchors.set(provider.provider, anchor);\n  }\n  return anchors;\n}\n\nexport function buildSourcePages(sourceCatalog) {\n  return SOURCE_DOMAINS.flatMap((domain) => {\n    const providers = sourceCatalog.filter((provider) => provider.domainId === domain.id);\n    const pages = [];\n    for (let offset = 0; offset < providers.length; offset += 60) {\n      const number = offset / 60 + 1;\n      pages.push({\n        path: `/sources/${domain.id}/${number === 1 ? '' : `page/${number}/`}`,\n        name: `${domain.name}${number === 1 ? '' : ` — page ${number}`}`,","sourceCodeStart":758,"sourceCodeEnd":794,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/crawlable-sources-page.mjs#L758-L794","documentation":"sourceCardAnchors builds a deterministic DOM anchor for each provider card as provider-<slug>-<sha1-prefix>. It throws when two distinct provider names hash/slug to the same anchor, because two cards sharing an id would break linking and duplicate-id validity. Note the guard: if the same provider name repeats, it is tolerated; only genuinely different providers colliding on one anchor throw.","triggerScenarios":"Adding a new provider to sourceCatalog whose name differs from an existing one only in characters that slugify away or whose sha1 first-16-hex collision maps to the same key string — e.g. names differing only by case/punctuation that slugBase normalizes identically.","commonSituations":"Renaming a provider in a way that slugBase makes identical to another (e.g. 'Acme, Inc.' vs 'acme inc'), or duplicating a catalog entry with a slightly edited label.","solutions":["Find the two colliding provider names in the error message and rename one so its slugged form is distinct.","Review slugBase normalization; if it strips distinguishing characters, adjust the provider name or the slug function.","Add a catalog lint/test that runs sourceCardAnchors over sourceCatalog to catch collisions before deploy."],"exampleFix":"// before\n{ provider: 'acme inc' }, { provider: 'Acme, Inc.' } // same anchor\n// after\n{ provider: 'acme-inc' }, { provider: 'acme-labs' }","handlingStrategy":"validation","validationCode":"const names = sourceCatalog.map((p) => String(p.provider ?? ''));\nconst anchors = new Set(names.map((n) => `provider-${slugBase(n)}-${createHash('sha1').update(n).digest('hex').slice(0, 16)}`));\nif (anchors.size !== new Set(names).size) throw new Error('provider anchor collision in catalog');","typeGuard":null,"tryCatchPattern":"try {\n  const anchors = sourceCardAnchors(sourceCatalog);\n} catch (e) {\n  if (e.message.startsWith('Source card anchor collision')) {\n    console.error('Rename one of the colliding providers so slugs differ.');\n    process.exit(2);\n  }\n  throw e;\n}","preventionTips":["Run sourceCardAnchors over the catalog in a unit test on every catalog change.","Avoid provider names that differ only by case or punctuation.","Keep slugBase normalization well documented and reviewed."],"tags":["hash-collision","dom","catalog"],"backgroundTag":"internal-invariant-violation","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}