// THE VERDICT VOCABULARY — THE one copy: the rulings a fact-check claim may // carry, their default labels and colours, and the limits on an override. // // Plain JS (with JSDoc types) for the reason ytdlp/platformArgs.mjs is: umtool's // report-to-video scripts are `.mjs` run by bare `node` and cannot import // TypeScript, and factcheck.mjs (the video's stamps and tally) takes its // defaults from here. `./verdicts.ts` re-exports it with types for every TS // caller (report.json's schema and validator, the export site's report pages). // Never copy these values into another file. /** The verdicts a claim may carry, in the order a tally lists them. */ export const VERDICTS = Object.freeze(["CORROBORATED", "PARTLY", "CONTRADICTED", "NOT_FOUND", "UNTESTABLE"]); /** Each verdict's default label and colour. An override names only what it changes. */ export const VERDICT_DEFAULTS = Object.freeze({ CORROBORATED: Object.freeze({ label: "Corroborated", color: "#3fbf7f" }), PARTLY: Object.freeze({ label: "Partly true", color: "#e3b23c" }), CONTRADICTED: Object.freeze({ label: "Contradicted", color: "#e5534b" }), NOT_FOUND: Object.freeze({ label: "Not found", color: "#8b93a7" }), UNTESTABLE: Object.freeze({ label: "Untestable", color: "#7d8fd6" }), }); /** The longest label an override may set, in characters (trimmed). */ export const VERDICT_LABEL_MAX = 24; /** An override's colour: `#rgb` or `#rrggbb`. */ export const VERDICT_COLOR_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; /** @param {unknown} v */ export function isVerdict(v) { return typeof v === "string" && VERDICTS.includes(v); } const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v); /** * Every verdict's label and colour with the overrides laid over the defaults, * one verdict at a time and one key at a time. A non-object override, or one * for a verdict that is not in the vocabulary, is ignored: validation is the * caller's (factcheck.mjs `validateFactcheck`, report/validate.ts). * * @param {unknown} overrides * @returns {Record} */ export function resolveVerdicts(overrides) { const given = isObj(overrides) ? overrides : {}; /** @type {Record} */ const out = {}; for (const v of VERDICTS) { out[v] = { ...VERDICT_DEFAULTS[v], ...(isObj(given[v]) ? given[v] : {}) }; } return out; } /** * Every reason a `{ label?, color? }` override cannot be used, as sentences * naming `where` (empty: it can). Unknown keys are refused. The one rule for * both a report's `verdicts` and a video manifest's `render.chrome.factcheck.verdicts`. * * @param {unknown} v * @param {string} where * @returns {string[]} */ export function verdictOverrideProblems(v, where) { if (!isObj(v)) return [`${where} must be { label, color }`]; const errors = []; for (const k of Object.keys(v)) { if (k !== "label" && k !== "color") errors.push(`${where}.${k} is not a verdict setting (label, color)`); } if (v.label !== undefined) { if (typeof v.label !== "string" || !v.label.trim()) errors.push(`${where}.label must be words, not empty`); else if (/[\r\n]/.test(v.label)) errors.push(`${where}.label must be one line`); else if (v.label.trim().length > VERDICT_LABEL_MAX) { errors.push(`${where}.label is ${v.label.trim().length} characters (at most ${VERDICT_LABEL_MAX})`); } } if (v.color !== undefined && !(typeof v.color === "string" && VERDICT_COLOR_RE.test(v.color))) { errors.push(`${where}.color must be a hex colour, #rgb or #rrggbb`); } return errors; }