Skip to content

Refactor/huge simplify - #415

Merged
i-xtsu-sixyou-ken-mei merged 59 commits into
mainfrom
refactor/huge-simplify
Sep 8, 2026
Merged

Refactor/huge simplify#415
i-xtsu-sixyou-ken-mei merged 59 commits into
mainfrom
refactor/huge-simplify

Conversation

@i-xtsu-sixyou-ken-mei

Copy link
Copy Markdown
Collaborator

Intent

Context

Scope

Out of scope

Product contract / invariants

  • This PR does not change a documented product/architecture invariant.
  • This PR intentionally changes an invariant with explicit product approval.

Affected invariant:

Why:

Acceptance criteria

  • [ ]

Implementation

Contract alignment

  • Implementation matches the invariant.
  • Regression / contract tests cover it.
  • Scoped AGENTS.md remains accurate.
  • README / runbook remains accurate.

Validation

  • ... — PASS

Validation gaps

Known unrelated failures

Reviewer notes

i-xtsu-sixyou-ken-mei and others added 30 commits September 7, 2026 14:56
…e_feed_page_v1

Delete the legacy multi-request feed fallback (listEpisodeFeedPaged, its
column projection, and the isMissingFeedRpcError/NODE_ENV=test seams);
GET /episodes now always reads from listHydratedEpisodeFeedPage, which
throws on a real RPC failure instead of silently degrading.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…adations

The record_episode_video_visual_failure_diagnostics, mark_episode_video_completion_notified,
mark_episode_video_visual_failure_notified, and retry-RPC migrations are confirmed applied in
prod, so the "missing migration" degradation branches (isMissingSupabaseRpc, the telegram
'unavailable' outcome/copy, isTelegramRetryMigrationMissing) are no longer reachable and are
removed along with their rollout-compatibility tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s env keys

Storyboard scene-splitting always uses the deterministic provider now;
VIDEO_STORYBOARD_PROVIDER and NVIDIA_STORYBOARD_MODEL are dropped from the
manifest and both env files (NVIDIA_API_KEY/NVIDIA_BASE_URL stay, used by
scene-alignment). Removed the nvidia storyboard test suite and the
'unsupported provider' branch it guarded in
episode-video-visual-processor.test.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…cape renderer

Only podcast-slide-video.v4 manifests exist now: manifest.ts drops the
legacy landscape/v1/v2/v3 schemas, asset kinds (none, bundledMap), and
the fullBleed license gate; renderer.ts, ffmpeg-video.ts, templates.tsx,
satori-stage.ts, sharp-stage.ts, and rasterizer.ts drop the static
slide-video render path (buildStaticSlideFilter, renderStaticSlideVideo,
rasterizeSlide, the legacy Cover/PhotoFact/Statistic/Document/SourceQuote
templates, and the bundled US-states map asset) since every render now
goes through the vertical pipeline. subtitles.ts's portraitSubtitleLayoutFor
no longer branches on a 1080x1920 legacy clip size, since that shape can
no longer parse.

Tests are updated to match: legacy-manifest and static-path fixtures/tests
are removed, and the handful of tests that exercised the same behavior
through the vertical path (renderer.test.ts, ffmpeg-video-v8.test.ts,
manifest.test.ts's contiguous-slide/asset-source/caption-bounds/schema
cases) are re-pointed at vertical fixtures instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
remoteImageAssetSchema's `motion` field is now required (every completed
v10 visual payload already carries it), so ffmpeg-video.ts no longer needs
a legacy branch: legacyKenBurnsFilter, editorialMotionOf, and the
kenBurnsFilter dispatcher are deleted, editorialKenBurnsFilter is renamed
to stillFrameFilter (the only still-frame path left), imagePreparationFilter
always takes the contain/pad branch, and transitionForSlide always
alternates smoothright/smoothleft instead of falling back to a crossfade.
kenBurnsSeedForEpisode/kenBurnsPanForScene/KEN_BURNS_MOTIONS stay, since the
editorial drift filter still uses them for pan direction.

ffmpeg-video.test.ts drops the legacy-path and legacy-zoom-formula tests and
adds `motion` to its manifest fixture; manifest.test.ts, renderer.test.ts,
episode-video.test.ts, episode-video-processor.test.ts, assets.test.ts, and
storyboard.test.ts each gain `motion` on their remoteImage fixtures to
satisfy the now-required field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
These process/CLI entry points are already package.json scripts, which
knip discovers on its own; only the two `node --import` preload targets
need an explicit entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n frame math

escapeFilterPath was byte-identical in ffmpeg-video.ts and runtime-preflight.ts;
moved it to a shared src/lib/ffmpeg-filter-path.ts. The still-frame and
editorial-drift filters in ffmpeg-video.ts also computed the same
duration-in-frames expression independently; both now call one
slideDurationFrames helper. Pre-existing jscpd clones (predating this
sequence's Phase A1 commits), surfaced by the phase's aggregate dup:check gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Concept-card copy now reads its OpenRouter completion cost through the
shared completionMetadata() helper (usage.cost), the same path every
other LLM ledger line uses, instead of a bespoke completion.costUsd
field the client never populates. Unreported cost now records as 0
instead of null, matching every other ledger line; slide-copy.test.ts
fixtures and assertions move to usage.cost accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LANGUAGE_CLASSROOM_LANGUAGE_CODES is now a re-export of
SUPPORTED_PRIMARY_LANGUAGE_CODES (kept as a distinct name via a knip @alias
tag for an upcoming cross-package swap), and the single
isLanguageClassroomLanguageCode guard now lives in classroom-language.ts.
video-jobs.ts and video-completion-notifier.ts both import it instead of
each rolling their own membership check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tIntEnv

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d key reads

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace sentry.ts's local trimToUndefined with lib/typeGuards.ts's
nonemptyString, and supabase-client.ts's local readOptionalString with
lib/string.ts's readNullableString (error-message formatting only).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
render-capacity's selectRows and loadOptionalVisualFailureNotices now
throw through the shared throwSupabaseError helper instead of a local
supabaseErrorMessage formatter, restoring the supabaseError property
isTransientNetworkError walks. The message-less-error fallback text in
render-capacity.test.ts changes accordingly to match
formatSupabaseError's output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…l loops

Extract the single-flight, immediate-first-run, unref'd setInterval lifecycle
shared by render-capacity's reconciler and the two Telegram reap-and-notify
sweepers (video completion, video visual failure) into
src/lib/polling-sweeper.ts (createPollingSweeper, plus createSweepNotifier for
the two notifiers' start/sweep/stop shape). render-capacity.ts keeps its own
runOnce export and its start/stop logging; the two notifiers collapse to a
few lines each. Deleted the old jscpd:ignore pairs that wrapped the duplicated
timer state machine in both notifier files; a much smaller jscpd:ignore
remains around the still-parallel default-notify/logger/interval wiring in
each factory, since that thin adapter code is genuinely irreducible between
the two notifiers' differing notify/logger types.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tatus guard

src/services/video-progress.ts is now the single source for the public
video-generation status whitelist and the completed-video asset check.
db.ts, episode-feed-page.ts, and video-status.ts each delegate to it instead
of repeating the guard and the URL/thumbnail/duration validation; the
episode-feed-page.ts jscpd:ignore block covering that duplication is removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract the `[video-worker] event k=v ...` formatter into
src/services/video/log.ts (logVideoWorkerEvent), and have the render
processor, the visual processor's logVisualProgress wrapper, and the
worker's own lifecycle log lines call it instead of each formatting the
line inline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…effort call

Extract buildTelegramWarning(title, detail, footer) for the three
title/reason/footer Telegram warning messages, and
bestEffortTelegramCall(operation, fn) for the identical
log-and-Sentry-capture wrapper shared by sendTelegramNotification and
answerTelegramCallbackQuery. Output text is byte-identical; covered by
the existing telegram.test.ts assertions with no test changes needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…o lib

Move src/services/video/abort.ts (and its test) to src/lib/abort.ts and
update every importer's path. Convert the narration-download deadline in
episode-video-processor.ts and the remote-image download deadline in
assets.ts to the shared runWithDeadline helper; leave the render deadline
in episode-video-processor.ts and the OpenRouter request deadline in
llm.ts on their existing combineAbortSignalWithTimeout pattern, since both
need their own finally/catch handling around the race. Updated the
narration-download timeout test to match runWithDeadline's message
phrasing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract the settle-once/abort-with-grace-period pattern shared by
ffmpeg-video.ts's runProcess and rasterizer.ts's runRasterStage into
src/lib/spawn-process.ts (settleOnce, killOnAbort), dropping the
jscpd:ignore pair in ffmpeg-video.ts. rasterizer.ts keeps its own
`typeof child.kill !== 'function'` guard for test doubles that stub
only the EventEmitter surface.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…r for the CLIs

Move src/social/is-main-module.ts to src/lib/is-main-module.ts and adopt it
across every dev-CLI entry point, replacing each file's own
process.argv[1]/import.meta.url comparison (including two buggy
`file://${process.argv[1]}` forms in slide-preview-cli.ts and
review/cli.ts). Each try/catch wrapper now goes through the shared
lib/cli-runner.ts runCli(), which now logs the full error (keeping the
stack) instead of just its message, since this is dev-CLI-only output.

video/cli.ts and storyboard/smoke-cli.ts now build their flag parsing on
lib/cli-args.ts's parseFlagArgs, with a new assertOnlyKnownFlags() helper
(and its own test) factored out to keep the two parsers' shared
positional/unknown-flag validation from re-duplicating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…peline)

Add src/services/supabase-rows.ts (many/maybeOne/expectNoError) and use it to
replace the repeated `{ data, error } = await query; if (error) throw...`
boilerplate in db.ts, video-jobs.ts, ingest-jobs.ts, telegram-commands.ts,
video/review/review-store.ts, and render-capacity.ts (selectRows -> many).
No test files changed; existing mocks already return {data,error} thenables.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the repeated `{ data, error } = await query; if (error) throw...`
boilerplate in src/social/daemon-store.ts and release-cohort-store.ts with
the shared many()/maybeOne()/expectNoError() helpers from
services/supabase-rows.ts, including daemon-store.ts's local
unwrapCandidates() (now redundant with many()). affectedSocialPublishJobRow
(the CAS upsert/update helper) is left untouched, and
claimReleaseCohortJobs' response.data/response.error RPC unwrap is left as
is. No test files changed; existing mocks already return {data,error}
thenables.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…snapshot compat shape

daemon-store.ts drops latestPendingSocialPublishSchedule,
listPastDueSocialPublishJobs/PastDueSocialPublishJob,
rescheduleSocialPublishJob, and claimSocialPublishBatch (the daemon
claims through claimReleaseCohortJobs instead), none of which any
production caller reached. getSocialQueueSnapshot's queue snapshot
drops the nextByPlatform compat field and the withQueueLanes
non-enumerable-property trick in favor of a plain object exposing
nextByLane/waitingVideos/laneCount/lanes directly; daemon.ts's queue
logging reads those fields without the old fallback.

Touches fixture shape only (nextByPlatform -> nextByLane, added
laneCount/lanes/waitingVideos) in the guarded
daemon-release-cohort-contract.test.ts, daemon.test.ts, and
daemon-store-queue-snapshot-timing.test.ts; deletes
daemon-store-latest-pending-schedule.test.ts and the corresponding
cases in daemon-store.test.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tput

recoveryEpisodes was threaded from planPendingSocialReleaseCohorts through
alignPendingSocialReleaseCohorts's return value, but daemon.ts never reads
it (only alignedLanes/rescheduledEpisodes drive the log line). Removed the
field from ReleaseCohortPlan and ReleaseCohortAlignmentResult and updated
the fixtures/assertions that mirrored its shape, including the guarded
release-cohort-store.test.ts (fixture-shape only: two toEqual assertions
dropped the recoveryEpisodes key, no new behavior asserted).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Route the daemon's platform/language log lines through log-format.js's
platformLabel/languageLabel (account-snapshots.ts, rolling-metrics.ts,
daemon.ts) instead of composing icon+name inline, and extract the four
identical cohort-not-ready log blocks in daemon.ts into logCohortNotReady().
Add a shared jst.ts (JST_OFFSET_MS) used by slot-policy.ts,
language-allocation.ts, release-cohort-plan.ts, and daemon.ts's formatJst;
export metric-collectors.ts's EMPTY_COUNTS for daemon.ts's unavailable-metric
write. Output is byte-identical to before.

daemon-metric-terminal.test.ts's metric-collectors.js factory mock gains
EMPTY_COUNTS (fixture-shape only) so the new import resolves under mock.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add publishStep(platform) in publish-error.ts and reuse it as each
publisher's per-step SocialPublishError wrapper (threads/x/rednote's
identical local helpers, and youtube's four try/catch blocks). Add
requireVideoPath(platform, input) in publishers.ts to replace the three
duplicated "select video path or throw" blocks, preserving each
platform's exact error message. createSocialPublishJobs drops its
now-unnecessary async, so its one caller (publish-batch.ts) and
publishers.test.ts's direct-throw assertions were updated from
promise-rejection to synchronous-throw form to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lpers; drop the threads-auth jscpd exclusion

Extract the Threads/YouTube OAuth session read-parse-or-invalid and
atomic-write logic into a new src/social/session-file.ts
(readJsonSessionFile, writeJsonSessionFileAtomically), and move the shared
*AuthOptions fields and authorization-URL query params into
src/social/oauth-loopback.ts (OAuthLoopbackAuthOptions,
AuthorizationUrlInput, applyAuthorizationParams). Drop the
**/social/threads-auth.ts jscpd exclusion now that jscpd reports zero
clones between threads-auth.ts and youtube-auth.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… post matching

Extract the shared nearest-timestamp-index scan used by
inspectXPublishedPostAt (X timeline) and closestRednoteCardToTimestamp
(Rednote manager cards) into one closestIndexByTimestamp helper.
Both call sites keep their own error messages and thresholds unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract the shared Chrome-launch call (system Chrome channel, fixed
1440x900 viewport, per-call headless) out of x-playwright.ts,
rednote-browser.ts, and metric-collectors.ts into a new
src/social/browser.ts, so each platform still owns its own
profile/navigation lifecycle but no longer repeats the launch options.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract the shared episode/video/teaser-asset preparation out of
cli.ts's loadSocialAssets and publish-batch.ts's publishSocialBatch
into a new src/social/prepare-batch-assets.ts, so the CLI's progress
log lines and the daemon's already-prepared-asset short-circuit both
route through one function instead of two near-identical copies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
i-xtsu-sixyou-ken-mei and others added 25 commits September 8, 2026 00:53
podcastPlayer.web.ts now returns createPodcastPlayerSnapshot(...) like the
native player, instead of hand-building the PodcastPlayer object (including
hasPreviousEpisode/hasNextEpisode and the finiteSeconds wrapping the helper
already does). Dropped the now-unused hasNextPodcastEpisode/
hasPreviousPodcastEpisode imports.

Tried lifting the surrounding jscpd:ignore block entirely; it exposed several
pre-existing native/web structural duplicates well beyond the useState twin
(cancelPendingHandoff, playEpisodeSection, skipToSection, etc.), so the
ignore stays at its original scope with only the return body's content
changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract the repeated Sentry.ErrorBoundary + QueryClientProvider wrapping
in AppProviderShell into a module-level ProviderChrome component instead
of duplicating the same JSX across the missing-Privy-config, wallet, and
non-wallet return branches.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract the sign-in prompt shell (ScreenScrollView + padded ConnectGateCard)
into components/connect/ConnectGatePage.tsx and use it from both
AuthenticatedRoute.tsx and AuthenticatedRoute.ios.tsx; the web-only
AccountUnavailableCard branch is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Share the AuthenticatedActionContextValue interface from
integration/authenticatedActionModel.ts between the web/native and iOS
providers, and have the iOS hook return a module-level PASSTHROUGH_ACTION
constant instead of recreating callbacks on every render.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract the duplicated net_portfolio_value/total_net_usd fallback from
useHomeData and usePortfolioData into portfolioMetrics.netPortfolioValueFrom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
toggle and playFromQueue shared an identical auth+gating block; both now
route through a single startAudio(episode, run) helper, letting the
jscpd:ignore markers around the duplicated block come off (0 clones).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
useInvest's amountUsd now reuses amountUsdFromInput from investAmountModel
instead of re-parsing the grouped input inline. The four draft setters that
only clear the frozen single-chain execution (amount, destination, base and
Arbitrum funding tokens) now go through a shared withFreezeClear helper;
setScope keeps its bespoke body since it also resets destination.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add formatOr(value, format, fallback='—') to lib/format.ts and reuse it
for the "no data yet" pattern in useDefaultStrategyBacktest,
usePortfolioData's numeric metric formatter, HlpPlanSummary's
usd6/address labels, DecisionPacketCard's evidence number formatter,
and simulationPreviewModel's address/integer formatters.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract AssetRow/AssetListSkeleton, PartialWalletWarning,
PortfolioImportState, StrategyStatusCard, and ActionButton (renamed
HomeActionButton) into components/home/, leaving HomeScreen.tsx as
composition + data wiring.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ChainKey, DemoAsset, MetricTone, ActivityKind/Status/Step, ActivityCategoryDelta/Flow,
ActivityWalletRef, ActivityEvent, ActivityGroup, and ActivityFilter move to
integration/activityTypes.ts; data/demo.ts keeps only the DEMO fixture and its
DemoData container type, importing the moved types where it needs them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Collapse pillarsFromTarget/allocationFromTarget and their empty-state
twins into one compositionRows(target, demoRows, isDemo, {valueKey,
round}) helper backed by a shared COMPOSITION_ROWS table, and drop the
usePortfolioData allocationColor palette (only ever called for
'Stables'). Strategy/Portfolio/AllocationBar now render
tokens.color.pillar.* / tokens.color.accent directly instead of a
'var(--x)' string resolved at render time, so lib/colors.ts and
resolveColor are unused and removed.

data/demo.ts now imports design-tokens, so tests/HomeScreen.test.tsx
defers its @/data/demo import the same way it already defers the
harness import (a static import ran the mocked
@zapengine/design-tokens/tokens factory before its own `harness`
binding initialized); tests/support/homeScreenHarness.tsx's
design-tokens mock gains the pillar swatch fixture-shape it was
missing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Activity groups now carry a stable bucket code ('today'/'week'/'earlier')
instead of a display string, moving Today/This week/Earlier translation to
the screen via activity.group.* keys. Deletes the now-orphaned
date.today/thisWeek/earlier dictionary entries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consolidate the ten identical `return null` iOS screen stubs (Home,
Activity, Portfolio, Send, Strategy, Wallets, HlpProgress, InvestAmount,
InvestProgress, InvestRoute) into a single shared PodcastOnlyStubScreen
in src/screens/podcastOnlyStub.tsx. Each `.ios.tsx` file now re-exports
it under its route's expected name, keeping the file itself in place so
route imports and knip's `.ios.{ts,tsx}` entry pattern still resolve.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Introduce appKeyValueStorage.{web,native,ts} as the single place the
key-value backend forks by platform; localeStorage.ts and
podcastStorage.ts become plain single files built on top of it, and
the four separate .web/.native store files are deleted. Extract the
duplicated write-serialization queue from podcastStorageCore.ts and
localeStorageCore.ts into a shared createSerializedWriter helper in
keyValueStorage.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wrap estimateTranscriptTiming's per-render call in useMemo, keyed on
the episode script and active clock duration, so segments are only
recomputed when those inputs change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consolidate EpisodeMediaPlayer's six mutable video-clock refs (time,
duration, playing, rate, last-persisted time, failure-handled) into a
single useRef<VideoClockState>. Memoize the media-tab availability,
classroom sections, video panel state, and video progress derivations
on the specific episode fields they read (hlsUrl, languageCode,
audioTracks, video, videoGeneration) instead of the whole episode
object, since mergeEpisodeProgress spreads a new episode object on
every position/listened update and would otherwise force a recompute
on every playback tick.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wire already-translated podcast.* keys into EpisodeMediaPlayer and
EpisodeDetailScreen (Video/Transcript/Language classroom/Back/playback
speed a11y/no-classroom/no-transcript/episode-unavailable), align
podcast.videoReady/videoUnavailable copy with what the UI already shows,
and reuse podcast.listened/podcast.unheard for the episode hero badge
instead of its own ad-hoc "Listened"/"Unplayed" strings.

Add a needsVerification entry to HomeScreen's PORTFOLIO_IMPORT_COPY
(home.etlNeedsVerificationTitle/Body, en values unchanged from the prior
inline strings, plus JA/zh-Hant) so the ownership-not-verified branch
goes through the same t()/copy table as the other ETL states.

Delete the 52 orphaned keys (common/podcast/home/strategy/portfolio
partials plus all of wallets.*/send.*/connect.*) from all three
dictionaries now that nothing references them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PodcastPlayerProvider now exposes a status context (everything except
currentTime/duration) alongside a clock context, so PodcastScreen (which
never reads the running position) subscribes only to the former and stops
re-rendering on every playback tick. usePodcastPlayer() keeps merging both
for existing consumers (EpisodeDetailScreen, PodcastProgressTracker) that
need the ticking clock. PodcastScreen isolates the one clock read behind a
small NowPlayingBarConnected wrapper so the now-playing bar still updates
live without pulling the rest of the screen along with it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract the shared queue-mutations/reduce-onto-stored-value/single-post-hydration-write
pattern out of usePodcastSpeedPreferences and PodcastProgressProvider into one hook,
useHydratedStore. useEpisodeSortDirection and ContentLanguageProvider write immediately
instead of merging, so they keep their own shape and are left untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The status memo's ~21-field object literal and its dependency array were
token-identical, tripping the repo's zero-tolerance jscpd gate. Split into a
data half and an actions half (mirroring this file's existing status/clock
split), combined by a two-dependency memo — same fine-grained per-field
dependency tracking as before, just no single list long enough to clone
against itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the shared podcast language vocabulary (PODCAST_LANGUAGE_CODES,
PodcastLanguageCode, DEFAULT_PODCAST_LANGUAGE_CODE, isPodcastLanguageCode,
PODCAST_LANGUAGE_LABELS) and the video-stage vocabulary
(PODCAST_VIDEO_VISUAL_STAGES / PODCAST_VIDEO_RENDER_STAGES /
PODCAST_VIDEO_PROGRESS_STAGES) to packages/types/src/shared/podcast.ts, a new
shared/errors.ts (errorMessage, toError), and a <= 0 fast path plus optional
AbortSignal support on shared/async.ts's sleep. All reachable from
shared/index.ts. podcast-pipeline, control-center, and the app will consume
this in follow-up commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
types.ts's SUPPORTED_PRIMARY_LANGUAGE_CODES/DEFAULT_LANGUAGE_CODE/PrimaryLanguageCode
now re-export @zapengine/types/shared's podcast language vocabulary instead of
duplicating the ['zh-Hant','ja','en'] literal; LANGUAGE_CLASSROOM_LANGUAGE_CODES
keeps its own distinct alias per the existing @alias note. video-progress.ts's
VISUAL_JOB_PROGRESS_STAGES/RENDER_JOB_PROGRESS_STAGES now re-export the shared
stage lists (migration-023 CHECK comment and STAGE_SPANS untouched).
lib/sleep.ts and lib/errorMessage.ts become thin re-exports of the shared
implementations. Remaining literal language-code guards in
telegram-commands.ts, social/cli.ts, and social/daemon-store.ts now read
SUPPORTED_PRIMARY_LANGUAGE_CODES instead of hardcoding the list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
server/services/podcast-pipeline.ts, pipeline-queues.ts,
shared/podcast-pipeline.ts, shared/podcast-visual.ts, and
client/components/episode-queue.ts now read @zapengine/types/shared's
PODCAST_LANGUAGE_CODES/PodcastLanguageCode instead of duplicating the
['zh-Hant','ja','en'] literal. PodcastUnitEconomics.tsx's language
ordering/labels derive from PODCAST_LANGUAGE_LABELS for the three
canonical codes, keeping an explicit local 'zh-Hans' for legacy cost
rows the shared vocabulary has no notion of. The three private
errorMessage copies (app.ts, register-podcast-abandon.ts,
operations/signal.ts) now consume @zapengine/types/shared's
errorMessage for the Error-instance case; the two route-handler
copies with PostgREST duck-typed message extraction are consolidated
into one postgrestErrorMessage helper in services/supabase.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
config/contentLanguages.ts's CONTENT_LANGUAGE_OPTIONS now sources its
badge/native labels from @zapengine/types/shared's PODCAST_LANGUAGE_LABELS
instead of duplicating the literal strings, keeping the app's own English-first
display order local. lib/intlDates.ts's CONTENT_LANGUAGE_LOCALES reads each
code's intlLocale from the same source. podcastFeed.ts's
PODCAST_VIDEO_GENERATION_STAGES/PodcastVideoGenerationStage now re-export the
shared PODCAST_VIDEO_PROGRESS_STAGES/PodcastVideoProgressStage stage union, and
its PodcastLanguageClassroomKeyword/PodcastLanguageClassroomLesson plain data
shapes are re-exported from a new shared/podcast.ts addition (mirroring the
pipeline's LanguageClassroomKeyword/LanguageClassroomLesson shape) rather than
being redefined locally; the two apps' classroom parsers stay separate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
zap-engine-control-center Skipped Skipped Sep 8, 2026 1:27am UTC

@vercel
vercel Bot temporarily deployed to Preview – zap-engine-control-center September 8, 2026 01:27 Inactive
@i-xtsu-sixyou-ken-mei
i-xtsu-sixyou-ken-mei merged commit 0afb94c into main Sep 8, 2026
2 checks passed
@i-xtsu-sixyou-ken-mei
i-xtsu-sixyou-ken-mei deleted the refactor/huge-simplify branch September 8, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant