Skip to content

refactor: tighten the seams left after #211 (store/templates/transcription/settings) - #212

Merged
chhoumann merged 5 commits into
masterfrom
chhoumann/refactor-architecture
Jun 21, 2026
Merged

chhoumann merged 5 commits into
masterfrom
chhoumann/refactor-architecture

Conversation

@chhoumann

@chhoumann chhoumann commented Jun 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

A second, behavior-preserving architecture pass that finishes the seams the #211 god-module split left behind. No user-facing feature change; net −103 LOC across 10 files. refactor: subject → no semantic-release version bump.

Five focused, independently-gated steps:

  • refactor(store) — keep vault file I/O out of the downloads store. downloadedEpisodes.removeEpisode deleted a vault file through a global app behind the only production @ts-ignore. The store is the pure state core, so it now just removes the entry and the file deletion lives in the download module — the same "separate the side effect from the state mutation" move refactor: declarative persistence + split store/main god-modules #211 applied to the queue.
  • refactor(templates) — dedupe the shared file-name tag registration. FilePath/DownloadPath/Transcript (and FeedFilePath) engines repeated an identical {{title}}/{{podcast}}/{{date}}/{{currentdate}}/{{episodenumber}} block; extracted addEpisodeFileNameTags + legalizedNameTag. NoteTemplateEngine is deliberately untouched (its {{title}} is the raw episode title, not a file name).
  • refactor(transcripts) — extract pure WAV chunking into src/services/audioChunker.ts, shrinking TranscriptionService 631 → 400 LOC. The chunker tests previously kept hand-maintained copies of these functions (and reached createChunkFiles via an as unknown as cast); they now exercise the real exports and can no longer drift.
  • refactor(settings) — factor out repeated settings-tab boilerplate: stackSettingVertically (replaces a 7×-copied settingEl.style triplet) and renderMarkdownPreview (replaces a 4×-copied empty() + MarkdownRenderer block); OPML import now reuses the existing pickFile helper.
  • refactor — tighten review findings: after an adversarial review (3 reviewers on the opposite model), made pickFile's size cap/message per-caller so OPML import no longer shows a settings-file error, and added a composed removeDownloadedEpisode() so the remove-then-delete invariant can't be half-applied (deleteEpisodeFile is now module-private).

Behavioral impact

Behavior-preserving. The only observable deltas are improvements: OPML import shows a correct "too large to be an OPML file" message (it previously would have shown a settings-file message under the shared helper), and removing a downloaded episode is now leak-safe by construction.

Verification

Commands (Node 22): npm run typecheck, npm run lint, npm run check:a11y, npm run build, npx vitest run — all green, 657 unit tests (58 files), after every step.

Obsidian runtime verification (isolated per-worktree vault): plugin loads with zero console errors, 19 commands registered, player view opens, transcribe command degrades gracefully with no key/episode, settings tab renders with all 7 stacked layouts applied and the OPML Import button present.

Real-app e2e suite (tests/e2e/podnotes-runtime.test.ts): the two segment-URI playback tests ("No PodNotes audio element found") flaked non-deterministically (9/11 then 10/11 across identical runs) — a known audio-element-mount race. This diff touches none of the player/audio/URI/segment code paths, and all deterministic unit coverage of that logic passes.

Adversarial review (3 reviewers): verdict PASS, no high-severity findings. Two medium findings (OPML picker policy leak, shallow download-remove seam) and one low (comment trim) were actioned in the final commit; the rest were deliberate, documented tradeoffs (test-only chunker exports that eliminate the test copies; a pre-existing deprecated-renderMarkdown pattern left as-is to stay behavior-preserving).

I'm not merging this — leaving it for maintainer review.


Open in Devin Review

downloadedEpisodes.removeEpisode deleted the backing vault file via a
global `app` reference behind the only production `@ts-ignore`, mixing
file I/O into the pure state core. Make removeEpisode return the removed
file's path and move the deletion to deleteEpisodeFile() in the download
module (symmetric with createEpisodeFile, where the vault/app access
already lives). Same separation #211 applied to the queue automation.

The sole caller (spawnEpisodeContextMenu) deletes the returned path.
deleteEpisodeFile now awaits the delete inside its own try/catch, so an
async deletion failure is caught instead of becoming an unhandled
rejection. Adds a unit test now that the store op is pure.
FilePathTemplateEngine, DownloadPathTemplateEngine, and the first five
tags of TranscriptTemplateEngine registered a byte-identical block of
{{title}}/{{podcast}}/{{date}}/{{currentdate}}/{{episodenumber}} tags.
Extract addEpisodeFileNameTags(addTag, episode) plus a legalizedNameTag
helper; FeedFilePathTemplateEngine reuses the latter for its own
identical name closure. NoteTemplateEngine is intentionally untouched —
its {{title}} is the raw episode title, not a file name.

Behavior-preserving: -50 net LOC, all 50 TemplateEngine tests pass.
…rvice

The service mixed ~230 lines of pure audio work — chunk sizing, m4a->WAV
decode/re-encode, and WAV header/PCM byte writing — into a class otherwise
concerned with the queue, OpenAI client, diarization routing, and saving.
Move it to a standalone src/services/audioChunker.ts of pure functions
(createChunkFiles/getMimeType/...), shrinking the class 631->400 LOC.

The chunker tests previously kept hand-maintained COPIES of getMimeType,
shouldConvertToWav, createBinaryChunkFiles, and writeWavHeader, and reached
createChunkFiles via an `as unknown as` cast on a live instance. They now
live in audioChunker.test.ts and exercise the real exported functions, so
they can no longer silently drift from production. No behavior change.
The settings tab repeated three patterns: a 3-line `settingEl.style`
column-layout triplet (7 copies), an `el.empty()` + MarkdownRenderer
demo-render block (4 copies), and a hand-rolled OPML file picker that
duplicated the existing pickFile() helper. Extract stackSettingVertically()
and renderMarkdownPreview(), and route OPML import through pickFile().

Net -36 LOC. OPML import now also gets pickFile()'s niceties — it cleans
up its orphaned <input> element, handles cancel, and applies a sanity
size cap — instead of leaking the input. No change to the happy-path flow.
Adversarial review (3 Codex reviewers) flagged two behavior/seam issues:

- OPML import routed through pickFile() inherited the settings-import 5MB
  cap AND its "too large to be a PodNotes settings file" message. Make the
  cap and message per-caller (maxBytes/tooLargeMessage options) and give
  OPML its own copy — keeps the memory-safety cap, drops the wrong text.

- M1's remove-download seam was shallow: a caller had to remember to delete
  the file after removeEpisode() or leak it. Add a composed
  removeDownloadedEpisode() that owns remove-then-delete and make
  deleteEpisodeFile module-private (symmetric with createEpisodeFile). The
  store stays pure; the context menu calls one function.

Also trimmed the M1 doc comments. Happy-path behavior unchanged; gates
green, 657 tests.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: d85e7aa
Status: ✅  Deploy successful!
Preview URL: https://ffab15ca.podnotes.pages.dev
Branch Preview URL: https://chhoumann-refactor-architect.podnotes.pages.dev

View logs

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@chhoumann
chhoumann merged commit 73c0067 into master Jun 21, 2026
2 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.17.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@chhoumann
chhoumann deleted the chhoumann/refactor-architecture branch June 29, 2026 05:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant