Skip to content

Replace ad-hoc parsing with Zod schemas; dedupe shared helpers#4

Merged
willwashburn merged 2 commits into
mainfrom
claude/refactor-pear-codebase-Talve
May 21, 2026
Merged

Replace ad-hoc parsing with Zod schemas; dedupe shared helpers#4
willwashburn merged 2 commits into
mainfrom
claude/refactor-pear-codebase-Talve

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

Centralises validation of disk + IPC payloads in Zod schemas, shares project / broker‑event helpers between main and renderer, removes dead code and unsafe casts. Net ‑350 LOC with zero new as any / @ts-ignore, and 4 baseline tsc errors fixed.

Why

A deep read of pear surfaced repeated patterns that hurt readability and safety:

  • Project shape was hand‑validated in two placessrc/main/store.ts and src/renderer/src/stores/project-store.ts had ~110 lines each of nearly‑identical normalizeRoot / normalizeIntegration / normalizeProject helpers, all built on as Record<string, unknown> casts.
  • JSON.parse(...) as T everywhere in auth.ts, avatar-cache.ts, broker.ts — disk and network payloads getting trusted by type assertion.
  • localStorage.getItem(...) as Theme | null in ui-store.ts with no actual validation.
  • (await pear.git.status(path)) as FileStatus[] in git-store.ts because the IPC return type was loose.
  • as Promise<T> sprinkled across preload/index.ts.
  • Dead pear.broker.sendInput — preload exposed it, IPC handler existed, renderer never called it (all input goes through sendInputFast).
  • Duplicate helpersformatTime in ChatMessage.tsx and ThreadPanel.tsx; compactBrokerEvent and normalizeEventTimestamp in both main/broker.ts and renderer/stores/agent-store.ts.

What changed

New shared modules

  • src/shared/schemas/project.ts — single source of truth for Project / ProjectRoot / ProjectIntegration. makeProjectSchema(rootSchema) lets each process specialize the per‑root shape (main stores {id,name,path}; renderer enriches with pathExists).
  • src/main/schemas.ts — Zod schemas for StoredTokens, AuthMeta, UserInfo, broker connection.json, generated commit drafts, and the avatar cache manifest.
  • src/shared/lib/broker-events.ts — shared compactBrokerEvent + normalizeEventTimestamp.
  • format.tsformatClockTime, formatRelativeShort (replaces duplicated formatters in chat components).

Both tsconfigs and electron.vite.config.ts were updated with a @shared/* path so main and renderer can both import the shared code.

Unsafe casts removed

  • ui-store.ts: as Theme | nullz.enum([...]).safeParse(localStorage.getItem(...)).
  • git-store.ts: narrowed the IPC return type via a new GitFileStatus interface in lib/ipc.ts; the as FileStatus[] cast is gone.
  • preload/index.ts: 4× as Promise<T> collapsed into one generic invoke<T>(channel, ...args) helper.
  • broker.ts: two (err as { status?: unknown }).status casts → one getErrorStatus helper; event.name / event.from access on a discriminated union narrowed with the in operator instead of casting.

Dead code removed

  • pear.broker.sendInput (preload + IPC handler + PearAPI type entry) — never called.
  • normalizeGeneratedCommitDraft, compactEventText, plus dup helpers folded into the shared module.

Bonus baseline cleanups

  • AppTopBar.tsx: added missing 'dm' branch so the AppTabKind switch is exhaustive (fixes TS2366).
  • broker.ts: narrowed this.sessions.values().next().value so getSessionForAgent returns BrokerSession, not BrokerSession | undefined (fixes TS2322).
  • .gitignore: ignore *.tsbuildinfo.

Verification

  • npx tsc -b → 4 baseline errors fewer than main, 0 new errors introduced.
  • npm run build → clean (✓ built in 7.24s).
  • No @ts-ignore / @ts-expect-error / as any exist in the tree (was already true; preserved).
  • Channel name normalization regex preserved verbatim, spot‑checked against 'café', '#general', etc.

Test plan

  • npm run dev — open app, load an existing project, confirm projects still parse from projects.json.
  • Add a new project; create / rename / delete a channel; confirm channel people round‑trips.
  • Sign out and back in; confirm auth.json decrypts and whoami populates the avatar.
  • Open Agent Relay Status; confirm broker events list and connection details still render.
  • Source Control: stage, generate commit message, commit; confirm draft parses out of agent output.
  • Theme + terminal layout: change in UI, reload, confirm persisted value picked up; corrupt the localStorage value manually and confirm the schema falls back to defaults instead of breaking.

🤖 Generated with Claude Code

https://claude.ai/code/session_013TyabW37ec75uPpdFhYkS5


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: c873e562-660c-4389-b337-5bdb9b677ea2

📥 Commits

Reviewing files that changed from the base of the PR and between 608a7b7 and e09b064.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • package.json
  • src/main/auth.ts
  • src/renderer/src/stores/agent-store.ts
  • src/renderer/src/stores/ui-store.ts
✅ Files skipped from review due to trivial changes (1)
  • package.json

📝 Walkthrough

Walkthrough

This pull request refactors the Pear application to use Zod for runtime data validation, consolidating ad-hoc normalization logic across main and renderer processes. It establishes shared validation schemas, improves IPC type safety, removes the non-fast broker input method, and centralizes timestamp formatting utilities.

Changes

Zod Schema Validation & IPC Type Safety Refactor

Layer / File(s) Summary
Zod dependency and shared validation modules
package.json, src/shared/lib/broker-events.ts, src/shared/schemas/project.ts
Zod is added as a runtime dependency. Shared modules provide broker event timestamp validation and payload compaction, plus project data helpers for channel normalization, people canonicalization, root/integration/project schemas, and a store schema factory.
Main process schema definitions
src/main/schemas.ts
Centralized Zod schemas for auth data (UserInfoSchema, StoredTokensSchema, AuthMetaSchema), broker connections (BrokerConnectionFileSchema with api_key → apiKey remapping), generated commit drafts, and avatar cache manifests.
Auth module schema-based validation
src/main/auth.ts
Validates auth payloads using UserInfoSchema, StoredTokensSchema, and AuthMetaSchema. Normalizes user info from multiple field variants, validates token loading and auth metadata parsing with fallback behaviors.
Main process data modules
src/main/avatar-cache.ts, src/main/broker.ts, src/main/store.ts, src/main/ipc-handlers.ts
Avatar-cache validates manifest via AvatarCacheManifestSchema. Broker parses connection files and commit drafts using schemas and shared event utilities. Store uses schema-derived type aliases and helper-based mutations (withProject, setProjectChannelPeople via PeopleListSchema). Non-fast sendInput IPC handler is removed.
IPC type safety and Git file status types
src/preload/index.ts, src/renderer/src/lib/ipc.ts
Generic typed IPC helpers (invoke, subscribe) enable type-safe IPC communication. GitFileStatusKind and GitFileStatus types constrain git status responses. Broker/auth/git/fs endpoints are re-typed with explicit generic returns. Non-fast broker.sendInput method is removed.
Renderer store schemas
src/renderer/src/stores/project-store.ts, src/renderer/src/stores/ui-store.ts
Project-store uses RendererProjectSchema with pathExists defaulting and rootPathExists computed property; normalizeChannelName is re-exported from shared. UI-store validates Theme and TerminalLayout via Zod schemas with fallback defaults.
Renderer store utilities
src/renderer/src/stores/agent-store.ts, src/renderer/src/stores/git-store.ts
Agent-store delegates broker event normalization to shared utilities. Git-store re-exports FileStatus as type alias to GitFileStatus.
Chat formatting consolidation
src/renderer/src/lib/format.ts, src/renderer/src/components/chat/ChatMessage.tsx, src/renderer/src/components/chat/ThreadPanel.tsx
Extracts formatClockTime and formatRelativeShort into reusable helpers. ChatMessage and ThreadPanel use shared formatters instead of local implementations.
Build configuration and path aliases
.gitignore, electron.vite.config.ts, tsconfig.node.json, tsconfig.web.json
Ignores TypeScript build info files. Adds @shared alias in Electron config and TypeScript configs, includes src/shared in compilation.

🎯 4 (Complex) | ⏱️ ~45 minutes

🐰 A rabbit hops through schemas, tidying the warren,
Zod guards each tunnel, no more casting to borrow,
Type-safe IPC paths wind through the shared tree,
Formatters gathered, timestamps now free!


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15505c7d14

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/auth.ts Outdated
Comment on lines +100 to +101
const user = UserInfoSchema.parse(candidate)
return Object.keys(user).length > 0 ? user : undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve empty user payload as undefined

normalizeUserInfo now parses a candidate object that always includes every user key (often with undefined values), so Object.keys(user).length > 0 is effectively always true and this function no longer returns undefined for empty inputs. In paths like mergeUserInfo, that means a sparse/empty whoami response can overwrite previously stored fields with undefined (e.g., clearing cached avatar/identity metadata) instead of leaving prior values intact.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — fixed in 608a7b7. Zod preserves keys whose value is undefined, so the Object.keys(user).length > 0 guard would always be truthy and mergeUserInfo would shadow previously cached fields (e.g. wipe cachedAvatarUrl on every whoami refresh).

The fix copies only defined entries off the parsed result before the emptiness check, restoring the prior semantics:

const parsed = UserInfoSchema.parse(candidate)
const user: UserInfo = {}
for (const [key, value] of Object.entries(parsed) as Array<[keyof UserInfo, string | undefined]>) {
  if (value !== undefined) user[key] = value
}
return Object.keys(user).length > 0 ? user : undefined

Generated by Claude Code

@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 found 1 potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread src/main/auth.ts Outdated
Comment on lines +100 to +101
const user = UserInfoSchema.parse(candidate)
return Object.keys(user).length > 0 ? user : undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 normalizeUserInfo never returns undefined for record inputs due to Zod preserving keys with undefined values

The new normalizeUserInfo constructs a candidate object with all 7 keys explicitly set (to string | undefined from firstString). When UserInfoSchema.parse(candidate) processes this, Zod preserves all keys in the output even when their values are undefined. This means Object.keys(user).length is always 7 (never 0), so the > 0 check always passes and the function never returns undefined for any record input.

Verified Zod behavior with installed version

UserInfoSchema.parse({ name: undefined, email: undefined, ... }) produces an object with 7 keys (all undefined), confirmed by test. Object.keys(result).length is 7.

The old code built the object conditionally (if (name) user.name = name), so empty inputs produced {} with 0 keys → returned undefined.

This changes behavior of downstream callers like mergeUserInfo and withCachedAvatar (src/main/auth.ts:107-108, src/main/auth.ts:151-152): they'll process/propagate empty-but-truthy user objects instead of undefined, potentially storing user: {} in auth tokens and returning empty user info in auth status.

Suggested change
const user = UserInfoSchema.parse(candidate)
return Object.keys(user).length > 0 ? user : undefined
const user = UserInfoSchema.parse(candidate)
const hasContent = Object.values(user).some((v) => v !== undefined)
return hasContent ? user : undefined
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — fixed in 608a7b7. Took the same approach as your suggestion but materialised a fresh object so the result is Partial<UserInfo>-shaped (no undefined keys leaking into the merge spread in mergeUserInfo / the JSON we write to auth-meta.json):

const parsed = UserInfoSchema.parse(candidate)
const user: UserInfo = {}
for (const [key, value] of Object.entries(parsed) as Array<[keyof UserInfo, string | undefined]>) {
  if (value !== undefined) user[key] = value
}
return Object.keys(user).length > 0 ? user : undefined

Generated by Claude Code

Centralizes validation of disk + IPC payloads in Zod schemas, shares
project/broker-event helpers between the main and renderer processes, and
removes dead code and unsafe casts.

Highlights:
- src/shared/schemas/project.ts: single source of truth for Project /
  ProjectRoot / ProjectIntegration shape, used by both the main process
  store and the renderer project store (was duplicated ~110 lines x2).
- src/main/schemas.ts: Zod schemas for StoredTokens, AuthMeta, UserInfo,
  broker connection.json, generated commit drafts, and the avatar cache
  manifest. Replaces `JSON.parse(...) as Record<string, unknown>` plus
  hand-rolled type guards.
- ui-store: validate localStorage theme/layout with z.enum instead of
  casting `as Theme | null` / `as TerminalLayout | null`.
- git-store: narrow `pear.git.status` return type so the `as FileStatus[]`
  cast is gone.
- preload: collapse 4 `as Promise<T>` casts into one generic `invoke<T>`
  helper; drop dead `broker:send-input` IPC handler / type / wrapper that
  no caller used.
- broker.ts: replace two `(err as { status?: unknown }).status` casts with
  a tiny `getErrorStatus` helper; narrow BrokerEvent with `in` operator
  instead of accessing fields that don't exist on every variant.
- shared/lib/broker-events.ts: single `compactBrokerEvent` /
  `normalizeEventTimestamp` for both main and renderer (was duplicated).
- format.ts: extract `formatClockTime` / `formatRelativeShort` shared by
  ChatMessage and ThreadPanel.

normalizeUserInfo fix (review feedback): Zod preserves keys with
`undefined` values from the input candidate, so the previous
`Object.keys(user).length > 0` check would always be true and an empty
whoami payload would shadow previously cached fields via mergeUserInfo
(clearing the cached avatar URL on every refresh). Now strips undefined
entries before the emptiness check, preserving the prior semantics.

Net effect on `tsc -b`: 5 baseline errors fewer than main, 0 new errors,
0 new `as any` / `@ts-ignore` / `@ts-expect-error`. `npm run build`
succeeds.

https://claude.ai/code/session_013TyabW37ec75uPpdFhYkS5
@willwashburn willwashburn force-pushed the claude/refactor-pear-codebase-Talve branch from 15505c7 to 608a7b7 Compare May 21, 2026 04:26
@willwashburn

Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: willwashburn <957608+willwashburn@users.noreply.github.com>

Copilot AI commented May 21, 2026

Copy link
Copy Markdown

@copilot resolve the merge conflicts in this pull request

Resolved and pushed in commit e09b064. I merged origin/main into this branch and fixed the src/renderer/src/stores/ui-store.ts conflict by keeping the Zod-based localStorage parsing while including the new graph terminal layout option.

@willwashburn willwashburn merged commit 0f7c3b9 into main May 21, 2026
2 checks passed
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.

3 participants