koala73/worldmonitor · error

is missing whyWeWinBody

Error message

 is missing whyWeWinBody

What it means

applyComparisonNarrative requires every page's merged content to include a non-empty whyWeWinBody array (joined and whitespace-normalized must be non-empty). This is the expanded 'why we win' prose that follows the summary claim; a page with only the headline whyWeWin text is rejected.

Solutions

  1. Add a whyWeWinBody array of paragraph strings to the slug's COMPARISON_NARRATIVES entry.
  2. Ensure the entries are non-empty after whitespace normalization.
  3. Check the field name spelling in the narrative object.
  4. Make sure whyWeWinBody does not simply duplicate whyWeWin (that throws separately).

Example fix

// before
'worldmonitor-vs-x': { whyWeWin: 'More datasets.' }
// after
'worldmonitor-vs-x': {
  whyWeWin: 'More datasets.',
  whyWeWinBody: ['WorldMonitor fuses maritime, aviation and conflict feeds into one live map.'],
}
Defensive patterns

Strategy: validation

Validate before calling

const body = (narrative.whyWeWinBody ?? []).join(' ').replace(/\s+/g, ' ').trim();
if (!body) throw new Error(page.slug + ' needs non-empty whyWeWinBody');

Type guard

const hasWhyBody = (n) => Array.isArray(n.whyWeWinBody) && n.whyWeWinBody.some((s) => String(s).trim() !== '');

Try / catch

try {
  page = applyComparisonNarrative(page);
} catch (e) {
  if (String(e.message).includes('is missing whyWeWinBody')) {
    console.error('Author whyWeWinBody paragraphs for:', e.message);
  } else throw e;
}

Prevention

When it happens

Trigger: Running the build when a slug's narrative lacks whyWeWinBody entirely, sets it to an empty array, or contains only empty/whitespace strings.

Common situations: Authoring a new comparison narrative and filling whyWeWin but not the body list; copy-editing that emptied the array; a merge step where the field key was misspelled (whyWeWinBodies etc.).

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at scripts/build-comparison-pages.mjs:465

    throw new Error('Missing comparison narrative for ' + page.slug);
  }
  const faqs = [...page.faqs, ...(narrative.extraFaqs ?? [])];
  if (faqs.length < 8 || faqs.length > 12) {
    throw new Error(page.slug + ' FAQ count must be 8-12, got ' + faqs.length);
  }
  const faqNames = new Set();
  for (const [question] of faqs) {
    const key = String(question).trim().toLowerCase();
    if (faqNames.has(key)) {
      throw new Error(page.slug + ' duplicate FAQ question: ' + question);
    }
    faqNames.add(key);
  }
  const merged = { ...page, ...narrative, faqs };
  delete merged.extraFaqs;
  const whyBody = (merged.whyWeWinBody ?? []).join(' ').replace(/\s+/g, ' ').trim();
  if (!whyBody) {
    throw new Error(page.slug + ' is missing whyWeWinBody');
  }
  if (whyBody === merged.whyWeWin.replace(/\s+/g, ' ').trim()) {
    throw new Error(page.slug + ' whyWeWinBody must not repeat whyWeWin');
  }
  if (merged.heading && !(merged.headingProse?.length || merged.competitorProfiles?.length)) {
    throw new Error(page.slug + ' H2 "' + merged.heading + '" has no following prose');
  }
  if (!merged.evaluationHeading || !merged.evaluationProse?.length) {
    throw new Error(page.slug + ' is missing the evaluation section');
  }
  if (!merged.switchHeading || !merged.switchProse?.length) {
    throw new Error(page.slug + ' is missing the switch-trigger section');
  }
  if (!merged.methodologyProse?.length) {
    throw new Error(page.slug + ' is missing methodology prose');
  }
  return merged;
}

View on GitHub (pinned to 7d06c8633d)