santifer/career-ops · error · Error

Fact check failed${options.label ? ` for ${options.label}` :

Error message

Fact check failed${options.label ? ` for ${options.label}` : ''}: ${details.join('; ')}

What it means

assertFacts() is the throwing variant of verify-cv-facts.mjs: it re-runs verifyFacts() and, when the verdict is 'block', throws one aggregated error listing metric-like claims absent from sources, unsupported non-metric facts (kind=value for employers/titles/tools), and forbidden phrases found. This is the tool's no-fabrication gate doing its job — the generated document contains claims with no source backing.

Source

Thrown at verify-cv-facts.mjs:416

      .filter(phrase => stripMarkup(targetText).toLowerCase().includes(String(phrase).toLowerCase()));
  return {
    verdict: invented.length || unsupportedFacts.length || forbidden.length ? 'block' : warnings.length ? 'warn' : 'pass',
    invented,
    unsupportedFacts,
    forbidden,
    warnings,
  };
}

/** Verify a document and throw when it contains a blocking unsupported claim. */
export function assertFacts(targetText, options = {}) {
  const result = verifyFacts(targetText, options);
  if (result.verdict === 'block') {
    const details = [];
    if (result.invented.length) details.push(`metric-like claims absent from sources: ${result.invented.join(', ')}`);
    if (result.unsupportedFacts.length) details.push(`non-metric facts absent from sources: ${result.unsupportedFacts.map(({ kind, value }) => `${kind}=${value}`).join(', ')}`);
    if (result.forbidden.length) details.push(`forbidden phrases found: ${result.forbidden.join(', ')}`);
    throw new Error(`Fact check failed${options.label ? ` for ${options.label}` : ''}: ${details.join('; ')}`);
  }
  return result;
}

/** Parse the fact-validator command-line arguments. */
function parseCliArgs(args) {
  const sourcePaths = [];
  let targetArg = '';
  let configPath = DEFAULT_CONFIG;
  let json = false;
  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--source' || arg === '--config') {
      if (!args[i + 1]) throw new Error(`${arg} requires a path`);
      if (arg === '--source') sourcePaths.push(args[++i]);
      else configPath = args[++i];
    } else if (arg === '--help' || arg === '-h') {
      return { help: true };

View on GitHub (pinned to 60398d6549)

Solutions

  1. Read the three detail segments in the message: they name each offending claim, so remove or correct those exact strings in the generated document
  2. If a claim is genuinely true, add it to a primary source (cv.md / config) and pass that file via --source, then re-run
  3. If a number is legitimately allowed, add it to allow_metrics in the fact-gate config instead of deleting it silently
  4. Use the non-throwing verifyFacts() export when you want the verdict object and to decide yourself how to handle 'block'

Example fix

// before
import { assertFacts } from './verify-cv-facts.mjs';
assertFacts(cvText, { sources, label: 'cv' }); // throws on block
// after
import { verifyFacts } from './verify-cv-facts.mjs';
const r = verifyFacts(cvText, { sources, label: 'cv' });
if (r.verdict === 'block') {
  console.error('blocking claims:', r.invented, r.unsupportedFacts.map((f) => f.value));
  process.exitCode = 1; // or strip the claims and regenerate
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Non-throwing pre-check: get the verdict and decide yourself
import { verifyFacts } from './verify-cv-facts.mjs';
const r = verifyFacts(text, { sources, config });
if (r.verdict === 'block') {
  console.error('blocking claims:', [...r.invented, ...r.unsupportedFacts.map((f) => `${f.kind}=${f.value}`), ...r.forbidden]);
}

Type guard

function isBlockedVerdict(v) {
  return v != null && typeof v === 'object' && v.verdict === 'block';
}

Try / catch

try {
  assertFacts(text, { sources, config, label: 'cv' });
} catch (err) {
  if (String(err.message).startsWith('Fact check failed')) {
    // parse the three detail segments, strip/correct those claims, regenerate, re-assert
    const details = err.message.slice('Fact check failed'.length);
    // ... handle, do not swallow: an unhandled block means fabricated content shipped
    process.exitCode = 1;
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling assertFacts(generatedText, {sources, config, label}) where the text contains a metric like 'increased revenue by 70%' that appears in no --source file, an employer/title/tool claim not present in the sources, or a phrase listed in forbidden_phrases. Severity of unsupported entries decides block vs warn.

Common situations: AI-generated CV/cover-letter text that embellished a number beyond cv.md; tailoring that imported the JD's tool names as the candidate's own; a stale or missing source file (forgetting --source cv.md) so even true claims look unsupported; forbidden-phrase lists catching hype wording.

Related errors


AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20). Data as JSON: /api/errors/ea9a4f106f0eb1d7. Report an issue: GitHub.