// The ONE place an attribution freshness target is derived — the direct mirror // of controller/digestTarget.ts, and split out of attributeOne.ts for the same // reason that module exists separately from digestVideo.ts: the COUNTERS need // the identity without needing the runner. // // lib/operations.ts is imported by controller/channelSnapshot.ts, which // classifies every video of every channel and sits on the editor's hot path. // Reaching the runner from there drags its whole import graph in with it — the // transcript normalizer, the markdown renderer, the digest prompt module and the // channel-context reader — none of which a CLASSIFICATION needs. So the target // resolver is eager (it is a settings read plus the app registry) and the runner // is loaded lazily at the point of actually running something. // // Honesty about what this is worth: it is a structural argument, not a measured // win. It was written while chasing a flaky drain e2e, on the theory that the // extra dev-mode module compilation was losing a race. That theory did not // survive measurement — the test fails at roughly the same rate with and without // this split (3/6 against 2/6 back-to-back), and it fails on a pristine HEAD too. // The split is kept because keeping a runner off a per-video hot path is right on // its own terms, not because it fixed anything. import { getSettings, type AttributionSettings } from "../lib/settings"; import { getDigestApp, type DigestApp, type DigestAppConfig, } from "../lib/digestApps"; import { attributionTarget, type AttributionFreshnessTarget, type AttributionMethod, } from "../lib/attribution"; export type ResolvedAttributionTarget = { target: AttributionFreshnessTarget; app: DigestApp; config: DigestAppConfig; // What the config asked for. Distinct from what the engine reports having run // — freshness compares this one (see AttributionProvenance.modelRequested). modelRequested: string; }; // The identity the current configuration would produce, minus the per-video // half. // // The diarized lane's identity also includes the generatedAt of the // diarization.json it names clusters from, and that is a disk read. Callers that // have the value pass it in; the registry's state() adds it only inside the one // branch that has already paid for the read. // `appConfig` overrides the digest app's config block wholesale — the fan-out // passes the primary's resolved config with only baseUrl swapped, and the unit // executor passes the primary's injected identity config. When it is supplied, // getSettings() is NOT consulted for the config: on a bare executor the // settings file is defaults, and defaults leaking into the identity here is // exactly how a remote machine writes permanently-stale records. export function resolveAttributionTarget( method: AttributionMethod, cfg?: AttributionSettings, appConfig?: DigestAppConfig, ): ResolvedAttributionTarget { const attribution = cfg ?? getSettings().attribution; const app = getDigestApp(attribution.appId); // The digest app's OWN config block — the ollama URL, context size and // timeout. Attribution is a digest-app workload; a second copy of that config // would be one more thing to keep in step for no benefit. const config: DigestAppConfig = appConfig ?? getSettings().digest.apps[app.id] ?? {}; const modelRequested = attribution.model.trim() || config.model?.trim() || app.defaultModel(); return { target: attributionTarget( { appId: app.id, model: modelRequested, promptVersion: attribution.promptVersion, }, method, ), app, config, modelRequested, }; }