From 06892b48389c5d2a78d917759e3521c9f8ecf241 Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 20 Feb 2026 03:45:45 -0800 Subject: [PATCH] refactor: simplify config and anki integration composition --- docs/subagents/INDEX.md | 3 +- .../codex-task85-20260219T233711Z-46hc.md | 5 ++ src/anki-integration.ts | 43 +++++++--- src/config/service.ts | 78 ++++++++++--------- 4 files changed, 80 insertions(+), 49 deletions(-) diff --git a/docs/subagents/INDEX.md b/docs/subagents/INDEX.md index 73bb452..227ca41 100644 --- a/docs/subagents/INDEX.md +++ b/docs/subagents/INDEX.md @@ -6,7 +6,7 @@ Read first. Keep concise. | ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- | | `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` | | `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` | -| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:35:21Z` | +| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` | | `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` | | `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T02:56:34Z` | | `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` | @@ -23,3 +23,4 @@ Read first. Keep concise. | `codex-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Verify whether Jellyfin token can use same secret-store path as AniList token` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-20T10:22:45Z` | | `codex-vitepress-subagents-ignore-20260220T101755Z-k2m9` | `codex-vitepress-subagents-ignore` | `Exclude docs/subagents from VitePress build` | `completed` | `docs/subagents/agents/codex-vitepress-subagents-ignore-20260220T101755Z-k2m9.md` | `2026-02-20T10:18:30Z` | | `codex-preserve-linebreak-display-20260220T110436Z-r8f1` | `codex-preserve-linebreak-display` | `Fix visible overlay display artifact when subtitleStyle.preserveLineBreaks is disabled` | `completed` | `docs/subagents/agents/codex-preserve-linebreak-display-20260220T110436Z-r8f1.md` | `2026-02-20T11:10:51Z` | +| `codex-review-refactor-cleanup-20260220T113818Z-i2ov` | `codex-review-refactor-cleanup` | `Review recent TASK-85 refactor effort and identify remaining cleanup work` | `handoff` | `docs/subagents/agents/codex-review-refactor-cleanup-20260220T113818Z-i2ov.md` | `2026-02-20T11:40:15Z` | diff --git a/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md b/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md index a55903f..30ebc22 100644 --- a/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md +++ b/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md @@ -9,6 +9,11 @@ ## Current Work (newest first) +- [2026-02-20T11:42:39Z] progress: pivoted from `src/main.ts` micro-extractions to targeted large-file ROI on `/src/config/service.ts` and `/src/anki-integration.ts`. +- [2026-02-20T11:42:39Z] progress: `src/config/service.ts` cleanup: centralized state application in `applyResolvedConfig(...)`, split path/content parsing into `resolveExistingConfigPath(...)` + `parseConfigContent(...)`, and simplified reload/save flows to remove duplicated assignment logic. +- [2026-02-20T11:42:39Z] progress: `src/anki-integration.ts` constructor decomposition: extracted `normalizeConfig(...)`, `createKnownWordCache(...)`, `createPollingRunner(...)`, `createCardCreationService(...)`, and `createFieldGroupingService(...)` to shrink constructor complexity and improve navigation. +- [2026-02-20T11:42:39Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + focused suites pass for `dist/config/config.test.js` and `dist/anki-integration.test.js` (37/37). +- [2026-02-20T11:42:39Z] scope: staging `src/config/service.ts`, `src/anki-integration.ts`, and subagent bookkeeping only; preserving external subagent INDEX row addition as approved. - [2026-02-20T11:35:21Z] progress: extracted numeric shortcut session composition from `src/main.ts` into `src/main/runtime/numeric-shortcut-session-runtime-handlers.ts`; `main.ts` now gets `cancel/start` handlers for multi-copy and mine-sentence sessions from one runtime factory. - [2026-02-20T11:35:21Z] progress: extracted overlay shortcuts lifecycle composition from `src/main.ts` into `src/main/runtime/overlay-shortcuts-runtime-handlers.ts`; `main.ts` now gets register/unregister/sync/refresh handlers via one runtime factory. - [2026-02-20T11:35:21Z] progress: added parity tests `src/main/runtime/numeric-shortcut-session-runtime-handlers.test.ts` and `src/main/runtime/overlay-shortcuts-runtime-handlers.test.ts`. diff --git a/src/anki-integration.ts b/src/anki-integration.ts index 297be01..5444a9f 100644 --- a/src/anki-integration.ts +++ b/src/anki-integration.ts @@ -98,7 +98,22 @@ export class AnkiIntegration { }) => Promise, knownWordCacheStatePath?: string, ) { - this.config = { + this.config = this.normalizeConfig(config); + this.client = new AnkiConnectClient(this.config.url!); + this.mediaGenerator = new MediaGenerator(); + this.timingTracker = timingTracker; + this.mpvClient = mpvClient; + this.osdCallback = osdCallback || null; + this.notificationCallback = notificationCallback || null; + this.fieldGroupingCallback = fieldGroupingCallback || null; + this.knownWordCache = this.createKnownWordCache(knownWordCacheStatePath); + this.pollingRunner = this.createPollingRunner(); + this.cardCreationService = this.createCardCreationService(); + this.fieldGroupingService = this.createFieldGroupingService(); + } + + private normalizeConfig(config: AnkiConnectConfig): AnkiConnectConfig { + return { ...DEFAULT_ANKI_CONNECT_CONFIG, ...config, fields: { @@ -131,15 +146,10 @@ export class AnkiIntegration { ...(config.isKiku ?? {}), }, } as AnkiConnectConfig; + } - this.client = new AnkiConnectClient(this.config.url!); - this.mediaGenerator = new MediaGenerator(); - this.timingTracker = timingTracker; - this.mpvClient = mpvClient; - this.osdCallback = osdCallback || null; - this.notificationCallback = notificationCallback || null; - this.fieldGroupingCallback = fieldGroupingCallback || null; - this.knownWordCache = new KnownWordCacheManager({ + private createKnownWordCache(knownWordCacheStatePath?: string): KnownWordCacheManager { + return new KnownWordCacheManager({ client: { findNotes: async (query, options) => (await this.client.findNotes(query, options)) as unknown, @@ -149,7 +159,10 @@ export class AnkiIntegration { knownWordCacheStatePath, showStatusNotification: (message: string) => this.showStatusNotification(message), }); - this.pollingRunner = new PollingRunner({ + } + + private createPollingRunner(): PollingRunner { + return new PollingRunner({ getDeck: () => this.config.deck, getPollingRate: () => this.config.pollingRate || DEFAULT_ANKI_CONNECT_CONFIG.pollingRate, findNotes: async (query, options) => @@ -169,7 +182,10 @@ export class AnkiIntegration { logInfo: (...args) => log.info(args[0] as string, ...args.slice(1)), logWarn: (...args) => log.warn(args[0] as string, ...args.slice(1)), }); - this.cardCreationService = new CardCreationService({ + } + + private createCardCreationService(): CardCreationService { + return new CardCreationService({ getConfig: () => this.config, getTimingTracker: () => this.timingTracker, getMpvClient: () => this.mpvClient, @@ -236,7 +252,10 @@ export class AnkiIntegration { this.previousNoteIds.add(noteId); }, }); - this.fieldGroupingService = new FieldGroupingService({ + } + + private createFieldGroupingService(): FieldGroupingService { + return new FieldGroupingService({ getEffectiveSentenceCardConfig: () => this.getEffectiveSentenceCardConfig(), isUpdateInProgress: () => this.updateInProgress, getDeck: () => this.config.deck, diff --git a/src/config/service.ts b/src/config/service.ts index 73cbcec..3a6b3e9 100644 --- a/src/config/service.ts +++ b/src/config/service.ts @@ -96,12 +96,7 @@ export class ConfigService { reloadConfig(): ResolvedConfig { const { config, path: configPath } = this.loadRawConfig(); - this.rawConfig = config; - this.configPathInUse = configPath; - const { resolved, warnings } = this.resolveConfig(config); - this.resolvedConfig = resolved; - this.warnings = warnings; - return this.getConfig(); + return this.applyResolvedConfig(config, configPath); } reloadConfigStrict(): ReloadConfigStrictResult { @@ -111,15 +106,11 @@ export class ConfigService { } const { config, path: configPath } = loadResult; - this.rawConfig = config; - this.configPathInUse = configPath; - const { resolved, warnings } = this.resolveConfig(config); - this.resolvedConfig = resolved; - this.warnings = warnings; + const resolvedConfig = this.applyResolvedConfig(config, configPath); return { ok: true, - config: this.getConfig(), - warnings: [...warnings], + config: resolvedConfig, + warnings: this.getWarnings(), path: configPath, }; } @@ -132,11 +123,7 @@ export class ConfigService { ? this.configPathInUse : this.configFileJsonc; fs.writeFileSync(targetPath, JSON.stringify(config, null, 2)); - this.rawConfig = config; - this.configPathInUse = targetPath; - const { resolved, warnings } = this.resolveConfig(config); - this.resolvedConfig = resolved; - this.warnings = warnings; + this.applyResolvedConfig(config, targetPath); } patchRawConfig(patch: RawConfig): void { @@ -159,11 +146,7 @@ export class ConfigService { error: string; path: string; } { - const configPath = fs.existsSync(this.configFileJsonc) - ? this.configFileJsonc - : fs.existsSync(this.configFileJson) - ? this.configFileJson - : this.configFileJsonc; + const configPath = this.resolveExistingConfigPath(); if (!fs.existsSync(configPath)) { return { ok: true, config: {}, path: configPath }; @@ -171,19 +154,7 @@ export class ConfigService { try { const data = fs.readFileSync(configPath, 'utf-8'); - const parsed = configPath.endsWith('.jsonc') - ? (() => { - const errors: ParseError[] = []; - const result = parseJsonc(data, errors, { - allowTrailingComma: true, - disallowComments: false, - }); - if (errors.length > 0) { - throw new Error(`Invalid JSONC (${errors[0]?.error ?? 'unknown'})`); - } - return result; - })() - : JSON.parse(data); + const parsed = this.parseConfigContent(configPath, data); return { ok: true, config: isObject(parsed) ? (parsed as Config) : {}, @@ -195,6 +166,41 @@ export class ConfigService { } } + private applyResolvedConfig(config: RawConfig, configPath: string): ResolvedConfig { + this.rawConfig = config; + this.configPathInUse = configPath; + const { resolved, warnings } = this.resolveConfig(config); + this.resolvedConfig = resolved; + this.warnings = warnings; + return this.getConfig(); + } + + private resolveExistingConfigPath(): string { + if (fs.existsSync(this.configFileJsonc)) { + return this.configFileJsonc; + } + if (fs.existsSync(this.configFileJson)) { + return this.configFileJson; + } + return this.configFileJsonc; + } + + private parseConfigContent(configPath: string, data: string): unknown { + if (!configPath.endsWith('.jsonc')) { + return JSON.parse(data); + } + + const errors: ParseError[] = []; + const result = parseJsonc(data, errors, { + allowTrailingComma: true, + disallowComments: false, + }); + if (errors.length > 0) { + throw new Error(`Invalid JSONC (${errors[0]?.error ?? 'unknown'})`); + } + return result; + } + private resolveConfig(raw: RawConfig): { resolved: ResolvedConfig; warnings: ConfigValidationWarning[];