import type { CheckUrlResult } from './broken-link-repair'; /** Just the bits of a repair result the policy looks at. */ export interface RepairOutcome { success: boolean; fixed?: string; } /** * The pre-fetch decision for a single Article when its `link` is checked * against the network. Mirrors the Feed-side error policy: a * pure function that names the three-strategy matrix (`fixBroken`, * `skipBroken`, `flagBroken`) and reports what the orchestrator should do. * * Decoupled from I/O — callers run `checkUrl` and (when applicable) * `tryFixBrokenUrl` themselves and pass the results in. * * Decision matrix (first matching row wins): * - accessible → proceed (original URL) * - fixBroken + successful repair → proceed (fixed URL) * - inconclusive/transient failure → fail (never mark as broken) * - definitive 404/410 + flagBroken → persist-broken * - definitive 404/410 + skipBroken → skip * - else → fail * * A successful fix wins over both skip and flag. Explicit `--flag-broken` * wins over default skip for a definitive failure, so the flag does what it * says without requiring the surprising `--no-skip-broken` companion. */ export type BrokenLinkAction = /** URL is reachable (or was repaired); fetch content from `url`. */ | { kind: 'proceed'; url: string } /** Don't fetch this Article at all (skipBroken set + URL inaccessible). */ | { kind: 'skip'; reason: string } /** Surface as an error to the caller (URL inaccessible, no fallback flags). */ | { kind: 'fail'; reason: string } /** Persist as a Broken Article (CONTEXT.md), recording `reason` in `brokenReason`. */ | { kind: 'persist-broken'; reason: string }; export interface BrokenLinkOptions { fixBroken?: boolean; skipBroken?: boolean; flagBroken?: boolean; } const FALLBACK_REASON = 'URL not accessible'; /** Only these status codes prove the original resource is gone enough to be deletion-eligible. */ export function isDefinitivelyBroken(check: CheckUrlResult): boolean { return check.statusCode === 404 || check.statusCode === 410; } export function decideBrokenLinkAction( url: string, check: CheckUrlResult, fix: RepairOutcome | undefined, options: BrokenLinkOptions, ): BrokenLinkAction { if (check.accessible) { return { kind: 'proceed', url }; } // A successful repair wins over skip so `--fix-broken` actually applies the // fix even when skipBroken is also set (it defaults on at the call site). if (options.fixBroken && fix?.success && fix.fixed) { return { kind: 'proceed', url: fix.fixed }; } const reason = check.error ?? FALLBACK_REASON; if (!isDefinitivelyBroken(check)) { return { kind: 'fail', reason }; } if (options.flagBroken) { return { kind: 'persist-broken', reason }; } if (options.skipBroken) { return { kind: 'skip', reason }; } return { kind: 'fail', reason }; }