Source code
// The rule book: every word list, weight, constant and test threshold the
// score uses. Everything here is published on the method page, and its
// SHA-256 fingerprint is shown there, so any change to a single character
// changes the fingerprint.
const crypto = require('crypto');
const ANCHORS = require('./anchors.json');
const EVIDENCE = require('./evidence.json');
const VERSION = '3.1.0';
// Newsroom rules each entry enforces. Every flag on the site links back to one.
const SOURCES = {
puffery: { name: 'Puffery', rule: 'Wikipedia "Words to watch": words that praise rather than describe.', url: 'https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Words_to_watch#Puffery' },
label: { name: 'Contentious label', rule: 'Wikipedia "Words to watch": value-laden labels need attribution. AP and the BBC use "terrorist" only in quotes or with attribution.', url: 'https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Words_to_watch#Contentious_labels' },
doubt: { name: 'Expression of doubt', rule: 'Reuters Handbook: "claimed" can suggest doubt. Wikipedia "Words to watch": "so-called", scare quotes and similar words cast doubt.', url: 'https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Words_to_watch#Expressions_of_doubt' },
editorial: { name: 'Editorializing', rule: 'Wikipedia "Words to watch": words such as "clearly" and "unfortunately" insert the writer\'s view. Reuters Handbook: keep judgment out of news copy.', url: 'https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Words_to_watch#Editorializing' },
saidverb: { name: 'Loaded word for "said"', rule: 'Wikipedia "Words to watch": "said" is neutral; "admitted", "insisted" and similar verbs imply a judgment. Reuters Handbook: verbs like "refute" imply editorial judgment.', url: 'https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Words_to_watch#Said' },
heat: { name: 'Raises the temperature', rule: 'Reuters Handbook: use neutral language. Wikipedia "Words to watch": avoid dramatic wording a plain description does not need. Published studies on how this kind of wording affects readers are on the evidence page.', url: '/evidence' },
euphemism: { name: 'Softens what happened', rule: 'Wikipedia "Words to watch": euphemisms blur what happened or who did it. Published studies on how softening and agent-free wording affect blame are on the evidence page.', url: '/evidence#G11' },
insult: { name: 'Insult or name-calling', rule: 'A report describes people; an insult in the reporter\'s own voice is a judgment. Experimental: shown on every report but not scored until the rating panel has reviewed it. Research on incivility in news is on the evidence page.', url: '/evidence#G12' },
unnamed: { name: 'Unnamed source', rule: 'Reuters Handbook: anonymous sources are the weakest sources. SPJ Code: reserve anonymity as a last resort and explain why.', url: 'https://www.spj.org/spj-code-of-ethics/' },
vague: { name: 'Vague attribution', rule: 'Wikipedia "Words to watch": "experts say" and "critics say" claim support without naming it.', url: 'https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Words_to_watch#Unsupported_attributions' },
};
const INSULT_TERMS = ['moron', 'morons', 'idiot', 'idiots', 'liar', 'liars', 'loser', 'losers', 'clown', 'clowns', 'buffoon', 'buffoons', 'crook', 'crooks', 'traitor', 'traitors', 'scum', 'insanity', 'lunacy', 'lunatic', 'lunatics', 'unhinged', 'deranged'];
// [word or phrase, severity 1 to 3, rule]. Severity is the points it adds to
// the rate before dividing by length.
const WORD_CHOICE = [
...['legendary', 'iconic', 'visionary', 'world-class', 'groundbreaking', 'renowned', 'beloved', 'acclaimed', 'prestigious', 'game-changing', 'trailblazing', 'storied'].map((w) => [w, 1, 'puffery']),
...['terrorist', 'terrorists', 'extremist', 'extremists', 'cult', 'racist', 'racists', 'sexist', 'bigot', 'bigots', 'bigoted', 'fascist', 'fascists', 'thug', 'thugs', 'mob', 'regime', 'propaganda', 'conspiracy theorist', 'conspiracy theorists'].map((w) => [w, 3, 'label']),
...['so-called', 'purported', 'purportedly', 'supposedly', 'dubious'].map((w) => [w, 2, 'doubt']),
['claimed', 1, 'doubt'],
...['notably', 'arguably', 'essentially', 'actually', 'utterly', 'absolutely', 'without a doubt', 'happily', 'tragically', 'aptly', 'regrettably'].map((w) => [w, 1, 'editorial']),
...['controversial', 'notorious', 'infamous', 'embattled', 'beleaguered', 'disgraced'].map((w) => [w, 1, 'label']),
...['clearly', 'obviously', 'of course', 'unfortunately', 'fortunately', 'sadly', 'interestingly', 'ironically', 'remarkably', 'needless to say', 'undoubtedly', 'it should be noted',
'shameful', 'disgrace', 'disgraceful', 'disgusting', 'pathetic', 'absurd', 'ridiculous', 'outrageous', 'insane', 'crazy', 'idiotic', 'stupid', 'dumb', 'genius', 'brilliant', 'heroic', 'courageous', 'cowardly', 'corrupt', 'evil', 'disastrous', 'reckless', 'disturbing', 'alarming', 'troubling', 'embarrassing', 'laughable', 'pitiful', 'despicable', 'vile', 'wonderful', 'amazing', 'incredible', 'unbelievable'].map((w) => [w, 2, 'editorial']),
...['admitted', 'admits', 'conceded', 'concedes', 'insisted', 'insists', 'refused to say', 'failed to say', 'boasted', 'boasts', 'bragged', 'brags', 'fumed', 'fumes', 'seethed', 'lectured', 'ranted', 'rants', 'whined', 'refuted'].map((w) => [w, 2, 'saidverb']),
// Delegitimizing labels in the reporter's own voice (moved from temperature in 3.0.0: their basis is the newsroom rule on contentious labels, not reader research).
...['witch hunt', 'sham', 'hoax', 'cover-up', 'coverup', 'rigged', 'weaponized', 'weaponization', 'tyranny', 'tyrannical'].map((w) => [w, 2, 'label']),
// Insults and name-calling (added in 3.0.0). Experimental: shown, not scored.
...INSULT_TERMS.map((w) => [w, 2, 'insult']),
];
// Partisan-coded labels, built as mirror pairs so neither side's vocabulary
// carries more weight. Each pair has equal severity. "R" marks terms more
// common in right-leaning speech, "L" in left-leaning speech.
const MIRROR_PAIRS = [
['radical left', 'radical right'], ['far-left', 'far-right'], ['extreme left', 'extreme right'],
['leftists', 'right-wingers'], ['woke mob', 'MAGA mob'], ['ultra-liberal', 'ultra-MAGA'],
['pro-abortion', 'anti-choice'], ['death tax', 'tax breaks for the rich'], ['illegal aliens', 'nativists'],
['gun-grabbers', 'gun nuts'], ['open-borders', 'anti-immigrant'], ['job-killing', 'corporate greed'],
['Democrat Party', 'GQP'], ['baby killers', 'forced-birth'], ['climate alarmists', 'climate deniers'],
];
const PARTISAN_SEVERITY = 3;
// Temperature: wording that turns heat up or down beyond what a plain
// description needs. Both directions count against the temperature layer.
const HEAT = [
...['slams', 'slammed', 'blasts', 'blasted', 'rips', 'ripped', 'torches', 'torched', 'unloads', 'lashes out', 'lashed out', 'mocks', 'mocked', 'smacks', 'smacked', 'dunks', 'dunked', 'scrambles', 'scrambling', 'hammers', 'hammered', 'erupts', 'erupted'].map((w) => [w, 2, 'heat']),
...['destroys', 'destroyed', 'obliterates', 'obliterated', 'eviscerates', 'eviscerated', 'demolishes', 'demolished', 'shreds', 'shredded', 'bombshell', 'meltdown', 'firestorm', 'frenzy', 'fury', 'furious', 'outrage', 'outraged', 'rage', 'raging', 'war on', 'all-out war', 'crusade', 'onslaught', 'implodes', 'imploded', 'humiliating', 'humiliation', 'humiliates', 'jaw-dropping', 'chaos', 'chaotic'].map((w) => [w, 3, 'heat']),
...['lawless', 'lawlessness', 'nightmare', 'out of control'].map((w) => [w, 2, 'heat']),
...['stunning', 'shocking', 'explosive', 'scathing', 'brutal', 'devastating', 'catastrophe', 'catastrophic', 'panic', 'epic'].map((w) => [w, 1, 'heat']),
...['very', 'extremely', 'totally', 'truly', 'deeply', 'hugely', 'massive', 'massively', 'huge', 'enormous', 'staggering', 'whopping', 'sweeping', 'dramatic', 'dramatically', 'fierce', 'fiercely', 'blistering', 'searing', 'rebuke', 'rebuked', 'rebukes', 'backlash', 'major blow', 'blow to'].map((w) => [w, 1, 'heat']),
];
const SOFT = [
...['officer-involved', 'collateral damage', 'misspoke', 'misspeaks', 'enhanced interrogation', 'kinetic action', 'mistakes were made', 'controversial remarks', 'misstep', 'missteps', 'gaffe', 'gaffes', 'downsizing', 'right-sizing', 'revenue enhancement', 'neutralized', 'friendly fire', 'raised eyebrows', 'raises eyebrows', 'sparked debate', 'sparks debate', 'pushback'].map((w) => [w, 2, 'euphemism']),
];
// Sourcing. An unnamed source with a stated reason for anonymity (SPJ) costs nothing.
const UNNAMED = ['sources say', 'sources said', 'source says', 'source said', 'according to sources', 'a source familiar', 'people familiar', 'person familiar', 'familiar with the matter', 'familiar with the discussions', 'insiders', 'an insider', 'reportedly', 'it is said', 'one official said', 'officials said privately'];
const VAGUE = ['experts say', 'experts said', 'critics say', 'critics said', 'some say', 'many say', 'many are saying', 'people are saying', 'observers say', 'some argue', 'many believe', 'it is widely believed'];
// An unnamed source whose anonymity the report explains costs nothing (3.0.0;
// it cost 1 point before). It is still listed on the report's page.
const REASON_POINTS = 0;
const REASON_GIVEN = /(not authorized|wasn't authorized|weren't authorized|requested anonymity|on condition of anonymity|to discuss (?:private|internal|sensitive)|because (?:they|he|she) (?:was|were) not|fear(?:ing)? of (?:retaliation|reprisal))/i;
// Name-swap test pairs: party words and matched public figures. A neutral
// score should not move when these are swapped.
const SWAP_PAIRS = [
['Democratic Party', 'Republican Party'], ['Democrats', 'Republicans'], ['Democratic', 'Republican'], ['Democrat', 'Republican'], ['liberals', 'conservatives'], ['liberal', 'conservative'],
['progressives', 'conservatives'], ['Schumer', 'Thune'], ['Jeffries', 'Johnson'], ['Pelosi', 'McConnell'], ['Newsom', 'DeSantis'], ['Sanders', 'Cruz'], ['Ocasio-Cortez', 'Greene'], ['Walz', 'Vance'],
// Added in 3.0.0: political adjectives, institutions, and this site's founder's own party.
['left-wing', 'right-wing'], ['Harris', 'Trump'], ['MSNBC', 'Fox News'], ['Center for American Progress', 'Heritage Foundation'], ['the Squad', 'the Freedom Caucus'], ['Transparency Party', 'Libertarian Party'],
];
// Swaps that are expected to change a score, published as exceptions.
const SWAP_EXCEPTIONS = ['"Democrat Party" and "ultra-liberal" are partisan-coded labels that contain a swapped word; swapping them to "Republican Party" and "ultra-conservative" removes a flag, which is the intended behavior. Reports that contain either label are left out of the test.'];
const SWAP_EXCEPTION_TERMS = ['Democrat Party', 'ultra-liberal'];
// Layer weights for the overall score (geometric mean). They sum to 1. A layer
// that is not established for a report (sourcing with no attribution found)
// is left out and the remaining weights are used in proportion.
const WEIGHTS = { words: 0.4, temperature: 0.3, sourcing: 0.2, headline: 0.1 };
const LAYER_NAMES = { words: 'Word choice', temperature: 'Temperature', sourcing: 'Flagged attribution phrases', headline: 'Headline wording' };
const CONSTANTS = {
minOwnWords: 150, // a report needs this many words in its own voice to be scored on full text
shrinkM: 5, // shrinkage strength: an outlet with 5 reports gets half its weight from the prior
outletFloorReports: 5, // no outlet score below 5 scored reports
outletFloorWords: 3000, // or below 3,000 scored words
provisionalBelow: 10, // 5 to 9 reports: provisional
intervalLevel: 0.9, // 90% range
bootstrapRuns: 2000, // re-draws for the 90% range at 30 or more reports (3.1.0; was 500)
smallSampleBelow: 30, // below 30 reports the range is mean ± t × sd ÷ √n (3.1.0)
geoShift: 10, // overall = Π (s + 10)^w − 10, so one zero cannot collapse the total (3.1.0)
windowWords: 200, // the densest 200-word stretch of a story ...
windowStep: 50, // ... looked for in windows starting every 50 words, plus one ending at the last word
windowAlpha: 0.5, // rate used = 0.5 × whole-story rate + 0.5 × densest-stretch rate (3.1.0)
eventMinOutlets: 3, // events used for the event-adjusted outlet score have 3 or more outlets
eventMinReports: 3, // an outlet needs 3 such reports for an event-adjusted score
anchorEveryDays: 91, // the data-based prior is recomputed every quarter
anchorMinOutlets: 10, // and is set only from 10 or more outlets with 5+ ranked reports ...
anchorMinReports: 150, // ... and 150 or more ranked reports in all
// The loaded end of each layer, in points per 1,000 words. A report at or
// beyond it scores 0 on that layer; each point per 1,000 words below it
// adds back an equal share, so scores slide in a straight line.
wordsAnchor: 40, // word choice: one 3-point word about every 75 words
temperatureAnchor: 40, // temperature: the same density
sourcingAnchor: 20, // flagged attribution phrases: one unexplained unnamed source per 100 words
// Headlines have their own scale (3.0.0). A headline is short, so its rate
// uses at least 25 words, and a single 3-point word in a short headline
// scores 50.
headlineAnchor: 240,
headlineMinWords: 25,
openingWords: 80, // the headline is also compared with the first 80 words of the story (shown, not scored)
loadedPercentile: 0.9, // the opinion samples are summarized at their 90th percentile for the diagnostic
historyDays: 120,
bands: [85, 70], // band edges: 85 and above, 70 to 84, below 70
};
// Severity follows the strength of the published research (3.0.0). For words
// whose basis is reader research (temperature and softening words), the
// listed severity is capped by the strength of the backing for the word's
// group on the evidence page: direct evidence up to 3 points, evidence for the
// wider category up to 2, indirect evidence 1, none 0. A word capped at 0 is
// experimental: it is shown on every report but not scored. Words whose basis
// is a newsroom style rule keep their listed severity.
const STRENGTH_CAP = { direct: 3, category: 2, indirect: 1, none: 0 };
function strengthOf(status) {
if (/^directly/i.test(status)) return 'direct';
if (/^supported/i.test(status)) return 'category';
if (/^(indirect|weak)/i.test(status)) return 'indirect';
return 'none';
}
const TERM_GROUP = new Map();
for (const g of EVIDENCE.groups) {
const strength = g.strength || strengthOf(g.status);
for (const t of g.terms) TERM_GROUP.set(t.toLowerCase(), { group: g.id, strength });
}
// Returns the scored severity for a list entry, with the reason.
function effectiveSeverity(term, listed, rule) {
if (rule === 'insult') return { s: 0, listed, experimental: true, basis: 'experimental' };
if (rule === 'heat' || rule === 'euphemism') {
const g = TERM_GROUP.get(term.toLowerCase());
const strength = g ? g.strength : 'none';
const cap = STRENGTH_CAP[strength];
return { s: Math.min(listed, cap), listed, experimental: cap === 0, basis: strength, group: g ? g.group : null };
}
return { s: listed, listed, experimental: false, basis: 'newsroom rule' };
}
// Context rules (3.0.0). A matched word counts only when the reporter uses it
// in their own voice. It is shown but not scored when:
// - negated: a negating word sits within three words before it ("not a hoax");
// - attributed: its sentence attributes the words to someone ("Smith called
// the probe a witch hunt"), unless the matched word is itself a loaded verb
// for "said", an expression of doubt, or a sourcing phrase;
// - literal: the word is used in its literal sense ("an explosive device",
// "a devastating earthquake", "admitted to the hospital");
// - discussed: the report is discussing the term itself ("the term 'rigged'").
const CONTEXT = {
negation: "(?:not|no|never|nor|without|isn't|wasn't|aren't|weren't|isn’t|wasn’t|aren’t|weren’t|denied|denies|deny|denying|rejected|rejects|dismissed|dismisses|false)",
negationWindow: 3,
attribution: /\b(said|says|told|tells|argued|argues|claimed|claims|(?<!so-)called|calls|accused|accuses|alleged|alleges|according to|described|describes|wrote|writes|posted|posts|tweeted|testified|warned|warns|declared|declares|charged|charges that|blasted|slammed)\b/i,
attributionExempt: ['saidverb', 'doubt', 'unnamed', 'vague'],
discussing: /\b(the (term|word|phrase|label|slogan|nickname|epithet)|terms? like|words? like|phrases? like)\s*$/i,
literal: {
explosive: '(device|devices|material|materials|vest|vests|ordnance|charge|charges|detonation)',
explosives: '',
devastating: '(earthquake|quake|fire|fires|flood|floods|flooding|hurricane|storm|storms|wildfire|wildfires|tornado|tornadoes|blaze|famine|drought|attack|attacks|injury|injuries|loss of life)',
destroyed: '(homes?|buildings?|houses?|vehicles?|structures?|bridges?|crops|records|evidence|by fire|in the fire|in the blaze)',
destroys: '(homes?|buildings?|houses?|vehicles?|structures?|bridges?|crops)',
brutal: '(cold|heat|winter|summer|weather|killings?|murders?|attacks?|assaults?|beating)',
catastrophic: '(flooding|floods?|damage|failure|injur(y|ies)|fires?|storms?|hurricane|earthquake)',
massive: '(earthquake|quake|fire|storm|explosion|flooding|hurricane|wildfire|blaze|heart attack|stroke)',
panic: '(attacks?|disorder|button|buying)',
admitted: '(to|into) (the )?(hospital|intensive care|emergency room|bar|practice)|guilt|guilty|to (the )?(charges?|crimes?|killing|murder|theft|fraud)',
admits: '(guilt|guilty|to (the )?(charges?|crimes?|killing|murder|theft|fraud))',
conceded: '(the (race|election|primary|contest)|defeat)',
concedes: '(the (race|election|primary|contest)|defeat)',
chaos: '',
},
};
// Attribution markers. A report with none of these in its own text (at any
// length since 3.1.0) has no detectable attribution, so its attribution layer
// is "not established" rather than a perfect score.
const ATTRIBUTION_MARKERS = /\b(said|says|told|according to|stated|wrote|testified|announced|reported|records show|documents show|data (?:from|show)|court (?:filings?|records|documents)|in a statement|spokes(?:man|woman|person)|filed|released)\b/gi;
// Wire copy is the wire service's writing: scored and shown, but left out of
// the outlet's own score. It is recognized only by the dateline credit, such as
// "WASHINGTON (AP)", so a story that merely cites a wire service is not counted.
const WIRE = [['AP', /\((?:AP|Associated Press)\)/], ['Reuters', /\(Reuters\)/], ['AFP', /\(AFP\)/]];
// Neutrality tests. Each has a published rule, what it covers, what happens
// when it fails, and (for tests that do not pass) the plan.
const TESTS = {
lexiconBalance: { rule: 'Severity points on each side of the partisan list match within 5%.', tolerance: 0.05, onFail: 'The list is rebalanced before the next version.' },
mirrorPairs: { rule: 'Every left-coded partisan term has a right-coded mirror with equal severity and the same context rules, so equivalent usage gets equal points.', onFail: 'The unpaired term is removed until a mirror is found.' },
nameSwap: { rule: 'Average score change when party, politician, institution and political-adjective names are swapped is 0.5 points or less, and no single report moves more than 2 points outside published exceptions. The swap includes "Transparency Party" and "Libertarian Party".', meanMax: 0.5, singleMax: 2, onFail: 'The rule that moved is found and fixed before the next version.' },
anchorParity: { rule: 'Diagnostic, not a pass mark: the left-leaning and right-leaning opinion samples are shown side by side. Different samples can legitimately use listed words at different rates, so equal results are not the target. The test that matters is equal treatment of equal usage (mirror pairs and name swap).', diagnostic: true },
ownParty: { rule: 'Every saved report that mentions the Transparency Party is listed with its score and matched words, so readers can check that coverage of the founder\'s party is scored like everything else.', onFail: 'Any difference is published with the fix.' },
agreement: { rule: 'Human panel agreement (Krippendorff\'s alpha) of 0.80 or higher is firm; 0.667 to 0.80 is tentative; below 0.667 the word is reviewed. Agreement is reported per word group as well as overall.', firm: 0.8, tentative: 0.667, onFail: 'Words below the review line become experimental (shown, not scored).' },
dif: { rule: 'Per-word fairness (Mantel-Haenszel, ETS scale): below 1.0 passes, 1.0 to 1.5 goes to review, 1.5 or above is revised or removed. Groups: passages from left-leaning and right-leaning outlets. Matched on: the panel\'s overall rating of the passage (1 to 5).', review: 1.0, remove: 1.5, onFail: 'The word is revised or removed in the next version.' },
sensitivity: { rule: 'Outlet rankings under alternative weights correlate with the default at 0.9 or higher (Spearman).', min: 0.9, onFail: 'The weights are reviewed and the result published.' },
methodChange: { rule: 'Every method change publishes how outlet rankings moved: the rank correlation of outlet scores under the old and new method on the same reports.' },
};
function partisanEntries() {
const out = [];
for (const [r, l] of MIRROR_PAIRS) {
out.push([r, PARTISAN_SEVERITY, 'label', 'R']);
out.push([l, PARTISAN_SEVERITY, 'label', 'L']);
}
return out;
}
const SEVERITY_TABLE = [...WORD_CHOICE, ...HEAT, ...SOFT].map(([t, s, r]) => [t, effectiveSeverity(t, s, r)]);
function ruleBook() {
return { VERSION, WORD_CHOICE, MIRROR_PAIRS, PARTISAN_SEVERITY, HEAT, SOFT, UNNAMED, VAGUE, REASON_GIVEN: REASON_GIVEN.source, REASON_POINTS, SWAP_PAIRS, SWAP_EXCEPTIONS, SWAP_EXCEPTION_TERMS, WEIGHTS, CONSTANTS, TESTS, ANCHORS, STRENGTH_CAP, SEVERITY_TABLE, CONTEXT: serial(CONTEXT), ATTRIBUTION_MARKERS: ATTRIBUTION_MARKERS.source, WIRE: WIRE.map(([n, re]) => [n, re.source]) };
}
function serial(o) { return JSON.parse(JSON.stringify(o, (k, v) => (v instanceof RegExp ? v.source : v))); }
const FINGERPRINT = crypto.createHash('sha256').update(JSON.stringify(ruleBook())).digest('hex');
// Fingerprint of everything that decides what is matched and how it is
// classified. A saved report measured under a different fingerprint is
// measured again while it is still in the feeds.
// MEASURE notes changes to how text is measured (3.1.0: scare-quoted words
// count, word positions are kept, panels are counted).
const MEASURE = '3.1.0: scare quotes count; word positions; panels';
const LEXICON_FP = crypto.createHash('sha256').update(JSON.stringify({ MEASURE, WORD_CHOICE, MIRROR_PAIRS, PARTISAN_SEVERITY, HEAT, SOFT, UNNAMED, VAGUE, REASON_GIVEN: REASON_GIVEN.source, REASON_POINTS, SWAP_PAIRS, SEVERITY_TABLE, CONTEXT: serial(CONTEXT), ATTRIBUTION_MARKERS: ATTRIBUTION_MARKERS.source, WIRE: WIRE.map(([n, re]) => [n, re.source]) })).digest('hex').slice(0, 16);
module.exports = { VERSION, REASON_POINTS, SOURCES, WORD_CHOICE, MIRROR_PAIRS, PARTISAN_SEVERITY, HEAT, SOFT, UNNAMED, VAGUE, REASON_GIVEN, SWAP_PAIRS, SWAP_EXCEPTIONS, SWAP_EXCEPTION_TERMS, WEIGHTS, LAYER_NAMES, CONSTANTS, TESTS, ANCHORS, STRENGTH_CAP, TERM_GROUP, effectiveSeverity, SEVERITY_TABLE, CONTEXT, ATTRIBUTION_MARKERS, WIRE, INSULT_TERMS, partisanEntries, ruleBook, FINGERPRINT, LEXICON_FP };