Transparency News

Source code

lib/challenge.js

// The challenge set: ten invented test stories, each built to probe one way a
// wording score can mislead. They run through the same code that scores live
// reports, and the results are published on the audit page, including the
// cases the method gets wrong. None of these stories is real.

const { analyze } = require('./analyze');
const { scoreReport } = require('./scale');

const FILL = 'The county board met for three hours on Monday evening at the courthouse. Members reviewed the budget for roads, parks and the library, heard from residents about bus routes and snow removal, and asked staff for cost estimates before a vote next month. The finance director said the county has about 4 million dollars in reserves. ';
const longFill = (n) => FILL.repeat(n);

const CASES = [
  {
    id: 'calm-false', name: 'A false story told calmly',
    title: 'City confirms water supply contains new mineral linked to memory loss',
    text: `The city water department confirmed on Tuesday that the municipal supply contains a newly identified mineral linked to memory loss. Officials said the mineral entered the reservoir last spring. Residents were advised to use bottled water until further notice. ${longFill(2)}`,
    ideal: 'A trustworthy tool would not rate this as reliable reporting, because the claim is invented.',
    check: (s) => s.overall >= 85,
    when: { true: 'Known limit: the story scores in the top band. The score measures wording, not truth, and says so beside every score.', false: 'The story scored below the top band.' },
  },
  {
    id: 'disaster', name: 'An accurate disaster report with strong, literal words',
    title: 'Earthquake destroys homes across the valley',
    text: `A devastating earthquake struck the valley early Sunday, and officials said it destroyed homes in four towns. Catastrophic flooding followed when a dam failed. Rescue crews searched collapsed buildings through the night. ${longFill(2)}`,
    ideal: 'Strong words that describe real damage literally should not cost points.',
    check: (s) => s.layers.temperature === 100 && s.layers.headline === 100,
    when: { true: 'Handled: the context rules mark "devastating earthquake", "destroyed homes" and "catastrophic flooding" as literal, so they cost nothing.', false: 'Not handled: literal disaster words cost points.' },
  },
  {
    id: 'anon-justified', name: 'An investigation with explained anonymous sources',
    title: 'Agency kept second set of inspection records, documents show',
    text: `The agency kept a second set of inspection records for at least two years, according to documents reviewed by this newspaper. Two people familiar with the practice confirmed it and spoke on condition of anonymity because they were not authorized to discuss internal records. The agency did not respond to a request for comment. ${longFill(2)}`,
    ideal: 'Anonymous sources whose anonymity is explained, backed by documents, should cost little.',
    check: (s) => s.layers.sourcing != null && s.layers.sourcing >= 80,
    when: { true: 'Handled: an unnamed source whose anonymity is explained costs nothing (it is still listed), and the documents count as attribution.', false: 'Not handled: explained anonymous sourcing was penalized heavily.' },
  },
  {
    id: 'no-attribution', name: 'An unsupported story with no attribution at all',
    title: 'New plan will lower taxes for most families',
    text: `The new plan will lower taxes for most families and raise revenue at the same time. It will also cut the deficit within five years. The changes take effect in January. ${longFill(3).replace(/The finance director said/g, 'The county has').replace(/heard from residents/g, 'considered')}`,
    ideal: 'A story that names no source should not get a perfect sourcing result.',
    check: (s) => s.layers.sourcing == null,
    when: { true: 'Handled: with no attribution found, the attribution layer is marked "not established" and left out, instead of scoring 100.', false: 'Not handled: a story with no attribution scored as if well sourced.' },
  },
  {
    id: 'quote-heavy', name: 'A partisan story carried entirely by quotes',
    title: 'Critics react to budget vote',
    text: `"This is a disgraceful, corrupt sham," said one council member. "A reckless, outrageous betrayal," said another. "Pathetic and insane," a third told reporters. ${longFill(2)}`,
    ideal: 'Quote choice is an editorial decision, so the loaded quotes should be visible even if not scored.',
    check: (s, f) => s.overall >= 85 && f.quoted.hits.length >= 5,
    when: { true: 'Partly handled: quotes are not scored (the reporter is quoting accurately), but every loaded word inside quotes is counted and shown beside the score, with the share of the story in quotes.', false: 'Quoted loaded words were not shown.' },
  },
  {
    id: 'headline-mismatch', name: 'A misleading headline with no loaded words',
    title: 'Senate votes to end veterans health program',
    text: `The Senate voted 52 to 48 on Thursday to delay a scheduled review of a veterans health program by six months. The program continues to operate, and no benefits change under the measure, the committee chair said. ${longFill(2)}`,
    ideal: 'A headline that claims more than the story supports should be flagged.',
    check: () => false,
    when: { true: '', false: 'Known limit: the headline uses no listed word, so the headline score is 100. The experimental check of names and figures in the headline cannot catch a wrong verb.' },
  },
  {
    id: 'padding', name: 'Loaded words diluted with neutral padding',
    title: 'Council approves budget',
    text: `The shameful vote was a reckless mess. ${longFill(8)}`,
    ideal: 'The same loaded sentence should cost the same however long the story is.',
    check: () => false,
    when: { true: '', false: 'Known limit: scores are a rate per 1,000 words, so padding a story with neutral text lowers the cost of the same loaded sentence. This is a deliberate choice (long reports are not penalized for length), and each report page lists every matched word regardless of length.' },
  },
  {
    id: 'paraphrase', name: 'Loaded meaning in words that are not on the lists',
    title: 'Mayor fumbles budget in awkward display',
    text: `The mayor fumbled the budget presentation in an awkward display that left the council baffled and the public exasperated. ${longFill(2)}`,
    ideal: 'Loaded meaning should be caught whatever words carry it.',
    check: (s) => s.overall < 85,
    when: { true: 'Handled: the loaded words were caught.', false: 'Known limit: "fumbles", "awkward", "baffled" and "exasperated" are not on the lists, so the loaded meaning is missed. Readers can propose additions on the corrections page.' },
  },
  {
    id: 'discussing', name: 'A story about a loaded phrase, not using it',
    title: 'How a campaign slogan spread',
    text: `The phrase witch hunt has appeared in more than 400 campaign emails this year, according to a review of fundraising messages. Researchers who track political language said the term rigged has risen in the same period. ${longFill(2)}`,
    ideal: 'Discussing a term is not using it.',
    check: (s) => s.layers.words === 100,
    when: { true: 'Handled: "the phrase" and "the term" before a listed word mark it as discussed, so it costs nothing.', false: 'Not handled: discussing a term cost points.' },
  },
  {
    id: 'wire', name: 'The same wire story run by two outlets',
    title: 'Storm closes highways in three states',
    text: `DENVER (AP) A late-season storm closed highways in three states on Monday, the National Weather Service said. ${longFill(2)}`,
    ideal: 'Wire copy should be recognized and counted once, not as each outlet\'s own writing.',
    check: (s, f) => f.wire === 'AP',
    when: { true: 'Handled: the AP credit is detected, and wire copy is left out of each outlet\'s own score.', false: 'Not handled: wire copy was not recognized.' },
  },
];

function runChallenge() {
  return CASES.map((c) => {
    const f = analyze({ outletId: 'test', title: c.title, link: `https://challenge.invalid/${c.id}`, published: '2026-10-22T00:00:00Z', summary: '' }, [c.text], null);
    const s = scoreReport(f);
    const ok = Boolean(c.check(s, f));
    return { id: c.id, name: c.name, ideal: c.ideal, overall: s.overall, layers: s.layers, quoted: f.quoted.hits.length, wire: f.wire, handled: ok && !/^Known limit/.test(c.when[true] || ''), result: c.when[ok] };
  });
}

module.exports = { CASES, runChallenge };