Skip to content

Release: develop を main にマージ#793

Merged
otomatty merged 23 commits into
mainfrom
develop
May 2, 2026
Merged

Release: develop を main にマージ#793
otomatty merged 23 commits into
mainfrom
develop

Conversation

@otomatty
Copy link
Copy Markdown
Owner

@otomatty otomatty commented Apr 28, 2026

概要

develop にマージ済みの新機能・不具合修正・i18n 対応・Dependabot による依存更新を main に取り込むリリース PR です。ハッシュタグ入力ルール、タグサジェストポップオーバー、ノートのドメインアクセス共有タブ、admin / クライアント全体の国際化(i18n)対応、エディタ周辺の修正などが中心です。DB マイグレーション・環境変数の追加はありません。

変更点

領域 主な変更
src/components/editor/ ハッシュタグ入力ルール (#766/#769)、タグサジェストポップオーバー (#778)、見出し階層の修正 (#777)、スラッシュコマンド見出し ID 整合 (#783/#790)、AI 生成 Markdown から H1 を除去 (#784/#791)
src/components/note/ ドメインアクセス対応の共有モーダルタブ (#663/#792)、autosave とソフト削除の競合修正 (#768/#786)
src/(クライアント全般) i18n フェーズ 2(UI / Mermaid / シード / AI プロンプト)(#779/#787)、エラーメッセージと UI テキストの国際化 (#776)
admin/ 管理画面の i18n 対応(日本語・英語ロケール、AI モデル・監査ログ・ユーザー・Wiki ヘルス画面ほか) (#788)
packages/shared/, packages/claude-sidecar/ 共有定数の追加・サイドカーハンドラ更新
server/api/, server/hocuspocus/, server/mcp/ 依存更新と関連リファクタ・テスト追加
.github/workflows/ Dependabot bun.lock 同期ワークフロー追加 (#785)、release-please-action v4→v5 更新 (#770)
その他 PR #757 のレビュー追従修正 (#764/#765)、JSDoc 自動修正の無効化 (#789)、Dependabot による @hocuspocus/*, @hono/node-server, @anthropic-ai/sdk などの更新

変更の種類

  • 🐛 バグ修正 (Bug fix)
  • ✨ 新機能 (New feature)
  • 💥 破壊的変更 (Breaking change)
  • 📝 ドキュメント (Documentation)
  • 🎨 スタイル/リファクタリング (Style/Refactor)
  • 🧪 テスト (Tests)
  • 🔧 ビルド/CI (Build/CI)

テスト方法

  1. origin/developorigin/main より進んでいることを確認(本 PR の差分が意図どおりであること)。
  2. CI(lint / unit test / drizzle-migration-check ほか)がすべて成功することを確認。
  3. ステージングまたはローカルで以下の主要シナリオを回帰確認:
    • エディタ上でのハッシュタグ入力 (#name) がリアルタイムにタグ化されること、サジェストポップオーバーが動作すること。
    • スラッシュコマンドからの見出し挿入で見出しレベルと ID が一致すること。
    • AI 生成 Markdown を貼り付け/挿入したときに先頭の H1 が除去されること。
    • ノートの共有モーダルから「ドメインアクセス」タブの設定・解除ができること。
    • autosave 中にソフト削除を行っても削除が巻き戻らないこと。
    • クライアントおよび admin の言語切替(日本語 ↔ 英語)が UI 全体に反映されること。
  4. デプロイ後、本番環境でも上記主要シナリオを軽くスモークすること。

チェックリスト

  • テストがすべてパスする(CI を確認)
  • Lint エラーがない(CI を確認)
  • 必要に応じてドキュメントを更新した
  • コミットメッセージが Conventional Commits に従っている

スクリーンショット(UI 変更がある場合)

エディタ(タグ入力・サジェスト・スラッシュ見出し)、ノート共有モーダル(ドメインアクセスタブ)、admin 画面の i18n 切替などに UI 変更があります。必要に応じて Before/After を添付してください。

関連 Issue

Related to #663, #725, #767, #768, #784

含まれる主な PR: #764, #765, #766, #769, #770, #771, #772, #773, #775, #776, #777, #778, #779, #782, #783, #785, #786, #787, #788, #789, #790, #791, #792

Made with Cursor


Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Full EN/JA localization across app and admin, Mermaid diagram generator UI, onboarding tutorial pages, domain-based note-sharing UI, tag suggestion popup, and expanded editor headings (H2–H5).
  • Bug Fixes

    • Removed duplicated AI-produced page-title (leading H1), autosave cancellation to avoid races on delete, and normalized legacy H1 headings to H2.
  • Chores

    • CI/workflow updates and assorted dependency upgrades.

github-actions Bot and others added 20 commits April 26, 2026 14:32
chore: sync main to develop
- claude-sidecar: emit `tool-use-complete` on abort/exception paths when a
  tool is still active, keeping start/complete pairs balanced for downstream
  consumers; add regression tests for both paths.
- server/mcp tools test: normalize a malformed `/** ... /` helper comment to
  standard multiline JSDoc.
- useMermaidGenerator tests: switch shared mock reset from `vi.clearAllMocks`
  to `vi.resetAllMocks` so per-test `mockImplementation`/`mockResolvedValue`
  no longer leak between cases.
- useMermaidGenerator tests: realign "synchronous throws" case to use a true
  sync throw, and add a separate async-rejection case so the title matches the
  setup.
- admin useConfirmDialogs tests: add English to Japanese-only `it(...)` titles
  to match the JP/EN bilingual convention.

Co-authored-by: Claude <noreply@anthropic.com>
* feat(editor): add tag input rule so typed `#name` becomes a styled mark (#766)

Phase 1 (#725) only registered `addPasteRules`, so live typing in the
editor never produced the styled tag mark — only paste / pre-saved JSON
did. Add `addInputRules` with a custom `InputRule` (not `markInputRule`)
so the user-typed terminator (whitespace / CJK punctuation) is preserved
instead of being deleted, and the same exclusion contract as the paste
rule (`extractTagName` / `isExcludedTagName`) is reused so reject
reasons stay colocated.

* test(editor): use StarterKit in tag input-rule integration tests

knip CI flagged `@tiptap/extension-{document,paragraph,text,code}` as
unlisted dependencies because they are transitive deps via
`@tiptap/starter-kit`. Switch the test editor to depend on `StarterKit`
(already a direct dep used elsewhere in the codebase) so the test no
longer reaches into transitive packages, and relax the `# Heading`
assertion to only check that no tag mark is produced (StarterKit's
heading input rule may legitimately fire on `# `).

* feat(editor): broaden tag input-rule terminator to any non-name char

Address Gemini review feedback on PR #769: the previous terminator set
`[\s、。,.!?:;]` missed parentheses, brackets, quotes, and similar
boundary characters, so `(#tag)` / `"#tag"` / `[#tag]` never triggered
the input rule. Replace it with `[^TAG_NAME_CHAR_CLASS]` so any
character outside the tag-name class closes the tag — matching how
Twitter / GitHub treat hashtags. Add regex tests for parentheses /
brackets / quotes / `「」` and an editor integration test for `(#tag)`.

---------

Co-authored-by: Claude <noreply@anthropic.com>
…#776)

* refactor(i18n): ベタ書き日本語の主要箇所を i18n キーに置き換え

react-i18next を導入済みだが、ソース内に残っていた日本語ベタ書きを
errors / common 名前空間を中心に i18n 化する。

- common.json に untitledPage / sharedNoteContext / newConversation /
  date / search / wikiIndex キーを追加
- errors.json にトースト・例外メッセージ用キーを大幅追加
  (画像アップロード、AI 接続テスト、AI サービス、ストリーミング、
  Mermaid 生成、サムネイル commit、Web Clipper、コンテンツ移行 等)
- aiChat.json に context.disable / enable を追加

主な置換対象:
- src/pages/NotePageView.tsx: タイトル保存失敗 toast / loading 表示
  (useTranslation 導入済みなのにベタ書きが残っていた)
- src/pages/Notes.tsx: signInRequired のフォールバック削除
- src/pages/SearchResults.tsx: 検索結果ヘッダー・空状態・最小文字数
- src/pages/IndexPage.tsx: Wiki Index ページ全体
- src/components/ai-chat/*: 「新しい会話」「無題のページ」「コンテキスト
  有効/無効」
- src/hooks/*: useImageUpload / useMermaidGenerator / useTitleValidation
  / useStorageSettings / useAISettings / useAIChatExecute(Regenerate)
  / useGlobalSearch
- src/lib/aiClient.ts: 接続テスト系メッセージ
- src/lib/aiService.ts / aiServiceServer.ts / aiServiceModels.ts:
  API 呼び出しエラー、ストリーミングエラー
- src/lib/aiSettings.ts / storageSettings.ts: 保存失敗エラー
- src/lib/thumbnailCommit.ts: 画像保存タイムアウト等
- src/lib/webClipper/getClipErrorMessage.ts: クリップエラー全般
- src/lib/contentUtils.ts: 無題のページ / コンテンツ移行警告
- src/lib/dateUtils.ts: 今日 / 昨日 / たった今 等の相対時刻
  (日本語/英語で date-fns ロケール切り替え)

非 React 環境からは @/i18n を直接 import して i18n.t() を呼び出す
パターンを採用 (ImageCreateDialog や slashCommandItems と同じ方式)。

generalSettings.ts は i18n の初期化時に loadGeneralSettings を読み込む
ため、循環依存を避けて元のままとする。

テスト更新:
- src/pages/NotePageView.test.tsx: i18n モックがキーをそのまま返す
  実装に合わせ、期待値をキー文字列へ更新

* refactor(i18n): レビュー指摘の反映と Unit Tests の修復

PR #776 の自動レビュー(Gemini / Codex / CodeRabbit / Devin)と
Unit Tests CI 失敗を反映する。

### Unit Tests CI 修復
- AIChatMessage.test.tsx / MermaidGeneratorDialog.test.tsx の
  `react-i18next` モックに `initReactI18next` を追加。lib (例:
  contentUtils / aiSettings) が `@/i18n` を直接 import するように
  なったため、モックでも export が必要になった。
- PageEditorHeader.test.tsx に `useTranslation` モックを追加し、
  `editor.savedAt` テンプレートを再現。

### dateUtils
- `getActiveLocale` を export し、戻り値を `Locale` で明示
  (date-fns/locale)。日本語以外は `enUS` にフォールバック。
- 日付フォーマット文字列 (`M月d日(E)` / `MMM d (EEE)` / `M/d` /
  `yyyy年M月d日 HH:mm`) を common.json の `date.format.*` に移動。
- JSDoc の英語複数形誤記 "3 day ago" → "3 days ago" を修正。

### 複数形対応
- `common.search.resultsCount` と `common.date.{minutes,hours,days}Ago`
  に i18next の `_one` / `_other` 形式を導入。"1 day ago" / "5 days ago"
  のような英語の複数形が正しく出るようにする。

### IndexPage
- `Date#toLocaleString()` をやめ、`date-fns/format` +
  `getActiveLocale()` で `common.date.format.fullDateTime` を使う
  ように変更(ブラウザロケールではなくアプリの言語設定に従う)。

### contentUtils
- `parts.join("、")` を locale-aware にし、`common.listSeparator`
  を介して英語ロケールでは `, ` を使うようにする。

### useGlobalSearch / SearchResults
- 共有検索結果のタイトルが空文字列の場合のフォールバックを
  `??` から `r.title?.trim() ? r.title : t(...)` に修正。

### webClipper/getClipErrorMessage
- 上流エラー文言が日本語固定だった分岐に英語フラグメントを追加し、
  ロケール非依存のマッチングへ拡張。

### thumbnailCommit
- HTTP エラー時のフォールバックメッセージを `errors.imageSaveFailed`
  経由に変更(ja/en それぞれを定義)。

### aiServiceModels
- ネットワーク例外・レスポンス読み取り失敗時のメッセージを
  `errors.{networkUnreachable,requestFailed,responseReadFailed}` に
  分離して i18n 化。

### PageEditorHeader
- `{formatTimeAgo(lastSaved)}に保存` の文字列連結を
  `t("editor.savedAt", { relative: ... })` に置き換え(英語ロケールで
  「5 min agoに保存」のような言語混在を防止)。

### キー名の修正
- `errors.seedTestRunFailed` のタイポ(「seed」は無関係)を
  `errors.testRunFailed` に改名。useAISettings / useStorageSettings の
  参照箇所も合わせて更新。

### 自動生成された空 JSDoc の除去
- pre-commit hook の eslint --fix で挿入された空 JSDoc ブロックを
  aiChatInputHelpers.ts / useMermaidGenerator.ts / useStorageSettings.ts
  から除去(合計 58 ブロック)。

---------

Co-authored-by: Claude <noreply@anthropic.com>
Bumps [googleapis/release-please-action](https://github.com/googleapis/release-please-action) from 4 to 5.
- [Release notes](https://github.com/googleapis/release-please-action/releases)
- [Changelog](https://github.com/googleapis/release-please-action/blob/main/CHANGELOG.md)
- [Commits](googleapis/release-please-action@v4...v5)

---
updated-dependencies:
- dependency-name: googleapis/release-please-action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@hocuspocus/server](https://github.com/ueberdosis/hocuspocus) from 3.4.4 to 4.0.0.
- [Release notes](https://github.com/ueberdosis/hocuspocus/releases)
- [Changelog](https://github.com/ueberdosis/hocuspocus/blob/main/CHANGELOG.md)
- [Commits](ueberdosis/hocuspocus@v3.4.4...v4.0.0)

---
updated-dependencies:
- dependency-name: "@hocuspocus/server"
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Manual replacement for dependabot #774, which only updated
package.json and left server/api/bun.lock out of sync, causing
`bun install --frozen-lockfile` (and hence "API Type Check") to
fail in CI. This commit bumps both package.json and the lockfile
together.

v2 only drops Node.js 18 support and removes the unused
`@hono/node-server/vercel` adapter; the public `serve` API is
unchanged, so no application code needs to be modified.

依存関係更新の手動反映: dependabot #774 は package.json のみを
更新し bun.lock が同期されないため CI が落ちていた。本コミット
では両者をまとめて更新する。v2 の破壊的変更は Node.js 18 の
サポート終了と未使用の Vercel アダプタ削除のみで、`serve` API
は互換のためアプリ側のコード変更は不要。

Co-authored-by: Claude <noreply@anthropic.com>
* fix(editor,layout): single page h1; body headings h2–h4; migrate legacy h1

- Demote header logo from h1; wrap page title input in h1
- StarterKit heading levels 2–4; slash commands map to 2/3/4
- markdownToTiptap: #/##/### → levels 2/3/4; sanitize + HeadingLevelClamp for old data
- Fix aiChatActionHelpers test for ## → level 3

Made-with: Cursor

* fix: address PR #777 review comments

- HeadingLevelClamp: skip inline descendants once a heading is reached
  (Gemini #1) and clean up empty JSDoc blocks left by editor auto-fix.
- markdownToTiptap: drop empty JSDoc blocks scattered through the file
  and add a real TSDoc on the converter (Gemini #2/#3).
- Extend body heading hierarchy to four levels: StarterKit levels [2,3,4,5]
  and Markdown #/##/###/#### -> 2/3/4/5; add tiptap-editor h4/h5 styles
  to src/index.css so deeper headings stay visually distinct (Codex #4).
- htmlToTiptap: revert parsing levels to [1,2,3] to match server-side
  articleExtractor.ts so clipped <h1> survives generateJSON; the
  client-side clamp (HeadingLevelClamp + sanitizeTiptapContent) handles
  level 1 -> 2 demotion (Devin #5). Adds a regression test.

Made-with: Cursor

* refactor(editor): leave `# X` markdown lines as literal paragraphs

Previously `# X` was mapped to a body heading at level 2, which (a) collided
semantically with the page title input that already owns the document's only
`<h1>`, and (b) broke round-trip with `markdownExport.ts` (`"#".repeat(level)`
emits `##` for level 2, so re-importing shifted the hierarchy down by one).

New mapping (`convertMarkdownToTiptapContent`):
- `# X`  → preserved verbatim as a `# X` text paragraph (no heading conversion)
- `## X` → heading level 2
- `### X` → heading level 3
- `#### X` → heading level 4
- `##### X` → heading level 5

`markdownExport.ts` now falls back to `##` (the minimum body heading level on
the editor schema) when a heading node has no/legacy `level: 1`, keeping the
exporter symmetric with the importer.

Tests updated:
- `markdownToTiptap.test.ts`: assert `# X` stays as a literal paragraph and
  `##/###/####/#####` map to levels 2–5.
- `markdownExport.test.ts`: replace the level-1 case with the level 2–5 set
  and add a regression for the legacy/missing-level fallback to `##`.
- `aiChatActionHelpers.test.ts`: realign `## Heading` expectation to level 2.

Made-with: Cursor
…772)

Bumps [@hocuspocus/extension-redis](https://github.com/ueberdosis/hocuspocus) from 3.4.4 to 4.0.0.
- [Release notes](https://github.com/ueberdosis/hocuspocus/releases)
- [Changelog](https://github.com/ueberdosis/hocuspocus/blob/main/CHANGELOG.md)
- [Commits](ueberdosis/hocuspocus@v3.4.4...v4.0.0)

---
updated-dependencies:
- dependency-name: "@hocuspocus/extension-redis"
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…r-and-patch group (#773)

* chore(deps): bump @anthropic-ai/sdk in the minor-and-patch group

Bumps the minor-and-patch group with 1 update: [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript).


Updates `@anthropic-ai/sdk` from 0.90.0 to 0.91.1
- [Release notes](https://github.com/anthropics/anthropic-sdk-typescript/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/CHANGELOG.md)
- [Commits](anthropics/anthropic-sdk-typescript@sdk-v0.90.0...sdk-v0.91.1)

---
updated-dependencies:
- dependency-name: "@anthropic-ai/sdk"
  dependency-version: 0.91.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): sync bun.lock with @anthropic-ai/sdk 0.91.1

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
* chore(deps): bump @hocuspocus/provider from 3.4.4 to 4.0.0

Bumps [@hocuspocus/provider](https://github.com/ueberdosis/hocuspocus) from 3.4.4 to 4.0.0.
- [Release notes](https://github.com/ueberdosis/hocuspocus/releases)
- [Changelog](https://github.com/ueberdosis/hocuspocus/blob/main/CHANGELOG.md)
- [Commits](ueberdosis/hocuspocus@v3.4.4...v4.0.0)

---
updated-dependencies:
- dependency-name: "@hocuspocus/provider"
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): sync bun.lock with @hocuspocus/provider 4.0.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
* feat(editor): add tag suggestion (#name) plugin (Phase 2)

Adds a hashtag autocomplete experience comparable to the existing WikiLink
suggestion plugin: typing `#` opens an in-place picker scoped to the same
namespace as page titles, and confirming inserts a fully-formed `tag` mark
with resolved `exists` / `targetId` attributes.

- New `TagSuggestionPlugin` mirrors `WikiLinkSuggestionPlugin`'s shape:
  query detection, `.tag-typing` decoration (CSS already lived in
  `src/index.css`), and Esc / explicit close handling.
- Candidate sources: scope-aware page titles via `useWikiLinkCandidates`
  plus existing ghost tag names from `getGhostLinks` filtered by
  `linkType === 'tag'`. Combined and case-insensitively de-duplicated by
  `useTagCandidates`. The `/api/tags` endpoint stays out of scope per
  issue #767.
- `TagSuggestion` React popover handles arrow keys / Enter / Tab / Esc,
  showing existing tags with `Hash` and an inline create option for new
  ones (parallels `WikiLinkSuggestion`).
- Coordinated with `TagExtension.addInputRules` (#766): while the
  popover is active the input rule short-circuits to avoid double-marking
  on confirm. After Esc the rule fires normally — the documented
  Esc-then-terminator fallback contract.
- Wired through `editorConfig`, `useSuggestionControllers`,
  `useSuggestionEffects`, `useEditorSetup`, `useTiptapEditorController`,
  and `TiptapEditor` so `<TagSuggestionLayer>` renders alongside the
  existing WikiLink / Slash layers.

Tests cover plugin trigger / non-trigger conditions, code-block + inline
code suppression, the deferral contract with the input rule, the
fallback Esc-then-terminator path, and the new state utility comparator.

Refs: #767, parent #725, depends on #766

* fix(editor): address PR #778 tag suggestion review feedback

Addresses review feedback on issue #767 (Phase 2):

- Fix bug where an exact-match candidate that fell outside the first
  MAX_VISIBLE substring matches got dropped from the visible list while
  still suppressing the "create new" fallback, leaving the user unable to
  select the exact tag (gemini-code-assist HIGH). Items now sort exact
  matches to the front before slicing.

- Wrap items computation in useMemo so it only recalculates when
  candidates / query change (gemini-code-assist).

- Drop unused `editor` prop from `TagSuggestionProps` (coderabbitai nit).

- Gate `useTagCandidates`' ghost-tag react-query call with an `enabled`
  option; `TagSuggestionLayer` only enables it while the popover is
  active so we do not hit IndexedDB until the user types `#`
  (gemini-code-assist).

- Fix stale doc reference in `useTagCandidates` (composes
  `useWikiLinkCandidates`, not `useWikiLinkExistsChecker`)
  (coderabbitai).

- Replace empty JSDoc blocks (auto-inserted by lint-staged --fix on the
  previous commit) on `isSameSuggestionRange`,
  `isSameWikiLinkSuggestionState`, and `isSameSlashSuggestionState`
  with bilingual descriptions explaining each function's contract
  (gemini-code-assist + coderabbitai).

Adds `TagSuggestion.test.tsx` pinning the items-list contract — in
particular regression coverage for the exact-match-outside-MAX_VISIBLE
bug — and the keyboard handle (Enter / Tab confirm, Escape closes,
Escape works even when the items list is empty).

* fix(editor): scope tag ghost-link probe + add arrow-key tests

Addresses two follow-up review comments on PR #778:

- devin: `handleTagSuggestionSelect` was calling `checkReferenced(item.name,
  pageId)` which delegates to `useCheckGhostLinkReferenced`. That hook in
  turn called `repo.getGhostLinkSources(linkText)` without a `linkType`
  argument, so the lookup defaulted to `"wiki"` and never found tag
  ghost-link entries (which are stored with `linkType: "tag"`). The
  resulting `referenced` value was always `false`, so ghost tags rendered
  with `tag-ghost` styling instead of the correct `tag-referenced`
  styling whenever the same `#name` already appeared on other pages.
  Extends `checkReferenced` with an optional `linkType` (default
  `"wiki"` for backward compatibility) and forwards it to
  `getGhostLinkSources`. The tag confirm path now passes `"tag"`
  explicitly. The WikiLink confirm path is unchanged (uses the default).

- coderabbit nit: extends `TagSuggestion.test.tsx` with arrow-key
  navigation coverage — ArrowUp wrap from first to last, ArrowDown wrap
  from last to first, single-step ArrowDown, and a single-candidate
  no-op test. A small `pressKey` helper wraps `act` so consecutive key
  presses see the up-to-date imperative handle, and a
  `settleInitialMicrotasks` helper drains the component's
  `queueMicrotask` reset before navigation tests start.

Tests pinning the linkType contract (`useSuggestionEffects.test.ts`)
assert `checkReferenced` is invoked with `("newtag", "page-1", "tag")`.

---------

Co-authored-by: Claude <noreply@anthropic.com>
* ci: auto-sync bun.lock for Dependabot PRs

Dependabot uses the npm ecosystem and cannot regenerate Bun's bun.lock,
so its PRs touch only package.json. CI then fails immediately at
`bun install --frozen-lockfile` because the lockfile drifts. The new
workflow detects affected dependabot PRs, runs bun install in each
changed directory, and pushes the regenerated lockfile back to the PR
branch so CI can pass without manual intervention.

* ci: cover admin and server/mcp in bun.lock sync workflow

Address PR review: dependabot has historically modified admin/package.json
(PR #374) even though admin is not currently listed in dependabot.yml. Add
admin/ and server/mcp/ to both the `on.paths` filter and the install
directory list so the workflow stays robust against past behavior and any
future expansions of dependabot.yml. Both directories already ship their
own bun.lock.

* ci: correct loop-prevention rationale comment for bun.lock sync

Address PR review (devin): the previous comment claimed the paths filter
prevents the workflow from retriggering after a bun.lock-only push, but
the pull_request paths filter actually evaluates the entire PR diff
(base vs head), so the package.json change always remains in scope. The
real safeguard is GitHub Actions' built-in rule that pushes performed
with GITHUB_TOKEN do not trigger new workflow runs. Update the comment
to reflect the correct mechanism and warn future maintainers about the
implications of switching to a PAT or GitHub App token.

---------

Co-authored-by: Claude <claude@anthropic.com>
* fix(editor): cancel pending autosave before delete to prevent untitled page resurrect (#768)

`useEditorAutoSave` の unmount flush が `usePageDeletion` の論理削除と
並走し、IndexedDB 上で `isDeleted: true` を非削除メタで上書きして
「無題のページ」が `/home` に復活するレースを修正する。

- `useEditorAutoSave.cancelPendingSave` を公開し、保留中 debounce と
  unmount flush を抑止する。
- `usePageDeletion` の `handleDelete` / `handleBack` /
  `handleConfirmDelete` / `handleOpenDuplicatePage` で、
  `deletePageMutation.mutate` の直前に `cancelPendingSave` を呼ぶ。
- 安全弁として `StorageAdapterPageRepository.updatePage` で論理削除済み
  ページに対する update を no-op にする(将来 `getPage` 仕様変更時の
  ガード)。

Closes #768

https://claude.ai/code/session_01WE1NLZbYwDG4qkAaSGqoP7

* fix(editor): defer cancelPendingSave to onSuccess in handleDelete (#786 Codex P2)

`handleDelete` の `onError` ではユーザーがエディタに残るため、mutate 前に
`cancelPendingSave` を呼ぶと削除失敗時に保留中の autosave が落ちて最近の
編集が失われる(Codex P2 指摘)。`/home` へ navigate する直前
(`onSuccess` 内)でのみキャンセルするよう変更。

他の削除ハンドラ (`handleBack` / `handleConfirmDelete` /
`handleOpenDuplicatePage`) は mutate 直後に navigate して `onError` 分岐を
持たないため、issue #768 の修正どおり mutate の前にキャンセルしたまま。

加えて CodeRabbit の指摘どおり、`cancelPendingSave` のテストに
`shouldBlockSave: true` で content-only debounce 経路をスケジュールする
カバレッジを追加(既存の `not.toHaveBeenCalled()` だけでは vacuous
だったため)。

https://claude.ai/code/session_01WE1NLZbYwDG4qkAaSGqoP7

* docs(editor): add bilingual JSDoc to usePageDeletion describing cancelPendingSave contract (#786)

CodeRabbit の指摘どおり、エクスポート関数 `usePageDeletion` の JSDoc を
英語のみから日英併記に変更し、issue #768 で導入した `cancelPendingSave`
の呼び出し契約(各削除パスで mutate 直前に呼ぶ/`handleDelete` だけは
失敗時の保留保存を守るため `onSuccess` 内でのみ呼ぶ)を明記。
プロジェクト規約 (CLAUDE.md) のバイリンガルコメント要件に合わせた
スペック寄りのドキュメント。

https://claude.ai/code/session_01WE1NLZbYwDG4qkAaSGqoP7

---------

Co-authored-by: Claude <noreply@anthropic.com>
…lations (#788)

* feat(admin): introduce react-i18next for ja/en localisation (#780)

- Add `react-i18next` / `i18next` / `i18next-browser-languagedetector`
  to the admin workspace and wire init via `admin/src/i18n/`.
- Share the `zedi-i18next-lng` localStorage key with the main app so
  language preference stays consistent across surfaces.
- Replace hardcoded Japanese strings in Layout, Login, ConfirmActionDialog,
  ActivityLog, and the users / audit-logs / wiki-health / ai-models pages
  with `useTranslation()` lookups; ja resources mirror the previous copy
  and en resources provide an English translation.
- Locale JSON is namespaced per domain (common, nav, auth, users, audit,
  wikiHealth, activityLog, aiModels) following the main app's layout.
- Force JA in the admin vitest setup so existing assertions keep working.

* chore(admin): clean stub jsdoc blocks injected by autofix

Pre-commit lint-fix added empty `/** */` blocks above every const/state
declaration inside `AiModels` because the exported function lacked a
top-level description. Add a proper bilingual JSDoc on the export so the
auto-fixer no longer triggers, and remove the noise it produced.

---------

Co-authored-by: Claude <noreply@anthropic.com>
* fix(eslint): disable jsdoc/require-jsdoc auto-fixer (#781)

Stop `eslint --fix` (run via lint-staged) from auto-inserting empty
JSDoc stubs. The generated `/**\n *\n */` blocks contradicted
`jsdoc/no-blank-block-descriptions` and were repeatedly flagged as
review noise on PR #776. The warning is kept so contributors are still
nudged to document public APIs, but blocks must now be written
manually.

* fix(eslint): scope JSDoc contexts to exports, align severities (#781)

Address Gemini review feedback on PR #789:

- Wrap each `contexts` selector with `ExportNamedDeclaration > ...` so
  `jsdoc/require-jsdoc` no longer warns on internal, non-exported type
  aliases / interfaces / enums / variable declarations. `publicOnly`
  only filters the default `require` targets, leaving custom contexts
  unfiltered. Lint warning count dropped from 911 to 560.
- Lower `jsdoc/no-blank-block-descriptions` from `error` to `warn` to
  match `jsdoc/require-jsdoc`. Treating empty descriptions as a CI
  failure while missing JSDoc is only a warning created an inverted
  incentive (writing partial docs is punished harder than writing none).

---------

Co-authored-by: Claude <noreply@anthropic.com>
* feat(i18n): phase 2 shortcuts, mermaid, seed, and ai prompt strings (#779)

- Add ja/en domains: shortcuts, seedData, mermaid, aiPrompt; wire i18n index
- UI: keyboard shortcuts, mermaid, pages, notes, wiki-link, settings summaries
- Use common.page.* keys; mermaid not-configured via editor.slash.mermaid
- Tiptap placeholders, page menu, content parse i18n
- lib: seedTutorialPages, aiChatPrompt, mermaidGenerator; generalSettings types
- Tests: I18nextProvider, ja, header mocks, mermaid

Made-with: Cursor

* fix: address PR #787 review comments

Made-with: Cursor

* fix(i18n): restore missing exports stripped by jsdoc cleanup

PR #787 のレビュー対応で空 JSDoc を削除した際に、`NoteCard` /
`MermaidGeneratorDialog` / `AI_CHAT_ACTION_FORMAT_EN` の `export` 修飾子も
巻き込んで削除されており、Build / Unit Tests / Mutation (light) の 3 ジョブが
"Missing export" / "Element type is invalid" で失敗していた。
3 つの宣言に `export` を戻し、`aiChatPromptActionFormat.ts` に残った空 JSDoc も除去する。

Restore export modifiers that were accidentally removed alongside empty
JSDoc blocks during the PR #787 review-comment fixes. The build, unit-test
and mutation-light CI jobs were failing with "Missing export" /
"Element type is invalid" because `NoteCard`, `MermaidGeneratorDialog`,
and `AI_CHAT_ACTION_FORMAT_EN` were declared without `export`. Also drops
the leftover empty JSDoc in `aiChatPromptActionFormat.ts`.

Made-with: Cursor

* test(page): wire i18n provider into GhostLinkCard tests

i18n Phase 2 で `GhostLinkCard` の "クリックしてページを作成" を
`t("common.page.ghostCreateHint")` に置き換えたが、対応するテストに
`I18nextProvider` と `await i18n.changeLanguage("ja")` を追加し忘れており、
CI の Unit Tests ジョブで `should show creation prompt` が
`Unable to find element with text` で失敗していた。
他の i18n 対応済みテスト(`MermaidGeneratorNotConfiguredView.test.tsx` 等)と
同じヘルパー `renderWithI18n` で揃え、日本語ロケールを保証する。

Wrap the `GhostLinkCard` tests with `I18nextProvider` and ensure
`i18n.changeLanguage("ja")` resolves before each test, matching the
pattern already used by other Phase 2 component tests. The CI Unit Tests
job was failing on `should show creation prompt` because the test
expected the localized "クリックしてページを作成" string while the test
environment had no i18n provider, so `t()` returned the raw key.

Made-with: Cursor
…ls (#783) (#790)

PR #777 で本文 schema を `levels: [2, 3, 4, 5]` に縮小した一方、Slash コマンドの
ID/i18n ラベルは `heading1/2/3` のまま残り、表示と挿入結果が 1 つずれていた。
ID・ラベル・アイコンを実 level に揃え、`heading5` も追加して schema が許す
全レベルを Slash から到達可能にした。

Since PR #777 reduced the body schema to `levels: [2, 3, 4, 5]`, the slash
heading IDs and i18n labels still read `heading1/2/3` while the actions
inserted level 2/3/4 — a one-off mismatch users could see in the DOM. This
change renumbers the slash items to `heading2..heading5` (with matching
`Heading2..Heading5` icons and labels) and adds a `heading5` entry so every
schema-allowed body heading can be inserted from the slash menu.

Co-authored-by: Claude <noreply@anthropic.com>
* fix(ai): scrub stray `# Title` from AI markdown so it never reaches the body (issue #784)

After PR #777 the Markdown → Tiptap converter keeps `# X` as a literal paragraph
because the page-title input owns the document's only h1. AI chat actions, wiki
auto-generation, agent slash commands, and chat-driven page bodies all funnel
markdown through that converter, so any stray `# Title` line emitted by the
model surfaced in the body as a literal `# Title` paragraph.

This change adds a defensive `dropLeadingH1` option to the converter (default
off, preserving the human-input round-trip with `markdownExport.ts`) and turns
it on only for AI-fed paths. Prompts for the wiki generator, chat-action
content fields, and chat-page outline generator now also explicitly forbid
emitting a leading `# {Title}` body heading, and the wiki generator's closing
template was restructured to use a `<page_title>` block instead of
`## タイトル / {{title}}`, which the model sometimes mirrored.

Regression tests cover the option, the prompt instructions, and the chat-action
append path.

* refactor(markdownToTiptap): normalize line endings once instead of twice

`stripLeadingH1Line` and `convertMarkdownToTiptapContent` both ran the same
`\r\n?` → `\n` replacement, so CRLF input was normalized twice. Move the
normalization to the converter, document `stripLeadingH1Line`'s LF-only
contract, and have it operate on the already-normalized string.

Behavior is unchanged; addresses gemini-code-assist review on PR #791.

* fix(markdownToTiptap): only strip H1 after blank-only prelude lines

The previous `^(\s*)# (?!#)[^\n]*\n?` regex used `\s*`, which also matches
horizontal whitespace on the first content line. With `dropLeadingH1: true`
wired into AI insert/append paths, an indented line like `"    # comment\nnext"`
would have its first line silently dropped — a content-corruption bug for any
AI output that legitimately starts with an indented `#`.

Restrict the prelude to newline-separated blank-only lines via
`(?:[ \t]*\n)*`, matching the comment's intent. Adds two regression tests:
- indented `# X` is preserved as a paragraph;
- a tab/space-only blank line before `# Title` still works as prelude.

Addresses codex review on PR #791.

---------

Co-authored-by: Claude <noreply@anthropic.com>
* feat(notes): domain access share-modal tab (issue #663)

Wires the existing `note_domain_access` backend (POST/GET/DELETE
/notes/:id/domain-access) into the share modal as a new "domains" tab.
Adds the React Query hook, API client methods, client-side free-email
validation mirror, and Japanese / English i18n strings. Adding an
`editor` rule prompts a confirmation since it grants edit access to
the entire domain; `verifiedAt` stays null in v1, so each row carries
an "unverified" badge until DNS-TXT verification ships.

* fix(notes): address review feedback on domain tab (#792)

- Drop fragile substring parsing of server error messages in
  `submitCreate`; surface the server's `ApiError.message` verbatim as
  the toast description instead.
- Distinguish load failures from empty rule lists by reading `isError`
  from `useDomainAccessForNote` and rendering a dedicated message so
  owners aren't told "no rules" when the request actually failed.
- Reset `activeTab` to "members" if `showDomainsTab` flips to false
  while the domains tab is active, preventing the Tabs control from
  holding a non-existent value and rendering an empty panel.

* refactor(shared): move FREE_EMAIL_DOMAINS to @zedi/shared with drift test

Per AGENTS.md "Sharing constants between server and client" the canonical
copy of values duplicated between client and server must live in
`packages/shared` with a CI drift detector that reads the server source
via `fs.readFileSync` and asserts equality (existing pattern:
`src/lib/tagCharacterClassSync.test.ts`).

- Move `FREE_EMAIL_DOMAINS`, `DOMAIN_REGEX`, `normalizeDomainInput`,
  and validation types to `packages/shared/src/freeEmailDomains.ts`.
- Convert `src/lib/domainValidation.ts` to a thin re-export from
  `@zedi/shared/freeEmailDomains` so existing imports keep working.
- Move the unit tests for `normalizeDomainInput` to
  `packages/shared/src/freeEmailDomains.test.ts` (alongside source of truth).
- Add `src/lib/freeEmailDomainsSync.test.ts` to detect drift between
  `@zedi/shared` and the server-side duplicate
  (`server/api/src/lib/freeEmailDomains.ts`) for both the deny-list and
  the `DOMAIN_REGEX` pattern.
- Document the sync obligation in the server file's header.

---------

Co-authored-by: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

Adds widescale i18n across admin and main apps; implements editor heading-level changes (H2–H5), heading normalization, and tag suggestion UI/plugins; exposes cancelPendingSave and threads it into deletion flows; adds domain-access API/UX with shared free-email validation; introduces markdown H1 drop option for AI flows; expands tests; and updates CI/workflows and several dependencies.

Changes

i18n migration (admin + main app)

Layer / File(s) Summary
Initialization
admin/src/i18n/index.ts, src/i18n/index.ts
Add i18next bootstrap for admin; register many new locale domains and resources.
Resource files
admin/src/i18n/locales/*, src/i18n/locales/*
Add/extend numerous en/ja JSON locale files (common, editor, errors, mermaid, shortcuts, seedData, notes, aiPrompt, aiSettings, etc.).
Entrypoint wiring
admin/src/main.tsx, admin/src/test/setup.ts, many tests
Import i18n in admin entry; ensure tests load shared i18n and default language.
Surface replacements
admin/src/**/*.tsx, src/**/*.tsx, src/hooks/**/*
Replace hardcoded UI strings with useTranslation/t(...) across admin and main app components, pages, and hooks; update test suites to render with real i18n where applicable.
Tests / docs
**/*test*.ts(x)
Adjust and add tests to expect localized outputs and to use i18n test setup.

Editor: heading schema, normalization, and markdown mapping

Layer / File(s) Summary
Data/Representation
src/lib/markdownToTiptap.ts, src/lib/markdownExport.ts
Add ConvertMarkdownToTiptapOptions.dropLeadingH1; treat # H1 as literal paragraph unless dropped; map headings up to ##### → levels 2–5; export side clamps heading <2 → 2.
Normalization extension
src/components/editor/TiptapEditor/headingLevelClampExtension.ts
Add HeadingLevelClamp extension to promote legacy/level-1 headings to level 2 on appendTransaction and on mount.
Editor schema & wiring
src/components/editor/TiptapEditor/editorConfig.ts, src/components/editor/TiptapEditor.tsx
Remove h1 from editable schema (heading levels 2–5), add clamping and options plumbing into createEditorExtensions; resolve placeholder via i18n.
Slash menu / export / CSS
src/components/editor/TiptapEditor/slashCommandItems.ts, src/components/editor/TiptapEditor/slashSuggestionIcons.tsx, src/index.css
Remove Heading1 item, add Heading4/5 items/icons, and add stylesheet rules for h4/h5.
Tests
src/components/editor/**/test.tsx, src/lib/markdownToTiptap.test.ts, src/lib/markdownExport.test.ts
Update/add tests for heading mappings and dropLeadingH1 behavior; add HeadingLevelClamp tests.

Tag suggestion + Tag mark input-rule and plugin

Layer / File(s) Summary
Domain logic & types
src/components/editor/extensions/tagSuggestionPlugin.ts, src/components/editor/extensions/TagExtension.ts
Add TagSuggestionPlugin with public TagSuggestionState and plugin key; add TAG_INPUT_REGEX and input-rule to convert typed #name into tag marks while deferring to suggestion popover when active.
UI component
src/components/editor/extensions/TagSuggestion.tsx, src/components/editor/TiptapEditor/TagSuggestionLayer.tsx
Add TagSuggestion popover with keyboard handling and imperative onKeyDown; add TagSuggestionLayer to render/bridge suggestion UI.
Controllers / comparators
src/components/editor/TiptapEditor/suggestionStateUtils.ts, src/components/editor/TiptapEditor/useSuggestionControllers.ts
Introduce isSameTagSuggestionState and shallow-range comparator; add tag suggestion state/ref and guarded updates.
Effects / selection wiring
src/components/editor/TiptapEditor/useSuggestionEffects.ts, src/components/editor/TiptapEditor/useTiptapEditorController.ts
Handle tag selection, position computation, check referenced ghost tags, and expose select/close handlers.
Candidates hook
src/hooks/useTagCandidates.ts
New hook aggregating existing page candidates and ghost tag names (optionally enabled).
Tests
src/components/editor/**/tagSuggestionPlugin.test.ts, TagSuggestion.testx, TagExtension.test.ts
Add extensive tests covering activation rules, selection, input-rule interactions, and keyboard behavior.

Autosave cancellation and deletion flow

Layer / File(s) Summary
Hook API change
src/components/editor/PageEditor/useEditorAutoSave.ts
Expose cancelPendingSave() on hook return; implementation clears pending timeout and payload.
Consumer wiring
src/components/editor/PageEditor/usePageDeletion.ts, src/components/editor/PageEditor/usePageEditorAutoSaveWithMutation.ts, src/components/editor/PageEditor/usePageEditorStateAndSync.ts
Require/pass cancelPendingSave into deletion flows; call it before delete mutations (except handleDelete calls it on mutation success).
Tests
src/components/editor/PageEditor/useEditorAutoSave.test.ts, usePageDeletion.test.ts
Add tests asserting cancelPendingSave prevents debounced saves and that deletion handlers call cancel in correct order.

Domain-access feature: shared module, API client, hooks, and UI

Layer / File(s) Summary
Canonical validation
packages/shared/src/freeEmailDomains.ts, packages/shared/src/index.ts, packages/shared/package.json
Add FREE_EMAIL_DOMAINS, DOMAIN_REGEX, normalizeDomainInput types/exports; export subpath and re-export from package index.
Client re-export
src/lib/domainValidation.ts
Re-export shared validation utilities for client usage.
API types & client
src/lib/api/types.ts, src/lib/api/apiClient.ts
Add DomainAccessRow & CreateDomainAccessBody types; add listDomainAccess, createDomainAccess, deleteDomainAccess client methods.
Hooks
src/hooks/useDomainAccess.ts
Add domainAccessKeys and React Query hooks: useDomainAccessForNote, useCreateDomainAccess, useDeleteDomainAccess.
Server sync test
src/lib/freeEmailDomainsSync.test.ts, server/api/src/lib/freeEmailDomains.ts
Add CI test to assert shared and server copies of free-email list & regex remain synchronized; document duplication constraints.
UI component
src/pages/NoteView/ShareModal/ShareModalDomainTab.tsx, src/pages/NoteView/ShareModal/NoteShareModal.tsx
Add ShareModalDomainTab with add/remove flows, validation (free-email/format), confirm dialogs, role/readOnly handling; wire domain tab visibility by userRole.
Tests
src/pages/NoteView/ShareModal/ShareModalDomainTab.test.tsx
Add tests for domain tab behaviors, validation, and mutation flows.

AI / Mermaid / prompts localization and H1 handling in AI flows

Layer / File(s) Summary
Prompts & action format
src/lib/aiChatPrompt.ts, src/lib/aiChatPromptActionFormat.ts
Localize system prompt assembly and action-format blocks; extract helper builders and locale-aware blocks.
Mermaid generator
src/lib/mermaidGenerator.ts, src/components/editor/MermaidGenerator*.tsx, src/i18n/locales/*/mermaid.json
Replace static DIAGRAM_TYPES with i18n-backed diagram metadata, add getMermaidDiagramTypes and buildMermaidGeneratorUserPrompt; localize UI and tests.
AI content H1 handling
src/lib/aiChatActionHelpers.ts, src/lib/agentSlashCommands/*, src/hooks/useAIChatActions.ts, src/hooks/useWikiGenerator.ts, src/pages/NotePageView.tsx
Thread dropLeadingH1: true option through AI/chat/wiki flows and agent insertion to strip leading H1 results and avoid title duplication; update tests for regression #784.
Error strings / service messages
src/lib/aiService*.ts, src/lib/aiClient.ts, src/i18n/locales/*/errors.json
Replace many hardcoded error/success messages with i18n lookups across AI service/client/server modules.
Tests
src/lib/aiChatPrompt.test.ts, src/lib/aiChatActionHelpers.test.ts, src/hooks/useAIChatActions.test.ts
Add/regress tests verifying prompts forbid leading H1 and conversion behaves per options.

Workflows, dependencies, lint and small tooling

Layer / File(s) Summary
CI workflows
.github/workflows/dependabot-bun-lock.yml, .github/workflows/release-please.yml
Add Dependabot Bun lock regeneration workflow; bump release-please action v4→v5.
Dependencies
package.json, admin/package.json, server/api/package.json, server/hocuspocus/package.json
Bump several package versions (Anthropic SDK, Hocuspocus, Hono server); add admin i18n deps; remove some transitive deps from hocuspocus package.
ESLint & config
eslint.config.js
Adjust jsdoc rule contexts and fixer behavior, downgrade blank-block description rule to warn.
Shared exports
packages/shared/src/index.ts
Re-export free-email domain utilities and types.
Server Hocuspocus v4 migration
server/hocuspocus/src/index.ts
Migrate bootstrap to HocuspocusServer API and adapt startup/shutdown and HTTP request wiring.

Sequence Diagram(s)

sequenceDiagram
    participant Editor as Editor (Tiptap)
    participant Plugin as TagSuggestionPlugin
    participant UI as TagSuggestion UI
    participant Candidates as useTagCandidates
    Editor->>Plugin: User types "#name" (input)
    Plugin->>Plugin: Match detection → set TagSuggestionState(range, query)
    Plugin-->>UI: onStateChange(state)
    UI->>Candidates: fetch candidates(query)
    Candidates-->>UI: return candidates (pages + ghost tags)
    UI->>Editor: onSelect(name)
    Editor->>Plugin: apply command → replace range with tag mark
    Plugin->>Plugin: clear TagSuggestionState (close)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

"🐰
I hopped through strings and code today,
Tucked headings, tags, and locales in play.
Domains fenced, autosaves stayed clear,
Prompts translated so AI will hear.
A tiny rabbit cheers — the UI speaks two-way!"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements comprehensive internationalization (i18n) for both the admin panel and the main application, replacing hardcoded strings with translation keys and adding locale files for English and Japanese. It introduces a domain-based access control system for notes, including a shared deny-list for free email providers and normalization utilities. Significant enhancements were made to the editor, including a new tag suggestion system, real-time hashtag conversion via input rules, and a heading level clamping extension to maintain document structure consistency. Additionally, the PR fixes a race condition in the autosave logic during page deletion and updates ESLint configurations to manage JSDoc requirements. Feedback focuses on removing numerous empty JSDoc blocks added throughout the codebase, which serve as noise and clutter the implementation.

Comment on lines +15 to +17
/**
*
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

内容が空の JSDoc ブロックがファイル内の多数の箇所(15, 19, 34, 42, 46, 50, 75, 85, 89, 93, 97, 108, 112, 118, 129, 139, 143, 148, 153, 158, 162, 166, 170, 175, 179, 187, 192行目)に追加されています。これらは情報の付加価値がなく、コードの可読性を損なうノイズとなるため、すべて削除することを推奨します。特に、関数内部のローカル変数宣言に JSDoc を付与するのは一般的ではありません。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。これらの空 JSDoc ブロックは、以前有効化していた JSDoc 自動生成ルールの副作用で混入したものです。原因のオートフィクサーは PR #789Disable JSDoc auto-fixer to prevent empty stub generation)で既に無効化済みのため、新規追加は止まっています。本 PR は develop → main のリリース PR のため、ここではコミットを追加せず、develop 上の cleanup PR で残存する空 JSDoc を一括削除する形で対応します。

Comment on lines +32 to +34
/**
*
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

このファイルにも内容のない空の JSDoc ブロックが大量に追加されています(32, 37, 41, 45, 49, 53, 57, 63, 69, 86, 91, 114, 128, 134, 153, 158, 172, 179, 183, 191, 206行目)。これらはコードを冗長にするだけでメリットがないため、削除してください。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。useStorageSettings.ts 内の空 JSDoc ブロックも、以前の JSDoc 自動生成ルールの副作用です。オートフィクサー自体は PR #789 で無効化済みのため新規追加は発生しません。本リリース PR ではコミットを積まず、develop 上の cleanup PR で空 JSDoc 一括削除に含めて対応します。

Comment on lines +10 to +12
/**
*
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

内容のない空の JSDoc ブロックが複数箇所(10, 25, 29, 33, 37, 41, 46, 60, 102行目)に含まれています。ドキュメントとしての役割を果たしていないため、削除を推奨します。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。useMermaidGenerator.ts の空 JSDoc も同様に過去のオートフィクサーの副作用で、原因は PR #789 で無効化済みです。本リリース PR ではコミットを積まず、develop 側の空 JSDoc 一括削除 PR にまとめて対応します。

Copy link
Copy Markdown

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

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 8 additional findings in Devin Review.

Open in Devin Review

<Plus className="text-primary h-4 w-4 shrink-0" />
)}
<span className="truncate">
{item.exists ? `#${item.name}` : `"#${item.name}" を作成`}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Hardcoded Japanese string in new TagSuggestion component bypasses i18n

The newly added TagSuggestion.tsx renders the "create new tag" label as a hardcoded Japanese string "#${item.name}" を作成 at line 190. Every other UI string in this PR (and the codebase at large) is routed through useTranslation() / t(), but this component does not import or call useTranslation. English-locale users will see Japanese text in the tag suggestion popup's "create" row. The component's test (TagSuggestion.test.tsx:125) also asserts against the hardcoded Japanese, confirming this was not an intentional exception.

Prompt for agents
The TagSuggestion component at src/components/editor/extensions/TagSuggestion.tsx renders a hardcoded Japanese string on line 190: `item.exists ? `#${item.name}` : `"#${item.name}" を作成``. This needs to be internationalized.

1. Import `useTranslation` from react-i18next in the component.
2. Inside the forwardRef callback, call `const { t } = useTranslation();`.
3. Replace the hardcoded string with a t() call, e.g. `t("editor.tagSuggestion.createNew", { name: item.name })` or a similar key.
4. Add the corresponding key to both locale files:
   - src/i18n/locales/ja/editor.json (or a new tag-specific namespace): value like `"#{{name}}" を作成`
   - src/i18n/locales/en/editor.json: value like `Create "#{{name}}"`
5. Update the test assertion in src/components/editor/extensions/TagSuggestion.test.tsx line 125 that currently checks for the hardcoded Japanese string `'"#tec" を作成'` to use the real i18n-resolved string or set up the i18n provider in the test wrapper.
Open in Devin Review

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。仰る通り、TagSuggestion.tsx の "create new tag" 行は i18n 化の取りこぼしです。本 PR は develop → main のリリース PR のため修正コミットは積まず、develop 側の後続 PR で次の対応を行います: (1) useTranslation() を import して t() 経由に置換、(2) src/i18n/locales/{ja,en}/notes.json(または該当 namespace)に tagSuggestionCreate キー追加(ja: "#{{name}}" を作成 / en: Create "#{{name}}")、(3) TagSuggestion.test.tsx の固定文字列アサーションを i18n 解決後の値に合わせて更新。

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/hooks/useGlobalSearch.ts (1)

161-184: ⚠️ Potential issue | 🟠 Major

These fallback labels won’t update when the locale changes.

buildGlobalSearchResults() now reads i18n.t(...), but useGlobalSearch() memoizes its output without any language dependency. After a ja/en switch, existing results can keep showing the old fallback title/context until the query or result set changes. Please pass translated labels (or t) into the builder, or include the current language in the memo dependencies.

💡 Suggested direction
-export function buildGlobalSearchResults(
+export function buildGlobalSearchResults(
   personalPages: Page[],
   sharedRows: SharedResultRow[],
   query: string,
   keywords: string[],
+  labels: { untitledPage: string; sharedNoteContext: string },
   limit = 10,
 ): GlobalSearchResultItem[] {
@@
-        title: page.title || i18n.t("common.untitledPage"),
+        title: page.title || labels.untitledPage,
@@
-      title: r.title?.trim() ? r.title : i18n.t("common.untitledPage"),
-      highlightedText: highlightedText || i18n.t("common.sharedNoteContext"),
+      title: r.title?.trim() ? r.title : labels.untitledPage,
+      highlightedText: highlightedText || labels.sharedNoteContext,

Then derive labels from useTranslation() in useGlobalSearch() and include them in the memo deps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useGlobalSearch.ts` around lines 161 - 184,
buildGlobalSearchResults() calls i18n.t(...) to create fallback labels but
useGlobalSearch() memoizes results without a locale dependency, so labels don't
update on language switch; update useGlobalSearch() to derive translations via
useTranslation() (or compute a labels object from t) and pass those translated
labels (or the t function/current language) into buildGlobalSearchResults(), and
include that labels/t/lang value in the useMemo dependency array so
buildGlobalSearchResults() is re-run when the locale changes (refer to
buildGlobalSearchResults, useGlobalSearch, and the i18n.t calls inside the
shared/personal result mapping).
admin/src/pages/ActivityLog.tsx (1)

41-60: ⚠️ Potential issue | 🟡 Minor

Localize formatDetail output to avoid mixed-language rows.

Line 192 renders formatDetail(entry), but formatDetail (Line 41-60) still returns hardcoded English strings (findings, pages / categories, content: ... chars). This leaves Activity Log partially untranslated.

💡 Suggested fix
+import type { TFunction } from "i18next";
 ...
-function formatDetail(entry: ActivityEntry): string {
+function formatDetail(entry: ActivityEntry, t: TFunction): string {
   const detail = entry.detail ?? {};
   if (entry.kind === "lint_run" && typeof detail.total === "number") {
-    return `${detail.total} findings`;
+    return t("activityLog.detail.lintRun", { total: detail.total });
   }
   if (entry.kind === "index_build") {
     const total = typeof detail.totalPages === "number" ? detail.totalPages : "?";
     const cats = typeof detail.categoryCount === "number" ? detail.categoryCount : "?";
-    return `${total} pages / ${cats} categories`;
+    return t("activityLog.detail.indexBuild", { total, cats });
   }
 ...
-    return `content: ${len} chars`;
+    return t("activityLog.detail.schemaUpdate", { len });
   }
   return JSON.stringify(detail);
 }
 ...
-                    {formatDetail(entry)}
+                    {formatDetail(entry, t)}

Also applies to: 192-192

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/src/pages/ActivityLog.tsx` around lines 41 - 60, formatDetail currently
returns hardcoded English strings; change it to accept a translation function
and return localized strings instead: update the signature to
formatDetail(entry: ActivityEntry, t: TFunction) (or similar), replace literal
strings like "findings", "pages / categories", "—", and "content: ... chars"
with t(...) calls using new keys (e.g. "activity.findings",
"activity.pages_categories" with placeholders for totals, "activity.title_url"
or join logic, "activity.empty", "activity.content_chars" for content length),
and then update the call site that renders formatDetail(entry) to pass the
translator from useTranslation() (e.g. formatDetail(entry, t)). Ensure
placeholders are used for numeric values where applicable.
src/components/editor/TiptapEditor/editorConfig.ts (1)

159-175: ⚠️ Potential issue | 🟠 Major

Markdown # shortcut is disabled with the current heading configuration.

With levels: [2, 3, 4, 5], only input rules for ##, ###, ####, and ##### are registered. Typing # produces no heading at all. The inline comment "Markdown #/##/###/#### map to 2/3/4/5" is incorrect—single # no longer works.

HeadingLevelClamp normalizes existing heading nodes but does not restore the missing keyboard shortcut. If the intended UX is "body headings are h2–h5, but single # still works as the top body heading", this regresses Markdown-style authoring.

Fix: Include level 1 in the configured levels and let HeadingLevelClamp demote h1 to h2 immediately after creation or import. This preserves the familiar Markdown input experience while enforcing the h2–h5 constraint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/editor/TiptapEditor/editorConfig.ts` around lines 159 - 175,
The heading config in StarterKit.configure currently omits level 1 so the
Markdown `#` shortcut no longer creates a heading; update the levels array in
StarterKit.configure to include 1 (e.g., levels: [1,2,3,4,5]) so the `#` input
rule is registered, and rely on the existing HeadingLevelClamp plugin to
immediately demote any h1 nodes to h2 after creation/import; this preserves the
`#` shortcut while enforcing the h2–h5 constraint via HeadingLevelClamp.
🟡 Minor comments (16)
admin/src/i18n/locales/ja/auth.json-6-6 (1)

6-6: ⚠️ Potential issue | 🟡 Minor

ja locale has an untranslated loading string.

guardLoading should be Japanese in the Japanese resource to avoid mixed-language UI.

💡 Suggested fix
-  "guardLoading": "Loading..."
+  "guardLoading": "読み込み中..."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/src/i18n/locales/ja/auth.json` at line 6, The Japanese locale's auth
resource has an English value for the key guardLoading; replace the value
"Loading..." with an appropriate Japanese translation (for example "読み込み中..." or
another approved string) in the ja auth.json so the guardLoading entry is fully
localized; update the value for the "guardLoading" key (in the auth locale
object) accordingly.
src/components/editor/MermaidGeneratorFormFields.test.tsx-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor

Restore i18n language after this suite to avoid cross-test leakage.

beforeEach forces global language to "ja" but never restores it, which can make other tests order-dependent.

✅ Minimal isolation fix
-import { describe, it, expect, vi, beforeEach } from "vitest";
+import { describe, it, expect, vi, beforeEach, afterAll } from "vitest";
@@
 describe("MermaidGeneratorFormFields", () => {
+  const originalLanguage = i18n.language;
+
   beforeEach(async () => {
     await i18n.changeLanguage("ja");
   });
+
+  afterAll(async () => {
+    await i18n.changeLanguage(originalLanguage);
+  });

Also applies to: 14-16

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/editor/MermaidGeneratorFormFields.test.tsx` at line 2, The
test suite mutates the global i18n language in beforeEach (setting it to "ja")
and never restores it, causing cross-test leakage; modify the setup to capture
the current language (e.g., const previousLanguage = i18n.language) inside the
beforeEach that sets i18n.changeLanguage("ja"), and add a matching afterEach (or
afterAll) that calls i18n.changeLanguage(previousLanguage) to restore the
original language; reference the existing beforeEach and use afterEach/afterAll
with i18n.changeLanguage to ensure isolation.
src/components/ai-chat/aiChatInputHelpers.ts-15-21 (1)

15-21: ⚠️ Potential issue | 🟡 Minor

Replace placeholder empty JSDoc blocks with meaningful docs (or remove them).

Lines in this range add many empty /** */ blocks, which add noise and don’t satisfy the documentation requirement. Please keep concise JP/EN docs on exported helpers and remove empty internal placeholders.

Proposed cleanup example
-/**
- *
- */
+/**
+ * メンション用のチップ要素を生成する。
+ * Creates a non-editable mention chip element.
+ *
+ * `@param` id - ページID / Page ID
+ * `@param` title - ページタイトル / Page title
+ */
 export function createChipElement(id: string, title: string): HTMLSpanElement {
-  /**
-   *
-   */
   const chip = document.createElement("span");

As per coding guidelines: **/*.{ts,tsx}: “Add TSDoc/JSDoc comments to exported functions, types, and interfaces” and **/*.{ts,tsx,js,md} / **/*.{ts,tsx,js,jsx,json,md}: “Include both Japanese and English comments/documentation in code and documentation files”.

Also applies to: 33-35, 41-51, 74-76, 84-94, 97-99, 106-113, 116-118, 129-131, 138-144, 147-149, 152-154, 157-176, 178-180, 186-193

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ai-chat/aiChatInputHelpers.ts` around lines 15 - 21, The empty
JSDoc blocks (e.g., above the exported helper createChipElement and other helper
functions in this file) should be replaced with concise TSDoc/JSDoc describing
the symbol's purpose, parameters, and return value (include both English and
Japanese per guidelines) or removed if redundant; update all exported
functions/types in aiChatInputHelpers (notably createChipElement and any other
exported helpers in the indicated ranges) to have proper bilingual docblocks and
delete internal placeholder /** */ comments to remove noise.
src/lib/markdownExport.ts-45-53 (1)

45-53: ⚠️ Potential issue | 🟡 Minor

Clamp heading level upper bound to match the documented h2–h5 contract.

Line 52 currently accepts any number >= 2, so invalid legacy values (e.g., 6, 999) generate out-of-schema heading prefixes. Clamp to 2..5.

💡 Suggested fix
-    const level = typeof rawLevel === "number" && rawLevel >= 2 ? rawLevel : 2;
+    const level =
+      typeof rawLevel === "number" ? Math.min(5, Math.max(2, Math.floor(rawLevel))) : 2;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/markdownExport.ts` around lines 45 - 53, The heading level
computation currently allows any number >=2; change the clamp so legacy values
are constrained to the documented 2..5 range: when computing level from rawLevel
(variable rawLevel) set level to rawLevel only if it's a number between 2 and 5
inclusive, otherwise fall back to 2, and then generate prefix from that clamped
level (prefix variable), ensuring functions/locations that reference
rawLevel/level/prefix use the clamped value.
src/lib/contentUtils.ts-250-258 (1)

250-258: ⚠️ Potential issue | 🟡 Minor

Clamp legacy heading levels to the full body-schema range.

This only fixes headings below 2. If old/imported JSON still contains attrs.level = 6, it survives sanitization even though the editor body is now constrained to 2-5, so unsupported heading levels can still leak back into content.

🛠️ Proposed fix
   if (nodeType === "heading") {
     const attrs = { ...((node.attrs as Record<string, unknown> | undefined) ?? {}) };
-    const level = typeof attrs.level === "number" ? attrs.level : 1;
-    if (level < 2) {
-      attrs.level = 2;
+    const rawLevel = typeof attrs.level === "number" ? attrs.level : 1;
+    const normalizedLevel = Math.min(5, Math.max(2, rawLevel));
+    if (attrs.level !== normalizedLevel) {
+      attrs.level = normalizedLevel;
       sanitizedNode.attrs = attrs;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/contentUtils.ts` around lines 250 - 258, The current heading
normalization only forces levels below 2 up to 2 but does not prevent overly
large legacy levels (e.g., 6) from surviving; in the heading branch (nodeType
=== "heading") compute the numeric level from attrs.level (fall back to 1 as
done) and clamp it into the editor's supported range (2–5) before assigning back
to attrs and sanitizedNode.attrs (use Math.min/Math.max or equivalent); update
the logic around attrs, level and sanitizedNode to always set attrs.level =
clampedLevel so no out-of-range heading levels remain.
src/lib/aiChatActionHelpers.test.ts-140-141 (1)

140-141: ⚠️ Potential issue | 🟡 Minor

Add Japanese text for this new inline comment.

The new comment at Line 140 is English-only; please add a Japanese counterpart to keep bilingual documentation consistency.

As per coding guidelines **/*.{ts,tsx,js,md} / **/*.{ts,tsx,js,jsx,json,md}: "Comments and documentation should include both Japanese and English."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/aiChatActionHelpers.test.ts` around lines 140 - 141, The inline
comment "// `##` → Tiptap level 2 (body: ##/###/####/##### = 2/3/4/5; `# X`
stays literal because the page h1 lives in the title field)" is English-only;
add a Japanese translation directly adjacent to this comment (e.g., on the next
line or separated by "/" or "(日本語)") so the comment becomes bilingual, ensuring
the meaning about Tiptap heading level mapping and literal `# X` behavior is
preserved in Japanese.
admin/src/pages/users/UserCard.tsx-81-81 (1)

81-81: ⚠️ Potential issue | 🟡 Minor

Avoid hardcoded ja-JP number formatting in translated UI.

At Line 81, forcing toLocaleString("ja-JP") couples display to Japanese even in English admin UI. Prefer passing raw numeric count (or format using current runtime locale).

♻️ Suggested change
-            {t("users.card.pageCount", { count: user.pageCount.toLocaleString("ja-JP") })}
+            {t("users.card.pageCount", { count: user.pageCount })}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/src/pages/users/UserCard.tsx` at line 81, Replace the hardcoded
Japanese number formatting in the translation call: remove the explicit
toLocaleString("ja-JP") on user.pageCount in UserCard.tsx and instead pass the
raw numeric value into t("users.card.pageCount", { count: user.pageCount }) or
format using the app/runtime locale before passing (e.g., via Intl.NumberFormat
with the current locale); update the expression that currently reads
user.pageCount.toLocaleString("ja-JP") to use the raw number or locale-aware
formatter so translations render correctly for the active UI locale.
src/hooks/useMermaidGenerator.ts-10-12 (1)

10-12: ⚠️ Potential issue | 🟡 Minor

Replace placeholder JSDoc with meaningful bilingual docs.

The exported type/hook docblocks are currently empty. Please provide concise JA/EN descriptions (or remove empty placeholders) so the docs are actually useful.

As per coding guidelines **/*.{ts,tsx}: "Add TSDoc / JSDoc comments to exported functions, types, and interfaces" and **/*.{ts,tsx,js,md}: "Include both Japanese and English comments/documentation in code and documentation files."

Also applies to: 25-27

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useMermaidGenerator.ts` around lines 10 - 12, Replace the empty
JSDoc block with concise bilingual (JA/EN) TSDoc for the exported hook
useMermaidGenerator and any exported types in this file: briefly describe
purpose, inputs/outputs, and usage in English and Japanese (e.g., "Generates
Mermaid diagrams from … / Mermaid 図を … 生成する"), include param/return tags for
functions (e.g., `@param`, `@returns`) and type descriptions for exported types, and
remove any remaining placeholder /** */ blocks so all exported symbols have
meaningful docs per the project's guidelines.
admin/src/pages/audit-logs/AuditLogsContent.tsx-43-43 (1)

43-43: ⚠️ Potential issue | 🟡 Minor

Make the new inline comment bilingual (JA/EN).

The comment at Line 43 is English-only, while this repo requires bilingual comments/documentation in TSX files.

💡 Suggested edit
-/** UI selector sentinel for "all actions" (no filter). */
+/** "全アクション"(フィルタなし)を表す UI センチネル値。 / UI selector sentinel for "all actions" (no filter). */
 const ANY_ACTION = "__any__";

As per coding guidelines **/*.{ts,tsx,js,jsx,json,md}: Comments and documentation should include both Japanese and English.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/src/pages/audit-logs/AuditLogsContent.tsx` at line 43, The inline
comment "UI selector sentinel for \"all actions\" (no filter)." is English-only;
update it to a bilingual comment including Japanese and English, e.g. add a
Japanese translation alongside the existing English text so both languages
appear in the same comment above the same code (keep the original English text
intact and append the Japanese equivalent for the phrase "UI selector sentinel
for 'all actions' (no filter)"). Ensure the exact comment string is preserved
while adding the Japanese version.
admin/src/i18n/locales/ja/aiModels.json-22-22 (1)

22-22: ⚠️ Potential issue | 🟡 Minor

Fix extra whitespace in preview description string.

Line 22 has a double space after されます。 which can render as inconsistent spacing in UI text.

✏️ Proposed fix
-    "description": "新規モデルは追加され、同期対象から外れた既存モデルは非アクティブ化されます。 既存モデルの表示名や料金は上書きされません。Sonnet 系は非アクティブで追加されます。",
+    "description": "新規モデルは追加され、同期対象から外れた既存モデルは非アクティブ化されます。既存モデルの表示名や料金は上書きされません。Sonnet 系は非アクティブで追加されます。",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/src/i18n/locales/ja/aiModels.json` at line 22, The "description" value
in aiModels.json contains an extra double space after "されます。" — open the JSON
entry for the "description" key in admin/src/i18n/locales/ja/aiModels.json and
remove the duplicate space so the sentence reads with single spacing; also scan
the same "description" string for any other unintended consecutive spaces (e.g.,
around "Sonnet 系は非アクティブで追加されます。") and normalize them to single spaces to keep UI
spacing consistent.
admin/src/i18n/locales/en/aiModels.json-16-16 (1)

16-16: ⚠️ Potential issue | 🟡 Minor

Pluralize the English model count.

"{{total}} models" will render as 1 models for the singular case. Please switch this to plural-aware keys before shipping the English admin UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/src/i18n/locales/en/aiModels.json` at line 16, The "summary" string in
aiModels.json uses a single plural form ("{{total}} models") which will render
incorrectly for singular counts; update the "summary" key to use plural-aware
i18n entries (e.g., plural keys or ICU plural syntax) so that total (and
optionally active) render "1 model" vs "2 models"; locate the "summary" key in
aiModels.json and replace the flat string with a pluralized structure that
references the same symbols (total and active) using your project's i18n plural
format.
src/lib/markdownToTiptap.test.ts-71-74 (1)

71-74: ⚠️ Potential issue | 🟡 Minor

Add English mirrors to the remaining JP-only test comments.

The new block comment at Line 71 and the inline comments at Line 119 and Line 150 are Japanese-only, while the surrounding test docs are bilingual. Please mirror those in English as well for consistency.

As per coding guidelines, "Include both Japanese and English comments/documentation in code and documentation files".

Also applies to: 119-120, 150-150

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/markdownToTiptap.test.ts` around lines 71 - 74, The JP-only comments
in the test describing the H1 drop case and two inline comments need English
mirrors: update the block comment containing "AI が `# Title`
行から本文を始めた典型ケース。`dropLeadingH1: true` を渡すと最初の H1 行が除去され…" to include an English
sentence explaining the same (e.g., "Typical case where AI starts body with `#
Title`; with `dropLeadingH1: true` the leading H1 is removed and following `##
Section` becomes h2"), and similarly add English translations for the inline
comments that appear near the `dropLeadingH1` test expectations (the JP comments
at the other two locations) so every comment now has both Japanese and English
equivalents; locate these by searching for the Japanese phrases or the
`dropLeadingH1: true` test block and the short JP inline comments around the
assertions.
src/lib/webClipper/getClipErrorMessage.ts-23-37 (1)

23-37: ⚠️ Potential issue | 🟡 Minor

Specific fetch/proxy classification is shadowed by generic network check.

At Line 23, msg.includes("Failed to fetch") returns webClipNetworkError before Line 32-36 can classify proxy/fetch-page failures as webClipFetchFailed.

🛠️ Suggested fix (check specific case first)
-    if (msg.includes("Failed to fetch") || msg.includes("NetworkError")) {
-      return i18n.t("errors.webClipNetworkError");
-    }
@@
     if (
       msg.includes("プロキシ") ||
       msg.includes("FETCH_FAILED") ||
       /failed to fetch (the )?page|proxy/i.test(msg)
     ) {
       return i18n.t("errors.webClipFetchFailed");
     }
+    if (msg.includes("Failed to fetch") || msg.includes("NetworkError")) {
+      return i18n.t("errors.webClipNetworkError");
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/webClipper/getClipErrorMessage.ts` around lines 23 - 37, The generic
network check in getClipErrorMessage (the msg.includes("Failed to fetch") /
"NetworkError" branch) preempts the more specific proxy/fetch classification
(the FETCH_FAILED / "プロキシ" / /failed to fetch (the )?page|proxy/i branch);
reorder the conditions so the specific fetch/proxy branch runs before the
generic network branch, ensuring msg is tested against the FETCH_FAILED/proxy
regex first and still returning i18n.t("errors.webClipFetchFailed") for those
cases while leaving i18n.t("errors.webClipNetworkError") for other network
errors.
src/i18n/locales/ja/common.json-102-113 (1)

102-113: ⚠️ Potential issue | 🟡 Minor

Japanese locale has untranslated English UI strings.

Line 102 ("Suggestions ({{count}})") and Line 111 ("Unknown error") will show English in JA UI. Please localize these keys for consistency.

🌐 Suggested localization fix
-    "lintTitle": "Suggestions ({{count}})",
+    "lintTitle": "提案 ({{count}})",
@@
-    "lintUnknownError": "Unknown error",
-    "lintFetchFailed": "Suggestions の取得に失敗しました",
-    "suggestionMarkResolved": "この Suggestion を解決済みにする / Mark suggestion as resolved",
+    "lintUnknownError": "不明なエラー",
+    "lintFetchFailed": "提案の取得に失敗しました",
+    "suggestionMarkResolved": "この提案を解決済みにする",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/ja/common.json` around lines 102 - 113, Translate the
remaining English strings in the Japanese locale by updating the values for the
"lintTitle" and "lintUnknownError" keys: replace "Suggestions ({{count}})" with
a proper Japanese string that preserves the placeholder (e.g. "提案({{count}})")
and replace "Unknown error" with "不明なエラー"; ensure you do not alter the
placeholder syntax ({{count}}) or surrounding JSON structure and also scan
nearby keys like "suggestionMarkResolved" for any mixed-language text to keep
the JA locale consistent.
admin/src/pages/users/UsersContent.tsx-91-91 (1)

91-91: ⚠️ Potential issue | 🟡 Minor

Avoid hardcoded ja-JP in a localized admin view.

Line 388 always renders Japanese date format, so English UI still shows Japanese-localized dates. Use the current i18n language instead.

💡 Suggested fix
-  const { t } = useTranslation();
+  const { t, i18n } = useTranslation();
...
-                    date: new Date(confirm.deleteTarget.impact.lastAiUsageAt).toLocaleDateString(
-                      "ja-JP",
-                    ),
+                    date: new Date(confirm.deleteTarget.impact.lastAiUsageAt).toLocaleDateString(
+                      i18n.resolvedLanguage ?? i18n.language,
+                    ),

Also applies to: 387-390

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/src/pages/users/UsersContent.tsx` at line 91, The UsersContent
component is hardcoding 'ja-JP' when formatting dates (lines ~387-390), causing
Japanese date format to appear regardless of UI language; update the date
formatting to use the current i18n language from the useTranslation hook (e.g.,
use i18n.language or the translation hook returned locale) instead of the
literal 'ja-JP' so dates follow the active locale. Locate the
rendering/formatting code in UsersContent (where dates are formatted) and
replace the hardcoded locale string with the dynamic i18n language value
accessed from useTranslation() or the i18n instance so English UI shows
English-formatted dates. Ensure any helper like formatDate or inline
Intl.DateTimeFormat calls reference that dynamic language value.
src/pages/NoteView/ShareModal/ShareModalDomainTab.test.tsx-94-105 (1)

94-105: ⚠️ Potential issue | 🟡 Minor

Restore the jsdom prototype shims after this suite.

This mutates Element.prototype for the whole test worker and never puts the original methods back, so unrelated tests can observe these no-op Radix shims and pass/fail for the wrong reason.

♻️ Suggested fix
-import { describe, it, expect, vi, beforeEach } from "vitest";
+import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from "vitest";
@@
-// Radix UI の Select は PointerEvent / scrollIntoView を直接触るが jsdom には
-// 実装がないので、最低限のスタブを当てる。
-// Radix Select touches PointerEvent / scrollIntoView APIs that jsdom does not
-// implement; stub them so the dropdown can open under test.
-beforeEach(() => {
+// Radix UI の Select は PointerEvent / scrollIntoView を直接触るが jsdom には
+// 実装がないので、最低限のスタブを当てる。
+// Radix Select touches PointerEvent / scrollIntoView APIs that jsdom does not
+// implement; stub them so the dropdown can open under test.
+const originalElementMethods = {
+  hasPointerCapture: Element.prototype.hasPointerCapture,
+  setPointerCapture: Element.prototype.setPointerCapture,
+  releasePointerCapture: Element.prototype.releasePointerCapture,
+  scrollIntoView: Element.prototype.scrollIntoView,
+};
+
+beforeAll(() => {
   Object.assign(Element.prototype, {
     hasPointerCapture: () => false,
     setPointerCapture: () => undefined,
     releasePointerCapture: () => undefined,
     scrollIntoView: () => undefined,
   });
 });
+
+afterAll(() => {
+  Object.assign(Element.prototype, originalElementMethods);
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/NoteView/ShareModal/ShareModalDomainTab.test.tsx` around lines 94 -
105, The test suite mutates Element.prototype in beforeEach (hasPointerCapture,
setPointerCapture, releasePointerCapture, scrollIntoView) and never restores
originals; change the setup to save the original methods into local variables
before assigning stubs, and add an afterEach that restores those original
methods back onto Element.prototype (or deletes the stub if original was
undefined) so other tests aren't affected; keep the beforeEach stubbing of the
four methods but wrap it with saved originals and an afterEach cleanup that
reinstitutes the saved values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/dependabot-bun-lock.yml:
- Around line 50-59: Change the checkout step to stop persisting credentials and
prevent lifecycle script execution during installs: in the actions/checkout
usage (actions/checkout@v6.0.2) add persist-credentials: false and keep
fetch-depth: 0; change the bun install invocation to include --ignore-scripts so
package scripts cannot run; and update the final push step (the step that pushes
lockfile changes) to use an explicit token for auth (e.g., use env
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} or configure remote with that token)
rather than relying on checkout-persisted credentials.

In `@server/hocuspocus/package.json`:
- Around line 12-13: The package.json currently uses caret versions "^4.0.0" for
`@hocuspocus/server` and `@hocuspocus/extension-redis` while the codebase relies on
v3 semantics; either pin both dependencies to the stable v3.4.4 in package.json
or update all Hocuspocus-related code to v4 APIs: adjust transport configuration
to use crossws if needed, migrate hook signatures (onStoreDocument payload: use
lastContext/lastTransactionOrigin; replace Node.js Request/Headers usage with
web-standard Request/Headers), and update any timeout or other breaking-change
usages to v4 defaults; pick one approach and make package.json and related
server/redis hook code consistent and tested in staging.

In `@src/components/editor/extensions/TagSuggestion.tsx`:
- Line 190: The popover row in TagSuggestion.tsx currently hardcodes Japanese
("を作成") when item.exists is false; replace this string concatenation with a
localized message using the project's i18n/localization helper (e.g., call t or
i18n.t) so the text becomes something like t('tag.createLabel', { name:
`#${item.name}` }) or similar; update the JSX expression that renders
{item.exists ? `#${item.name}` : `"#${item.name}" を作成`} to use the localization
key and add the new key to your translation files for supported languages
(include a sensible fallback).

In `@src/components/editor/TiptapEditor/headingLevelClampExtension.ts`:
- Around line 52-53: appendTransaction currently always calls
buildHeadingClampTr(newState) which walks the whole doc even for selection/meta
transactions; guard it by checking the incoming transactions array for actual
document changes (use the _transactions parameter and short-circuit: if none
have docChanged, return null) so buildHeadingClampTr is only invoked when
content changed; keep the existing call to buildHeadingClampTr(newState) but
only after the docChanged check to avoid unnecessary full-document scans on the
typing hot path.

In `@src/hooks/useTagCandidates.ts`:
- Around line 97-104: The hook useTagCandidates currently calls
repo.getGhostLinks(userId) and returns all tag-type ghost links without
respecting the current pageNoteId/scope; update useTagCandidates to restrict
results to the current scope by either calling a scoped repository method (e.g.,
add a getGhostLinks(userId, scope) overload) or post-filtering the returned
array by matching each ghost link's sourcePageId against the scoped pages list
(the same scope logic used by useWikiLinkCandidates), ensuring only ghost links
from the current pageNoteId/scope are included before mapping to linkText.

In `@src/lib/mermaidGenerator.ts`:
- Around line 76-80: The template replacement assumes the placeholder
"__ZEDI_MERMAID_SOURCE__" always exists in the localized string (variable
withTypes derived from t("mermaid.generatorPrompt")), which can silently drop
sourceText if a locale renames/removes it; fix by checking if
withTypes.includes("__ZEDI_MERMAID_SOURCE__") and only doing withTypes.replace
when present, otherwise inject sourceText safely (e.g. append or prepend with a
separator) so the prompt still contains the source; also emit a warning (via
existing logger or console.warn) when the placeholder is missing to aid
localization fixes, referencing withTypes and sourceText in the logic.

In `@src/pages/IndexPage.tsx`:
- Around line 200-205: The date formatting call using format(new
Date(data.timestamp), ...) can throw on malformed timestamps; update the render
guard to parse and validate the date with isValid from date-fns (e.g., const
parsed = new Date(data.timestamp); use isValid(parsed) before calling format)
and only call format when valid; add the isValid import from date-fns and render
a safe fallback (empty string or a placeholder) when validation fails,
referencing the existing symbols data.timestamp, format, t, and getActiveLocale.

---

Outside diff comments:
In `@admin/src/pages/ActivityLog.tsx`:
- Around line 41-60: formatDetail currently returns hardcoded English strings;
change it to accept a translation function and return localized strings instead:
update the signature to formatDetail(entry: ActivityEntry, t: TFunction) (or
similar), replace literal strings like "findings", "pages / categories", "—",
and "content: ... chars" with t(...) calls using new keys (e.g.
"activity.findings", "activity.pages_categories" with placeholders for totals,
"activity.title_url" or join logic, "activity.empty", "activity.content_chars"
for content length), and then update the call site that renders
formatDetail(entry) to pass the translator from useTranslation() (e.g.
formatDetail(entry, t)). Ensure placeholders are used for numeric values where
applicable.

In `@src/components/editor/TiptapEditor/editorConfig.ts`:
- Around line 159-175: The heading config in StarterKit.configure currently
omits level 1 so the Markdown `#` shortcut no longer creates a heading; update
the levels array in StarterKit.configure to include 1 (e.g., levels:
[1,2,3,4,5]) so the `#` input rule is registered, and rely on the existing
HeadingLevelClamp plugin to immediately demote any h1 nodes to h2 after
creation/import; this preserves the `#` shortcut while enforcing the h2–h5
constraint via HeadingLevelClamp.

In `@src/hooks/useGlobalSearch.ts`:
- Around line 161-184: buildGlobalSearchResults() calls i18n.t(...) to create
fallback labels but useGlobalSearch() memoizes results without a locale
dependency, so labels don't update on language switch; update useGlobalSearch()
to derive translations via useTranslation() (or compute a labels object from t)
and pass those translated labels (or the t function/current language) into
buildGlobalSearchResults(), and include that labels/t/lang value in the useMemo
dependency array so buildGlobalSearchResults() is re-run when the locale changes
(refer to buildGlobalSearchResults, useGlobalSearch, and the i18n.t calls inside
the shared/personal result mapping).

---

Minor comments:
In `@admin/src/i18n/locales/en/aiModels.json`:
- Line 16: The "summary" string in aiModels.json uses a single plural form
("{{total}} models") which will render incorrectly for singular counts; update
the "summary" key to use plural-aware i18n entries (e.g., plural keys or ICU
plural syntax) so that total (and optionally active) render "1 model" vs "2
models"; locate the "summary" key in aiModels.json and replace the flat string
with a pluralized structure that references the same symbols (total and active)
using your project's i18n plural format.

In `@admin/src/i18n/locales/ja/aiModels.json`:
- Line 22: The "description" value in aiModels.json contains an extra double
space after "されます。" — open the JSON entry for the "description" key in
admin/src/i18n/locales/ja/aiModels.json and remove the duplicate space so the
sentence reads with single spacing; also scan the same "description" string for
any other unintended consecutive spaces (e.g., around "Sonnet 系は非アクティブで追加されます。")
and normalize them to single spaces to keep UI spacing consistent.

In `@admin/src/i18n/locales/ja/auth.json`:
- Line 6: The Japanese locale's auth resource has an English value for the key
guardLoading; replace the value "Loading..." with an appropriate Japanese
translation (for example "読み込み中..." or another approved string) in the ja
auth.json so the guardLoading entry is fully localized; update the value for the
"guardLoading" key (in the auth locale object) accordingly.

In `@admin/src/pages/audit-logs/AuditLogsContent.tsx`:
- Line 43: The inline comment "UI selector sentinel for \"all actions\" (no
filter)." is English-only; update it to a bilingual comment including Japanese
and English, e.g. add a Japanese translation alongside the existing English text
so both languages appear in the same comment above the same code (keep the
original English text intact and append the Japanese equivalent for the phrase
"UI selector sentinel for 'all actions' (no filter)"). Ensure the exact comment
string is preserved while adding the Japanese version.

In `@admin/src/pages/users/UserCard.tsx`:
- Line 81: Replace the hardcoded Japanese number formatting in the translation
call: remove the explicit toLocaleString("ja-JP") on user.pageCount in
UserCard.tsx and instead pass the raw numeric value into
t("users.card.pageCount", { count: user.pageCount }) or format using the
app/runtime locale before passing (e.g., via Intl.NumberFormat with the current
locale); update the expression that currently reads
user.pageCount.toLocaleString("ja-JP") to use the raw number or locale-aware
formatter so translations render correctly for the active UI locale.

In `@admin/src/pages/users/UsersContent.tsx`:
- Line 91: The UsersContent component is hardcoding 'ja-JP' when formatting
dates (lines ~387-390), causing Japanese date format to appear regardless of UI
language; update the date formatting to use the current i18n language from the
useTranslation hook (e.g., use i18n.language or the translation hook returned
locale) instead of the literal 'ja-JP' so dates follow the active locale. Locate
the rendering/formatting code in UsersContent (where dates are formatted) and
replace the hardcoded locale string with the dynamic i18n language value
accessed from useTranslation() or the i18n instance so English UI shows
English-formatted dates. Ensure any helper like formatDate or inline
Intl.DateTimeFormat calls reference that dynamic language value.

In `@src/components/ai-chat/aiChatInputHelpers.ts`:
- Around line 15-21: The empty JSDoc blocks (e.g., above the exported helper
createChipElement and other helper functions in this file) should be replaced
with concise TSDoc/JSDoc describing the symbol's purpose, parameters, and return
value (include both English and Japanese per guidelines) or removed if
redundant; update all exported functions/types in aiChatInputHelpers (notably
createChipElement and any other exported helpers in the indicated ranges) to
have proper bilingual docblocks and delete internal placeholder /** */ comments
to remove noise.

In `@src/components/editor/MermaidGeneratorFormFields.test.tsx`:
- Line 2: The test suite mutates the global i18n language in beforeEach (setting
it to "ja") and never restores it, causing cross-test leakage; modify the setup
to capture the current language (e.g., const previousLanguage = i18n.language)
inside the beforeEach that sets i18n.changeLanguage("ja"), and add a matching
afterEach (or afterAll) that calls i18n.changeLanguage(previousLanguage) to
restore the original language; reference the existing beforeEach and use
afterEach/afterAll with i18n.changeLanguage to ensure isolation.

In `@src/hooks/useMermaidGenerator.ts`:
- Around line 10-12: Replace the empty JSDoc block with concise bilingual
(JA/EN) TSDoc for the exported hook useMermaidGenerator and any exported types
in this file: briefly describe purpose, inputs/outputs, and usage in English and
Japanese (e.g., "Generates Mermaid diagrams from … / Mermaid 図を … 生成する"),
include param/return tags for functions (e.g., `@param`, `@returns`) and type
descriptions for exported types, and remove any remaining placeholder /** */
blocks so all exported symbols have meaningful docs per the project's
guidelines.

In `@src/i18n/locales/ja/common.json`:
- Around line 102-113: Translate the remaining English strings in the Japanese
locale by updating the values for the "lintTitle" and "lintUnknownError" keys:
replace "Suggestions ({{count}})" with a proper Japanese string that preserves
the placeholder (e.g. "提案({{count}})") and replace "Unknown error" with
"不明なエラー"; ensure you do not alter the placeholder syntax ({{count}}) or
surrounding JSON structure and also scan nearby keys like
"suggestionMarkResolved" for any mixed-language text to keep the JA locale
consistent.

In `@src/lib/aiChatActionHelpers.test.ts`:
- Around line 140-141: The inline comment "// `##` → Tiptap level 2 (body:
##/###/####/##### = 2/3/4/5; `# X` stays literal because the page h1 lives in
the title field)" is English-only; add a Japanese translation directly adjacent
to this comment (e.g., on the next line or separated by "/" or "(日本語)") so the
comment becomes bilingual, ensuring the meaning about Tiptap heading level
mapping and literal `# X` behavior is preserved in Japanese.

In `@src/lib/contentUtils.ts`:
- Around line 250-258: The current heading normalization only forces levels
below 2 up to 2 but does not prevent overly large legacy levels (e.g., 6) from
surviving; in the heading branch (nodeType === "heading") compute the numeric
level from attrs.level (fall back to 1 as done) and clamp it into the editor's
supported range (2–5) before assigning back to attrs and sanitizedNode.attrs
(use Math.min/Math.max or equivalent); update the logic around attrs, level and
sanitizedNode to always set attrs.level = clampedLevel so no out-of-range
heading levels remain.

In `@src/lib/markdownExport.ts`:
- Around line 45-53: The heading level computation currently allows any number
>=2; change the clamp so legacy values are constrained to the documented 2..5
range: when computing level from rawLevel (variable rawLevel) set level to
rawLevel only if it's a number between 2 and 5 inclusive, otherwise fall back to
2, and then generate prefix from that clamped level (prefix variable), ensuring
functions/locations that reference rawLevel/level/prefix use the clamped value.

In `@src/lib/markdownToTiptap.test.ts`:
- Around line 71-74: The JP-only comments in the test describing the H1 drop
case and two inline comments need English mirrors: update the block comment
containing "AI が `# Title` 行から本文を始めた典型ケース。`dropLeadingH1: true` を渡すと最初の H1
行が除去され…" to include an English sentence explaining the same (e.g., "Typical case
where AI starts body with `# Title`; with `dropLeadingH1: true` the leading H1
is removed and following `## Section` becomes h2"), and similarly add English
translations for the inline comments that appear near the `dropLeadingH1` test
expectations (the JP comments at the other two locations) so every comment now
has both Japanese and English equivalents; locate these by searching for the
Japanese phrases or the `dropLeadingH1: true` test block and the short JP inline
comments around the assertions.

In `@src/lib/webClipper/getClipErrorMessage.ts`:
- Around line 23-37: The generic network check in getClipErrorMessage (the
msg.includes("Failed to fetch") / "NetworkError" branch) preempts the more
specific proxy/fetch classification (the FETCH_FAILED / "プロキシ" / /failed to
fetch (the )?page|proxy/i branch); reorder the conditions so the specific
fetch/proxy branch runs before the generic network branch, ensuring msg is
tested against the FETCH_FAILED/proxy regex first and still returning
i18n.t("errors.webClipFetchFailed") for those cases while leaving
i18n.t("errors.webClipNetworkError") for other network errors.

In `@src/pages/NoteView/ShareModal/ShareModalDomainTab.test.tsx`:
- Around line 94-105: The test suite mutates Element.prototype in beforeEach
(hasPointerCapture, setPointerCapture, releasePointerCapture, scrollIntoView)
and never restores originals; change the setup to save the original methods into
local variables before assigning stubs, and add an afterEach that restores those
original methods back onto Element.prototype (or deletes the stub if original
was undefined) so other tests aren't affected; keep the beforeEach stubbing of
the four methods but wrap it with saved originals and an afterEach cleanup that
reinstitutes the saved values.

---

Nitpick comments:
In @.github/workflows/release-please.yml:
- Line 32: Replace the floating tag "uses: googleapis/release-please-action@v5"
with a pinned commit SHA (for example: "uses:
googleapis/release-please-action@<commit-sha> # v5") to ensure reproducible
runs; update the line containing the symbol uses:
googleapis/release-please-action@v5 to use the chosen commit SHA and keep the
original tag as a trailing comment for readability.

In `@admin/src/i18n/locales/en/wikiHealth.json`:
- Line 14: Update the "pagesRelated" localization to avoid singular/plural
awkwardness by replacing the current value with a neutral or plural-aware form;
e.g. change the pagesRelated entry from "{{count}} pages related" to a neutral
string like "{{count}} page(s) related" or, preferably, use your i18n
pluralization format (one/other) so the key pagesRelated correctly renders "1
page related" vs "2 pages related".

In `@admin/src/pages/wiki-health/WikiHealthContent.tsx`:
- Around line 20-27: ALL_RULES as a LintRule[] is permissive and can silently
omit new backend rules; change it to an exhaustive compile-time structure (e.g.,
replace ALL_RULES with a Record<LintRule, true> or a const object keyed by
LintRule) so any missing rule will break the build, then derive a list (e.g.,
ALL_RULE_VALUES = Object.keys(ALL_RULES) as LintRule[]) and use that for
rendering the dropdown/map. Update references to ALL_RULES in this file (and
spread to where the dropdown is rendered) to use the new ALL_RULE_VALUES to
ensure additions to the LintRule union fail fast at compile time.

In `@eslint.config.js`:
- Around line 110-113: The jsdoc/require-jsdoc rule contexts currently only
include named-export contexts ("ExportNamedDeclaration >
TSTypeAliasDeclaration", "ExportNamedDeclaration > TSInterfaceDeclaration",
"ExportNamedDeclaration > TSEnumDeclaration", "ExportNamedDeclaration >
VariableDeclaration"), so add the default-export contexts
"ExportDefaultDeclaration > TSInterfaceDeclaration" and
"ExportDefaultDeclaration > TSEnumDeclaration" to that contexts array so
default-exported interfaces and enums are also enforced; update the same
contexts array used by the jsdoc/require-jsdoc rule to include those two
entries.

In `@packages/claude-sidecar/src/handlers/query.test.ts`:
- Around line 618-653: Add an assertion in the "flushes an active tool with
tool-use-complete when aborted mid-tool" test to ensure the tracker returns to
idle; after the existing expect(parsed()).toEqual([...]) call, assert
tracker.snapshot().status === "idle" (use
expect(tracker.snapshot().status).toBe("idle")) so the test verifies the tracker
lifecycle alongside the balanced tool events for runQuery and the abort path.

In `@packages/shared/src/index.ts`:
- Around line 10-16: The index re-exports the normalization function, turning
the shared package into logic; remove normalizeDomainInput from the export list
so only data/types remain (keep DOMAIN_REGEX, FREE_EMAIL_DOMAINS, type
DomainValidationError, and type DomainValidationResult exported), and move/keep
normalizeDomainInput as an implementation-level function in the app or a
non-shared module; after removing it from the barrel, update any consumers to
import normalizeDomainInput from its original module (or the new app wrapper)
rather than from the shared entrypoint.

In `@server/mcp/src/__tests__/tools/index.test.ts`:
- Around line 50-63: The stubbed registerTool in createServerStub currently has
signature (name: string) => ... but registerAllTools calls registerTool with
three arguments (name, tool definition, handler); update createServerStub so its
registerTool accepts the same parameter shape as the real method (e.g., match
McpServer["registerTool"] or use a rest/Parameters<T> signature) and ignore the
extra args, still pushing name into registered and returning the no-op value;
keep the return cast as needed so types align with McpServer["registerTool"] and
tests remain deterministic.

In `@src/components/editor/extensions/TagSuggestion.test.tsx`:
- Around line 116-134: The test currently asserts the presence/absence of the
create-fallback by matching a hard-coded Japanese CTA string; change it to a
locale-agnostic assertion: in the renderTagSuggestion helper (and the test),
stop using screen.getByText('"#tec" を作成') and instead locate the create option
by a stable selector (e.g., data-testid like "tag-suggestion-create" or by
role/aria-label), or assert behavior by simulating selection and checking the
onSelect callback payload for { name: "tec", exists: false, targetId: undefined
} (or the app's equivalent), using the TagSuggestionCandidate inputs and the
renderTagSuggestion/onSelect mocks to verify the create option appears only when
no exact TagSuggestionCandidate with name "tec" and exists: true is present.

In `@src/components/editor/MermaidGeneratorDialog.tsx`:
- Around line 62-80: The async renderPreview inside the useEffect may update
state after a newer run starts; add a local cancellation guard (e.g., let
cancelled = false or a runId token) scoped to the effect, check it before
calling setPreviewSvg and setPreviewError, and set cancelled = true in the
effect cleanup to prevent stale updates; keep using
getMermaid/mermaid.parse/mermaid.render but bail out if cancelled before any
state mutation or further processing so only the latest render run updates
state.

In `@src/components/editor/MermaidGeneratorNotConfiguredView.test.tsx`:
- Around line 9-11: The helper renderWithI18n used in tests (the function that
wraps UI with <I18nextProvider i18n={i18n}> and returns render(...)) is
duplicated across files; extract it into a shared test utility (e.g., a new
exported function renderWithI18n in a test-utils or test/helpers module) and
update tests like MermaidGeneratorNotConfiguredView.test.tsx to import and use
that shared renderWithI18n instead of defining it locally, ensuring the helper
still references the same i18n instance and I18nextProvider.

In `@src/components/editor/PageEditor/usePageDeletion.test.ts`:
- Around line 45-47: Replace vi.clearAllMocks() with vi.resetAllMocks() in the
test setup to ensure mock implementations (set via mockImplementation) are reset
between tests; update the beforeEach blocks that currently call
vi.clearAllMocks() (the one around the top of the file and the similar one near
lines 182-184) so they call vi.resetAllMocks() instead, ensuring no
implementation leakage across tests that use usePageDeletion.test.ts.

In `@src/components/layout/KeyboardShortcutsDialog.tsx`:
- Around line 45-47: Replace the unstable list key in the
KeyboardShortcutsDialog render: inside the group.shortcuts.map iteration (the
map over group.shortcuts in component KeyboardShortcutsDialog), use the stable
unique identifier shortcut.id as the React key for each shortcut row instead of
the numeric index to prevent identity churn when ordering changes; ensure
shortcut.id is unique and fallback to a stable alternative only if id can be
absent.

In `@src/components/page/LinkSection.tsx`:
- Around line 12-15: Add a meaningful bilingual (English + Chinese) TSDoc block
for the exported function LinkSection describing its purpose and behavior, list
`@param` entries for title, icon, pages, and onPageClick referencing the
LinkSectionProps prop names, include an `@returns` tag that explains the rendered
React element (or void/JSX.Element), and mark it as `@public`; place the comment
immediately above the export function LinkSection declaration so the documented
API is clear in both languages.

In `@src/components/page/PageLinkCard.tsx`:
- Around line 12-15: Add a bilingual (English + Japanese) TSDoc block above the
exported PageLinkCard component that briefly describes the component's purpose
and its props; reference the PageLinkCard function and PageLinkCardProps type,
document the onClick callback and page prop, and include one-line summaries in
both languages plus param tags for page and onClick and a `@returns` note
describing the rendered element.

In `@src/hooks/usePageQueries.ts`:
- Around line 653-657: The exported async function currently declared as
(linkText: string, currentPageId?: string, linkType: LinkType = "wiki"):
Promise<boolean> can be miscalled due to positional ambiguity; change it to
accept a single options object like ({ linkText, currentPageId?, linkType =
"wiki" }: { linkText: string; currentPageId?: string; linkType?: LinkType }):
Promise<boolean>, update all internal callers (e.g., usages like
checkReferenced(...)) to pass a named object, preserve the same defaults and
return type, and adjust any related type exports or overloads so consumers
cannot accidentally pass a string as the wrong positional parameter.

In `@src/hooks/useStorageSettings.ts`:
- Around line 32-38: Replace the empty JSDoc/TSDoc placeholders with real
bilingual (JA/EN) documentation for all exported symbols in this file (starting
with the exported function useStorageSettings and any other exported functions,
types, or interfaces referenced around the empty blocks), or remove the comment
blocks for non-exported locals; ensure exported members have concise English and
Japanese descriptions, parameter and return annotations where applicable, and
follow the repo rule to include both languages in comments.

In `@src/lib/aiChatPrompt.test.ts`:
- Around line 144-146: The test "forbids leading `# Title` headings in
chat-action content fields (issue `#784`)" redundantly calls
i18n.changeLanguage("ja") even though beforeEach already sets the locale; remove
the extra i18n.changeLanguage("ja") invocation on the test (the line immediately
before calling buildSystemPrompt) so the test relies on the beforeEach setup and
simply calls buildSystemPrompt(null, []) and asserts the result.

In `@src/lib/contentUtils.ts`:
- Line 250: The inline comment in src/lib/contentUtils.ts that reads "本文最上位を h2
に揃え、古い Tiptap JSON(level:1 や欠損=1 相当)を 2 へ" must include an English translation
immediately adjacent so comments are bilingual; update the comment near the
logic that normalizes heading levels (look for the comment and code in
contentUtils.ts that aligns top-level headings to h2 and migrates old Tiptap
JSON levels) by adding an English sentence such as "Align top-level content to
h2 and convert old Tiptap JSON (level:1 or missing -> 1) to 2" (or equivalent
clear translation) alongside the original Japanese text.

In `@src/lib/dateUtils.ts`:
- Around line 15-19: getActiveLocale currently reads the global i18n singleton;
make the locale dependency explicit by changing getActiveLocale to accept a
Locale (or optional language string) parameter and returning the matching Locale
instead of reading i18n internally, then update the related helpers (the
functions that produce date, timestamp and pages labels referenced as date /
timestamp / pages in this file) to accept a Locale (or a translate function/t)
parameter and use that passed-in value when selecting ja vs enUS; update all
call sites to pass through the caller's current Locale/language (or
i18n.language) so caching/memoization is safe and the functions remain pure.

In `@src/lib/htmlToTiptap.test.ts`:
- Around line 148-150: Update the English-only regression comment about
preserving <h1> in parsing to include a Japanese translation alongside it; edit
the comment that mentions "Regression for PR `#777` review (Devin): keep <h1> as a
heading node so that editor-time clamping (HeadingLevelClamp /
sanitizeTiptapContent) can demote it to level 2. Dropping <h1> at parse time
would silently lose semantics." by adding an equivalent Japanese sentence
immediately before or after the English line so the comment follows the
repository's JA/EN documentation style; target the comment near the test file's
htmlToTiptap tests where the <h1> regression is documented.

In `@src/lib/wikiGenerator.test.ts`:
- Around line 73-79: Add the same `<page_title>` invariants to the no-search
template test: in the existing test that checks WIKI_GENERATOR_PROMPT, duplicate
the assertions for WIKI_GENERATOR_PROMPT_NO_SEARCH so it also
.toContain("<page_title>") and .toContain("</page_title>"), .not.toMatch(/^##
タイトル$/m), and .toContain("{{title}}") and .toContain("{{schema}}"); update the
test block around WIKI_GENERATOR_PROMPT to include these checks for
WIKI_GENERATOR_PROMPT_NO_SEARCH to prevent divergence between the derived
prompts.

In `@src/lib/wikiGenerator/wikiGeneratorPrompt.ts`:
- Around line 127-129: Add an explicit newline delimiter between the {{schema}}
placeholder and the <page_title> tag in the template so that formatting remains
stable regardless of how {{schema}} is populated; locate the template string
that contains "{{schema}}<page_title>{{title}}</page_title>" in
wikiGeneratorPrompt.ts and change it to insert a newline (or two) after
{{schema}} (i.e., "{{schema}}\n\n<page_title>") so <page_title> and {{title}}
are always on their own lines.

In `@src/pages/NotePageView.test.tsx`:
- Around line 300-341: The console.error spy in this test (consoleError created
via vi.spyOn(console, "error")) must be restored even if assertions throw; wrap
the test flow that sets consoleError, mocks useNote/useNotePage, calls
renderNotePageView, and runs assertions (including waits and mockToast checks)
in a try/finally and call consoleError.mockRestore() in the finally block so the
spy is always cleaned up regardless of failures; ensure you keep the existing
mock setup/teardown (mockApi, vi.useRealTimers, etc.) inside the try and only
perform the restore in finally to avoid leaking the mocked console to other
tests.

In `@src/pages/NoteView/ShareModal/ShareModalDomainTab.test.tsx`:
- Around line 125-137: The test currently claims to verify role badges and
domain validation but only checks domain names and unverified badges; update the
spec in ShareModalDomainTab.test.tsx by adding assertions that the role badges
are rendered for each rule (assert getByText("notes.domainTabRoleViewer") and
getByText("notes.domainTabRoleEditor") after calling renderTab with row(...)
entries) and add a separate assertion covering malformed domain input (e.g.,
renderTab or a new test that submits "not_a_domain" and expects the component's
invalid-domain validation message, e.g.,
getByText("notes.domainTabInvalidDomain") or the same UI error element used
elsewhere) so both behaviors the comment promises are actually asserted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment on lines +50 to +59
- uses: actions/checkout@v6.0.2
with:
# PR ブランチに直接書き戻すため、head ref をチェックアウトする。
# Check out the PR head ref so we can push commits back to it.
ref: ${{ github.event.pull_request.head.ref }}
# ベースブランチとの差分計算用に履歴を全部取る。
# Fetch full history so we can diff against the PR base.
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

wf="$(fd -t f 'dependabot-bun-lock.yml' .github/workflows | head -n1)"
echo "Checking workflow: $wf"

rg -n 'permissions:|contents:\s*write|uses:\s*actions/checkout@|persist-credentials|bun install|git push' "$wf"

Repository: otomatty/zedi

Length of output: 749


🏁 Script executed:

cat -n .github/workflows/dependabot-bun-lock.yml | sed -n '50,59p;101,109p;111,137p'

Repository: otomatty/zedi

Length of output: 2409


Harden Dependabot lock sync against install-script supply-chain execution.

The workflow runs bun install without --ignore-scripts in an environment with contents: write permission and checkout credentials (lines 50–59, 107). This allows package lifecycle scripts to execute with write access to the PR branch and pre-authenticated git credentials. For lockfile-only sync, disable install scripts, drop credential persistence from checkout, and use explicit token auth for the final push.

🔧 Hardening changes needed
      - uses: actions/checkout@v6.0.2
        with:
          ref: ${{ github.event.pull_request.head.ref }}
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}
+         persist-credentials: false
@@
            (cd "$dir" && bun install --no-summary)
+           (cd "$dir" && bun install --no-summary --ignore-scripts)
@@
-         git push origin "HEAD:${HEAD_REF}"
+         git push "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" "HEAD:${HEAD_REF}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/dependabot-bun-lock.yml around lines 50 - 59, Change the
checkout step to stop persisting credentials and prevent lifecycle script
execution during installs: in the actions/checkout usage
(actions/checkout@v6.0.2) add persist-credentials: false and keep fetch-depth:
0; change the bun install invocation to include --ignore-scripts so package
scripts cannot run; and update the final push step (the step that pushes
lockfile changes) to use an explicit token for auth (e.g., use env
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} or configure remote with that token)
rather than relying on checkout-persisted credentials.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

セキュリティ強化のご提案ありがとうございます。--ignore-scripts の追加と persist-credentials: false + x-access-token 明示認証への切替は、ロックファイル同期ワークフローとして妥当な強化案です。本 PR は develop → main のリリース PR でワークフロー自体は既に develop 側で稼働中のため、追従の develop PR で 3 点 (1) actions/checkoutpersist-credentials: false、(2) bun install --no-summary --ignore-scripts、(3) push に https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git を利用、を一括で適用します。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@otomatty ご確認ありがとうございます!3点すべてのセキュリティ強化を次の develop PR でご対応いただける旨、承知しました。適用後、ワークフローが引き続き正常に動作することを確認いただければ幸いです。

(ˆ▿ˆ)っ✉


✏️ Learnings added
Learnt from: otomatty
Repo: otomatty/zedi PR: 793
File: .github/workflows/dependabot-bun-lock.yml:50-59
Timestamp: 2026-04-28T02:13:19.536Z
Learning: In the zedi repository, the `dependabot-bun-lock.yml` workflow security hardening best practice is to apply these three changes together: (1) add `persist-credentials: false` to the `actions/checkout` step, (2) use `bun install --no-summary --ignore-scripts` to prevent lifecycle script execution, and (3) use `https://x-access-token:${{ secrets.GITHUB_TOKEN }}github.com/${{ github.repository }}.git` for the explicit push instead of relying on checkout-persisted credentials.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: otomatty
Repo: otomatty/zedi PR: 158
File: server/api/Dockerfile:0-0
Timestamp: 2026-03-01T21:44:49.751Z
Learning: In the zedi repository, for Bun-based Docker builds in server/api/Dockerfile, the monorepo's root bun.lock file is copied to server/api/ during CI/deployment steps before building the Docker image, and the Dockerfile uses `bun install --frozen-lockfile` for deterministic builds.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: When merging `main` → `develop` sync PRs, always use 'Create a merge commit' instead of Squash to avoid reintroducing conflicts

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: PR title should follow Conventional Commits format and accurately describe changes (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`)

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: .cursor/rules/pr-and-branch-naming.mdc:0-0
Timestamp: 2026-03-08T07:34:11.938Z
Learning: PR titles must accurately represent the changes made. Use Conventional Commits format (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`). For single-topic PRs, use the representative commit message directly. Match the language (Japanese or English) to the majority language of commits.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: PR review focus: security, performance impact, breaking API/type changes, error handling, logging appropriateness, and Japanese/English comment tone alignment

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: PR review focus areas: check for security and performance impacts, breaking changes in public APIs or types, proper error handling and logging, and ensure comments in Japanese/English match project tone.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: PR titles must follow Conventional Commits format (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`) and accurately represent the changes. When using Cursor Cloud Agent, include instructions in the prompt to ensure PR titles follow Conventional Commits format.

Learnt from: otomatty
Repo: otomatty/zedi PR: 785
File: .github/workflows/dependabot-bun-lock.yml:101-109
Timestamp: 2026-04-27T05:29:23.690Z
Learning: For CI GitHub Actions workflows that run Bun (Bun v1.3.11+), prefer `bun install --no-summary` over `bun install --silent` when you still need error and warning visibility. `--silent` suppresses all logging (including errors and warnings), while `--no-summary` only removes the install summary and keeps progress output plus errors and warnings.

Comment thread server/hocuspocus/package.json
<Plus className="text-primary h-4 w-4 shrink-0" />
)}
<span className="truncate">
{item.exists ? `#${item.name}` : `"#${item.name}" を作成`}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Localize the “create tag” row text instead of hardcoding Japanese.

Line 190 hardcodes を作成, so English users will still see Japanese in this popover row.

💡 Proposed fix
 import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
 ...
 export const TagSuggestion = forwardRef<TagSuggestionHandle, TagSuggestionProps>(
   ({ query, candidates, onSelect, onClose }, ref) => {
+    const { t } = useTranslation();
     const [selectedIndex, setSelectedIndex] = useState(0);
 ...
               <span className="truncate">
-                {item.exists ? `#${item.name}` : `"#${item.name}" を作成`}
+                {item.exists
+                  ? `#${item.name}`
+                  : t("notes.tagSuggestionCreate", {
+                      name: item.name,
+                      defaultValue: 'Create "#{{name}}"',
+                    })}
               </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/editor/extensions/TagSuggestion.tsx` at line 190, The popover
row in TagSuggestion.tsx currently hardcodes Japanese ("を作成") when item.exists
is false; replace this string concatenation with a localized message using the
project's i18n/localization helper (e.g., call t or i18n.t) so the text becomes
something like t('tag.createLabel', { name: `#${item.name}` }) or similar;
update the JSX expression that renders {item.exists ? `#${item.name}` :
`"#${item.name}" を作成`} to use the localization key and add the new key to your
translation files for supported languages (include a sensible fallback).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Devin の同箇所指摘 (#3151098819) と同じ内容です。useTranslation() 経由への置換、src/i18n/locales/{ja,en} への tagSuggestionCreate キー追加、テストアサーションの i18n 解決値への更新を、develop 側の後続 PR でまとめて対応します。本リリース PR ではコミットを追加しません。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@otomatty、了解です。useTranslation() への置換・tagSuggestionCreate キー追加・テスト更新のセットを develop 側の後続 PR でまとめて対応いただける件、承知しました。

よろしければ、追跡用の GitHub Issue を作成しましょうか?

( Understood — the fix (swapping the hardcoded を作成 for a useTranslation()-based call, adding the tagSuggestionCreate key to both ja/en locale files, and updating test assertions) will be bundled in a follow-up PR on develop. No changes needed in this release PR. Would you like me to open a tracking issue for it? )


🧠 Learnings used
Learnt from: CR
Repo: otomatty/zedi PR: 0
File: .cursor/rules/pr-and-branch-naming.mdc:0-0
Timestamp: 2026-03-08T07:34:11.938Z
Learning: PR titles must accurately represent the changes made. Use Conventional Commits format (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`). For single-topic PRs, use the representative commit message directly. Match the language (Japanese or English) to the majority language of commits.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: PR title should follow Conventional Commits format and accurately describe changes (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`)

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: PR titles must follow Conventional Commits format (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`) and accurately represent the changes. When using Cursor Cloud Agent, include instructions in the prompt to ensure PR titles follow Conventional Commits format.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: Applies to **/*.{ts,tsx,js,jsx,json,md} : Comments and documentation should include both Japanese and English

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: PR review focus areas: check for security and performance impacts, breaking changes in public APIs or types, proper error handling and logging, and ensure comments in Japanese/English match project tone.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: PR review focus: security, performance impact, breaking API/type changes, error handling, logging appropriateness, and Japanese/English comment tone alignment

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: Ensure Japanese and English comments and documentation align with the project's tone.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: .cursor/rules/pr-and-branch-naming.mdc:0-0
Timestamp: 2026-03-08T07:34:11.938Z
Learning: Branch names should follow the format `feature/説明` or `fix/説明` (e.g., `feature/ai-models-ui`, `fix/search-crash`), or `feature/123` for Issue-based branches. Cursor Cloud Agent may auto-generate branches with `cursor/` prefix, but manual PR creation should use the recommended format.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-26T11:28:05.038Z
Learning: Applies to **/*.{ts,tsx,js,md} : Include both Japanese and English comments/documentation in code and documentation files

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: When merging `main` → `develop` sync PRs, always use 'Create a merge commit' instead of Squash to avoid reintroducing conflicts

Learnt from: otomatty
Repo: otomatty/zedi PR: 604
File: src/components/ai-chat/PromoteToWikiDialog.tsx:223-275
Timestamp: 2026-04-17T07:09:45.518Z
Learning: In otomatty/zedi, custom modal/dialog components (e.g., components like PromoteToWikiDialog) are intentionally implemented without the standard dialog accessibility wiring (no role="dialog", no aria-modal, no focus trap, and no aria-* attribute setup). When reviewing similar custom modals/dialogs, do not flag missing dialog accessibility attributes in isolation; these will be handled in a separate cross-app a11y PR to keep consistency across dialogs.

Learnt from: otomatty
Repo: otomatty/zedi PR: 604
File: src/pages/WikiSchemaPage.tsx:31-44
Timestamp: 2026-04-17T07:10:01.088Z
Learning: In the otomatty/zedi repo (with eslint-plugin-react-hooks7.0.1 using the recommended config), do not treat a two-`useEffect` pattern as a `react-hooks/set-state-in-effect` lint violation when: (1) one effect sets a state value (e.g., `syncedId`), and (2) a second effect reads that state value to perform syncing (e.g., remote → local updates). This exact pattern in `src/pages/WikiSchemaPage.tsx` produces 0 errors when running `bunx eslint src/pages/WikiSchemaPage.tsx` / `bun run lint`, so it should not be flagged as a lint error based on sandbox static analysis false positives.

Learnt from: otomatty
Repo: otomatty/zedi PR: 604
File: src/pages/IndexPage.tsx:58-74
Timestamp: 2026-04-17T07:12:27.068Z
Learning: In otomatty/zedi, ESLint’s findings may differ between the review sandbox and the repository’s pinned `eslint-plugin-react-hooks7.0.1`. Before flagging an issue from `eslint-plugin-react-hooks` (notably `react-hooks/set-state-in-effect`), confirm the violation matches the behavior of the project’s actual pinned plugin version. If the sandbox is running a newer plugin (e.g., v7.1.0) that detects indirect setState via intermediate `useCallback` references, treat sandbox-only differences as potential false positives and verify against the pinned dependency before raising the review concern.

Comment thread src/components/editor/TiptapEditor/headingLevelClampExtension.ts Outdated
Comment on lines +97 to +104
queryKey: ["tag-candidates", "ghost", userId, pageNoteId ?? null],
queryFn: async (): Promise<string[]> => {
const repo = await getRepository();
const all = await repo.getGhostLinks(userId);
return all
.filter((g) => g.linkType === "tag")
.map((g) => g.linkText.trim())
.filter((t): t is string => t.length > 0);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== getGhostLinks definitions/usages =="
rg -n -C3 '\bgetGhostLinks\s*\('

echo
echo "== Ghost link model fields (check for note/page scope fields) =="
rg -n -C3 '\b(linkType|linkText|noteId|pageId|note_id|page_note_id)\b'

echo
echo "== useWikiLinkCandidates scope logic (for expected behavior comparison) =="
rg -n -C3 '\bfunction\s+useWikiLinkCandidates\b|\buseWikiLinkCandidates\s*\('

Repository: otomatty/zedi

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Full useTagCandidates.ts file (check for TODOs/comments) =="
cat -n src/hooks/useTagCandidates.ts

echo
echo "== Check page-note relationship: does a page belong to one or multiple notes? =="
rg -A5 "sourcePageId|noteId.*page|page.*noteId" src/types/page.ts src/lib/pageRepository.ts | head -50

echo
echo "== Check if pages are stored per-note or globally =="
rg -C3 "linkType.*tag|tag.*linkType" src/hooks/useTagCandidates.ts

Repository: otomatty/zedi

Length of output: 8500


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check if sourcePageId can be used to determine note scope =="
rg -B5 -A5 "sourcePageId" src/lib/pageRepository/StorageAdapterPageRepository.ts | head -40

echo
echo "== Verify: does getGhostLinks return all pages or only current note context? =="
sed -n '429,439p' src/lib/pageRepository/StorageAdapterPageRepository.ts

Repository: otomatty/zedi

Length of output: 2006


Filter ghost-tag candidates by current scope — unscoped getGhostLinks returns cross-note tags.

Lines 99–104 fetch all ghost links for the user via repo.getGhostLinks(userId) with no filtering by note or scope, even though the query key includes pageNoteId. The cache invalidates correctly on scope change, but the fetched dataset remains globally unscoped: when viewing a note (or personal scope), tag suggestions will include unresolved tags from other notes in the user's workspace.

Contrast: useWikiLinkCandidates (which provides page titles on line 83) is properly scoped by pageNoteId. Ghost tags should match that same scope.

Fix: Filter the result to include only ghost links whose sourcePageId belongs to the current scope. This requires either:

  • Matching sourcePageId against the scoped pages list, or
  • Adding a scope parameter to the repository method to filter at the source.

See also usePageQueries.ts line 921, which blocks ghost links entirely for note-scoped views (a related but incomplete workaround).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useTagCandidates.ts` around lines 97 - 104, The hook
useTagCandidates currently calls repo.getGhostLinks(userId) and returns all
tag-type ghost links without respecting the current pageNoteId/scope; update
useTagCandidates to restrict results to the current scope by either calling a
scoped repository method (e.g., add a getGhostLinks(userId, scope) overload) or
post-filtering the returned array by matching each ghost link's sourcePageId
against the scoped pages list (the same scope logic used by
useWikiLinkCandidates), ensuring only ghost links from the current
pageNoteId/scope are included before mapping to linkText.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ご指摘の通り、repo.getGhostLinks(userId) は現状ユーザー全体のゴーストリンクを返しており、pageNoteId でのスコープ絞り込みは行っていません。コメント中で言及いただいた usePageQueries.ts:921 のガードでノートスコープでの ghost link 表示は止めていますが、リポジトリ層に scope 引数を追加して根本的に絞り込むのが望ましい設計です。設計の影響範囲(PageRepository インターフェース・StorageAdapterPageRepository 実装・関連クエリ)が広く、本リリース PR では対応せず、別 issue を立てて develop 上で getGhostLinks(userId, { pageNoteId }) 相当の追加を行います。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@otomatty、ご説明ありがとうございます。現状のガード(usePageQueries.ts:921)により、ノートスコープでの意図しないゴーストタグ表示は実質的に防がれているため、本リリース PR での影響は限定的であることを確認しました。

getGhostLinks(userId, { pageNoteId }) のスコープ対応については、PageRepository インターフェース・StorageAdapterPageRepository・関連クエリへの変更が伴うため、別 issue での対応は妥当な判断です。

フォローアップ issue を作成しましょうか?


✏️ Learnings added
Learnt from: otomatty
Repo: otomatty/zedi PR: 793
File: src/hooks/useTagCandidates.ts:97-104
Timestamp: 2026-04-28T02:14:12.390Z
Learning: In otomatty/zedi, `useTagCandidates` (src/hooks/useTagCandidates.ts) calls `repo.getGhostLinks(userId)` without scope filtering by `pageNoteId`. A guard in `usePageQueries.ts` (around line 921) prevents ghost links from showing in note-scoped views as a short-term mitigation. The long-term fix — adding a `getGhostLinks(userId, { pageNoteId })` overload to the `PageRepository` interface and `StorageAdapterPageRepository` — is intentionally deferred to a future issue on the develop branch due to the wide scope of the change. Do not re-flag the unscoped call as a new issue without checking whether the dedicated follow-up issue has been resolved.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: otomatty
Repo: otomatty/zedi PR: 719
File: src/pages/NoteSettings/index.tsx:91-91
Timestamp: 2026-04-24T07:16:57.586Z
Learning: In otomatty/zedi, the post-delete redirect destination intentionally differs by entity type:
- `src/pages/NoteSettings/index.tsx` `handleDeleteNote` deletes a **note (container)** and redirects to `/notes` (note list) — the natural parent destination.
- `src/components/editor/PageEditor/usePageDeletion.ts` and `src/pages/NotePageView.tsx` handle **personal page** deletion and redirect to `/home` (personal home) — the natural parent destination for personal pages.
This is by design and should NOT be flagged as a routing inconsistency in future reviews. PR `#732` adds bilingual (ja/en) comments inside `handleDeleteNote` to document this intent.

Learnt from: otomatty
Repo: otomatty/zedi PR: 599
File: server/api/src/schema/pageSources.ts:27-45
Timestamp: 2026-04-16T02:13:00.770Z
Learning: In `otomatty/zedi`, junction tables such as `links`, `ghost_links`, and `page_sources` (in `server/api/src/schema/pageSources.ts`) rely on **application-level** tenant-scoping enforcement (checking `owner_id` matches the authenticated user before insert) rather than composite FK or unique constraints at the database level. Do not flag absence of composite FKs/owner_id columns on these junction tables as a schema deficiency — it is an intentional design choice consistent with the codebase convention.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: PR title should follow Conventional Commits format and accurately describe changes (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`)

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: .cursor/rules/pr-and-branch-naming.mdc:0-0
Timestamp: 2026-03-08T07:34:11.938Z
Learning: PR titles must accurately represent the changes made. Use Conventional Commits format (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`). For single-topic PRs, use the representative commit message directly. Match the language (Japanese or English) to the majority language of commits.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: PR review focus: security, performance impact, breaking API/type changes, error handling, logging appropriateness, and Japanese/English comment tone alignment

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: PR review focus areas: check for security and performance impacts, breaking changes in public APIs or types, proper error handling and logging, and ensure comments in Japanese/English match project tone.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: PR titles must follow Conventional Commits format (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`) and accurately represent the changes. When using Cursor Cloud Agent, include instructions in the prompt to ensure PR titles follow Conventional Commits format.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: When merging `main` → `develop` sync PRs, always use 'Create a merge commit' instead of Squash to avoid reintroducing conflicts

Learnt from: otomatty
Repo: otomatty/zedi PR: 465
File: packages/claude-sidecar/src/handlers/query.ts:109-123
Timestamp: 2026-04-02T06:02:33.826Z
Learning: `anthropic-ai/claude-agent-sdk` の `query()` で使う `Options` 型には、`cwd`, `maxTurns`, `includePartialMessages`, `permissionMode`, `resume`, `abortController`, `allowedTools`, `settingSources` が含まれます。コードレビュー時に、これらのフィールドを「SDK 非互換(存在しない/無効なオプション)」として誤検知しないでください。実際に `Options` と整合していれば、その前提で判定してください。

Learnt from: otomatty
Repo: otomatty/zedi PR: 604
File: src/pages/IndexPage.tsx:58-74
Timestamp: 2026-04-17T07:12:27.068Z
Learning: In otomatty/zedi, ESLint’s findings may differ between the review sandbox and the repository’s pinned `eslint-plugin-react-hooks7.0.1`. Before flagging an issue from `eslint-plugin-react-hooks` (notably `react-hooks/set-state-in-effect`), confirm the violation matches the behavior of the project’s actual pinned plugin version. If the sandbox is running a newer plugin (e.g., v7.1.0) that detects indirect setState via intermediate `useCallback` references, treat sandbox-only differences as potential false positives and verify against the pinned dependency before raising the review concern.

Comment on lines +76 to +80
const withTypes = t("mermaid.generatorPrompt", {
diagramTypes: diagramTypesList,
});
// `sourceText` は i18n 展開に渡さない(`{{` を含みうる) / Do not pass sourceText to i18n.
return withTypes.replace("__ZEDI_MERMAID_SOURCE__", sourceText);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against missing __ZEDI_MERMAID_SOURCE__ in localized templates.

Line 80 assumes the placeholder always exists. If a locale string drops/renames it, sourceText is silently omitted from prompts.

💡 Proposed fix
   const withTypes = t("mermaid.generatorPrompt", {
     diagramTypes: diagramTypesList,
   });
   // `sourceText` は i18n 展開に渡さない(`{{` を含みうる) / Do not pass sourceText to i18n.
-  return withTypes.replace("__ZEDI_MERMAID_SOURCE__", sourceText);
+  return withTypes.includes("__ZEDI_MERMAID_SOURCE__")
+    ? withTypes.replace("__ZEDI_MERMAID_SOURCE__", sourceText)
+    : `${withTypes}\n\n${sourceText}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const withTypes = t("mermaid.generatorPrompt", {
diagramTypes: diagramTypesList,
});
// `sourceText` は i18n 展開に渡さない(`{{` を含みうる) / Do not pass sourceText to i18n.
return withTypes.replace("__ZEDI_MERMAID_SOURCE__", sourceText);
const withTypes = t("mermaid.generatorPrompt", {
diagramTypes: diagramTypesList,
});
// `sourceText` は i18n 展開に渡さない(`{{` を含みうる) / Do not pass sourceText to i18n.
return withTypes.includes("__ZEDI_MERMAID_SOURCE__")
? withTypes.replace("__ZEDI_MERMAID_SOURCE__", sourceText)
: `${withTypes}\n\n${sourceText}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/mermaidGenerator.ts` around lines 76 - 80, The template replacement
assumes the placeholder "__ZEDI_MERMAID_SOURCE__" always exists in the localized
string (variable withTypes derived from t("mermaid.generatorPrompt")), which can
silently drop sourceText if a locale renames/removes it; fix by checking if
withTypes.includes("__ZEDI_MERMAID_SOURCE__") and only doing withTypes.replace
when present, otherwise inject sourceText safely (e.g. append or prepend with a
separator) so the prompt still contains the source; also emit a warning (via
existing logger or console.warn) when the placeholder is missing to aid
localization fixes, referencing withTypes and sourceText in the logic.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

防御的プログラミングのご提案ありがとうございます。現状の ja / en locale には __ZEDI_MERMAID_SOURCE__ プレースホルダが含まれており即時の不具合はないですが、locale 編集時の事故防止として withTypes.includes("__ZEDI_MERMAID_SOURCE__") チェック + フォールバック合流(${withTypes}\n\n${sourceText})の対応は妥当です。本リリース PR ではコミットを追加せず、develop 側の後続 PR で mermaidGenerator.ts に適用します。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@otomatty ご確認いただきありがとうございます。現状のロケールファイルにプレースホルダが存在しているため即時影響はなく、リリース PR への追加コミットを避けるご判断は理にかなっています。

develop 側の後続 PR で適用予定とのこと、承知しました。もし追跡用の Issue を作成しておきたい場合はお知らせください。作成をお手伝いします。


🧠 Learnings used
Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: PR titles must follow Conventional Commits format (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`) and accurately represent the changes. When using Cursor Cloud Agent, include instructions in the prompt to ensure PR titles follow Conventional Commits format.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: PR review focus: security, performance impact, breaking API/type changes, error handling, logging appropriateness, and Japanese/English comment tone alignment

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: .cursor/rules/pr-and-branch-naming.mdc:0-0
Timestamp: 2026-03-08T07:34:11.938Z
Learning: PR titles must accurately represent the changes made. Use Conventional Commits format (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`). For single-topic PRs, use the representative commit message directly. Match the language (Japanese or English) to the majority language of commits.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: PR review focus areas: check for security and performance impacts, breaking changes in public APIs or types, proper error handling and logging, and ensure comments in Japanese/English match project tone.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: Applies to **/*.{ts,tsx,js,jsx,json,md} : Comments and documentation should include both Japanese and English

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: PR title should follow Conventional Commits format and accurately describe changes (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`)

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: When merging `main` → `develop` sync PRs, always use 'Create a merge commit' instead of Squash to avoid reintroducing conflicts

Learnt from: otomatty
Repo: otomatty/zedi PR: 465
File: packages/claude-sidecar/src/handlers/query.ts:109-123
Timestamp: 2026-04-02T06:02:33.826Z
Learning: `anthropic-ai/claude-agent-sdk` の `query()` で使う `Options` 型には、`cwd`, `maxTurns`, `includePartialMessages`, `permissionMode`, `resume`, `abortController`, `allowedTools`, `settingSources` が含まれます。コードレビュー時に、これらのフィールドを「SDK 非互換(存在しない/無効なオプション)」として誤検知しないでください。実際に `Options` と整合していれば、その前提で判定してください。

Learnt from: otomatty
Repo: otomatty/zedi PR: 604
File: src/pages/IndexPage.tsx:58-74
Timestamp: 2026-04-17T07:12:27.068Z
Learning: In otomatty/zedi, ESLint’s findings may differ between the review sandbox and the repository’s pinned `eslint-plugin-react-hooks7.0.1`. Before flagging an issue from `eslint-plugin-react-hooks` (notably `react-hooks/set-state-in-effect`), confirm the violation matches the behavior of the project’s actual pinned plugin version. If the sandbox is running a newer plugin (e.g., v7.1.0) that detects indirect setState via intermediate `useCallback` references, treat sandbox-only differences as potential false positives and verify against the pinned dependency before raising the review concern.

Comment thread src/pages/IndexPage.tsx
Comment on lines +200 to +205
{t("common.wikiIndex.lastBuiltAt", {
date: format(
new Date(data.timestamp),
t("common.date.format.fullDateTime"),
{ locale: getActiveLocale() },
),
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In the date-fns version used by this repo, what happens when calling format(new Date("invalid"), "yyyy-MM-dd")? Does it throw (e.g., RangeError)?

💡 Result:

In recent versions of date-fns (v2 and later, up to v4.1.0 as of 2024), calling format(new Date("invalid"), "yyyy-MM-dd") throws a RangeError: Invalid time value. This is because new Date("invalid") creates an Invalid Date (a Date object with NaN timestamp), and date-fns' format function throws RangeError when processing such invalid dates, mirroring native JS behavior like toISOString on Invalid Date. Earlier versions (v1) returned garbage like "undefined NaN NaN", but this was fixed to throw in v2+. The repo likely uses a modern version where it throws.

Citations:


🏁 Script executed:

fd -t f package.json | head -5 | xargs cat

Repository: otomatty/zedi

Length of output: 13739


🏁 Script executed:

wc -l src/pages/IndexPage.tsx && sed -n '195,210p' src/pages/IndexPage.tsx

Repository: otomatty/zedi

Length of output: 843


🏁 Script executed:

# Search for where data.timestamp comes from in IndexPage.tsx
rg "data\." src/pages/IndexPage.tsx -B 3 -A 1 | head -50

Repository: otomatty/zedi

Length of output: 1630


🏁 Script executed:

head -50 src/pages/IndexPage.tsx

Repository: otomatty/zedi

Length of output: 1724


🏁 Script executed:

rg "const data|let data|data =" src/pages/IndexPage.tsx -B 2 -A 2 | head -40

Repository: otomatty/zedi

Length of output: 39


🏁 Script executed:

# Find where data is fetched from
rg "useQuery|useEffect|fetch" src/pages/IndexPage.tsx -B 2 -A 2 | head -60

Repository: otomatty/zedi

Length of output: 917


🏁 Script executed:

sed -n '40,150p' src/pages/IndexPage.tsx

Repository: otomatty/zedi

Length of output: 3209


🏁 Script executed:

# Search for the view model definition and data mapping
rg "interface.*ViewModel|setData|data\." src/pages/IndexPage.tsx -A 1 | head -80

Repository: otomatty/zedi

Length of output: 1224


Guard date formatting against malformed timestamps to prevent render crashes.

The current check {data.timestamp ? (...)} only guards against null/undefined/empty string. If the API returns a malformed non-empty string (e.g., corrupted data or API bug), new Date(data.timestamp) creates an Invalid Date and format() throws a RangeError, crashing the render. Date-fns v4.1.0 throws on invalid dates.

Add isValid() from date-fns to validate the parsed date before formatting:

Proposed fix
-import { format } from "date-fns";
+import { format, isValid } from "date-fns";
...
-                      {data.timestamp ? (
-                        <div className="text-muted-foreground mt-1 text-xs">
-                          {t("common.wikiIndex.lastBuiltAt", {
-                            date: format(
-                              new Date(data.timestamp),
-                              t("common.date.format.fullDateTime"),
-                              { locale: getActiveLocale() },
-                            ),
-                          })}
-                        </div>
-                      ) : null}
+                      {data.timestamp ? (
+                        <div className="text-muted-foreground mt-1 text-xs">
+                          {t("common.wikiIndex.lastBuiltAt", {
+                            date: (() => {
+                              const parsed = new Date(data.timestamp);
+                              if (!isValid(parsed)) return data.timestamp;
+                              return format(
+                                parsed,
+                                t("common.date.format.fullDateTime"),
+                                { locale: getActiveLocale() },
+                              );
+                            })(),
+                          })}
+                        </div>
+                      ) : null}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{t("common.wikiIndex.lastBuiltAt", {
date: format(
new Date(data.timestamp),
t("common.date.format.fullDateTime"),
{ locale: getActiveLocale() },
),
import { format, isValid } from "date-fns";
...
{t("common.wikiIndex.lastBuiltAt", {
date: (() => {
const parsed = new Date(data.timestamp);
if (!isValid(parsed)) return data.timestamp;
return format(
parsed,
t("common.date.format.fullDateTime"),
{ locale: getActiveLocale() },
);
})(),
})}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/IndexPage.tsx` around lines 200 - 205, The date formatting call
using format(new Date(data.timestamp), ...) can throw on malformed timestamps;
update the render guard to parse and validate the date with isValid from
date-fns (e.g., const parsed = new Date(data.timestamp); use isValid(parsed)
before calling format) and only call format when valid; add the isValid import
from date-fns and render a safe fallback (empty string or a placeholder) when
validation fails, referencing the existing symbols data.timestamp, format, t,
and getActiveLocale.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

防御的プログラミングのご提案ありがとうございます。data.timestamp が wiki-index API の出力で invalid になるケースは稀ですが、new Date(invalid) を date-fns format() に渡すと RangeError を throw する挙動は事実なので、isValid() ガードを入れるのは妥当です。本リリース PR ではコミットを追加せず、develop 側の後続 PR で import { isValid } from "date-fns" を追加し、IndexPage.tsxdata.timestamp レンダリング箇所に isValid(parsed) ガード + フォールバック(data.timestamp を生のまま表示)を入れます。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@otomatty、ご確認とご計画のご共有ありがとうございます。isValid() ガードの追加方針(data.timestamp を生のままフォールバック表示)は適切な対応だと思います。後続の develop PR での修正をお待ちしております。

もし希望であれば、このフォローアップ作業を追跡するための GitHub Issue を作成しましょうか?


🧠 Learnings used
Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: PR review focus: security, performance impact, breaking API/type changes, error handling, logging appropriateness, and Japanese/English comment tone alignment

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: PR review focus areas: check for security and performance impacts, breaking changes in public APIs or types, proper error handling and logging, and ensure comments in Japanese/English match project tone.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: PR title should follow Conventional Commits format and accurately describe changes (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`)

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: .cursor/rules/pr-and-branch-naming.mdc:0-0
Timestamp: 2026-03-08T07:34:11.938Z
Learning: PR titles must accurately represent the changes made. Use Conventional Commits format (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`). For single-topic PRs, use the representative commit message directly. Match the language (Japanese or English) to the majority language of commits.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-14T12:40:46.017Z
Learning: PR titles must follow Conventional Commits format (e.g., `feat(admin): AIモデル管理UI拡張 (`#218`)`) and accurately represent the changes. When using Cursor Cloud Agent, include instructions in the prompt to ensure PR titles follow Conventional Commits format.

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-26T12:37:45.354Z
Learning: When merging `main` → `develop` sync PRs, always use 'Create a merge commit' instead of Squash to avoid reintroducing conflicts

Learnt from: otomatty
Repo: otomatty/zedi PR: 604
File: src/pages/WikiSchemaPage.tsx:31-44
Timestamp: 2026-04-17T07:10:01.088Z
Learning: In the otomatty/zedi repo (with eslint-plugin-react-hooks7.0.1 using the recommended config), do not treat a two-`useEffect` pattern as a `react-hooks/set-state-in-effect` lint violation when: (1) one effect sets a state value (e.g., `syncedId`), and (2) a second effect reads that state value to perform syncing (e.g., remote → local updates). This exact pattern in `src/pages/WikiSchemaPage.tsx` produces 0 errors when running `bunx eslint src/pages/WikiSchemaPage.tsx` / `bun run lint`, so it should not be flagged as a lint error based on sandbox static analysis false positives.

Learnt from: otomatty
Repo: otomatty/zedi PR: 604
File: src/pages/IndexPage.tsx:58-74
Timestamp: 2026-04-17T07:12:27.068Z
Learning: In otomatty/zedi, ESLint’s findings may differ between the review sandbox and the repository’s pinned `eslint-plugin-react-hooks7.0.1`. Before flagging an issue from `eslint-plugin-react-hooks` (notably `react-hooks/set-state-in-effect`), confirm the violation matches the behavior of the project’s actual pinned plugin version. If the sandbox is running a newer plugin (e.g., v7.1.0) that detects indirect setState via intermediate `useCallback` references, treat sandbox-only differences as potential false positives and verify against the pinned dependency before raising the review concern.

@otomatty otomatty self-assigned this Apr 28, 2026
@otomatty
Copy link
Copy Markdown
Owner Author

レビューコメントへの方針サマリー

本 PR は developmain のリリース PR のため、追加コミットは積まず、各レビュー指摘は develop 上の後続 PR で対応します。各コメントには個別に返信済みです。以下が follow-up として develop 上で扱う項目です:

後続 develop PR にまとめる予定の改善

  • 空 JSDoc ブロックの一括削除(#3151092590, #3151092598, #3151092601): 過去の JSDoc 自動生成ルールの副作用。原因は PR Disable JSDoc auto-fixer to prevent empty stub generation #789 で無効化済み、残骸を一括クリーンアップ。
  • TagSuggestion.tsx の i18n 化(#3151098819, #3151131869): useTranslation() 経由に置換、ja/en locale 追加、テスト更新。
  • dependabot-bun-lock.yml の強化(#3151131863): persist-credentials: false, bun install --ignore-scripts, x-access-token 明示認証 push。
  • headingLevelClampExtension.tsdocChanged ガード(#3151131874): appendTransaction の早期リターン追加。
  • useTagCandidates の ghost-link スコープ絞り込み(#3151131881): 別 issue を立て、PageRepository.getGhostLinks(userId, { pageNoteId }) 相当の API 追加で対応。
  • mermaidGenerator.ts のプレースホルダ欠落ガード(#3151131893): withTypes.includes(...) チェック + フォールバック合流。
  • IndexPage.tsxisValid() ガード(#3151131907): date-fns format()RangeError 防御。

本リリース PR ではそのままマージ

CI が通ったらマージ予定です。

* feat(notes): add editor / viewer read-only modes to share modal (#675)

V1 of the share modal (#661) was owner-only by design. This follow-up
opens the modal to editor and viewer roles with the matrix from the
original spec:

- ShareButton: now shown to anyone with `canView` (signed-in
  viewer/editor/owner). Owners keep the dropdown with Settings; editors
  and viewers see a single share button.
- NoteShareModal: new `userRole` prop drives tab visibility and
  read-only state. Owner = all tabs editable; editor = all tabs visible
  read-only (transparency over who has access); viewer = visibility tab
  only, read-only.
- NoteMembersManageSection: new `readOnly` prop hides the invite form
  and per-row action buttons while keeping status badges intact.
- NoteInviteLinksSection: new `readOnly` prop hides the create form +
  revoke buttons; the per-row copy button stays so editors can still
  distribute existing links.
- ShareModalDomainTab: new `readOnly` prop hides the add form, remove
  buttons, and editor-confirmation dialog.
- ShareModalVisibilityTab: when `canEdit=false`, renders the
  `notes.shareReadOnlyNotice` inline note (the i18n key was already
  defined but unused).
- /notes/:noteId/members: now openable by editors in read-only mode;
  viewers still hit the permission-denied branch for privacy.

Tests added for NoteShareModal role matrix, NoteViewHeaderActions
visibility, NoteMembersManageSection read-only rendering, and the
NoteMembers page editor branch.

https://claude.ai/code/session_01CbJXeSEbL9m6Cg3GeRob9y

* fix: address PR #794 review comments

Made-with: Cursor

* fix(notes): least-privilege NoteShareModal default + userRole click-path test (#794)

Address CodeRabbit re-review feedback on PR #794:

- NoteShareModal: change `userRole` default from "owner" to "none" so a
  caller that omits the prop cannot accidentally surface owner-only edit
  controls. Both production call sites (NoteViewHeaderActions) already
  pass `userRole` explicitly, so this is safe; the change adds a
  fail-safe for future callers. The test helper `renderModal` now
  injects `userRole="owner"` explicitly to keep tab-structure tests
  focused, and a new test asserts the new least-privilege default.
- NoteViewHeaderActions test: add a click-path assertion that the
  editor's `userRole` propagates into the rendered NoteShareModal so
  the read-only matrix cannot be silently bypassed by a future plumbing
  regression.

https://claude.ai/code/session_01CbJXeSEbL9m6Cg3GeRob9y

* fix(notes): align test mock fallback + bilingual comments (#794)

CodeRabbit follow-up review on PR #794:

- NoteViewHeaderActions test mock: fall back to `"none"` instead of
  `"owner"` so the stub mirrors the component's least-privilege default.
  Otherwise a missing-prop regression would silently render
  `data-user-role="owner"` and pass the click-path test.
- NoteShareModal test: add Japanese alongside the new English-only
  inline comments to match the project's bilingual comment rule.

https://claude.ai/code/session_01CbJXeSEbL9m6Cg3GeRob9y

* fix(notes): renderActions test helper defaults userRole to "none" (#794)

CodeRabbit nitpick on PR #794. All existing renderActions callers pass
userRole explicitly, so changing the helper's fallback to the
least-privilege "none" is safe and surfaces missing overrides instead
of silently rendering owner UI.

https://claude.ai/code/session_01CbJXeSEbL9m6Cg3GeRob9y

* fix(notes): bilingual inline comment in NoteViewHeaderActions test (#794)

CodeRabbit nitpick: add English companion to a Japanese-only comment per
project bilingual-comments rule.

https://claude.ai/code/session_01CbJXeSEbL9m6Cg3GeRob9y

---------

Co-authored-by: Claude <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

* fix(hocuspocus): sync bun lockfile for v4 dependencies

Made-with: Cursor

* fix(hocuspocus): adapt server integration to v4

Made-with: Cursor

* fix(hocuspocus): handle listen() promise rejection on startup

Server.listen() in @hocuspocus/server v4 returns a Promise that can
reject when the port is already in use or the onListen hook throws.
Previously the result was discarded with `void`, so failures were
silently swallowed and the process kept running in a broken state.

Chain a `.catch` handler that logs the error and exits with status 1
so the supervisor (Railway) restarts the service and surfaces the
problem in logs.

Refs: PR #795 review by coderabbitai
Made-with: Cursor
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@otomatty otomatty mentioned this pull request May 2, 2026
11 tasks
* fix(admin): locale-aware date and number formatting

The admin UI uses i18next, but several user-management views were still
calling toLocaleDateString("ja-JP") / toLocaleString("ja-JP") directly.
When the admin language is switched to English, dates and counts kept
rendering in Japanese format, defeating the i18n setup.

- admin/src/lib/dateUtils.ts: add getActiveLocale() and formatNumber(),
  make formatDate() honour the active i18n locale.
- UsersContent.tsx / UserCard.tsx: route the three remaining hardcoded
  ja-JP call sites through these helpers (table row pageCount, mobile
  card pageCount, and the delete-impact lastAiUsageAt date).
- dateUtils.test.ts: cover ja/en locale switching and formatNumber.
- UsersContent.test.tsx: keep the mock backwards compatible with the
  existing "1,234" assertion under the test setup's forced ja locale.

Addresses devin-ai PR #793 review.

Made-with: Cursor

* fix: address PR 793 review comments

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
@railway-app railway-app Bot temporarily deployed to Zedi / development May 2, 2026 02:24 Inactive
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 22 additional findings in Devin Review.

Open in Devin Review

Comment on lines +15 to +17
/**
*
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Empty JSDoc stubs added to aiChatInputHelpers.ts contradict the ESLint config intent in the same PR

This PR adds enableFixer: false to the jsdoc/require-jsdoc ESLint rule (at eslint.config.js:95) and documents: "Disable the auto-fixer so --fix does not insert empty JSDoc stubs. The auto-inserted blocks conflict with jsdoc/no-blank-block-descriptions." However, src/components/ai-chat/aiChatInputHelpers.ts introduces ~20 empty JSDoc blocks (/** \n * \n */) on internal local variables like const chip, const sel, let inserted, const spacer, etc. The same pattern appears in src/hooks/useStorageSettings.ts and src/hooks/useMermaidGenerator.ts. These are exactly the noise the ESLint config change was meant to prevent, and they violate the jsdoc/no-blank-block-descriptions rule (now warn).

Prompt for agents
Multiple files in this PR have had empty JSDoc stubs inserted on local variables and internal functions. This directly contradicts the eslint.config.js change in the same PR that adds `enableFixer: false` specifically to prevent such stubs.

Affected files:
- src/components/ai-chat/aiChatInputHelpers.ts (~20 empty `/** */` blocks on local vars)
- src/hooks/useStorageSettings.ts (multiple empty blocks)
- src/hooks/useMermaidGenerator.ts (multiple empty blocks)

The fix is straightforward: remove all empty JSDoc comment blocks (those containing only `/**\n *\n */` with no description text) from these files. They add no documentation value and will trigger the `jsdoc/no-blank-block-descriptions` lint warning.
Open in Devin Review

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@admin/src/lib/dateUtils.test.ts`:
- Around line 33-46: The tests for formatDate are flaky because
toLocaleDateString depends on machine timezone; update the test cases that call
i18n.changeLanguage and formatDate to use timezone-stable timestamps (e.g., use
a midday UTC time like "2026-04-25T12:00:00Z" and "2026-01-02T12:00:00Z") or
alternatively set the TZ for the test suite before running these specs; change
the three expectations that currently use "2026-04-25T01:23:45Z" and
"2026-01-02T00:00:00Z" to midday UTC timestamps so formatDate produces stable
results across timezones when invoked from the tests that call
i18n.changeLanguage("ja") and i18n.changeLanguage("en").
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8043119e-56a4-40c4-86a6-f93bcb20fc9d

📥 Commits

Reviewing files that changed from the base of the PR and between 346c98d and 74cdbaa.

📒 Files selected for processing (8)
  • admin/src/lib/dateUtils.test.ts
  • admin/src/lib/dateUtils.ts
  • admin/src/pages/users/UserCard.tsx
  • admin/src/pages/users/UsersContent.test.tsx
  • admin/src/pages/users/UsersContent.tsx
  • server/hocuspocus/src/index.ts
  • src/components/editor/TiptapEditor/headingLevelClampExtension.ts
  • src/pages/NoteMembers/NoteMembersManageSection.tsx
✅ Files skipped from review due to trivial changes (2)
  • admin/src/pages/users/UsersContent.test.tsx
  • server/hocuspocus/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/editor/TiptapEditor/headingLevelClampExtension.ts
  • admin/src/pages/users/UsersContent.tsx
  • src/pages/NoteMembers/NoteMembersManageSection.tsx

Comment on lines +33 to +46
it("ja で YYYY/MM/DD に整形する / formats as YYYY/MM/DD in ja", async () => {
await i18n.changeLanguage("ja");
expect(formatDate("2026-04-25T01:23:45Z")).toBe("2026/04/25");
});

it("月日が 1 桁でも 0 埋めされる / zero-pads single-digit month/day", () => {
it("ja で 1 桁の月日を 0 埋めする / zero-pads single-digit month/day in ja", async () => {
await i18n.changeLanguage("ja");
expect(formatDate("2026-01-02T00:00:00Z")).toBe("2026/01/02");
});

it("en で MM/DD/YYYY に整形する / formats as MM/DD/YYYY in en", async () => {
await i18n.changeLanguage("en");
expect(formatDate("2026-04-25T01:23:45Z")).toBe("04/25/2026");
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the date assertions timezone-stable.

toLocaleDateString() depends on the machine timezone, and these UTC timestamps are close enough to midnight that the expected day can shift outside UTC. Use a midday timestamp or pin TZ for this suite so the locale assertions do not flake.

Suggested test tweak
-    expect(formatDate("2026-04-25T01:23:45Z")).toBe("2026/04/25");
+    expect(formatDate("2026-04-25T12:00:00Z")).toBe("2026/04/25");
@@
-    expect(formatDate("2026-01-02T00:00:00Z")).toBe("2026/01/02");
+    expect(formatDate("2026-01-02T12:00:00Z")).toBe("2026/01/02");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/src/lib/dateUtils.test.ts` around lines 33 - 46, The tests for
formatDate are flaky because toLocaleDateString depends on machine timezone;
update the test cases that call i18n.changeLanguage and formatDate to use
timezone-stable timestamps (e.g., use a midday UTC time like
"2026-04-25T12:00:00Z" and "2026-01-02T12:00:00Z") or alternatively set the TZ
for the test suite before running these specs; change the three expectations
that currently use "2026-04-25T01:23:45Z" and "2026-01-02T00:00:00Z" to midday
UTC timestamps so formatDate produces stable results across timezones when
invoked from the tests that call i18n.changeLanguage("ja") and
i18n.changeLanguage("en").

@otomatty otomatty merged commit 25eac6a into main May 2, 2026
23 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.

1 participant