koala73/worldmonitor · error · Error

${pagePath} heading leaks ISO code: ${text}

Error message

${pagePath} heading leaks ISO code: ${text}

What it means

The crawlable-corpus build script validates that generated country/corpus pages do not leak machine ISO-3166 codes in visible headings. A heading (h1-h6) in the brief or main HTML matched MEANS_FOR_ISO_RE, meaning human-facing text contains an ISO code where a country name is expected. The build throws to stop a low-quality page from being published to the SEO corpus.

Solutions

  1. Find the page (pagePath is in the message) and inspect its heading source; identify which field supplied the heading text.
  2. Replace the ISO code with the country's display name in the data entry or template that renders the heading.
  3. If a name is missing upstream, add a code->name mapping fallback in the generator before rendering headings.
  4. Re-run the build to confirm the page passes the ISO-leak guard.

Example fix

// before
template `<h2>Security briefing: ${country.iso2}</h2>`
// after
template `<h2>Security briefing: ${country.displayName ?? NAME_BY_ISO[country.iso2]}</h2>`
Defensive patterns

Strategy: validation

Validate before calling

const ISO_LEAK = /\b[A-Z]{2,3}\b/;
const text = heading.replace(/<[^>]+>/g, ' ').trim();
if (ISO_LEAK.test(text)) throw new Error(`heading leaks ISO code: ${text}`);

Prevention

When it happens

Trigger: Running the crawlable-corpus build (or its per-page validator) when a generated page's brief or main document contains an <h1>-<h6> whose stripped text matches the ISO-code detection regex, e.g. a heading like '<h2>Situation in DE</h2>'.

Common situations: Data templates that interpolate the country's ISO alpha-2/alpha-3 code instead of its display name; upstream data feeds returning codes in name fields; a newly added country lacking a localized display name so the template falls back to the code.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/d387c21bde6360db. Report an issue: GitHub.

Appendix: source

Thrown at scripts/build-crawlable-corpus.mjs:3590

    throw new Error(`${pagePath} renders literal markdown emphasis in <main>`);
  }
  const brief = intelBriefHtml(html);
  if (brief && sources !== undefined) {
    // Check the rendered claim blocks as well as the input. A later formatter
    // must not add an entity or change a citation after publish-time validation.
    const claims = [...brief.matchAll(/<(p|li)\b([^>]*)>([\s\S]*?)<\/\1>/gi)]
      .filter((match) => !/\bclass="source"/.test(match[2]))
      .map((match) => corpusVisibleText(match[3]).replace(/&(amp|lt|gt|quot|#39);/g,
        (entity) => ({ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'" })[entity]));
    const gap = briefCitationGroundingGap({ text: claims.join('\n'), sources });
    if (gap) throw new Error(`${pagePath} brief has unsupported citation: ${gap}`);
  }
  const headingSource = brief ?? main;
  const headingHits = [...headingSource.matchAll(/<h[1-6]\b[^>]*>([\s\S]*?)<\/h[1-6]>/gi)];
  for (const hit of headingHits) {
    const text = hit[1].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
    if (MEANS_FOR_ISO_RE.test(text)) {
      throw new Error(`${pagePath} heading leaks ISO code: ${text}`);
    }
  }
  const briefLines = corpusMainHtml(brief ?? html)
    .replace(/<br\b[^>]*>|<\/(?:p|h[1-6]|li|div)>/gi, '\n')
    .replace(/<[^>]+>/g, ' ');
  if (MEANS_FOR_ISO_RE.test(briefLines)) {
    throw new Error(`${pagePath} brief heading leaks an ISO-3166 alpha-2 code`);
  }
}

// A floor, not completeness. #7615 shipped this as
// `developmentsPageCount !== indexedCountryPageCount` -- every indexed country
// owed a dated development -- which no real capture can satisfy: the news cycle
// simply does not mention most countries. A fully keyed freeze on 2026-09-04
// covered 61 of 196 pages (54 headline-matched, 40 briefs, 18 timelines), so
// the gate rejected every snapshot the freeze could produce. That left the
// weekly refresh unable to publish and armed the
// MAX_LIVE_PULSE_SNAPSHOT_AGE_DAYS fuse against the whole corpus build.

View on GitHub (pinned to 7d06c8633d)