refactor: split shared type entrypoints

This commit is contained in:
2026-03-26 23:17:04 -07:00
parent 5b06579e65
commit 5dd8bb7fbf
52 changed files with 1498 additions and 1346 deletions

View File

@@ -1,4 +1,4 @@
import type { AiConfig } from '../types';
import type { AiConfig } from '../types/integrations';
import { requestAiChatCompletion } from '../ai/client';
const DEFAULT_AI_SYSTEM_PROMPT =

View File

@@ -4,7 +4,7 @@ import * as os from 'node:os';
import * as path from 'node:path';
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
import type { AnkiConnectConfig } from '../types';
import type { AnkiConnectConfig } from '../types/anki';
type NoteInfoLike = {
noteId: number;
@@ -36,9 +36,7 @@ export function extractSoundFilenames(value: string): string[] {
}
function shouldSyncAnimatedImageToWordAudio(config: Pick<AnkiConnectConfig, 'media'>): boolean {
return (
config.media?.imageType === 'avif' && config.media?.syncAnimatedImageToWordAudio !== false
);
return config.media?.imageType === 'avif' && config.media?.syncAnimatedImageToWordAudio !== false;
}
export async function probeAudioDurationSeconds(

View File

@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import { CardCreationService } from './card-creation';
import type { AnkiConnectConfig } from '../types';
import type { AnkiConnectConfig } from '../types/anki';
test('CardCreationService counts locally created sentence cards', async () => {
const minedCards: Array<{ count: number; noteIds?: number[] }> = [];

View File

@@ -3,10 +3,11 @@ import {
getConfiguredWordFieldName,
getPreferredWordValueFromExtractedFields,
} from '../anki-field-config';
import { AiConfig, AnkiConnectConfig } from '../types';
import { AnkiConnectConfig } from '../types/anki';
import { createLogger } from '../logger';
import { SubtitleTimingTracker } from '../subtitle-timing-tracker';
import { MpvClient } from '../types';
import { AiConfig } from '../types/integrations';
import { MpvClient } from '../types/runtime';
import { resolveSentenceBackText } from './ai';
import { resolveMediaGenerationInputPath } from './media-source';

View File

@@ -1,4 +1,4 @@
import { AnkiConnectConfig } from '../types';
import { AnkiConnectConfig } from '../types/anki';
import { getConfiguredWordFieldName } from '../anki-field-config';
interface FieldGroupingMergeMedia {

View File

@@ -1,7 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { FieldGroupingWorkflow } from './field-grouping-workflow';
import type { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types';
import type { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types/anki';
type NoteInfo = {
noteId: number;

View File

@@ -1,4 +1,4 @@
import { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types';
import { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types/anki';
import { getPreferredWordValueFromExtractedFields } from '../anki-field-config';
export interface FieldGroupingWorkflowNoteInfo {
@@ -181,7 +181,8 @@ export class FieldGroupingWorkflow {
return {
noteId: noteInfo.noteId,
expression:
getPreferredWordValueFromExtractedFields(fields, this.deps.getConfig()) || fallbackExpression,
getPreferredWordValueFromExtractedFields(fields, this.deps.getConfig()) ||
fallbackExpression,
sentencePreview: this.deps.truncateSentence(
fields[(sentenceCardConfig.sentenceField || 'sentence').toLowerCase()] ||
(isOriginal ? '' : this.deps.getCurrentSubtitleText() || ''),

View File

@@ -1,4 +1,4 @@
import { KikuMergePreviewResponse } from '../types';
import { KikuMergePreviewResponse } from '../types/anki';
import { createLogger } from '../logger';
import { getPreferredWordValueFromExtractedFields } from '../anki-field-config';

View File

@@ -4,7 +4,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import type { AnkiConnectConfig } from '../types';
import type { AnkiConnectConfig } from '../types/anki';
import { KnownWordCacheManager } from './known-word-cache';
async function waitForCondition(
@@ -351,10 +351,7 @@ test('KnownWordCacheManager preserves cache state key captured before refresh wo
scope: string;
words: string[];
};
assert.equal(
persisted.scope,
'{"refreshMinutes":1,"scope":"is:note","fieldsWord":"Word"}',
);
assert.equal(persisted.scope, '{"refreshMinutes":1,"scope":"is:note","fieldsWord":"Word"}');
assert.deepEqual(persisted.words, ['猫']);
} finally {
fs.rmSync(stateDir, { recursive: true, force: true });

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
import { getConfiguredWordFieldName } from '../anki-field-config';
import { AnkiConnectConfig } from '../types';
import { AnkiConnectConfig } from '../types/anki';
import { createLogger } from '../logger';
const log = createLogger('anki').child('integration.known-word-cache');
@@ -316,9 +316,9 @@ export class KnownWordCacheManager {
const currentDeck = this.deps.getConfig().deck?.trim();
const selectedDeckEntry =
currentDeck !== undefined && currentDeck.length > 0
? trimmedDeckEntries.find(([deckName]) => deckName === currentDeck) ?? null
? (trimmedDeckEntries.find(([deckName]) => deckName === currentDeck) ?? null)
: trimmedDeckEntries.length === 1
? trimmedDeckEntries[0] ?? null
? (trimmedDeckEntries[0] ?? null)
: null;
if (!selectedDeckEntry) {
@@ -329,7 +329,10 @@ export class KnownWordCacheManager {
if (Array.isArray(deckFields)) {
const normalizedFields = [
...new Set(
deckFields.map(String).map((field) => field.trim()).filter((field) => field.length > 0),
deckFields
.map(String)
.map((field) => field.trim())
.filter((field) => field.length > 0),
),
];
if (normalizedFields.length > 0) {
@@ -353,7 +356,14 @@ export class KnownWordCacheManager {
continue;
}
const normalizedFields = Array.isArray(fields)
? [...new Set(fields.map(String).map((field) => field.trim()).filter(Boolean))]
? [
...new Set(
fields
.map(String)
.map((field) => field.trim())
.filter(Boolean),
),
]
: [];
scopes.push({
query: `deck:"${escapeAnkiSearchValue(trimmedDeckName)}"`,
@@ -402,7 +412,10 @@ export class KnownWordCacheManager {
private async fetchKnownWordNoteFieldsById(): Promise<Map<number, string[]>> {
const scopes = this.getKnownWordQueryScopes();
const noteFieldsById = new Map<number, string[]>();
log.debug('Refreshing known-word cache', `queries=${scopes.map((scope) => scope.query).join(' | ')}`);
log.debug(
'Refreshing known-word cache',
`queries=${scopes.map((scope) => scope.query).join(' | ')}`,
);
for (const scope of scopes) {
const noteIds = (await this.deps.client.findNotes(scope.query, {
@@ -414,10 +427,7 @@ export class KnownWordCacheManager {
continue;
}
const existingFields = noteFieldsById.get(noteId) ?? [];
noteFieldsById.set(
noteId,
[...new Set([...existingFields, ...scope.fields])],
);
noteFieldsById.set(noteId, [...new Set([...existingFields, ...scope.fields])]);
}
}

View File

@@ -1,5 +1,5 @@
import { isRemoteMediaPath } from '../jimaku/utils';
import type { MpvClient } from '../types';
import type { MpvClient } from '../types/runtime';
export type MediaGenerationKind = 'audio' | 'video';
@@ -50,7 +50,7 @@ function resolvePreferredUrlFromMpvEdlSource(
// mpv EDL sources usually list audio streams first and video streams last, so
// when classifyMediaUrl cannot identify a typed URL we fall back to stream order.
return kind === 'audio' ? urls[0] ?? null : urls[urls.length - 1] ?? null;
return kind === 'audio' ? (urls[0] ?? null) : (urls[urls.length - 1] ?? null);
}
export async function resolveMediaGenerationInputPath(

View File

@@ -2,7 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
import type { AnkiConnectConfig } from '../types';
import type { AnkiConnectConfig } from '../types/anki';
import { AnkiIntegrationRuntime } from './runtime';
function createRuntime(

View File

@@ -1,5 +1,5 @@
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
import type { AnkiConnectConfig } from '../types';
import type { AnkiConnectConfig } from '../types/anki';
import {
getKnownWordCacheLifecycleConfig,
getKnownWordCacheRefreshIntervalMinutes,

View File

@@ -1,4 +1,4 @@
import { NotificationOptions } from '../types';
import { NotificationOptions } from '../types/anki';
export interface UiFeedbackState {
progressDepth: number;