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
- Update the brief's sources array so it includes the source referenced by the unsupported citation
- Remove or correct the ungrounded citation in the brief claim text
- Re-run brief generation with grounding enforcement so claims and sources are produced together
- 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
- Generate brief claims and their sources array together so citations are grounded at authoring time
- Never renumber, deduplicate, or slice the sources list independently of the claim text
- Pass the exact same sources array to rendering and to assertion helpers
- Validate citation grounding immediately after any model regeneration of brief text, before freezing
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
- ${pagePath} dropped its frozen intel brief
- is missing methodology prose
- is missing following prose
- profile "' + profile.name + '" has no prose
- H2 "' + page.heading + '" has no following content
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', ''': "'" })[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 asView on GitHub (pinned to 7d06c8633d)