koala73/worldmonitor · error · Error

${pagePath} brief has unsupported citation: ${gap}

Error message

${pagePath} brief has unsupported citation: ${gap}

What it means

As part of assertCountryBriefPresentation, the rendered claim blocks (<p>/<li> excluding class="source" blocks, entity-decoded to visible text) are joined and passed to briefCitationGroundingGap along with the brief's sources. If the gap helper finds a citation in the claims not grounded in the provided sources, the build throws naming the gap. This guarantees every published citation in the intel brief traces to a listed source, preventing fabricated or orphaned citations.

Solutions

  1. Update the brief's sources array so it includes the source referenced by the unsupported citation
  2. Remove or correct the ungrounded citation in the brief claim text
  3. Re-run brief generation with grounding enforcement so claims and sources are produced together
  4. Pass the same sources array used at render time to assertCountryBriefPresentation (mismatched inputs cause false gaps)

Example fix

// before
assertCountryBriefPresentation({ pagePath, html, sources: sources.slice(0, 3) });
// after
assertCountryBriefPresentation({ pagePath, html, sources: brief.sources });
Defensive patterns

Strategy: validation

Validate before calling

function ungroundedCitations(claimText, sources) {
  const available = new Set(sources.map((s) => s.key));
  return extractCitationKeys(claimText).filter((k) => !available.has(k));
}

Try / catch

try {
  assertCountryBriefPresentation({ pagePath, html, sources: brief.sources });
} catch (err) {
  if (err.message.includes('unsupported citation')) {
    console.error(`Ungrounded citation in ${pagePath}: ${err.message}`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Asserting a page where the brief claim text cites a source that does not match any entry in the sources array passed to assertCountryBriefPresentation. Triggered by reordering, renaming, or dropping a source while the brief text still references it, or adding a claim with a new citation without updating sources.

Common situations: Brief text edited (by hand or model regeneration) to add a citation but the frozen sources array was not updated; sources list deduplicated or renumbered so citation keys no longer align; a formatter rewrites a citation or entity in the claim block after validation; a mismatched sources array passed to the assertion.

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/75b64b4874a459ff. Report an issue: GitHub.

Appendix: source

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

// when either artifact reaches <main>, including section titles that are
// still plain text rather than <h*> tags.
const MEANS_FOR_ISO_RE = /^\s*what this means for [a-z]{2}(?=\s*(?::|$))/im;

export function assertCountryBriefPresentation({ pagePath, html, sources }) {
  const main = corpusMainHtml(html);
  if (main.includes('**')) {
    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

View on GitHub (pinned to 7d06c8633d)