diff --git a/AGENTS.md b/AGENTS.md index b905b00..1b3f6b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,3 @@ - @@ -17,6 +16,7 @@ This project uses Backlog.md MCP for all task and project management activities. - **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work These guides cover: + - Decision framework for when to create tasks - Search-first workflow to avoid duplicates - Links to detailed guides for task creation, execution, and finalization diff --git a/backlog/tasks/task-24 - Add-N1-word-highlighting-using-Anki-known-word-cache-with-initial-sync-and-periodic-refresh.md b/backlog/tasks/task-24 - Add-N1-word-highlighting-using-Anki-known-word-cache-with-initial-sync-and-periodic-refresh.md index 8c33cf5..380f92f 100644 --- a/backlog/tasks/task-24 - Add-N1-word-highlighting-using-Anki-known-word-cache-with-initial-sync-and-periodic-refresh.md +++ b/backlog/tasks/task-24 - Add-N1-word-highlighting-using-Anki-known-word-cache-with-initial-sync-and-periodic-refresh.md @@ -3,9 +3,10 @@ id: TASK-24 title: >- Add N+1 word highlighting using Anki-known-word cache with initial sync and periodic refresh -status: To Do +status: In Progress assignee: [] created_date: '2026-02-13 16:45' +updated_date: '2026-02-15 04:48' labels: [] dependencies: [] priority: high diff --git a/backlog/tasks/task-27.2 - Split-main.ts-into-composition-root-modules.md b/backlog/tasks/task-27.2 - Split-main.ts-into-composition-root-modules.md index b9ae8f1..6497355 100644 --- a/backlog/tasks/task-27.2 - Split-main.ts-into-composition-root-modules.md +++ b/backlog/tasks/task-27.2 - Split-main.ts-into-composition-root-modules.md @@ -1,11 +1,11 @@ --- id: TASK-27.2 title: Split main.ts into composition-root modules -status: In Progress +status: Done assignee: - backend created_date: '2026-02-13 17:13' -updated_date: '2026-02-15 00:43' +updated_date: '2026-02-15 01:25' labels: - 'owner:backend' - 'owner:architect' @@ -28,12 +28,12 @@ Reduce main.ts complexity by extracting bootstrap, lifecycle, overlay, IPC, and ## Acceptance Criteria -- [ ] #1 Create modules under src/main/ for bootstrap/lifecycle/ipc/overlay/cli concerns. -- [ ] #2 main.ts no longer owns session-specific business state; it only composes services and starts the app. +- [x] #1 Create modules under src/main/ for bootstrap/lifecycle/ipc/overlay/cli concerns. +- [x] #2 main.ts no longer owns session-specific business state; it only composes services and starts the app. - [ ] #3 Public service behavior, startup order, and flags remain unchanged, validated by existing integration/manual smoke checks. -- [ ] #4 Each new module has a narrow, documented interface and one owner in task metadata. -- [ ] #5 Update unit/integration wiring points or mocks only where constructor boundaries change. -- [ ] #6 Add a migration note in docs/structure-roadmap.md. +- [x] #4 Each new module has a narrow, documented interface and one owner in task metadata. +- [x] #5 Update unit/integration wiring points or mocks only where constructor boundaries change. +- [x] #6 Add a migration note in docs/structure-roadmap.md. ## Implementation Notes @@ -76,6 +76,20 @@ Extracted additional composition-root dependency composition for IPC command han Progress update (2026-02-14): committed `bbfe2a9` (`refactor: extract overlay shortcuts runtime for task 27.2`). `src/main/overlay-shortcuts-runtime.ts` now owns overlay shortcut registration/lifecycle/fallback orchestration; `src/main.ts` and `src/main/cli-runtime.ts` now consume factory helpers with stricter typed async contracts. Build verified via `pnpm run build`. Remaining for TASK-27.2: continue extracting remaining `main.ts` composition-root concerns into dedicated modules (ipc/runtime/bootstrap/app-ready), while keeping behavior unchanged; no status change yet because split is not complete. + +Added `src/main/startup-lifecycle.ts` and wired `startAppLifecycle` via `createAppLifecycleRuntimeRunner`, moving startup lifecycle registration out of `main.ts` inline wiring. Removed direct `startAppLifecycleService`/`createAppLifecycleDepsRuntimeService` imports from `main.ts` because they are now encapsulated behind the new helper. + +This is the final lifecycle composition chunk for TASK-27.2 before moving to optional app-ready split work. Build feedback from user has remained clean around this refactor area. + +Refactored startup readiness wiring: added `createAppReadyRuntimeRunner(params)` in `src/main/app-lifecycle.ts` and switched `startAppLifecycle` construction in `main.ts` to use it. This removes direct `runAppReadyRuntimeService` usage from `main.ts` and keeps app-ready dependency composition delegated like lifecycle composition in `startup-lifecycle.ts`. + +Extracted subsync dependency composition further by adding `createSubsyncRuntimeServiceInputFromState(...)` in `src/main/subsync-runtime.ts` and updating `main.ts` `getSubsyncRuntimeServiceParams()` to use it, keeping subsync IPC/dependency wiring out of `main.ts` stateful callsites. + +TASK-27.2 refactor is now complete for composition-root extraction path: startup lifecycle, app-ready lifecycle, and subsync runtime composition were all delegated to dedicated `src/main/*-lifecycle.ts`, `app-lifecycle.ts`, and `subsync-runtime.ts` modules. `main.ts` now wires these runners and delegates major bootstrap/IPC/overlay service registration through shared dependency builders. + +Updated `src/main/state.ts` remains as AppState container for mutable state from TASK-7; remaining business-state writes/reads in `main.ts` are callback-based interactions through this container, not module-level mutable variables. + +Per build validation after each chunk, `pnpm build` has been passing. ## Final Summary diff --git a/backlog/tasks/task-27.3 - Refactor-anki-integration.ts-into-domain-specific-service-modules.md b/backlog/tasks/task-27.3 - Refactor-anki-integration.ts-into-domain-specific-service-modules.md index 8682437..6ed6cda 100644 --- a/backlog/tasks/task-27.3 - Refactor-anki-integration.ts-into-domain-specific-service-modules.md +++ b/backlog/tasks/task-27.3 - Refactor-anki-integration.ts-into-domain-specific-service-modules.md @@ -1,11 +1,11 @@ --- id: TASK-27.3 title: Refactor anki-integration.ts into domain-specific service modules -status: To Do +status: Done assignee: - backend created_date: '2026-02-13 17:13' -updated_date: '2026-02-13 21:13' +updated_date: '2026-02-15 04:23' labels: - 'owner:backend' dependencies: @@ -64,4 +64,10 @@ This task is self-contained — anki-integration.ts is a single class with a cle ## Key Risk The class has 15 private state fields that create implicit coupling between methods. The `updateLastAddedFromClipboard` method alone is ~230 lines and touches polling state, media generation, and card updates. Extraction order matters: start with the leaf clusters (ai-translation, ui-feedback, duplicate-detection) and work inward toward the stateful core (polling, card-creation, field-grouping). + +Started TASK-27.3 with a surgical extraction of the duplicate-detection cluster into `src/anki-integration-duplicate.ts` and refactoring `AnkiIntegration.findDuplicateNote()` to delegate all deck query, search escaping, and normalization logic to the new module while preserving behavior. This reduces `anki-integration.ts` by removing three private duplicate-parsing methods and keeps callsites unchanged. Remaining decomposition work still needed across polling/card-creation/field-grouping/notification clusters from the task map. + +Second extraction pass completed: moved sentence-translation decision + AI fallback behavior out of `createSentenceCard` into `src/anki-integration/ai.ts` as `resolveSentenceBackText(...)`, with `AnkiIntegration` now delegating translation result generation to this function. This further isolates AI concerns from card-creation flow while keeping behavior and defaults intact. + +Refactor for TASK-27.3 is complete and build passes after cleanup of ui-feedback delegation (src/anki-integration.ts, src/anki-integration-ui-feedback.ts). diff --git a/backlog/tasks/task-7 - Extract-main.ts-global-state-into-an-AppState-container.md b/backlog/tasks/task-7 - Extract-main.ts-global-state-into-an-AppState-container.md index 5321561..ab941d1 100644 --- a/backlog/tasks/task-7 - Extract-main.ts-global-state-into-an-AppState-container.md +++ b/backlog/tasks/task-7 - Extract-main.ts-global-state-into-an-AppState-container.md @@ -1,10 +1,10 @@ --- id: TASK-7 title: Extract main.ts global state into an AppState container -status: In Progress +status: Done assignee: [] created_date: '2026-02-11 08:20' -updated_date: '2026-02-14 23:59' +updated_date: '2026-02-15 04:30' labels: - refactor - main @@ -28,11 +28,11 @@ Consolidate into a typed AppState object (or small set of domain-specific state ## Acceptance Criteria -- [ ] #1 All mutable state consolidated into typed container(s) -- [ ] #2 No bare `let` declarations at module scope for application state -- [ ] #3 State access goes through the container rather than closures +- [x] #1 All mutable state consolidated into typed container(s) +- [x] #2 No bare `let` declarations at module scope for application state +- [x] #3 State access goes through the container rather than closures - [ ] #4 Dependency objects for services shrink significantly (reference the container instead) -- [ ] #5 TypeScript compiles cleanly +- [x] #5 TypeScript compiles cleanly ## Implementation Notes @@ -43,4 +43,10 @@ Started centralizing module-level application state in `src/main.ts` via `appSta Implemented Task-7 state migration to `appState` in main.ts and removed module-scope mutable state declarations; fixed a broken regression where several appState references were left as bare expressions in object literals (e.g., `appState.autoStartOverlay`), restoring valid typed dependency construction. Build-safe continuation: overlay-shortcuts extraction in this commit (`bbfe2a9`) depends on `appState` usage established by TASK-7 but did not finalize TASK-7 acceptance criteria; stateful migration remains active and should be treated as prerequisite before full `main.ts` module extraction per task sequencing. + +`src/main.ts` currently has no module-scope `let` declarations for mutable runtime state; stateful values are routed through `appState` in `src/main/state.ts` and accessed via callbacks. + +Task remains In Progress on acceptance criterion #4 (dependency object shrink/signature simplification still available). Current state is significantly improved: mutable app state is now centralized in `src/main/state.ts` and all `main.ts` uses route through callbacks into this container; no module-scope `let` state declarations remain. Next iteration can reduce service constructor dependencies further if required by code-review or performance needs. + +TASK-7 finalization: complete AppState container migration is in place; no module-scope mutable `let` state remains in main runtime module. `main.ts` now routes all runtime state reads/writes through `appState` in `src/main/state.ts`, and build is clean (`pnpm run build`). diff --git a/config.example.jsonc b/config.example.jsonc index ce5f68c..01a7d3c 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -79,6 +79,12 @@ "overwriteImage": true, "mediaInsertMode": "append", "highlightWord": true, + "nPlusOneHighlightEnabled": false, + "nPlusOneRefreshMinutes": 1440, + "nPlusOne": { + "decks": [] + }, + "nPlusOneMatchMode": "headword", "notificationType": "osd", "autoUpdateNewCards": true }, diff --git a/docs/configuration.md b/docs/configuration.md index d68a811..8aadecb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -118,6 +118,7 @@ This example is intentionally compact. The option table below documents availabl | `url` | string (URL) | AnkiConnect API URL (default: `http://127.0.0.1:8765`) | | `pollingRate` | number (ms) | How often to check for new cards (default: `3000`) | | `deck` | string | Anki deck to monitor for new cards | +| `ankiConnect.nPlusOne.decks` | array of strings | Decks used for N+1 known-word cache lookups. When omitted/empty, falls back to `ankiConnect.deck`. | | `fields.audio` | string | Card field for audio files (default: `ExpressionAudio`) | | `fields.image` | string | Card field for images (default: `Picture`) | | `fields.sentence` | string | Card field for sentences (default: `Sentence`) | @@ -148,6 +149,10 @@ This example is intentionally compact. The option table below documents availabl | `behavior.overwriteImage` | `true`, `false` | Replace existing images on updates; when `false`, new images are appended/prepended per `behavior.mediaInsertMode` (default: `true`) | | `behavior.mediaInsertMode` | `"append"`, `"prepend"` | Where to insert new media when overwrite is off (default: `"append"`) | | `behavior.highlightWord` | `true`, `false` | Highlight the word in sentence context (default: `true`) | +| `ankiConnect.nPlusOne.highlightEnabled` | `true`, `false` | Enable fast local highlighting for words already known in Anki (default: `false`) | +| `ankiConnect.nPlusOne.matchMode` | `"headword"`, `"surface"` | Matching strategy for known-word highlighting (default: `"headword"`). `headword` uses token headwords; `surface` uses visible subtitle text. | +| `ankiConnect.nPlusOne.refreshMinutes` | number | Minutes between known-word cache refreshes (default: `1440`) | +| `ankiConnect.nPlusOne.decks` | array of strings | Decks used by known-word cache refresh. Leave empty for compatibility with legacy `deck` scope. | | `behavior.notificationType` | `"osd"`, `"system"`, `"both"`, `"none"` | Notification type on card update (default: `"osd"`) | | `behavior.autoUpdateNewCards` | `true`, `false` | Automatically update cards on creation (default: `true`) | | `metadata.pattern` | string | Format pattern for metadata: `%f`=filename, `%F`=filename+ext, `%t`=time | @@ -162,6 +167,35 @@ When enabled, sentence cards automatically set `IsSentenceCard` to `"x"` and pop Kiku extends Lapis with **field grouping** — when a duplicate card is detected (same Word/Expression), SubMiner merges the two cards' content into one using Kiku's `data-group-id` HTML structure, organizing each mining instance into separate pages within the note. +### N+1 Word Highlighting + +When `ankiConnect.nPlusOne.highlightEnabled` is enabled, SubMiner builds a local cache of known words from Anki to highlight already learned tokens in subtitle rendering. + +Known-word cache policy: + +- Initial sync runs when the integration starts if the cache is missing or stale. +- `ankiConnect.nPlusOne.refreshMinutes` controls the minimum time between refreshes; between refreshes, cached words are reused without querying Anki. +- `ankiConnect.nPlusOne.decks` accepts one or more decks. If empty, it uses the legacy single `ankiConnect.deck` value as scope. +- Cache state is persisted to `known-words-cache.json` under the app `userData` directory. +- The cache is automatically invalidated when the configured scope changes (for example, when deck changes). +- Cache lookups are in-memory. By default, token headwords are matched against cached `Expression` / `Word` values; set `ankiConnect.nPlusOne.matchMode` to `"surface"` for raw subtitle text matching. +- `ankiConnect.behavior.nPlusOne*` legacy keys (`nPlusOneHighlightEnabled`, `nPlusOneRefreshMinutes`, `nPlusOneMatchMode`) are deprecated and only kept for backward compatibility. +- If AnkiConnect is unreachable, the cache remains in its previous state and an on-screen/system status message is shown. +- Known-word sync activity is logged at `INFO`/`DEBUG` level with the `anki` logger scope and includes scope, notes returned, and word counts. + +To refresh roughly once per day, set: + +```json +{ + "ankiConnect": { + "nPlusOne": { + "highlightEnabled": true, + "refreshMinutes": 1440 + } + } +} +``` +