fix(stats): stop counting duplicate typeset subtitle lines (#191)

This commit is contained in:
2026-08-14 00:12:56 -07:00
committed by GitHub
parent 046e74ea91
commit b98d4d65c7
34 changed files with 2644 additions and 35 deletions
+47 -3
View File
@@ -37,6 +37,9 @@ export interface CliInvocations {
statsCleanup: boolean;
statsCleanupVocab: boolean;
statsCleanupLifetime: boolean;
statsCleanupDuplicateLines: boolean;
statsCleanupDryRun: boolean;
statsCleanupLookbackDays: number | null;
statsLogLevel: string | null;
syncTriggered: boolean;
syncCliTokens: string[];
@@ -53,6 +56,16 @@ export interface CliInvocations {
texthookerOpenBrowser: boolean;
}
/** `--lookback-days` narrows the duplicate-line cleanup; fractions are floored. */
function parseStatsLookbackDays(value: unknown): number | null {
if (typeof value !== 'string' && typeof value !== 'number') return null;
const days = Number(value);
if (!Number.isFinite(days) || days < 1) {
throw new Error('Stats --lookback-days must be at least one day.');
}
return Math.floor(days);
}
function applyRootOptions(program: Command): void {
program
.option(
@@ -169,6 +182,9 @@ export function parseCliPrograms(
let statsCleanup = false;
let statsCleanupVocab = false;
let statsCleanupLifetime = false;
let statsCleanupDuplicateLines = false;
let statsCleanupDryRun = false;
let statsCleanupLookbackDays: number | null = null;
let statsLogLevel: string | null = null;
let syncTriggered = false;
let syncCliTokens: string[] = [];
@@ -269,6 +285,9 @@ export function parseCliPrograms(
.option('-s, --stop', 'Stop the background stats server')
.option('-v, --vocab', 'Clean vocabulary rows in the stats database')
.option('-l, --lifetime', 'Rebuild lifetime summary rows from retained data')
.option('-d, --duplicate-lines', 'Collapse repeated subtitle lines from typeset animations')
.option('--dry-run', 'Report what a cleanup would remove without changing anything')
.option('--lookback-days <days>', 'Only clean lines recorded in the last N days')
.option('--log-level <level>', 'Log level')
.action((action: string | undefined, options: Record<string, unknown>) => {
statsTriggered = true;
@@ -289,13 +308,35 @@ export function parseCliPrograms(
if (normalizedAction && (statsBackground || statsStop)) {
throw new Error('Stats background and stop flags cannot be combined with stats actions.');
}
if (normalizedAction !== 'cleanup' && (options.vocab === true || options.lifetime === true)) {
throw new Error('Stats --vocab and --lifetime flags require the cleanup action.');
if (
normalizedAction !== 'cleanup' &&
(options.vocab === true || options.lifetime === true || options.duplicateLines === true)
) {
throw new Error(
'Stats --vocab, --lifetime and --duplicate-lines flags require the cleanup action.',
);
}
if (
options.duplicateLines !== true &&
(options.dryRun === true || options.lookbackDays !== undefined)
) {
throw new Error('Stats --dry-run and --lookback-days require --duplicate-lines.');
}
if (normalizedAction === 'cleanup') {
statsCleanup = true;
statsCleanupLifetime = options.lifetime === true;
statsCleanupVocab = statsCleanupLifetime ? false : options.vocab !== false;
statsCleanupDuplicateLines = options.duplicateLines === true;
const explicitModeCount = [options.vocab, options.lifetime, options.duplicateLines].filter(
(value) => value === true,
).length;
if (explicitModeCount > 1) {
throw new Error('Stats cleanup runs one mode at a time.');
}
// Vocabulary cleanup stays the default so `stats cleanup` keeps its old meaning.
statsCleanupVocab =
statsCleanupLifetime || statsCleanupDuplicateLines ? false : options.vocab !== false;
statsCleanupDryRun = options.dryRun === true;
statsCleanupLookbackDays = parseStatsLookbackDays(options.lookbackDays);
} else if (normalizedAction === 'rebuild' || normalizedAction === 'backfill') {
statsCleanup = true;
statsCleanupLifetime = true;
@@ -483,6 +524,9 @@ export function parseCliPrograms(
statsCleanup,
statsCleanupVocab,
statsCleanupLifetime,
statsCleanupDuplicateLines,
statsCleanupDryRun,
statsCleanupLookbackDays,
statsLogLevel,
syncTriggered,
syncCliTokens,