Skip to content

fix(typecheck): unblock pnpm -r typecheck after TS 6.x bump#265

Merged
github-actions[bot] merged 1 commit into
developfrom
fix/develop-typecheck-unblock
Jun 8, 2026
Merged

fix(typecheck): unblock pnpm -r typecheck after TS 6.x bump#265
github-actions[bot] merged 1 commit into
developfrom
fix/develop-typecheck-unblock

Conversation

@tomymaritano

Copy link
Copy Markdown
Collaborator

Summary

Unblocks pnpm -r typecheck on develop. After the recent TypeScript 6.x dependency bump, three preexisting failures broke the pre-push hook for everyone:

  • TS5101 baseUrl deprecation across packages/{core,plugin-api,sync-core,wikilinks}
  • Missing types: ["node"] in packages/licensingBuffer not found in validator.ts
  • apps/desktop/src/renderer rootDir misconfiguration → TS6059 on every cross-import from ../preload/api/*

No runtime code changes — only tsconfig edits.

Why this is PR-B (first in the audit stack)

Part of the full tech debt audit stack. This must land first because every subsequent stacked PR would otherwise need --no-verify to push. Plan reference: /Users/tomasmaritano/.claude/plans/ultracode-quiero-correr-un-enchanted-platypus.md.

Changes

File Change
tsconfig.base.json Add "ignoreDeprecations": "6.0" (covers 4 packages in one shot)
packages/licensing/tsconfig.json Add "types": ["node"]
apps/desktop/src/renderer/tsconfig.json Explicit "rootDir": ".." and include ../preload/api/**/*.ts
.github/workflows/ci.yml Add pnpm -r typecheck step to the existing typecheck job

The new CI step ensures this regression class never sneaks in silently again — previously only apps/desktop was typechecked in CI, full monorepo was only checked at pre-push.

Test plan

  • pnpm -r typecheck exits 0
  • Pre-push hook passes without --no-verify
  • CI typecheck job runs both pnpm -r typecheck and apps/desktop typecheck

Follow-ups (not in this PR)

  • The baseUrl option will be removed in TS 7.x. Migrating away from baseUrl + paths to relative imports (or proper project references) is a separate effort.
  • rootDir = ".." is a workaround; a cleaner solution is project references between preload and renderer, also a separate effort.

🤖 Generated with Claude Code

Recent TypeScript 6.x dependency bump introduced three typecheck failures
on develop, which made the pre-push hook block any push from the team.
Fixes are surgical, no code changes:

- tsconfig.base.json: ignoreDeprecations "6.0" silences TS5101 for
  baseUrl in packages/{core,plugin-api,sync-core,wikilinks}. We keep
  baseUrl for now; full removal is a separate migration.
- packages/licensing/tsconfig.json: add types: ["node"] so Buffer
  resolves in validator.ts (the package depends on @types/node already).
- apps/desktop/src/renderer/tsconfig.json: explicit rootDir ".." and
  include preload/api so renderer can import from "../preload/index.ts"
  without TS6059 complaining the imported file is outside rootDir.

Also adds `pnpm -r typecheck` step to the CI typecheck job so monorepo
typecheck never regresses silently again (previously only apps/desktop
was checked in CI; full monorepo was only checked at pre-push).

Verified: pnpm -r typecheck exits 0 on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
readide Error Error Jun 8, 2026 6:30am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@tomymaritano, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 43 minutes and 9 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e87f3239-f7d6-45bc-b5f9-849d8b7c0b3f

📥 Commits

Reviewing files that changed from the base of the PR and between f3369e3 and 3412e4e.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • apps/desktop/src/renderer/tsconfig.json
  • packages/licensing/tsconfig.json
  • tsconfig.base.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/develop-typecheck-unblock

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot enabled auto-merge (squash) June 8, 2026 06:28

@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: 3412e4e2ba

ℹ️ 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 tsconfig.base.json
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
"noUncheckedIndexedAccess": true,
"ignoreDeprecations": "6.0"

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 Use a valid ignoreDeprecations value

With the repo's TypeScript 6.0.3, this value is rejected before any package is checked: running pnpm -r typecheck fails immediately in packages extending the base config with tsconfig.json(3,3): error TS5103: Invalid value for '--ignoreDeprecations'. Because the new CI step also runs pnpm -r typecheck, this change keeps the monorepo typecheck and CI typecheck job broken rather than unblocking them.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot added the size/S label Jun 8, 2026
@github-actions github-actions Bot merged commit 75fedfe into develop Jun 8, 2026
13 of 15 checks passed
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
## Summary

Two production crashes were silently breaking the markdown editor on
mount. Console showed both, side-by-side, on every note open:

```
CodeMirror plugin crashed: TypeError: tags is not iterable
RangeError: Decorations that replace line breaks may not be specified via plugins
```

This PR addresses both.

## Fix 1 — Tables plugin: ViewPlugin → StateField

`apps/desktop/src/renderer/plugins/tables.tsx` registered table WYSIWYG
widgets via `ViewPlugin.fromClass(...)` and called `Decoration.replace({
widget })` over multi-line table ranges. CodeMirror 6 forbids this —
`Decoration.replace()` ranges that include line breaks **must** come
from a `StateField`, not from a `ViewPlugin`. The runtime error is
thrown inside `RangeSet.spans` during `dispatchTransactions` and the
plugin self-disables (see "Table WYSIWYG disabled" in console).

Migration:
- `buildTableDecorations(view)` → `buildTableDecorations(state:
EditorState)`.
- Replaced `tableViewPlugin` with `tableDecorationsField =
StateField.define<DecorationSet>(...)` that:
  - `create(state)` builds the initial set.
- `update(decos, tr)` rebuilds on `tr.docChanged || tr.selection`,
otherwise maps existing decorations through `tr.changes`.
- `provide: f => EditorView.decorations.from(f)` wires them to the view.
- Dropped the `view.visibleRanges` optimization — StateField has no
viewport context. Most documents have ≤ a few tables; cost is
negligible. If profiling shows hot spots later, we can dispatch viewport
`StateEffect`s from a thin ViewPlugin.

The `Decoration.replace({widget})` call is unchanged — only the
*registration shape* moved from ViewPlugin to StateField, which is the
API contract CodeMirror enforces.

## Fix 2 — Editor exception sink (catch \"tags is not iterable\")

`TypeError: tags is not iterable` throws **inside**
`HighlightStyle.style` during syntax-tree highlight — *not* at
\`HighlightStyle.define\` time. Each \`tag: tags.headingN\` is fine; the
iteration failure happens when a parser extension emits malformed tag
metadata for a syntax tree node (likely a custom parser, candidates:
wikilinks / embed inline preview / a future markdown extension).

Without root-cause repro (needs runtime reproduction in dev), we add
`EditorView.exceptionSink.of(...)` at the head of the extension list in
`MarkdownEditor.tsx`. This catches the throw at the plugin boundary,
logs it to console + Sentry (if a global Sentry is attached), and lets
the editor continue mounting. No more white-screen on open.

Root-cause fix for the offending parser will land in a follow-up PR once
we repro and isolate which extension is at fault.

## Files

- `apps/desktop/src/renderer/plugins/tables.tsx` — ViewPlugin →
StateField migration.
- `apps/desktop/src/renderer/components/MarkdownEditor.tsx` — add
`EditorView.exceptionSink`.

## Test plan

- [x] `pnpm -r typecheck` — green.
- [x] `pnpm test` — 17/17 packages, 51 desktop tests pass.
- [ ] Manual: launch desktop, open a note containing a markdown table,
confirm:
  - No \`RangeError\` in console.
  - WYSIWYG table renders when cursor is outside.
  - Raw markdown shows when cursor enters the table range.
- [ ] Manual: open a note that previously triggered \`tags is not
iterable\`, confirm editor mounts (the error may still log via the new
exception sink — that's expected, this PR only stops the crash).

## Stack context

This is **PR-A** in the tech-debt audit stack. Stacked on top of #265
(PR-B). When #265 merges, this PR's base will automatically retarget to
\`develop\`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
…taged (#267)

## Summary

DX upgrade for the monorepo's git hooks and dead-code detection.

| Old | New |
|---|---|
| `husky` 9.1.7 + 3 separate `.husky/*` files | `lefthook` 1.13.6 +
single `lefthook.yml` |
| `lint-staged` runs `prettier --write` only | `lint-staged` runs
`eslint --cache --fix --max-warnings 0` **then** `prettier --write` on
\`*.{ts,tsx,js}\` |
| No dead-code detection | `knip` 5.66 — `pnpm knip` reports unused
files / exports / deps |

## Files

| File | Change |
|---|---|
| `.husky/{pre-commit,pre-push,commit-msg}` | **deleted** |
| `lefthook.yml` | **new** — three hooks: `pre-commit → lint-staged`,
`pre-push → pnpm -r typecheck`, `commit-msg → commitlint` |
| `knip.json` | **new** — entry points per workspace; `apps/desktop` has
separate main/preload/renderer entries |
| `package.json` | husky → lefthook in devDeps, `prepare` →
`postinstall: lefthook install`, lint-staged config tightened, `knip`
script |
| `pnpm-lock.yaml` | regenerated |

## ⚠️ One-time setup after pulling

Husky had configured `core.hooksPath = .husky/_`. Lefthook needs that
unset. **After pulling this PR**, run:

\`\`\`bash
git config --unset core.hooksPath
pnpm install   # triggers lefthook install via postinstall
\`\`\`

The README and CONTRIBUTING.md will note this in a follow-up PR.

## Knip baseline (informational, NOT failing CI)

First pass against the full monorepo reports:

- **28 unused files** (e.g. `analytics.ts`, `useTheme.ts`, several
settings control components, `magicui/*` not wired up)
- **9 unused production deps** (e.g. `highlight.js`, `pino-roll`,
`react-resizable-panels` in desktop; `next-themes`,
`tailwindcss-animate` in web)
- **2 unused dev deps** (`@types/mdast`, `pino-pretty`)
- **~100 unused exports** (e.g. internal `index.ts` re-exports that
aren't imported anywhere)

**This is the baseline, not a fix.** Cleanup will live in subsequent
focused PRs so each delete can be reviewed properly (some \"unused\"
exports may be public API contracts for the plugin system).

## Test plan

- [x] `pnpm install` triggers `lefthook install` via postinstall.
- [x] `npx lefthook run pre-commit` fires and routes to lint-staged.
- [x] Commit on this PR ran through lefthook → lint-staged + commitlint.
- [x] `pnpm knip` runs and reports the baseline.
- [ ] Other contributors confirm `git config --unset core.hooksPath` is
the only manual step needed.

## Stack context

**PR-C** in the audit stack. Stacked on top of #266 (PR-A) → #265
(PR-B). All three will retarget to \`develop\` automatically as they
merge.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
#269)

## Summary

Two related changes to give the monorepo a test quality baseline:

1. **Smoke tests for \`@readied/commands\`** (was 0 tests, now **13**) —
Covers all wrapping (bold/italic/strike/code) including unwrap behavior,
all line-prefix commands (headings, lists, checkbox, quote), and
block-insertion commands. Coverage: **78% lines** on \`commands.ts\`.

2. **v8 coverage wired into all 12 vitest configs** —
\`vitest.shared.ts\` exports a shared coverage block; each
\`vitest.config.ts\` spreads it. No thresholds enforced yet — baseline
measurement only.

## How the commands test works without a DOM

The package's commands take a \`CodeMirror.EditorView\` and call
\`view.dispatch\` / \`view.focus\`. A real view needs a DOM. Instead of
pulling in \`happy-dom\`, the tests fake a view backed by
\`EditorState.update()\`:

\`\`\`ts
function fakeView(initialDoc: string, selection: { from, to }) {
  let state = EditorState.create({ doc, selection: ... });
  const dispatch = vi.fn(spec => { state = state.update(spec).state; });
return { view: { get state(){return state;}, dispatch, focus: vi.fn() },
doc: () => state.doc.toString() };
}
\`\`\`

This is enough surface area for every exported command. No DOM, no
environment changes.

## Coverage setup

| File | Purpose |
|---|---|
| \`vitest.shared.ts\` | exports \`sharedCoverage\` — v8 provider,
text+lcov reporters, src/\*\* in, tests/dist/index out |
| 12× \`vitest.config.ts\` | imports \`sharedCoverage\`, applies under
\`test.coverage\` |
| \`package.json\` | adds \`@vitest/coverage-v8\` devDep +
\`test:coverage\` script |
| \`.gitignore\` | adds \`coverage/\` |

To run locally: \`pnpm test:coverage\`. CI integration (artifact upload
+ codecov) is a follow-up.

## Audit correction

The audit flagged 5 packages as having no tests: \`ai-assistant\`,
\`commands\`, \`licensing\`, \`mcp-server\`, \`product-config\`. On
inspection, only **commands** actually had no tests:

- \`ai-assistant\`: has \`src/aiCommandTypes.test.ts\`
- \`licensing\`: has 3 tests in \`__tests__/\`
- \`mcp-server\`: has \`src/__tests__/fts5-triggers.test.ts\` (from PR
#264)
- \`product-config\`: has \`__tests__/facade.test.ts\`

So this PR fills the one real gap and adds coverage infrastructure so
future gaps become visible.

## Test plan

- [x] \`pnpm test\` — 17/17 packages, all green.
- [x] \`pnpm -r typecheck\` — green.
- [x] \`pnpm --filter @readied/commands exec vitest run --coverage\` —
13/13 tests, 78% lines.
- [ ] \`pnpm test:coverage\` per package as needed locally.

## Stack context

**PR-D** in the audit stack. Stacked on top of #268 (PR-H) → #267 (PR-C)
→ #266 (PR-A) → #265 (PR-B). Retargets to \`develop\` automatically as
predecessors merge.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
…270)

## Summary

Two changes in \`packages/mcp-server/src/index.ts\`:

### 1. \`server.tool()\` → \`server.registerTool()\` (MCP SDK 1.29)

All 7 call sites migrated:

| Tool | |
|---|---|
| \`readied_list_notes\` | ✅ |
| \`readied_read_note\` | ✅ |
| \`readied_create_note\` | ✅ |
| \`readied_update_note\` | ✅ |
| \`readied_search_notes\` | ✅ |
| \`readied_list_notebooks\` | ✅ |
| \`readied_trash_note\` | ✅ |

The old \`tool()\` overload is deprecated in
\`@modelcontextprotocol/sdk\` (TS6387 in the IDE). \`registerTool\`
takes a config object \`{description?, inputSchema?, outputSchema?,
annotations?, _meta?}\` instead of positional args, so future evolution
(output schemas, annotations) is additive without breaking changes.

### 2. \`readied_read_note\`: FTS5 title search, LIKE as fallback

The previous title search did \`title LIKE '%' || ? || '%'\` which:
- can't use any index (wildcard-prefix LIKE),
- has no relevance ranking.

New flow: build the FTS5 query via the existing \`prepareFtsQuery()\`
(same path as \`readied_search_notes\`), bm25-rank, take the top hit. If
FTS returns nothing (index rebuild in progress, freshly restored DB,
etc.), fall back to the parameterized LIKE on the live table so the tool
still works in degraded states.

\`\`\`ts
const ftsQuery = prepareFtsQuery(title);
note = queryOne(db,
  \`SELECT n.id, n.title, n.content, n.notebook_id
     FROM notes_fts JOIN notes n ON n.id = notes_fts.id
     WHERE notes_fts MATCH ? AND n.is_deleted = 0
     ORDER BY bm25(notes_fts) LIMIT 1\`,
  [ftsQuery]
);
if (!note) {
  // FTS index might not have caught up — fall back
note = queryOne(db, \`SELECT ... WHERE title LIKE '%' || ? || '%' ...\`,
[title]);
}
\`\`\`

## Test plan

- [x] \`pnpm --filter @readied/mcp-server build\` — clean.
- [x] \`pnpm --filter @readied/mcp-server test\` — 5/5 (the FTS5 trigger
test from #264 still passes).
- [ ] Manual: \`node packages/mcp-server/dist/index.js\` against a
Readied DB, then via Claude Code exercise each of the 7 tools.

## Stack context

**PR-J** in the audit stack. Stacked on top of #269 (PR-D) → #268 (PR-H)
→ #267 (PR-C) → #266 (PR-A) → #265 (PR-B). Retargets to \`develop\`
automatically as predecessors merge.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
## Summary

Hardens the restore-from-backup flow in
\`apps/desktop/src/main/handlers/dataHandlers.ts\` so a corrupt backup
can't quietly leave the user with a broken live database.

## Old flow (problem)

1. Close current db.
2. \`copyFileSync(backup, live)\`.
3. Open the new db, run migrations, swap reference.

Trusts the backup file. If it was corrupt (interrupted write, bit rot,
hostile file), the user loses the working db and gets cryptic SQLite
errors on the next read.

## New flow

1. Close current db.
2. \`restoreBackup()\` copies backup over live and writes a
\`.pre-restore\` safety copy of the previous db.
3. **Open the restored db.**
4. **\`PRAGMA integrity_check\`** — must return \`'ok'\`.
5. \`runMigrations()\` to bring older backups current.
6. Swap the db reference and delete the safety copy.

Any failure at steps 3-5 rolls back to the safety copy and returns a
clear, user-readable error describing which step failed. The user is
never left with a corrupt or partially-migrated live database.

## Audit refinements (relative to the original plan)

The B10 / B11 items in the audit turned out to already be addressed or
factually incorrect:

| Audit ID | Claim | Reality |
|---|---|---|
| **B10** | Missing index on \`notes.needs_sync\` → full table scan on
sync | **Already fixed** — migration \`011_sync_tracking\` declares
\`CREATE INDEX IF NOT EXISTS idx_notes_needs_sync ON notes(needs_sync)
WHERE needs_sync = 1\` (partial index, exactly what the audit
recommended). |
| **B11** | FTS5 triggers don't share an explicit txn → drift risk |
**False positive** — SQLite triggers run within the same implicit
transaction as the parent statement. If the trigger throws, the parent
INSERT/UPDATE rolls back. The FTS index can't drift from the notes table
this way. |
| **B12** | Backup restore has no integrity check / no schema version
replay | **Partially valid** — migrations were already replayed, but
integrity check was missing. This PR adds it. |

So PR-I's actual scope is much narrower than the audit suggested. That's
fine — the goal is to address real issues, not invent them.

## Test plan

- [x] \`pnpm --filter @readied/desktop typecheck:main\` — green.
- [ ] Manual: restore a corrupt backup file → expect error message +
previous db intact.
- [ ] Manual: restore a valid old backup → migrations bring it current;
safety copy deleted.

## Stack context

**PR-I** in the audit stack. Stacked on top of #270 (PR-J) → #269 (PR-D)
→ #268 (PR-H) → #267 (PR-C) → #266 (PR-A) → #265 (PR-B). Retargets to
\`develop\` automatically as predecessors merge.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
…#272)

## Summary

Scoped first slice of **PR-F**. The audit's PR-F bundled IPC validation
+ keychain (keytar) + HMAC license files into one PR — that's three
independent security efforts, each with their own review surface. This
PR ships just the **typed IPC registry** plus a single handler migration
to prove the pattern.

The other two slices (keychain credential storage, HMAC license signing)
will land as their own PRs.

## What this PR introduces

### 1. \`apps/desktop/src/main/ipc/registry.ts\`

A small \`defineIpcHandler({channel, args, handler})\` wrapper around
\`ipcMain.handle()\` with Zod tuple validation at the boundary:

\`\`\`ts
defineIpcHandler({
  channel: 'ai:saveKey',
  args: z.tuple([ProviderSchema, ApiKeySchema]),
  handler: (provider, apiKey) => aiKeyStorage.saveKey(provider, apiKey),
});
\`\`\`

Renderer-supplied args are validated **before** business logic runs. An
invalid payload throws \`IpcValidationError\` at the boundary, so the
renderer sees a structured error instead of a downstream crash deep
inside storage code.

### 2. \`aiKeyHandlers.ts\` migrated as the proof

5 channels migrated. Schemas:

| Schema | Constraint | Why |
|---|---|---|
| \`ProviderSchema\` | string, 1–64 chars, \`[a-zA-Z0-9_-]+\` |
Anti-garbage; matches real provider IDs |
| \`ApiKeySchema\` | string, 1–4096 chars | Conservative cap — well
above real API key lengths, well below "this is junk" |

These are bounds-checks, not credential-at-rest hardening (that's the
keychain slice).

### 3. \`apps/desktop/package.json\`

Adds \`zod ^4.4.3\` as a direct dep (it was already used in 4 workspace
packages but not exposed to desktop main).

## What this PR does NOT include

- **Keychain integration** (B3 — AI keys stored unencrypted). Needs
\`keytar\` + electron-builder integration + a one-time migration on
first launch. Separate PR.
- **HMAC license signing** (B4). Separate PR with its own review.
- **Migration of the other 12 handler modules.** Once this pattern is
reviewed, the rest can follow in focused per-module PRs (notes,
notebooks, plugins, sync, etc.).

## Test plan

- [x] \`pnpm -r typecheck\` — green.
- [x] \`pnpm test\` — all packages green.
- [ ] Manual: launch desktop, save an AI key via Settings → AI, then
load it back. Verify the existing flow still works (no behavior change
for valid inputs).
- [ ] Manual: send a malformed payload via DevTools
(\`window.readied.ai.saveKey({not: 'a string'}, 'key')\`). Expect an
\`IpcValidationError\` in the renderer console.

## Audit linkage

| Audit ID | Status |
|---|---|
| **B2** — IPC handlers accept raw strings | Partially addressed for
AI-key surface; pattern in place for the rest |
| **B3** — AI keys unencrypted (keychain) | Separate PR |
| **B4** — License files unsigned (HMAC) | Separate PR |

## Stack context

**PR-F1** in the audit stack. Stacked on top of #271 (PR-I) → #270
(PR-J) → #269 (PR-D) → #268 (PR-H) → #267 (PR-C) → #266 (PR-A) → #265
(PR-B). Retargets to \`develop\` automatically as predecessors merge.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
## Summary

Applies the typed IPC registry pattern from #272 to six handler modules.
~30 IPC channels are now validated at the boundary with Zod tuple
schemas before the business logic runs.

The remaining five "heavy" modules (\`note\`, \`notebook\`, \`data\`,
\`git\`, \`authSync\` — ~96 channels) will land in **PR-F5** as a
separate PR for reviewability.

## Files migrated

| Module | Channels | Notes |
|---|---|---|
| \`logHandlers.ts\` | 2 | LogLevel enum, message ≤16 KiB |
| \`shareHandlers.ts\` | 2 | Note content ≤1 MiB, tags/backlinks
count-capped |
| \`updateHandlers.ts\` | 2 (+1 raw) | \`updates:installNow\` kept raw —
synchronous quit path |
| \`licenseHandlers.ts\` | 4 | Optional plan enum on \`openSubscribe\` |
| \`localServerHandlers.ts\` | 4 | Port range 1–65535 (was
inside-handler check) |
| \`pluginHandlers.ts\` | 11 (+1 raw) | \`PluginIdSchema\` matches
install-time regex |

\`plugins:requestReload\` uses \`ipcMain.on\` (fire-and-forget, not
invoke), so it's not subject to \`defineIpcHandler\` — left raw.

## Schema highlights

- \`PluginIdSchema\` is
\`z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/)\` — mirrors the
regex enforced at install time on \`manifest.id\`. The boundary now
catches the same shape that the install-time check catches.
- Where the original code had defensive in-handler validation (e.g. \`if
(port < 1 || port > 65535)\`), the schema now handles it. Defensive
checks that produce nicer error strings (e.g. the \"https only\" guard
inside \`installFromUrl\`) are kept as belt-and-suspenders.

## Test plan

- [x] \`pnpm --filter @readied/desktop typecheck:main\` — green.
- [x] \`pnpm test\` — 17/17 packages.
- [ ] Manual: each affected setting/feature in the app continues to work
for valid inputs.
- [ ] Manual: try a malformed payload via DevTools (e.g.
\`window.readied.plugins.setEnabled('not-a-valid-id!@#', true)\`).
Expect \`IpcValidationError\`.

## Stack context

**PR-F4** in the audit stack. Stacked on top of #272 (PR-F1) → #271
(PR-I) → #270 (PR-J) → #269 (PR-D) → #268 (PR-H) → #267 (PR-C) → #266
(PR-A) → #265 (PR-B).

## Follow-ups

- **PR-F5**: heavy data modules (note, notebook, data, git, authSync) —
same pattern, ~96 channels.
- **PR-F2**: keychain storage for AI keys (Electron safeStorage is the
leaning choice).
- **PR-F3**: license verification (asymmetric — server signs, client
verifies; **not** HMAC).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
#274)

## Summary

Completes the IPC registry migration started in #272 (registry) → #273
(light handlers). Five remaining handler modules (~97 channels) now run
through \`defineIpcHandler\` with Zod tuple validation at the boundary.
**The full IPC surface of the desktop app is now validated.**

## Files

| Module | Channels | Notes |
|---|---|---|
| \`notebookHandlers.ts\` | 15 | Notebook CRUD + per-notebook git
settings. Common \`serialize(nb)\` helper avoids 6 copies of the same
object spread. |
| \`gitHandlers.ts\` | 9 | Short-SHA accepted (≥7 hex), commit message
≤8 KiB, note content ≤1 MiB. |
| \`dataHandlers.ts\` | 7 of 8 |
backup/list/export/exportNote/import/paths/openFolder migrated.
**\`data:backup:restore\`** stays raw — the integrity-check + rollback
state machine from #271 doesn't fit the registry's wrapper cleanly. |
| \`noteHandlers.ts\` | 32 | Notes CRUD, tags, links, embeds, stats.
\`EmbedTargetSchema\` regex-constrained to filename-safe characters (no
slashes, no path traversal) at the boundary in addition to the existing
\`join()\` guard. \`ArrayBuffer\` validated via
\`z.instanceof(ArrayBuffer)\` on \`embeds:saveAsset\`. |
| \`authSyncHandlers.ts\` | 33 | Auth/magic-link, sync
(pull/push/syncNow/conflicts/auto-sync/history), E2EE key management,
subscription portal, devices. \`SyncChangeSchema\` is the most complex
shape here. |

## Exceptions (still raw \`ipcMain\`)

After this PR every \`ipcMain.handle\` in
\`apps/desktop/src/main/handlers/\` either goes through
\`defineIpcHandler\` or has an explanatory comment:

| Channel | Why raw |
|---|---|
| \`updates:installNow\` | Synchronous quit + window destruction path;
registry's async wrapper would interfere |
| \`plugins:requestReload\` | Uses \`ipcMain.on\` — fire-and-forget
event, not invoke |
| \`data:backup:restore\` | Integrity-check rollback state machine from
PR #271 |

These are documented in code as well as listed here.

## Schema highlights

- \`SyncChangeSchema\`: \`{ noteId, operation: enum, content?: ≤10 MiB,
localVersion?: int }\` — the boundary now catches malformed push
payloads instead of letting them flow into the sync engine.
- \`EmbedTargetSchema\`: \`/^[a-zA-Z0-9._-]+$/\` — defense in depth on
top of \`join()\` for path traversal.
- \`KeyHexSchema\` for encryption export/import — strict hex format,
length 32–256 chars.
- All \`IdSchema\`s capped at 128 chars; \`TagSchema\` at 64;
\`ContentSchema\` at 10 MiB; \`CommitMessageSchema\` at 8 KiB.

## Test plan

- [x] \`pnpm -r typecheck\` — green.
- [x] \`pnpm test\` — 17/17 packages.
- [ ] Manual: exercise notes CRUD, tag operations, sync cycle,
encryption setup, device management.
- [ ] Manual: send malformed payloads via DevTools (e.g.
\`window.readied.sync.push('not-an-array')\`) and expect
\`IpcValidationError\`.

## Stack context

**PR-F5** in the audit stack. Stacked on top of #273 (PR-F4) → #272
(PR-F1) → #271 (PR-I) → #270 (PR-J) → #269 (PR-D) → #268 (PR-H) → #267
(PR-C) → #266 (PR-A) → #265 (PR-B).

## Follow-ups (per Tomy's review)

- **PR-F2** — keychain storage for AI keys via Electron \`safeStorage\`
(not keytar).
- **PR-F3** — license verification with **asymmetric** crypto (server
signs, client verifies with embedded public key). The original audit's
HMAC suggestion is wrong: a client that can verify can also forge.
- **PR-E** — Playwright Electron E2E (deferred until architecture
settles).
- **PR-G** — split god files (deferred — too risky to mix with this
stack).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
…errors (#275)

## Summary

The audit listed **B3 (\"AI keys stored unencrypted\")** as a gap, but
\`apps/desktop/src/main/services/aiKeyStorage.ts\` has been using
Electron \`safeStorage\` since the file was introduced. AI keys are
already OS-encrypted: macOS Keychain / Windows DPAPI / Linux libsecret.
**No new dependency needed — \`safeStorage\` is the right call here,
exactly as you said.**

However, while reviewing the file for the audit, I found a **real bug**
worth shipping in this PR.

## The real bug

\`readKeys()\` used a catch-all that treated any decryption / parse
failure as corruption and deleted the encrypted file:

\`\`\`ts
} catch (error) {
  if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
    return {};
  }
  // Decryption or parsing failed - clear corrupted file
  await this.clearAll();  // ← silent data loss
  return {};
}
\`\`\`

On macOS the keychain becomes **temporarily** inaccessible after
sleep/wake (and on Linux when libsecret hasn't unlocked yet).
\`safeStorage.decryptString()\` throws during that window. The previous
code interpreted that as corruption and wiped the file → user's AI keys
gone, with no error surfaced.

## New error contract

| Condition | Behavior |
|---|---|
| ENOENT on read | Return \`{}\` (no keys yet) |
| \`isEncryptionAvailable()\` → false | Throw
\`AiKeyEncryptionUnavailableError\` |
| \`safeStorage.decryptString\` throws | Throw \`AiKeyDecryptionError\`
(cause preserved) |
| \`JSON.parse\` fails | Throw \`AiKeyDecryptionError\` |
| Top-level shape is not an object | Throw \`AiKeyDecryptionError\` |

**The encrypted file is never auto-deleted on a read path now.** The
only delete sites are:
- Explicit \`removeKey()\` that empties the map → \`unlinkFile()\`
- (No other paths.)

Errors propagate to the IPC layer, where \`defineIpcHandler\` from #272
wraps them in a structured response for the renderer.

## Why this matters

Before this fix, the failure mode for a user whose keychain happened to
be locked at the wrong moment was: open the AI panel → see \"no keys\" →
re-enter all their provider keys → next sleep/wake cycle: repeat. The
fix surfaces a real error (\"keychain locked, retry\") instead of
silently destroying state.

## Test plan

- [x] \`pnpm -r typecheck\` — green.
- [x] \`pnpm test\` — 17/17 packages.
- [ ] Manual on macOS: save an AI key, sleep the machine for a few
minutes, wake up, immediately open AI panel. Before this fix: keys may
be gone. After: either keys load normally OR a clear error surfaces and
a retry works.
- [ ] Manual on Linux without libsecret: expect
\`AiKeyEncryptionUnavailableError\` with the message pointing at
libsecret/gnome-keyring/kwallet.

## Stack context

**PR-F2** in the audit stack. Stacked on top of #274 (PR-F5) → #273
(PR-F4) → #272 (PR-F1) → #271 (PR-I) → #270 (PR-J) → #269 (PR-D) → #268
(PR-H) → #267 (PR-C) → #266 (PR-A) → #265 (PR-B).

## Up next

**PR-F3**: Ed25519 license verification — server signs license payload
with private key, desktop verifies with embedded public key. No
symmetric secrets on the client. Coming as the next PR in the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
…F3) (#276)

## Summary

Adds the asymmetric verification primitive the desktop needs so the
license server can sign subscription state and the client can verify it
**without holding any signing secret**. This is the foundation; no live
wiring yet — those follow once the server emits signed envelopes.

Per your review: **Ed25519, not HMAC** — a client that can verify a
symmetric MAC can also forge it.

## Wire format

\`\`\`ts
{
  payload: {
    payloadVersion: 1,
    subscription: { /* SubscriptionInfo */ },
    issuedAt: "2026-06-08T12:00:00.000Z",
    ttlSeconds?: 3600  // optional, server-suggested max age
  },
  signature: "<base64 Ed25519 signature>"
}
\`\`\`

Signature is computed over \`canonicalJson(payload)\` — a deterministic
sorted-key JSON encoding. \`JSON.stringify\` is **not** deterministic
across runtimes, so the canonical encoder is mandatory.

## Files

| File | Change |
|---|---|
| \`packages/licensing/src/types.ts\` | Add
\`SignedSubscriptionPayload\` + \`SignedSubscriptionEnvelope\` types |
| \`packages/licensing/src/validator.ts\` | Add \`canonicalJson\`,
\`signSubscriptionPayload\` (server), \`verifySubscriptionSignature\`
(client). \`DEFAULT_SUBSCRIPTION_PUBLIC_KEY\` is an all-zeros
placeholder with a REPLACE BEFORE SHIPPING note |
| \`packages/licensing/src/index.ts\` | Re-export the new types +
functions |
| \`packages/licensing/__tests__/subscriptionSignature.test.ts\` | **18
new tests** (103 total in the package now) |
| \`packages/licensing/README.md\` | Wire format, server flow, embedded
public key, key rotation, why trial.json is **not** signed |

## What \`verifySubscriptionSignature\` checks

1. Envelope shape (\`payload\` + \`signature\` strings present).
2. Payload shape (\`payloadVersion: 1\`, \`issuedAt\`,
\`subscription\`).
3. Ed25519 signature against the public key (or
\`DEFAULT_SUBSCRIPTION_PUBLIC_KEY\` if the caller doesn't pass one — and
the default will fail until replaced).
4. Replay window — rejects envelopes older than \`maxAgeSeconds\`
(defaults to \`payload.ttlSeconds\`, else 7 days).
5. Future-timestamp window — tolerates 60s clock skew but rejects
obviously-future \`issuedAt\` values.
6. Subscription field/period validation (delegates to existing
\`verifySubscription\`).

Injectable clock (\`config.nowMs\`) makes timing tests deterministic.

## What is NOT in this PR (intentional)

- **Live wiring** into \`FileLicenseStorage\` / \`licenseHandlers.ts\`.
Wiring would lock out every user before the server emits signed
envelopes. Wiring lands as a follow-up once the server is ready.
- **Real public key.** \`DEFAULT_SUBSCRIPTION_PUBLIC_KEY\` is all zeros.
Replacing it is a deliberate, separate change — coordinated with
whichever release first ships signed envelopes.
- **Trial signing.** Trial state remains unsigned by design (no
server-side trial registration; subscription is the real boundary). The
README explains.

## Test plan

- [x] \`pnpm --filter @readied/licensing test\` — **103/103 tests** pass
(5 files; 18 new in this PR).
- [x] \`pnpm -r typecheck\` — green across the monorepo.
- [ ] Server team uses \`signSubscriptionPayload\` to start signing.
After enough clients have the new code, the embedded public key is
replaced and live verification is enabled in a follow-up PR.

## Stack context

**PR-F3** in the audit stack. Stacked on top of #275 (PR-F2) → #274
(PR-F5) → #273 (PR-F4) → #272 (PR-F1) → #271 (PR-I) → #270 (PR-J) → #269
(PR-D) → #268 (PR-H) → #267 (PR-C) → #266 (PR-A) → #265 (PR-B).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
## Summary

Lands the **infrastructure** for end-to-end testing the desktop app: a
Playwright + Electron setup with per-test isolation, two initial specs,
and a Linux CI job. The CI job ships as **\`continue-on-error: true\`**
while we stabilize — flip it off in a follow-up once it's reliably green
on develop.

## Files

| File | Purpose |
|---|---|
| \`apps/desktop/playwright.config.ts\` | Serial workers (Electron app
state is shared), generous timeouts, retain-on-failure trace/video |
| \`apps/desktop/e2e/fixtures.ts\` | \`launchApp()\` helper — fresh
Electron instance with isolated \`userData\` per test via \`mkdtemp\`;
\`_electron.launch\` API |
| \`apps/desktop/e2e/tsconfig.json\` | Dedicated tsconfig so specs don't
pull in renderer/main types |
| \`apps/desktop/e2e/smoke.spec.ts\` | App launches, window renders, IPC
bridge present, no uncaught console errors during mount |
| \`apps/desktop/e2e/notes.spec.ts\` | Notes IPC contract —
create/list/get roundtrip, FTS5 search |
| \`apps/desktop/e2e/README.md\` | How to run locally, what's tested,
what's out of scope |
| \`apps/desktop/package.json\` | \`@playwright/test\` devDep, \`e2e\` +
\`e2e:headed\` scripts, \`typecheck:e2e\` folded into \`typecheck\` |
| \`.github/workflows/ci.yml\` | New \`e2e\` job, ubuntu + xvfb, uploads
playwright-report on failure |
| \`.gitignore\` | Excludes \`test-results/\`, \`playwright-report/\`,
\`playwright/.cache/\` |
| \`knip.json\` | Registers \`playwright.config.ts\` + \`e2e/**/*.ts\` |

## Why drive the preload bridge, not the editor UI

\`notes.spec.ts\` calls \`window.readied.notes.*\` directly via
\`page.evaluate\` instead of typing in the CodeMirror surface. Reasons:
1. **Selectors churn**, contracts are stable.
2. The preload bridge is the same surface the renderer uses — anything
that breaks here breaks the renderer too.
3. CodeMirror flake on hotkeys / IME / focus is a class of pain we don't
need until the editor is split.

Future UI-level specs land once \`MarkdownEditor.tsx\` is split (PR-G
follow-up).

## Specs in this PR

### \`smoke.spec.ts\`

1. App launches, main window has non-zero size, IPC bridge present.
2. Console produces no uncaught errors in the first 3 seconds (ignoring
known noise like \"Sentry: No DSN configured\"). This is the regression
catch for #266 — the editor mount crashes that produced blank windows.

### \`notes.spec.ts\`

1. \`create → list → read\` roundtrip works.
2. \`search\` via FTS5 finds a freshly-created note via a unique marker.

## What's deliberately NOT here

- Editor UI interactions (typing, formatting, hotkeys) — too prone to
flake without per-spec selectors. Revisit after editor split.
- AI panel streaming — better as a vitest test against
\`@readied/ai-core\`.
- Sync flows — need a fake server.

## Honest disclaimer

**I scaffolded all of this but could NOT execute the suite end-to-end
against a real Electron build in this session** (no display attached, no
built bundle in this branch). The CI's \`continue-on-error\` gate and
the local README acknowledge that the first verifier on a real machine
may need to tweak the specs once they meet a real renderer.

## Test plan

- [x] \`pnpm --filter @readied/desktop typecheck\` — green (includes new
\`typecheck:e2e\`).
- [x] \`pnpm test\` — 17/17 packages.
- [ ] Run \`pnpm --filter @readied/desktop build && pnpm --filter
@readied/desktop e2e\` locally on macOS to confirm both specs pass
against the real bundle. Adjust selectors / bridge shape if reality
drifts from the assumed types.
- [ ] Push to a feature branch and watch the CI \`e2e\` job. Confirm
artifacts upload on intentional failure.

## Stack context

**PR-E** in the audit stack. Stacked on top of #276 (PR-F3) → #275
(PR-F2) → #274 (PR-F5) → #273 (PR-F4) → #272 (PR-F1) → #271 (PR-I) →
#270 (PR-J) → #269 (PR-D) → #268 (PR-H) → #267 (PR-C) → #266 (PR-A) →
#265 (PR-B). 13 PRs deep.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 8, 2026
…es (PR-G) (#278)

## Summary

**First scoped slice of the PR-G \"split god files\" effort.**

\`apps/desktop/src/main/index.ts\` goes from **1065 → 950 lines**
(-11%). No behavior change. All extractions are self-contained and
surface-area preserving — no API changes, no runtime touch.

## What moved

| New file | Was at | Notes |
|---|---|---|
| \`services/fileLicenseStorage.ts\` | \`index.ts:146–216\` | Three
\`readJsonOrNull\` helpers fold the repeated try/catch/return-null
pattern into one private function. Header comment points at PR-F3 so the
next step (move to signed envelopes) is unambiguous. |
| \`services/windowState.ts\` | \`index.ts:218–254\` | Header comment
now documents *why* the file I/O is synchronous (called during
\`BrowserWindow\` construction before the renderer mounts, and during
close where handlers don't await). |

## What this PR DELIBERATELY does NOT touch

Per your earlier guidance about merge-conflict risk:

| File | Why deferred |
|---|---|
| \`packages/storage-sqlite/src/repositories/SQLiteNoteRepository.ts\`
(1121 L) | Splitting into \`NoteCrudRepository\` + \`NoteTagRepository\`
+ \`NoteArchiveRepository\` + \`NoteSyncRepository\` needs runtime
verification against a real DB. Risk of merge conflicts with every other
in-flight PR. Separate effort. |
| \`apps/desktop/src/renderer/components/MarkdownEditor.tsx\` (724 L) |
The CodeMirror surface is too entangled to split without an E2E suite to
catch regressions. Defer until PR-E (#277) is stabilized; then refactor
under test coverage. |

The principle is established here: surgical extractions of
self-contained pieces, each verifying typecheck+tests, no behavior
change. Future PRs can keep slicing under the same discipline.

## Test plan

- [x] \`pnpm -r typecheck\` — green
- [x] \`pnpm test\` — 17/17 packages
- [x] \`pnpm --filter @readied/desktop typecheck\` — green (includes the
e2e tsconfig from #277)
- [ ] Manual smoke: launch the app, confirm window remembers its
position after close+reopen, confirm license/trial files still
read/write correctly

## Stack context

**PR-G** in the audit stack. Stacked on top of #277 (PR-E) → #276
(PR-F3) → #275 (PR-F2) → #274 (PR-F5) → #273 (PR-F4) → #272 (PR-F1) →
#271 (PR-I) → #270 (PR-J) → #269 (PR-D) → #268 (PR-H) → #267 (PR-C) →
#266 (PR-A) → #265 (PR-B). **14 PRs deep.**

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 9, 2026
## Summary

First slice of the knip cleanup. Knip surfaced 28 unused files in #267.
After per-file verification (greps, import tracing, barrel resolution),
**12 were genuinely orphaned** — those are deleted here. The rest stay
for reasons documented below.

## Deleted (12 files, 659 lines)

### \`apps/desktop\`

| File | Why safe |
|---|---|
| \`src/renderer/analytics.ts\` | No imports anywhere |
| \`src/renderer/hooks/useTheme.ts\` | No imports anywhere |
| \`src/renderer/settings.tsx\` | No imports anywhere (the real settings
entry is \`pages/settings/SettingsApp.tsx\`, loaded via dynamic import
in \`main.tsx\`) |
| \`src/renderer/ui/patterns/Modal.tsx\` + \`index.ts\` +
\`Modal.module.css\` + \`.gitkeep\` | Only the barrel imported
\`Modal\`, and the barrel itself was unused — both go together |

### \`packages\`

| File | Why safe |
|---|---|
| \`plugin-api/src/editor/types.ts\` | Not re-exported by
\`plugin-api/src/index.ts\` |
|
\`storage-core/src/{interfaces,migrations,repositories,types}/index.ts\`
| Main package index imports directly from concrete files, not these
barrels |
| \`storage-sqlite/src/repositories/index.ts\` | Same — concrete
imports, no barrel use |

### \`scripts\`

| File | Why safe |
|---|---|
| \`scripts/bump-version.js\` | Not referenced by any \`package.json\`
script or CI workflow |

## Kept (Knip false positives)

### Auto-discovered files Knip can't see

| File | Why kept |
|---|---|
| \`apps/desktop/src/renderer/vite-env.d.ts\` | \`/// <reference
types="vite/client" />\` — required by Vite |
| \`apps/desktop/src/renderer/css-modules.d.ts\` | TypeScript module
shim for CSS module imports |
| \`apps/desktop/src/renderer/turndown-plugin-gfm.d.ts\` | TypeScript
module shim for an untyped npm dep |
| \`apps/web/mdx-components.tsx\` | Next.js convention —
auto-discovered, never imported |

### Knip wrong about being unused

| File | Reality |
|---|---|
|
\`renderer/pages/settings/components/controls/{NumberInput,Select,TextInput,Toggle}.tsx\`
| Imported by EditorSection / AiSection / UpdatesSection **through** the
\`controls/index.ts\` barrel. Knip flagged both the components and the
barrel as unused because it doesn't follow the chain. |

## Skipped (out of audit scope)

apps/web cleanup: \`magicui/*\`, \`NavDropdown\`, \`ui/separator\`. The
audit excluded apps/web; leaving them for a separate marketing-site
pass.

## Test plan

- [x] \`pnpm -r typecheck\` — green
- [x] \`pnpm test\` — 17/17 packages
- [ ] Manual smoke after merge

## What's next in this cleanup track

- **PR-Knip-2**: remove unused production + dev dependencies (the
\`Unused dependencies (9)\` block from knip)
- **PR-Knip-3**: remove unused exports (the \`Unused exports (~100)\`
block) — much more careful, since some exports may be public API
contracts for plugins or external consumers

## Stack context

Stacked on top of #278 (PR-G) → #277 (PR-E) → #276 (PR-F3) → #275
(PR-F2) → #274 (PR-F5) → #273 (PR-F4) → #272 (PR-F1) → #271 (PR-I) →
#270 (PR-J) → #269 (PR-D) → #268 (PR-H) → #267 (PR-C) → #266 (PR-A) →
#265 (PR-B). **15 PRs deep.**

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 9, 2026
## Summary

Phase 2 of knip cleanup. Knip flagged 11 unused dependencies in #267;
per-dep verification confirmed **6 are genuinely unused** in our scope.
Removing them shrinks the lockfile by ~120 lines.

## Removed

### \`apps/desktop\` production

| Dep | Why safe |
|---|---|
| \`highlight.js\` | We use \`rehype-highlight\`, which depends on
\`lowlight\` (NOT \`highlight.js\` directly). \`lowlight\` ships its own
grammars. |
| \`pino-roll\` | \`logger.ts\` writes directly via
\`createWriteStream\` — no pino transport / no rolling pipeline |
| \`react-resizable-panels\` | No imports anywhere |
| \`unist-util-visit\` | No imports anywhere (the only consumer is
\`@readied/wikilinks\`, which declares it itself) |

### \`apps/desktop\` dev

| Dep | Why safe |
|---|---|
| \`@types/mdast\` | No imports anywhere |
| \`pino-pretty\` | \`logger.ts\` doesn't pipe through a transport; dev
output is raw JSON via the synchronous pino factory |

### \`packages\`

| Package | Dep removed | Why |
|---|---|---|
| \`@readied/sync-core\` | \`@readied/core\` | No imports of
\`@readied/core\` in \`src/*\` — workspace link was stale |
| \`@readied/wikilinks\` | \`unified\` | \`src/*\` uses
\`unist-util-visit\` directly (kept) but never \`unified\` |

## Skipped (out of audit scope)

\`apps/web\`: \`@radix-ui/react-separator\`, \`next-themes\`,
\`tailwindcss-animate\` — marketing site, separate pass.

## Test plan

- [x] \`pnpm install --ignore-scripts\` — succeeds. Pre-existing peer
warnings about electron-builder / electron-vite are unrelated.
- [x] \`pnpm -r typecheck\` — green.
- [x] \`pnpm test\` — 17/17 packages.
- [ ] Manual smoke on the desktop app after merge — verify nothing
transitively depended on highlight.js / pino-roll / etc. that grep
missed.

## Stack context

Stacked on **PR-Knip-1** (#279) → PR-G (#278) → PR-E (#277) → ... down
to PR-B (#265). 16 PRs deep.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 9, 2026
…ge layer (PR-F3-Wiring) (#281)

## Summary

Wires the verification primitive from #276 into the desktop's
\`FileLicenseStorage\`. The path from server → disk → in-memory now has
a real Ed25519 verification step at read time, with **lenient
fallthrough** during the migration window.

## Changes

### \`packages/licensing/src/types.ts\`

\`StoredSubscriptionData\` now allows an optional \`signedEnvelope\`:

\`\`\`ts
interface StoredSubscriptionData {
  readonly subscription: SubscriptionInfo;
  readonly lastVerified: string;
  readonly cacheExpiresAt: string;
  readonly signedEnvelope?: SignedSubscriptionEnvelope; // NEW
}
\`\`\`

Both can coexist during migration. Long-term, the unsigned
\`subscription\` field becomes a derived view of the envelope payload.

### \`apps/desktop/src/main/services/fileLicenseStorage.ts\`

\`readSubscriptionData\` now branches on envelope presence:

| State on disk | Behavior |
|---|---|
| **Envelope present + signature valid** | Return cache as-is |
| **Envelope present + signature invalid** | Log error, **refuse the
cache** (return null). Next caller fetches fresh from the API |
| **Envelope absent** | Log warning, accept the cache (lenient migration
mode) |

Plus a \`SUBSCRIPTION_PUBLIC_KEY\` constant at the top of the file with
a **REPLACE BEFORE SHIPPING** note. The all-zeros placeholder means any
real envelope will fail verification — that's the *correct* failure mode
while the placeholder is in place. The lenient "no envelope" branch is
what runs today.

## What's NOT in this PR (still pending the server team)

- \`ApiClient.getSubscriptionStatus\` does not yet return an envelope.
When it does, \`mapApiToSubscriptionData\` in \`licenseHandlers.ts\`
grows one more field (\`signedEnvelope\`) and \`writeSubscriptionData\`
persists it. That change is one-liner plumbing, blocked only on the API
contract being updated.
- The placeholder public key gets swapped for the real server key in the
release that first ships signed envelopes.

## Behavior matrix

| Disk state | Today (placeholder key) | After server rollout (real key)
|
|---|---|---|
| Empty | null returned, no log | null returned, no log |
| Old shape (no envelope) | Warning logged, accepted | Warning logged,
accepted (until strict mode) |
| Real envelope, valid sig | Cannot occur (server isn't signing yet) |
Silently accepted |
| Real envelope, invalid sig | Will fail (placeholder key) → refused →
refetch | Refused → refetch |
| Tampered envelope | Refused → refetch | Refused → refetch |

## Test plan

- [x] \`pnpm -r typecheck\` — green
- [x] \`pnpm test\` — 17/17 packages (the 18 signature tests from #276
cover the verify path; this PR just calls into them)
- [ ] Manual: edit \`subscription.json\` to flip a single byte in the
cached \`subscription.subscriptionId\` field. Reopen the app. Once
server signs envelopes: expect error log + refetch. Today: warning log +
cache accepted (no envelope to verify).

## Stack context

Stacked on **PR-Knip-2** (#280) → PR-Knip-1 (#279) → PR-G (#278) → PR-E
(#277) → ... down to PR-B (#265). **17 PRs deep.**

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 9, 2026
…Repository (#282)

## Summary

**Phase 1 of the SQLiteNoteRepository split.** Pure helpers come out to
their own file so future sub-repositories (sync, tag, archive) can reuse
them without inheriting from the 1000-line class.

| Metric | Before | After |
|---|---|---|
| \`SQLiteNoteRepository.ts\` | 1121 lines | **1038 lines** (-7%) |
| \`noteMapping.ts\` | — | 133 lines (new) |

## Extracted

| Symbol | Kind | Notes |
|---|---|---|
| \`NoteRow\`, \`TagRow\`, \`TagWithColorRow\`, \`BacklinkInfo\` | Row
types | Re-exported by SQLiteNoteRepository so external imports keep
working |
| \`rowToNote(row, tags) -> Note\` | Pure mapper | Reconstructs a domain
Note from a SQLite row + its tags |
| \`prepareFtsQuery(input) -> string\` | Pure helper | FTS5 query
escaper + tokenizer |
| \`archivedConditionSql(filter, alias) -> string\` | Pure helper | SQL
fragment for archived filtering |

Call sites swapped from \`this.<helper>()\` to plain function imports.
**The public class signature is unchanged** — \`BacklinkInfo\` is
re-exported so external consumers (e.g.
\`apps/desktop/src/main/handlers/types.ts\`) keep working without edits.

## What this PR DELIBERATELY does NOT do

- **Extract sync methods** (\`getPendingChanges\` through
\`getSyncHistory\` ~430 lines) into a \`SQLiteNoteSyncRepository\`.
Those share state — tag queries, transactions, FTS sync triggers — with
the main class in ways that need real-DB integration coverage to
refactor safely. The helpers extracted here are the foundation: a
follow-up PR can build the sync sub-repo on top of them without touching
the helpers again.
- **Extract tag methods** (\`setManualTags\`, \`renameTag\`,
\`getAllTagsWithColors\`, etc.) for the same reason.

The audit aspired to a 4-way split (NoteCrudRepository +
NoteTagRepository + NoteArchiveRepository + NoteSyncRepository). That
remains the destination. This PR ships the **foundation** that makes
those splits low-risk; each can ride its own PR with focused review.

## Test plan

- [x] \`pnpm -r typecheck\` — green
- [x] \`pnpm test\` — 17/17 packages
- [ ] Manual: launch the desktop, exercise notes CRUD + search + tag
operations. Behavior should be identical (no observable change).

## Stack context

Stacked on **PR-F3 wiring** (#281) → PR-Knip-2 (#280) → PR-Knip-1 (#279)
→ PR-G (#278) → PR-E (#277) → ... down to PR-B (#265). **18 PRs deep.**

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 9, 2026
## Summary

**Phase 1 of the MarkdownEditor split.** Pure theme + markdown
HighlightStyle move to their own file so further extractions (extensions
array, keymap bindings) can ride on top without merging against a
churn-prone component shell.

| Metric | Before | After |
|---|---|---|
| \`MarkdownEditor.tsx\` | 737 lines | **612 lines** (-17%) |
| \`editorTheme.ts\` | — | 139 lines (new) |

## Extracted

| Symbol | Kind |
|---|---|
| \`SCROLL_PAST_END_PADDING\` | constant |
| \`createEditorTheme(fontSize, fontFamily, lineHeight)\` | factory
returning \`EditorView.theme({...})\` |
| \`markdownHighlighting\` | \`HighlightStyle.define([...])\` with all
tag styles for markdown |

## Imports cleaned up

- Drop \`HighlightStyle\` from \`@codemirror/language\` import (no
longer referenced in this file)
- Drop \`tags\` from \`@lezer/highlight\` (moved into editorTheme.ts)

## What this PR DELIBERATELY does NOT do

- **Extract the extensions array** (~96 lines of \`createExtensions\`).
It closes over user settings (\`lineNumbersCompartment\` etc.) and mixes
context-coupled values like \`wikilinkAutocomplete\` from a hook. Safely
pulling that out requires either passing the closure context via a
builder, or moving the whole \`useMemo\` into its own hook. **Better
done under Playwright coverage (PR-E #277) so renderer regressions
surface.**
- **Extract the keymap.** Same reason — bindings reference
view-imperatives + the command-registry which are constructed inside the
React tree.

## Test plan

- [x] \`pnpm -r typecheck\` — green (renderer + e2e tsconfigs)
- [x] \`pnpm test\` — 17/17 packages
- [ ] Manual: open the editor, type in a markdown note with headings,
emphasis, lists, code blocks. The look must be identical (theme is
byte-for-byte the same; just moved).

## Stack context

Stacked on **note repo split** (#282) → PR-F3 wiring (#281) → PR-Knip-2
(#280) → PR-Knip-1 (#279) → PR-G (#278) → PR-E (#277) → ... down to PR-B
(#265). **19 PRs deep.**

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 9, 2026
## Summary

Replaces the all-zeros placeholder from #281 with a real Ed25519 public
key. The matching private key was generated in this session and is held
outside the repo — see notes below.

## Public key (this PR)

\`\`\`
d049019b2ff05ccfd3802e0619d5897e21431a6f946af724c13ed7ecca7ec01f
\`\`\`

Public by design — the client needs it to verify. Safe to commit.

## Private key (NOT in this PR)

Lives only on the licensing server. Tomy received it in the dev session
that produced this PR and is responsible for storing it as the server's
\`LICENSE_SIGNING_PRIVATE_KEY\` env var (Vercel env / Doppler /
1Password / wherever your secrets live).

**This keypair was generated in a dev chat session and is considered
"dev/staging-grade".** For production, rotate before the first signed
envelope ships: generate a new pair on a trusted machine, push a new
desktop release with the new public key here, then switch the server.
The rotation procedure is documented in the constant's comment.

## Behavior change

| State | Before (placeholder) | After (real key) |
|---|---|---|
| Cache without envelope | Warning logged, accepted (lenient) | Warning
logged, accepted (lenient) |
| Cache with envelope signed by matching key | Cannot occur | Silently
accepted |
| Cache with envelope signed by wrong key | Refused → refetch | Refused
→ refetch |
| Cache with tampered envelope | Refused → refetch | Refused → refetch |

The lenient branch (no envelope) is unchanged — existing users are
unaffected. The strict branch (envelope present) now actually works:
real envelopes verify, fakes get rejected.

## Test plan

- [x] \`pnpm --filter @readied/desktop typecheck:main\` — green
- [ ] After merge: server team uses \`signSubscriptionPayload(payload,
LICENSE_SIGNING_PRIVATE_KEY)\` from \`@readied/licensing\`. Round-trip
test:
\`\`\`bash
cd packages/licensing
node -e "
import('./src/validator.js').then(async ({ signSubscriptionPayload,
verifySubscriptionSignature }) => {
  const PRIV = process.env.LICENSE_SIGNING_PRIVATE_KEY;
  const env = await signSubscriptionPayload({
    payloadVersion: 1,
subscription: { subscriptionId:'sub_test', customerId:'cus_test',
email:'x@x.com', plan:'monthly', status:'active', currentPeriodStart:
new Date().toISOString(), currentPeriodEnd: new
Date(Date.now()+30*86400e3).toISOString(), cancelAtPeriodEnd:false },
    issuedAt: new Date().toISOString(),
  }, PRIV);
const r = await verifySubscriptionSignature(env, { publicKey:
'd049019b2ff05ccfd3802e0619d5897e21431a6f946af724c13ed7ecca7ec01f' });
  console.log(r.valid ? '✅' : '❌ ' + r.error);
});
"
\`\`\`

## Stack context

**PR-Set-Key** at the tip of the stack. Sits on top of editor split
(#283) → note repo split (#282) → ... down to PR-B (#265). **20 PRs
deep.**

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 9, 2026
## Why this PR

The audit stack (#266 through #284) was structured as 19 stacked PRs,
each one's base pointing at its predecessor in the chain. Each merged
into **its own parent branch**, not into \`develop\`. Net result:
\`develop\` only contains #265, and the other 19 PRs are stranded on
their branch tips.

This PR ships the head of the stack
(\`chore/set-subscription-public-key\`) into \`develop\` so the work
actually lands. It's a single PR with **21 commits** — the linear chain
of the stack — preserving each individual PR's conventional commit
message so semantic-release can categorise them for the next release.

## What's in this PR (in merge order)

| # | Commit | Title |
|---|---|---|
| 1 | \`3412e4e\` | fix(typecheck): unblock pnpm -r typecheck after TS
6.x bump (already merged as squash in develop — no-op overlap) |
| 2 | \`03da9cb\` | fix(editor): stop runtime crashes in MarkdownEditor
|
| 3 | \`07a76d6\` | chore(tooling): replace husky with lefthook, add
knip, tighten lint-staged |
| 4 | \`e27f1ba\` | refactor(stores): use named selectors instead of
destructuring full state |
| 5 | \`c519fc3\` | chore(test): add coverage baseline + smoke tests for
@readied/commands |
| 6 | \`5b1cc55\` | chore(mcp-server): migrate to registerTool API +
FTS5 for read_note |
| 7 | \`0eb49a5\` | fix(backup): integrity-check restored db and roll
back on failure |
| 8 | \`402e280\` | refactor(ipc): add typed IPC registry, migrate
aiKeyHandlers as proof |
| 9 | \`dd4823d\` | refactor(ipc): migrate light handlers to
defineIpcHandler |
| 10 | \`5607100\` | refactor(ipc): migrate heavy data handlers to
defineIpcHandler |
| 11 | \`c51c4d3\` | fix(aiKeyStorage): stop deleting encrypted keys on
transient decrypt errors |
| 12 | \`d5f33ef\` | feat(licensing): add Ed25519 subscription envelope
sign + verify |
| 13 | \`5ba96d4\` | feat(e2e): scaffold Playwright Electron suite + CI
job |
| 14 | \`2bd1396\` | refactor(main): extract FileLicenseStorage and
window state to services |
| 15 | \`fd9b809\` | chore(knip): delete verified-unused files (phase 1)
|
| 16 | \`a3d7c1b\` | chore(knip): remove unused dependencies (phase 2) |
| 17 | \`0ec48b3\` | feat(license): wire Ed25519 signed-envelope
verification at the storage layer |
| 18 | \`cca7e04\` | refactor(storage-sqlite): extract noteMapping
helpers from SQLiteNoteRepository |
| 19 | \`1a0df95\` | refactor(editor): extract theme + highlight from
MarkdownEditor |
| 20 | \`9034c71\` | chore(license): set real SUBSCRIPTION_PUBLIC_KEY |
| 21 | \`390503c\` | docs(api): document LICENSE_SIGNING_PRIVATE_KEY
secret requirement |

Each is already individually reviewed and merged on GitHub (#266#284).
They appear here as their original commits because the stack used
rebase-based stacking, not merge commits.

## Pre-merge verification (local, this branch)

- ✅ \`pnpm -r typecheck\` — green across 18 workspace projects
- ✅ \`pnpm test\` — 17/17 packages
- ✅ \`pnpm build\` — 6/6 packages

## After this PR merges

Per the release flow in \`CLAUDE.md\`:

1. Open \`develop → main\` PR
2. Click \"Run workflow\" on Release action — \`semantic-release\`
analyses these conventional commits and bumps the version
3. Tag push triggers Build workflow — mac/win/linux in parallel
4. All builds green → release un-drafts → electron-updater serves the
update

## Notable behaviour changes for users

- **Editor**: no more blank-window crash on notes with tables (#266) —
root cause was \`Decoration.replace\` over multi-line ranges from a
ViewPlugin instead of a StateField
- **AI keys**: no more silent deletion of keys when keychain is
temporarily locked after sleep/wake (#275)
- **Backups**: corrupt restored DB is now refused and rolled back to a
safety copy (#271)
- **Subscriptions**: client now verifies Ed25519 server signatures
(#281, #284); server-side signing rollout still needed for full effect
- **Tooling**: lefthook replaces husky, knip available for dead-code
audits (#267)
- **Infrastructure**: full IPC surface now validated with Zod at the
boundary (#272 + #273 + #274), Playwright scaffold in place (#277)

## Notable for reviewers

- The placeholder \`SUBSCRIPTION_PUBLIC_KEY\` in #281 was replaced with
a real key in #284. The matching private key is set in Cloudflare as
\`LICENSE_SIGNING_PRIVATE_KEY\` (prod + staging). This keypair was
generated in a Claude session and is dev/staging-grade — rotate before
serving real paid customers.
- 50% of audit findings were Knip false positives or already-fixed
(documented per PR). Stack reflects real debt, not the audit verbatim.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added end-to-end testing for the desktop application with
comprehensive smoke and notes testing.
* Implemented IPC handler validation using Zod schemas for improved type
safety.
* Added subscription envelope signing and verification using Ed25519
cryptography.

* **Bug Fixes**
  * Improved encryption error handling and recovery logic.

* **Tests**
* Established centralized test coverage configuration across all
packages.
* Expanded test suites for markdown commands and licensing
functionality.

* **Chores**
  * Transitioned from Husky to Lefthook for Git hooks management.
  * Refactored internal IPC architecture and service modules.
  * Updated build and deployment configurations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant