{"record":{"id":"9c598b0e66011bc0","repo":"koala73/worldmonitor","slug":"chokepoint-id-editoriallinks-must-be-an-array","errorCode":null,"errorMessage":"${chokepoint.id}: editorialLinks must be an array","messagePattern":"(.+?): editorialLinks must be an array","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/build-crawlable-corpus.mjs","lineNumber":1372,"sourceCode":"      routeIds: Array.isArray(entry.routeIds) ? [...entry.routeIds] : [],\n      lat: Number(entry.lat),\n      lon: Number(entry.lon),\n      slug: slugify(entry.displayName || entry.id),\n    }))\n    .filter((entry) => entry.id && entry.displayName)\n    .sort((a, b) => a.displayName.localeCompare(b.displayName));\n}\n\nexport function buildChokepointPageLinks({ chokepoints, countries, crises, blogPostPaths = new Set(), content = CHOKEPOINT_CONTENT }) {\n  const countryByCode = new Map(countries.map((country) => [country.code, country]));\n  const crisisBySlug = new Map(crises.map((crisis) => [crisis.slug, crisis]));\n  const byChokepointId = new Map();\n  const byCountryCode = new Map();\n  const byCrisisSlug = new Map();\n  for (const chokepoint of chokepoints) {\n    const declaration = content[chokepoint.id] || {};\n    const editorialLinks = declaration.editorialLinks ?? [];\n    if (!Array.isArray(editorialLinks)) throw new Error(`${chokepoint.id}: editorialLinks must be an array`);\n    const editorial = new Map();\n    for (const link of editorialLinks) {\n      if (!blogPostPaths.has(link?.href) || typeof link.label !== 'string' || !link.label.trim()) {\n        throw new Error(`${chokepoint.id}: editorialLinks requires a canonical blog post path and label: ${link?.href}`);\n      }\n      if (!editorial.has(link.href)) editorial.set(link.href, link);\n    }\n    const resolve = (field, targets, inverse) => {\n      const ids = declaration[field] ?? [];\n      if (!Array.isArray(ids)) throw new Error(`${chokepoint.id}: ${field} must be an array`);\n      return [...new Set(ids)].map((id) => {\n        const target = targets.get(id);\n        if (!target) throw new Error(`${chokepoint.id}: ${field} contains unknown target ${id}`);\n        const entries = inverse.get(id) || [];\n        entries.push(chokepoint);\n        inverse.set(id, entries);\n        return target;\n      });","sourceCodeStart":1354,"sourceCodeEnd":1390,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/build-crawlable-corpus.mjs#L1354-L1390","documentation":"buildChokepointPageLinks() in the crawlable-corpus build script reads per-chokepoint content declarations (default: the CHOKEPOINT_CONTENT map) and expects declaration.editorialLinks to be an array of {href,label} objects. This throw is a build-time schema validation: if editorialLinks is present but not an array (e.g. an object, string, or number), the script refuses to continue so no corrupt chokepoint pages are rendered. An absent, undefined, or null key defaults to [] via ??, so only a non-nullish non-array value triggers this.","triggerScenarios":"Calling buildChokepointPageLinks({ chokepoints, countries, crises, blogPostPaths, content }) where content[chokepoint.id].editorialLinks is set to a truthy non-array value such as an object, string, number, or boolean. Undefined and null do not trigger it (they fall back to []).","commonSituations":"A contributor writes editorialLinks as a single object instead of an array of one link; a merge or codegen tool flattens a single-element array into a scalar; a content-module refactor changes the declared shape; an external CMS export emits the wrong JSON type.","solutions":["Open the offending chokepoint declaration in the content source (CHOKEPOINT_CONTENT in scripts/build-crawlable-corpus.mjs or the content option passed by the caller) and wrap the value in an array.","If content is produced by another tool, inspect and fix the producer to emit an array of {href,label}.","Add a schema/lint step over the content declarations so shape drift fails before the corpus build."],"exampleFix":"// before\neditorialLinks: { href: '/blog/strait-of-hormuz', label: 'Hormuz explainer' }\n// after\neditorialLinks: [{ href: '/blog/strait-of-hormuz', label: 'Hormuz explainer' }]","handlingStrategy":"validation","validationCode":"const links = content[chokepoint.id]?.editorialLinks;\nif (links != null && !Array.isArray(links)) {\n  throw new TypeError(`${chokepoint.id}: editorialLinks must be an array, got ${typeof links}`);\n}","typeGuard":"const isValidEditorialLinks = (v) => v == null || (Array.isArray(v) && v.every((l) => !!l && typeof l.href === 'string' && typeof l.label === 'string'));","tryCatchPattern":null,"preventionTips":["Always declare editorialLinks as an array, even for a single link.","Run the corpus build locally before pushing content edits.","Add a JSON schema or JSDoc @type check over CHOKEPOINT_CONTENT-style declarations in CI."],"tags":["build-time","schema-validation","configuration"],"backgroundTag":"schema-validation-failed","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"}