Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 3f5a9f50f512dbcfc15b9b96a22ed5bc21586db7
parent 9d38f625e5cf7b01a99cdba857b74aa52927477e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  5 Oct 2026 02:17:45 -0400

common: the verdict vocabulary moves to common/lib/report/verdicts.mjs; factcheck.mjs takes it from there

One copy of the verdicts, their default labels and colours and the override
rule, shared by umtool's fact-check stamps and tally and by report.json.
Plain JS, like ytdlp/platformArgs.mjs, so the report-to-video scripts can
import it under bare node; verdicts.ts re-exports it typed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Acommon/lib/report/verdicts.mjs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/lib/report/verdicts.ts | 40++++++++++++++++++++++++++++++++++++++++
Mcommon/package.json | 1+
Mumtool/report-to-video/factcheck.mjs | 45+++++++++++++++++----------------------------
Mumtool/report-to-video/factcheck.test.mjs | 14++++++++++++--
5 files changed, 151 insertions(+), 30 deletions(-)

diff --git a/common/lib/report/verdicts.mjs b/common/lib/report/verdicts.mjs @@ -0,0 +1,81 @@ +// 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<string, { label: string, color: string }>} + */ +export function resolveVerdicts(overrides) { + const given = isObj(overrides) ? overrides : {}; + /** @type {Record<string, { label: string, color: string }>} */ + 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; +} diff --git a/common/lib/report/verdicts.ts b/common/lib/report/verdicts.ts @@ -0,0 +1,40 @@ +// The verdict vocabulary, typed. The values live in ./verdicts.mjs — plain JS so +// umtool's report-to-video scripts can import the same copy — and this module +// is how every TS caller reads them: with the verdict as a union, not a string. + +import { + VERDICT_COLOR_RE as VERDICT_COLOR_RE_JS, + VERDICT_DEFAULTS as VERDICT_DEFAULTS_JS, + VERDICT_LABEL_MAX as VERDICT_LABEL_MAX_JS, + VERDICTS as VERDICTS_JS, + resolveVerdicts as resolveVerdictsJs, + verdictOverrideProblems as verdictOverrideProblemsJs, +} from "./verdicts.mjs"; + +export type Verdict = "CORROBORATED" | "PARTLY" | "CONTRADICTED" | "NOT_FOUND" | "UNTESTABLE"; + +export type VerdictStyle = { label: string; color: string }; + +export type VerdictOverride = { label?: string; color?: string }; + +export const VERDICTS = VERDICTS_JS as readonly Verdict[]; + +export const VERDICT_DEFAULTS = VERDICT_DEFAULTS_JS as Readonly<Record<Verdict, Readonly<VerdictStyle>>>; + +export const VERDICT_LABEL_MAX: number = VERDICT_LABEL_MAX_JS; + +export const VERDICT_COLOR_RE: RegExp = VERDICT_COLOR_RE_JS; + +export function isVerdict(v: unknown): v is Verdict { + return typeof v === "string" && (VERDICTS as readonly string[]).includes(v); +} + +// Defaults with the overrides laid over them, one key at a time. +export function resolveVerdicts(overrides: unknown): Record<Verdict, VerdictStyle> { + return resolveVerdictsJs(overrides) as Record<Verdict, VerdictStyle>; +} + +// Every reason an override cannot be used, as sentences naming `where`. +export function verdictOverrideProblems(v: unknown, where: string): string[] { + return verdictOverrideProblemsJs(v, where); +} diff --git a/common/package.json b/common/package.json @@ -33,6 +33,7 @@ "./components/*": "./components/*.tsx", "./lib/detectPlatform.mjs": "./lib/detectPlatform.mjs", "./lib/ports.mjs": "./lib/ports.mjs", + "./lib/report/verdicts.mjs": "./lib/report/verdicts.mjs", "./lib/toolProbe.mjs": "./lib/toolProbe.mjs", "./ytdlp/platformArgs.mjs": "./ytdlp/platformArgs.mjs", "./lib/*": "./lib/*.ts", diff --git a/umtool/report-to-video/factcheck.mjs b/umtool/report-to-video/factcheck.mjs @@ -17,8 +17,20 @@ // round -- so the build, the composition and umtool all read one copy. // chrome-stamp.mjs draws the stamps; chrome-deck.mjs draws the tally. +// THE VERDICT VOCABULARY IS COMMON'S (common/lib/report/verdicts.mjs): the +// verdicts, their default labels and colours and the override rules are one +// copy shared with report.json and the export site's report pages. This file +// re-exports the list and lays the stamp and tally settings beside the labels. +import { + VERDICT_DEFAULTS, + VERDICT_LABEL_MAX, + VERDICTS, + resolveVerdicts, + verdictOverrideProblems, +} from "yt-dlp-transcript-common/lib/report/verdicts.mjs"; + /** The verdicts a claim may carry, in the order the tally lists them. */ -export const VERDICTS = Object.freeze(["CORROBORATED", "PARTLY", "CONTRADICTED", "NOT_FOUND", "UNTESTABLE"]); +export { VERDICTS }; /** Where the stamp sits in the footage box. */ export const STAMP_POSITIONS = Object.freeze(["top-left", "top-right", "bottom-left", "bottom-right", "center"]); @@ -33,19 +45,13 @@ export const TALLY_POSITIONS = Object.freeze(["right", "left"]); * (top-right by default) do not use. */ export const FACTCHECK_DEFAULTS = Object.freeze({ - verdicts: 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" }), - }), + verdicts: VERDICT_DEFAULTS, stamp: Object.freeze({ seconds: 3, position: "top-left" }), tally: Object.freeze({ show: true, position: "right" }), }); /** The limits: a stamp's seconds on screen, a label's characters. */ -export const FACTCHECK_LIMITS = Object.freeze({ seconds: Object.freeze([1, 10]), label: 24 }); +export const FACTCHECK_LIMITS = Object.freeze({ seconds: Object.freeze([1, 10]), label: VERDICT_LABEL_MAX }); /** * The stamp's motion, in seconds: it slams in over `slam` (landing is when the @@ -60,7 +66,6 @@ const CLAIM_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/; const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v); const numIn = (v, lo, hi) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi; -const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; function unknownKeys(obj, allowed, where, errors) { for (const k of Object.keys(obj)) { @@ -71,13 +76,8 @@ function unknownKeys(obj, allowed, where, errors) { /** The fact-check settings with every default filled in. */ export function resolveFactcheck(render) { const f = isObj(render?.chrome?.factcheck) ? render.chrome.factcheck : {}; - const given = isObj(f.verdicts) ? f.verdicts : {}; - const verdicts = {}; - for (const v of VERDICTS) { - verdicts[v] = { ...FACTCHECK_DEFAULTS.verdicts[v], ...(isObj(given[v]) ? given[v] : {}) }; - } return { - verdicts, + verdicts: resolveVerdicts(f.verdicts), stamp: { ...FACTCHECK_DEFAULTS.stamp, ...(isObj(f.stamp) ? f.stamp : {}) }, tally: { ...FACTCHECK_DEFAULTS.tally, ...(isObj(f.tally) ? f.tally : {}) }, }; @@ -101,18 +101,7 @@ export function validateFactcheck(f, where = "render.chrome.factcheck") { for (const [k, v] of Object.entries(f.verdicts)) { const w = `${where}.verdicts.${k}`; if (!VERDICTS.includes(k)) { errors.push(`${w} is not a verdict (${VERDICTS.join(", ")})`); continue; } - if (!isObj(v)) { errors.push(`${w} must be { label, color }`); continue; } - unknownKeys(v, ["label", "color"], w, errors); - if (v.label !== undefined) { - if (typeof v.label !== "string" || !v.label.trim()) errors.push(`${w}.label must be words, not empty`); - else if (/[\r\n]/.test(v.label)) errors.push(`${w}.label must be one line`); - else if (v.label.trim().length > FACTCHECK_LIMITS.label) { - errors.push(`${w}.label is ${v.label.trim().length} characters (at most ${FACTCHECK_LIMITS.label})`); - } - } - if (v.color !== undefined && !(typeof v.color === "string" && HEX_RE.test(v.color))) { - errors.push(`${w}.color must be a hex colour, #rgb or #rrggbb`); - } + errors.push(...verdictOverrideProblems(v, w)); } } } diff --git a/umtool/report-to-video/factcheck.test.mjs b/umtool/report-to-video/factcheck.test.mjs @@ -11,10 +11,13 @@ import path from "node:path"; import test from "node:test"; import { - claimOf, FACTCHECK_DEFAULTS, normalizeClaim, originalUrlAt, resolveFactcheck, roundStamps, STAMP_MOTION, stampSchedule, + claimOf, FACTCHECK_DEFAULTS, FACTCHECK_LIMITS, normalizeClaim, originalUrlAt, resolveFactcheck, roundStamps, STAMP_MOTION, stampSchedule, tallyCues, tallyOf, tallySteps, tallyVerdicts, validateClaims, validateFactcheck, VERDICTS, } from "./factcheck.mjs"; import { + VERDICT_DEFAULTS, VERDICT_LABEL_MAX, VERDICTS as COMMON_VERDICTS, +} from "yt-dlp-transcript-common/lib/report/verdicts.mjs"; +import { deckGeometry, deckLayout, deckQrUrl, deckSchedule, DECK_DEFAULTS, estimateSchedule, feedGeometry, stampGeometry, validateChrome, } from "./deck.mjs"; @@ -42,6 +45,13 @@ const sched = (render = RENDER, entries = ENTRIES, D = 0.5, metas = []) => // ---- the claim ---------------------------------------------------------------- +test("the verdict vocabulary is common's, one copy: the same objects, not equal copies", () => { + assert.equal(VERDICTS, COMMON_VERDICTS); + assert.equal(FACTCHECK_DEFAULTS.verdicts, VERDICT_DEFAULTS); + assert.deepEqual(Object.keys(FACTCHECK_DEFAULTS.verdicts), [...VERDICTS]); + assert.equal(FACTCHECK_LIMITS.label, VERDICT_LABEL_MAX); +}); + test("normalizeClaim: trims the id, refuses a bad shape, an unknown field, a verdict off the list", () => { assert.equal(normalizeClaim(undefined), null); assert.equal(normalizeClaim(null), null); @@ -114,7 +124,7 @@ test("validateChrome refuses a bad factcheck block in sentences, unknown keys in for (const want of [ /render\.chrome\.factcheck\.colour is not a factcheck setting/, /render\.chrome\.factcheck\.verdicts\.MAYBE is not a verdict/, - /render\.chrome\.factcheck\.verdicts\.PARTLY\.size is not a factcheck setting/, + /render\.chrome\.factcheck\.verdicts\.PARTLY\.size is not a verdict setting/, /render\.chrome\.factcheck\.verdicts\.PARTLY\.label is 25 characters \(at most 24\)/, /render\.chrome\.factcheck\.verdicts\.PARTLY\.color must be a hex colour/, /render\.chrome\.factcheck\.verdicts\.NOT_FOUND must be \{ label, color \}/,