Skip to content

feat(wiki): index ページ・活動ログ・stale 検出を追加 (P4, #598)#604

Merged
otomatty merged 7 commits into
developfrom
claude/condescending-johnson-a9cd98
Apr 17, 2026
Merged

feat(wiki): index ページ・活動ログ・stale 検出を追加 (P4, #598)#604
otomatty merged 7 commits into
developfrom
claude/condescending-johnson-a9cd98

Conversation

@otomatty
Copy link
Copy Markdown
Owner

@otomatty otomatty commented Apr 17, 2026

Summary

Karpathy "LLM Wiki" パターンの P4 フェーズを実装する(#598, 親エピック #594)。

  • Index: 全ページを先頭文字 / 言語カテゴリで自動分類し、特殊ページ __index__pages.special_kind = '__index__')として Markdown 本文を保存する。POST /api/activity/index/rebuild で再構築。初期実装はルールベースで LLM 不要 — 将来 LLM/embedding クラスタに差し替え可能。
  • Activity Log: activity_log テーブルを追加し、clip_ingest / chat_promote / lint_run / wiki_generate / index_build / wiki_schema_update を記録。既存の ingest apply / wiki schema upsert / lint run から書き込む。GET /api/activity で取得、管理画面に一覧ページ(admin/activity-log)を追加。
  • Stale 検出: 新 Lint ルール stalesources.extracted_at > pages.updated_at のページを warn レベルで検出。lint_findings に既存スキーマのまま追加できるよう LintRule 型を拡張。
  • 書き込みは非致命: recordActivity は DB 失敗時に throw せず console.error するのみ(ログが本処理を巻き込まないように)。
  • Vitest ユニットテスト 25 件追加(indexBuilder 13 / activityLogService 7 / stale 5)。

Test plan

  • bun run test:run — 新規 3 テストファイル / 25 テスト すべて pass
  • bun x tsc --noEmit(server/api, admin)— 既存の無関係なエラー以外クリーン
  • bun x eslint — 新規ファイルはエラー 0(@Property TSDoc 警告は既存 schema 同様)
  • bun x prettier --check — 新規 / 変更ファイルすべてクリーン
  • 実 DB に対するマイグレーション検証(drizzle:migrate の適用確認はレビュー時に)
  • __index__ ページ生成 → /page/:id で開いて Markdown 表示を確認
  • lint runstale が finding に現れることを page_sources 付きページで確認

Notes for reviewer

  • activity_logai_usage_logs(課金)・admin_audit_logs(管理監査)とは用途が別で、ユーザー自身の Wiki 成長ログ。管理画面から見えるのは今のところ「自分の」データのみ。
  • special_kind の部分ユニーク制約で、オーナーごとに各 kind は最大 1 ページ。
  • Stale ルールは raw SQL(date-compared JOIN の方が drizzle relational builder より素直)。
  • ルールベースのインデックスは非同期化せず同期実行(ページ数が少ないうち問題なし)。将来的にはバッチ化可能な構造に留めている。

🤖 Generated with Claude Code


Open with Devin

Summary by CodeRabbit

Release Notes

  • New Features

    • Activity log tracking for admin users to monitor wiki actions
    • Auto-generated wiki index page organizing content by category
    • Stale content detection lint rule
  • Improvements

    • YouTube Shorts URL recognition for content extraction
    • Enhanced link detection and orphan page filtering
    • Per-user schema caching to prevent data conflicts
    • Better number pattern recognition in fact checking
  • Bug Fixes

    • Fixed image upload dialog cleanup when closed externally
    • Improved single-digit claim extraction accuracy

LLM Wiki パターンの P4 フェーズを実装する。Karpathy "LLM Wiki" の
補助インフラである index.md / log.md 相当と、P1 で導入した page_sources
を活用した Stale claim 検出を追加する。

- 新スキーマ: activity_log テーブル、pages.special_kind カラム
- 新サービス: activityLogService / indexBuilder(ルールベースの
  カテゴリ分け。LLM / embedding クラスタへの差し替え余地を確保)
- 新ルート: GET /api/activity, POST /api/activity/index/rebuild
- 既存ルートに活動ログ書き込みを追加: ingest.apply / wikiSchema.put /
  lint.run
- 新 Lint ルール: stale(sources.extracted_at > pages.updated_at)
- 管理画面: 活動ログページ / フィルタ付き
- フロント: /index ページで __index__ 再構築 + カテゴリ概要表示
- Vitest ユニットテスト 25 件追加

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

coderabbitai Bot commented Apr 17, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a125725a-f323-4a2a-bb8d-5e7a4c85977e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive activity logging system that records user actions (clip ingestion, chat promotion, lint runs, wiki updates, index rebuilds) across the application, alongside a wiki auto-indexing service that maintains a special __index__ page. It also adds a "stale" lint rule detecting outdated claims, updates lint rule scoping to respect ownership boundaries, extends the page schema with special page kinds, and adds an admin activity log viewer. The public UI gains an index viewer and rebuild interface.

Changes

Cohort / File(s) Summary
Activity Log: Database & Schema
server/api/drizzle/0010_add_activity_log.sql, server/api/src/schema/activityLog.ts, server/api/drizzle/meta/_journal.json
Added activity_log table with UUID primary key, owner FK, kind/actor enums, target page IDs, optional detail JSONB, and creation timestamp. Created indexes on (owner_id, created_at) and (owner_id, kind, created_at) for efficient filtering. Exported TypeScript types for kind and actor constraints.
Activity Log: Server Service
server/api/src/services/activityLogService.ts, server/api/src/__tests__/services/activityLogService.test.ts
Implemented recordActivity for insert-and-return with error suppression and listActivityForOwner with limit/offset clamping and date-range filtering. Exported pagination constants (default 50, max 200). Added comprehensive unit tests validating input normalization and DB error handling.
Activity Log: API Routes
server/api/src/routes/activity.ts, server/api/src/app.ts
Added authenticated /api/activity GET endpoint with kind/actor/date validation returning JSON with entries, total count, and limit. Added POST /api/activity/index/rebuild that records index rebuild activity with category summary. Mounted routes in app.
Activity Log: Admin UI
admin/src/api/activity.ts, admin/src/pages/ActivityLog.tsx, admin/src/App.tsx, admin/src/pages/Layout.tsx
Created typed client (listActivity), ActivityLog page with kind/actor filters and detail formatting (including kind-specific summaries for lint/index/clip/chat), and wired routing with sidebar link. Exported types for entry, response, and filter params.
Page Schema: Special Kinds
server/api/drizzle/0011_add_page_special_kind.sql, server/api/src/schema/pages.ts, server/api/src/schema/index.ts
Added special_kind column (__index__ / __log__) to pages table. Created index on (owner_id, special_kind) and partial unique index for non-deleted special pages. Exported PageSpecialKind type union.
Index Builder: Service
server/api/src/services/indexBuilder.ts, server/api/src/__tests__/services/indexBuilder.test.ts
Implemented category bucketing (digits, A-Z, Japanese, Other), locale-aware sorting, Markdown rendering with wiki link syntax. buildIndexForOwner queries and groups pages; rebuildIndexForOwner transactionally upserts special __index__ page with content. Comprehensive tests validate categorization, ordering, and concurrency handling.
Index: Public Route & UI
src/pages/IndexPage.tsx, src/App.tsx, server/api/src/routes/ingest.ts
Added /index route rendering IndexPage component with rebuild trigger, category table-of-contents, and generated timestamp. Updated ingest route to record clip/chat promotion activities with source metadata.
Stale Lint Rule: Implementation
server/api/src/services/lintEngine/rules/stale.ts, server/api/src/services/lintEngine/rules/stale.test.ts
Created new rule comparing page updated_at against source extracted_at, grouping stale sources by page with ISO timestamp serialization and fallback title. Returns findings with detail payload listing sources and bilingual suggestion. Added unit tests for row folding and data normalization.
Lint Rule Updates
admin/src/api/lint.ts, server/api/src/schema/lintFindings.ts, admin/src/pages/wiki-health/WikiHealthContent.tsx
Extended LintRule union to include "stale". Added Japanese label "古い情報 / Stale" to RULE_LABELS for UI rendering.
Lint Rules: Owner Scoping
server/api/src/services/lintEngine/rules/brokenLink.ts, server/api/src/services/lintEngine/rules/orphan.ts
Constrained broken-link deleted-target and orphan-link-source queries to match target/source owner_id with calling user, preventing cross-user lint findings.
Lint Rule: Pattern Update
server/api/src/services/lintEngine/rules/conflict.ts
Changed numeric-claim regex quantifier from + to *, enabling single-digit number detection (e.g., 3人, 7km).
Lint Engine Integration
server/api/src/services/lintEngine/index.ts
Added runStaleRule to parallel rule execution and integrated recordActivity call post-persistence to log lint runs with total count and per-rule summary.
Wiki Schema: Per-User Caching
src/hooks/useWikiSchema.ts, src/pages/WikiSchemaPage.tsx
Changed React Query key from static ["wiki-schema"] to computed per-user key using userId (fallback "anonymous"). Disabled form inputs during updates with Tailwind opacity/cursor styles.
Image Upload & Dialog
src/components/layout/ImageCreateDialog.tsx, src/hooks/useImageUpload.ts
Added useEffect to reset dialog state and abort in-flight operations on parent-driven closure. Removed post-upload throwIfAborted call, relying on signal propagation through provider.
Markdown & Content Processing
src/lib/markdownExport.ts, src/lib/wikiGenerator/wikiGeneratorFromChatPrompt.ts
Added YouTube Shorts (/shorts/<id>) URL detection for extraction. Added escapeUserSchemaContent helper to escape closing </user_schema> tags in user-supplied schema to prevent injection during prompt generation.
Schema & Routing Updates
server/api/src/schema/index.ts, server/api/src/schema/relations.ts
Re-exported PageSpecialKind from pages schema and new activityLog module with its types. Added activityLogRelations defining owner foreign-key relation.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client as Client<br/>(Admin)
    participant API as API Server
    participant DB as Database
    participant Service as Activity<br/>Service

    User->>Client: Trigger action<br/>(ingest clip,<br/>lint, etc.)
    Client->>API: POST /api/...<br/>(action endpoint)
    API->>Service: recordActivity()<br/>{kind, actor,<br/>targetPageIds}
    Service->>DB: INSERT activity_log
    DB-->>Service: ActivityLog row
    Service-->>API: success/null
    API-->>Client: response
    Client-->>User: action complete

    Note over User,Client: Later...
    User->>Client: Navigate to<br/>Activity Log
    Client->>API: GET /api/activity<br/>?kind=...&actor=...
    API->>Service: listActivityForOwner()<br/>with filters
    Service->>DB: SELECT count(*)<br/>& SELECT rows<br/>ORDER BY created_at DESC
    DB-->>Service: {rows, total}
    Service-->>API: ListActivityResponse
    API-->>Client: entries[], total
    Client-->>User: Display log table
Loading
sequenceDiagram
    participant User
    participant Client as Client<br/>(Public)
    participant API as API Server
    participant IndexSvc as Index<br/>Service
    participant DB as Database

    User->>Client: Click "再構築 / Rebuild"<br/>on /index page
    Client->>API: POST /api/activity/<br/>index/rebuild
    API->>IndexSvc: rebuildIndexForOwner()
    IndexSvc->>DB: SELECT pages<br/>(non-deleted,<br/>non-schema)
    DB-->>IndexSvc: page rows
    IndexSvc->>IndexSvc: buildIndexFromPages()<br/>→ categorize &<br/>sort entries
    IndexSvc->>DB: Lock/SELECT __index__<br/>page row (transaction)
    IndexSvc->>DB: INSERT new __index__<br/>if not exists
    IndexSvc->>DB: UPSERT pageContents<br/>with markdown
    DB-->>IndexSvc: PersistIndexResult<br/>{pageId, created,<br/>document}
    IndexSvc->>DB: recordActivity()<br/>kind: "index_build"
    IndexSvc-->>API: result with<br/>categories[], pageId
    API-->>Client: JSON response
    Client-->>User: Show summary &<br/>link to __index__
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 Hops with glee
Activity logs now gleam,
Pages find their index place,
Stale claims caught with grace,
Wiki wisdom grows complete! 📖

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically summarizes the three main changes: index page feature, activity log feature, and stale detection. It is concise, uses standard feat() convention, includes reference ID, and directly describes what was added.
Docstring Coverage ✅ Passed Docstring coverage is 88.06% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/condescending-johnson-a9cd98

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

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 the "LLM Wiki" pattern, introducing a comprehensive suite of features for content ingestion, wiki health monitoring, and automated organization. Key updates include a new YouTube ingestion pipeline with AI-generated summaries, client-side OCR and image description capabilities using Tesseract.js and LLM Vision, and a "Promote to Wiki" flow for chat conversations. The PR also adds a linting engine to detect issues like orphan pages or factual conflicts, along with an activity log and automated index generation. Feedback identifies a critical atomicity issue in the wiki schema update logic where database operations are not correctly grouped within a single transaction, and notes a UX limitation where only the first page in a multi-page promotion triggers AI content generation.

Comment thread server/api/src/routes/wikiSchema.ts Outdated
Comment on lines +95 to +138
const pageId = await db.transaction(async (tx) => {
const [existing] = await tx
.select({ id: pages.id })
.from(pages)
.where(and(eq(pages.ownerId, userId), eq(pages.isSchema, true), eq(pages.isDeleted, false)))
.for("update")
.limit(1);

if (existing) {
await tx.update(pages).set({ title, updatedAt: now }).where(eq(pages.id, existing.id));
return existing.id;
}

const [newPage] = await tx
.insert(pages)
.values({
ownerId: userId,
title,
isSchema: true,
createdAt: now,
updatedAt: now,
})
.returning({ id: pages.id });

if (!newPage) {
throw new HTTPException(500, { message: "Failed to create schema page" });
}
return newPage.id;
});

// Upsert page_contents in a single round-trip.
// page_contents を 1 回の往復で upsert する。
await db
.insert(pageContents)
.values({
pageId,
ydocState: Buffer.alloc(0),
contentText: content,
updatedAt: now,
})
.onConflictDoUpdate({
target: pageContents.pageId,
set: { contentText: content, updatedAt: now },
});
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

Wiki スキーマの更新処理において、pages テーブルの更新と page_contents テーブルの更新が別々のクエリとして実行されており、アトミック性が保証されていません。また、page_contents の更新にトランザクション用のハンドル tx ではなく db が使用されています。これにより、途中でエラーが発生した場合に不整合なデータが残る可能性があります。すべての更新処理を一つのトランザクション内にまとめてください。

  const pageId = await db.transaction(async (tx) => {
    const [existing] = await tx
      .select({ id: pages.id })
      .from(pages)
      .where(and(eq(pages.ownerId, userId), eq(pages.isSchema, true), eq(pages.isDeleted, false)))
      .for("update")
      .limit(1);

    let pid: string;
    if (existing) {
      await tx.update(pages).set({ title, updatedAt: now }).where(eq(pages.id, existing.id));
      pid = existing.id;
    } else {
      const [newPage] = await tx
        .insert(pages)
        .values({
          ownerId: userId,
          title,
          isSchema: true,
          createdAt: now,
          updatedAt: now,
        })
        .returning({ id: pages.id });

      if (!newPage) {
        throw new HTTPException(500, { message: "Failed to create schema page" });
      }
      pid = newPage.id;
    }

    await tx
      .insert(pageContents)
      .values({
        pageId: pid,
        ydocState: Buffer.alloc(0),
        contentText: content,
        updatedAt: now,
      })
      .onConflictDoUpdate({
        target: pageContents.pageId,
        set: { contentText: content, updatedAt: now },
      });

    return pid;
  });

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.

対応しました(commit cbcd9a3)。

pages の更新と page_contents の upsert を同一トランザクション内に揃え、いずれかが失敗した場合に部分書き込みが残らないようにしました。

Fixed in commit cbcd9a3 — both the pages update and the page_contents upsert now run inside the same transaction, so a partial failure cannot leave the schema row and its body out of sync.

Comment on lines +185 to +204
selectedEntities.map((entity) =>
createPage({ title: entity.title, content: "" }).catch(() => null),
),
);

const firstCreated = created.find((p): p is NonNullable<typeof p> => p != null);
if (!firstCreated?.id) throw new Error("no pages created");

const firstEntity = selectedEntities[created.indexOf(firstCreated)];
const pending: PendingChatPageGenerationState = {
outline: `- ${firstEntity.summary}`,
conversationText,
userSchema: schemaData?.content,
conversationId,
};
toast({ title: t("aiChat.notifications.promoteSuccess") });
onClose();
navigate(`/page/${firstCreated.id}`, {
state: { pendingChatPageGeneration: pending },
});
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

複数のエンティティを選択して Wiki ページを作成する場合、すべてのページが作成されますが、AI による本文生成(pendingChatPageGeneration)が予約されるのは最初の 1 ページのみです。残りのページは空の状態で作成されるため、ユーザーが「選択したすべての項目について AI が記事を書いてくれる」と期待している場合、混乱を招く可能性があります。現状の制限事項として受け入れるか、将来的に複数ページの連続生成をサポートするなどの検討が必要です。

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.

対応不要(意図された現状仕様)。

コメント 3098313366 と同件です。複数選択時の AI 生成は最初の 1 ページのみ、それ以外は空ボディで作成し、開いたタイミングで個別生成する暫定設計になっています。並行 LLM 生成のコスト/レートポリシーが定まり次第、別 PR で対応予定です。

Intentional — see #3098313366. Multi-page AI generation is deferred to a follow-up PR; remaining pages are created empty and generated on open.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c413a4c33

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread server/api/src/routes/ingest.ts Outdated
Comment on lines +170 to +171
const apiKeyName = getProviderApiKeyName(provider);
const apiKey = process.env[apiKeyName];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Derive ingest API key from validated model provider

/api/ingest/plan validates the model and then calls the provider from modelInfo.provider, but the API key is selected from the raw body.provider. If those differ (for example, stale/mismatched client settings), the request uses a valid model with the wrong credential and fails as an upstream auth error. Use modelInfo.provider for key lookup (or reject mismatched provider/model pairs) so valid models don't fail with avoidable 502s.

Useful? React with 👍 / 👎.

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.

対応しました(commit cbcd9a3)。

getProviderApiKeyName の引数を modelInfo.provider に変更し、clip.ts / ext.ts と同じく DB 解決済みの provider で API キーを引くようにしました。

Fixed in commit cbcd9a3 — the API key is now derived from the validated modelInfo.provider.

Comment thread server/api/src/routes/ingest.ts Outdated

// --- Model access & usage enforcement (mirrors /api/ai/chat) ---
const tier = await getUserTier(userId, db);
const modelInfo = await validateModelAccess(model, tier, db);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return 4xx for invalid ingest model ids

This route calls validateModelAccess directly, so known client input errors like a removed/unknown model id throw regular errors that fall through to the global handler as 500s. In practice, a stale model selection from the client is treated as server failure instead of a request problem. Map these validation errors to HTTPException (as done in other routes) so clients receive correct 400/403 responses.

Useful? React with 👍 / 👎.

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.

対応しました(commit cbcd9a3)。

validateModelAccessvalidateModelAccessOrThrow に置換し、未知モデル / tier 制限を 4xx として返すようにしました。

Fixed in commit cbcd9a3 — invalid / forbidden ingest models now return 4xx via validateModelAccessOrThrow.

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: 20

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 (2)
src/lib/storage/providers/S3Provider.ts (1)

8-14: 🛠️ Refactor suggestion | 🟠 Major

Replace empty JSDoc on exported symbols with meaningful bilingual docs.

Line 8 and Line 35 currently have empty doc blocks, which leaves exported API contracts undocumented.

📘 Proposed fix
-/**
- *
- */
+/**
+ * Context required by {`@link` S3Provider} for auth and API resolution.
+ * {`@link` S3Provider} が認証と API 解決に利用するコンテキスト。
+ */
 export interface S3ProviderContext {
   getToken: () => Promise<string | null>;
   baseUrl?: string;
 }

-/**
- *
- */
+/**
+ * Default storage provider backed by Zedi media endpoints (S3 upload flow).
+ * Zedi のメディア API(S3 アップロードフロー)を利用するデフォルトストレージプロバイダー。
+ */
 export class S3Provider implements StorageProviderInterface {

As per coding guidelines: Add TSDoc/JSDoc comments to all exported functions, types, and interfaces and Include both Japanese and English in comments and documentation.

Also applies to: 35-38

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

In `@src/lib/storage/providers/S3Provider.ts` around lines 8 - 14, Replace the
empty JSDoc blocks on exported API symbols with meaningful bilingual (English +
Japanese) TSDoc comments: document the S3ProviderContext interface (describe its
purpose and each member: getToken and baseUrl) and the other exported symbol
around lines 35-38 (replace its empty block with a similar bilingual description
and param/return details). Use short English sentences followed by Japanese
translations for each description and annotate parameter types/nullable behavior
and any usage notes so external consumers can understand the contract in both
languages.
src/components/layout/ImageCreateDialog.tsx (1)

39-45: ⚠️ Potential issue | 🟡 Minor

空の JSDoc ではガイドライン未達です。

export している ImageCreateDialog に説明のない空コメントだけが残っています。ここは用途と主要 props を日英で書いた TSDoc/JSDoc に置き換えたいです。

As per coding guidelines, "Add TSDoc/JSDoc comments to all exported functions, types, and interfaces" and "Include both Japanese and English in comments and documentation".

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

In `@src/components/layout/ImageCreateDialog.tsx` around lines 39 - 45, Replace
the empty JSDoc blocks above the exported const ImageCreateDialog with a proper
TSDoc/JSDoc comment (for the exported symbol ImageCreateDialog) that briefly
describes the component's purpose in both Japanese and English and documents the
main props: open (boolean), onOpenChange (callback), and onCreated (callback)
including their types/behavior; ensure the comment follows project's TSDoc style
and sits immediately above the ImageCreateDialog declaration.
🟡 Minor comments (5)
src/components/ai-chat/EntityRow.tsx-24-31 (1)

24-31: ⚠️ Potential issue | 🟡 Minor

Expose selected state to assistive tech.

The row is a toggle-like control, but Line 24 button does not expose pressed/selected state. Add aria-pressed so screen readers can announce state changes.

♿ Proposed fix
     <button
       type="button"
+      aria-pressed={isSelected}
       className={`border-border hover:bg-muted w-full rounded-md border p-3 text-left transition-colors ${
         isSelected ? "bg-muted border-primary" : ""
       }`}
       onClick={() => onToggle(index)}
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ai-chat/EntityRow.tsx` around lines 24 - 31, The button in
EntityRow.tsx is a toggle-like control but doesn't expose its state; update the
button element (the one with onClick={() => onToggle(index)}) to include
aria-pressed={isSelected} so assistive tech can announce pressed/selected state,
keeping the existing class logic and onToggle handler intact.
src/lib/aiChat/extractEntitiesPrompt.ts-83-96 (1)

83-96: ⚠️ Potential issue | 🟡 Minor

Drop blank entities after trimming.

Whitespace-only title / summary values survive the current filter and become empty strings in the returned array. That can leak blank candidates into the promote flow.

💡 Minimal fix
   return arr
     .filter(
       (item): item is { title: string; summary: string; isNew: boolean } =>
         typeof item === "object" &&
         item !== null &&
         typeof (item as Record<string, unknown>).title === "string" &&
         typeof (item as Record<string, unknown>).summary === "string",
     )
     .map((item) => ({
       title: item.title.trim(),
       summary: item.summary.trim(),
       isNew: item.isNew !== false,
     }))
+    .filter((item) => item.title.length > 0 && item.summary.length > 0)
     .slice(0, 5);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/aiChat/extractEntitiesPrompt.ts` around lines 83 - 96, The current
chain in extractEntitiesPrompt trims title/summary only in map, so
whitespace-only values pass the filter and become empty strings; update the
filter to trim and reject items whose title.trim() or summary.trim() are empty
(keep the type-guard checks), then map as before using the trimmed values and
isNew defaulting to true when absent; reference the existing
arr.filter(...).map(...).slice(0, 5) flow and the title/summary/isNew properties
when making the change.
server/api/src/schema/lintFindings.ts-56-63 (1)

56-63: ⚠️ Potential issue | 🟡 Minor

Replace the empty TSDoc blocks with real bilingual docs.

These exported aliases are part of the public schema surface, but the current /** */ stubs still leave them undocumented.

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Add TSDoc/JSDoc comments to all exported functions, types, and interfacesandInclude both Japanese and English in comments and documentation`.

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

In `@server/api/src/schema/lintFindings.ts` around lines 56 - 63, Replace the
empty TSDoc blocks above the exported type aliases with concise bilingual
(English + Japanese) documentation describing what each alias represents:
document LintFinding as the inferred select/result shape from lintFindings
(lintFindings.$inferSelect) and NewLintFinding as the inferred insert/input
shape for creating new records (lintFindings.$inferInsert); ensure each TSDoc
explains purpose, typical usage, and notes about mutability/nullable fields in
both languages and attach them directly above the respective type aliases
(LintFinding and NewLintFinding).
admin/src/api/lint.ts-7-7 (1)

7-7: ⚠️ Potential issue | 🟡 Minor

LintRule type missing stale rule.

The backend schema includes "stale" as a valid lint rule (used in runStaleRule), but the admin client type excludes it. This causes a type mismatch when the backend returns stale findings.

Fix
-export type LintRule = "orphan" | "ghost_many" | "title_similar" | "conflict" | "broken_link";
+export type LintRule = "orphan" | "ghost_many" | "title_similar" | "conflict" | "broken_link" | "stale";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/src/api/lint.ts` at line 7, The LintRule union type is missing the
"stale" member which causes type mismatches with backend responses (backend uses
runStaleRule); update the exported type LintRule in admin/src/api/lint.ts to
include "stale" (e.g., export type LintRule = "orphan" | "ghost_many" |
"title_similar" | "conflict" | "broken_link" | "stale") so the admin client
accepts stale findings from the backend.
server/api/src/services/ingestPlanner.ts-235-248 (1)

235-248: ⚠️ Potential issue | 🟡 Minor

Fail fast on malformed conflicts to keep parser truly strict.

Line 235–248 currently drops invalid conflicts values silently. That makes schema violations look valid and hides output drift from the LLM.

💡 Suggested strict validation
 function parseConflicts(value: unknown): IngestConflict[] | undefined {
   if (value === undefined) return undefined;
-  if (!Array.isArray(value)) return undefined;
+  if (!Array.isArray(value)) {
+    throw new IngestPlanParseError(`"conflicts" must be an array when provided`);
+  }
   const out: IngestConflict[] = [];
   for (const entry of value) {
-    if (!isRecord(entry)) continue;
+    if (!isRecord(entry)) {
+      throw new IngestPlanParseError(`Each "conflicts" entry must be an object`);
+    }
     const claim = asNonEmptyString(entry.claim);
     const existing = asNonEmptyString(entry.existing);
-    if (!claim || !existing) continue;
+    if (!claim || !existing) {
+      throw new IngestPlanParseError(
+        `Each "conflicts" entry requires non-empty "claim" and "existing"`,
+      );
+    }
     const note = asNonEmptyString(entry.note);
     out.push(note ? { claim, existing, note } : { claim, existing });
   }
-  return out.length > 0 ? out : undefined;
+  return out;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/services/ingestPlanner.ts` around lines 235 - 248, The parser
currently silently ignores malformed conflicts; update parseConflicts to fail
fast by throwing a descriptive Error when the input is not an array or when any
element is not a record or lacks required non-empty strings for claim/existing
(use isRecord and asNonEmptyString to validate each entry), while preserving the
existing behavior of returning undefined for an undefined input and still
returning undefined only for an empty but well-formed array of IngestConflict;
ensure error messages reference "conflicts", "claim", and "existing" so the
failure is actionable.
🧹 Nitpick comments (26)
src/components/editor/extensions/WikiLinkExtension.ts (1)

21-21: Consider consolidating wiki-link parsing primitives into one shared utility.

WIKI_LINK_PASTE_REGEX/title extraction here and wiki-link text parsing in the transform module encode nearly the same contract. Centralizing them would reduce future drift between paste-time and post-parse behavior.

♻️ Optional refactor sketch
- export const WIKI_LINK_PASTE_REGEX = /\[\[[^[\]]+\]\]/g;
- const WIKI_LINK_TITLE_REGEX = /\[\[([^[\]]+)\]\]/;
-
- function extractWikiLinkTitle(fullMatch: string): string | null {
-   const m = fullMatch.match(WIKI_LINK_TITLE_REGEX);
-   const title = (m?.[1] ?? "").trim();
-   return title || null;
- }
+ import {
+   WIKI_LINK_LITERAL_REGEX,
+   extractWikiLinkTitle,
+ } from "./wikiLinkSyntax";
+
+ export const WIKI_LINK_PASTE_REGEX = WIKI_LINK_LITERAL_REGEX;

Also applies to: 27-37

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

In `@src/components/editor/extensions/WikiLinkExtension.ts` at line 21, The
WIKI_LINK_PASTE_REGEX and the title-extraction logic in the transform module
implement the same wiki-link parsing rules separately; consolidate them into a
single shared utility (e.g., export a parseWikiLink / extractWikiTitle function
and a single regex constant) and replace occurrences in WikiLinkExtension
(WIKI_LINK_PASTE_REGEX) and the transform module to call that utility so
paste-time detection and post-parse transformation use the identical
parsing/validation code paths.
src/hooks/usePromoteToWiki.ts (1)

13-27: Add an explicit return type for this exported hook.

Right now the hook’s public shape is inferred from the implementation. A named return type makes the API easier to follow and catches accidental surface changes earlier.

♻️ Suggested change
+type UsePromoteToWikiResult = {
+  open: boolean;
+  conversationText: string;
+  handlePromote: (messageContent: string) => void;
+  close: () => void;
+};
+
-export function usePromoteToWiki(messages: ChatMessage[]) {
+export function usePromoteToWiki(messages: ChatMessage[]): UsePromoteToWikiResult {

As per coding guidelines: **/*.{ts,tsx}: any type is forbidden; type all values explicitly.

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

In `@src/hooks/usePromoteToWiki.ts` around lines 13 - 27, The exported hook
usePromoteToWiki currently relies on inferred return types; add an explicit
named return type (e.g., an interface or type alias) describing the public
shape: open: boolean; conversationText: string; handlePromote: (messageContent:
string) => void; close: () => void — then annotate usePromoteToWiki with that
return type and ensure the signatures of handlePromote and close match the
declared types (reference usePromoteToWiki, handlePromote, close and
ChatMessage/serializeChatMessagesForPageGeneration to locate the code).
package.json (1)

84-86: Consider pinning these overrides to specific versions for reproducibility.

While "lodash-es": ">=4.18.0" and "esbuild": ">=0.25.0" do resolve against published versions, using >= in overrides allows future releases to be pulled, making builds non-reproducible. Pin both overrides to specific known versions (e.g., "lodash-es": "4.18.1", "esbuild": "0.25.0" or latest stable) instead.

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

In `@package.json` around lines 84 - 86, The overrides in package.json currently
use open ranges ("esbuild": ">=0.25.0" and "lodash-es": ">=4.18.0") which can
break reproducibility; update the overrides to pin both dependencies to specific
known-good versions by replacing "esbuild": ">=0.25.0" with a concrete version
string (e.g., "esbuild": "0.25.0" or the chosen stable version) and "lodash-es":
">=4.18.0" with a concrete version string (e.g., "lodash-es": "4.18.1" or the
chosen stable version) so builds resolve deterministically.
server/api/src/lib/aiAccessHelpers.ts (1)

35-42: Replace string-based error message matching with typed error codes.

The helper maps "FORBIDDEN" and "Model not found or inactive" by exact message comparison. The upstream validateModelAccess() in usageService.ts throws plain Error instances with these hardcoded strings. If any wording changes upstream, this mapping silently breaks and errors surface as 500s instead of the intended 4xx codes. Consider refactoring usageService.ts to throw typed error codes or a custom error class, then branch on that instead of prose strings.

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

In `@server/api/src/lib/aiAccessHelpers.ts` around lines 35 - 42, Refactor error
handling to use typed error codes instead of exact message text: introduce a
custom error type (e.g., UsageError with a code string like "FORBIDDEN" or
"MODEL_NOT_FOUND") in usageService.ts and make validateModelAccess() throw that;
then update the catch in aiAccessHelpers.ts to check if err is an instance of
that UsageError and branch on err.code to throw the appropriate HTTPException
(403 for "FORBIDDEN", 400 for "MODEL_NOT_FOUND"), falling back to the existing
generic error path for unknown errors.
src/lib/storage/types.ts (1)

6-11: Consider adding inline documentation for the signal property.

The new signal property enables abortable uploads, which is a good addition. Adding a brief JSDoc comment would improve discoverability.

📝 Optional: Add inline documentation
 export interface UploadOptions {
   fileName?: string;
   folder?: string;
   onProgress?: (progress: UploadProgress) => void;
+  /** Optional AbortSignal to cancel the upload. / アップロードをキャンセルするための AbortSignal(任意)。 */
   signal?: AbortSignal;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/storage/types.ts` around lines 6 - 11, Add a short JSDoc comment for
the UploadOptions.signal property explaining that it accepts an AbortSignal to
cancel an in-flight upload, how consumers should pass an AbortController.signal,
and that implementations should listen for signal.aborted to stop the upload and
reject/cleanup accordingly; annotate it directly above the signal field in the
UploadOptions interface (referencing UploadOptions.signal and AbortSignal) so
IDEs show the usage and behavior.
server/api/drizzle/0011_add_page_special_kind.sql (1)

6-14: special_kind の値域を DB 制約で固定してください。

text のままだと typo/不正値が入り得るため、__index__ / __log__(または NULL)に制限する CHECK 制約を追加すると安全です。

Proposed migration tweak
 ALTER TABLE "pages" ADD COLUMN "special_kind" text;
+ALTER TABLE "pages"
+  ADD CONSTRAINT "chk_pages_special_kind_allowed"
+  CHECK (
+    "special_kind" IS NULL
+    OR "special_kind" IN ('__index__', '__log__')
+  );
 --> statement-breakpoint
 CREATE INDEX "idx_pages_owner_special_kind" ON "pages" ("owner_id", "special_kind");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/drizzle/0011_add_page_special_kind.sql` around lines 6 - 14, Add a
CHECK constraint to the pages table to restrict "special_kind" to the allowed
set (NULL, '__index__', '__log__'); modify the migration that adds the
"special_kind" column and indexes by adding a constraint like ALTER TABLE
"pages" ADD CONSTRAINT "ck_pages_special_kind_enum" CHECK ("special_kind" IS
NULL OR "special_kind" IN ('__index__','__log__')); ensure this constraint is
applied after creating the "special_kind" column and keep the existing indexes
("idx_pages_owner_special_kind" and "idx_pages_unique_special_kind_per_owner")
unchanged so they continue to reference the same column and partial-unique
behavior.
src/components/ai-chat/EntityRow.tsx (1)

45-47: Avoid hardcoded "New" label; use i18n key.

Line 46 hardcodes English text in a localized UI flow. Please route this through translation resources.

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

In `@src/components/ai-chat/EntityRow.tsx` around lines 45 - 47, The span in
EntityRow.tsx currently hardcodes the label "New"; change it to use the app's
i18n translation function instead (e.g., replace the literal with
t('entity.status.new') or the chosen key), import or access the translation
hook/provider used in this component (such as useTranslation or i18n.t), and
ensure a sensible fallback string is provided when the key is missing; update
any tests or story usage to expect the translation key instead of the hardcoded
English text.
src/lib/storage/providers/GitHubProvider.ts (1)

36-38: Drop the empty constructor JSDoc block.

Line 36-38 currently adds no useful API contract information.

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

In `@src/lib/storage/providers/GitHubProvider.ts` around lines 36 - 38, Remove the
empty JSDoc block above the constructor in the GitHubProvider class: delete the
multi-line "/** ... */" comment that provides no API information so the
constructor (constructor(...)/class GitHubProvider) is not preceded by an empty
doc comment; if any meaningful docs are needed, replace it with a single-line or
populated JSDoc describing the constructor parameters and behavior instead of
leaving it blank.
src/lib/storage/providers/GoogleDriveProvider.ts (1)

49-51: Remove the empty constructor JSDoc block.

Line 49-51 is a blank doc block and reduces readability without adding contract details.

♻️ Proposed cleanup
-  /**
-   *
-   */
   constructor(config: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/storage/providers/GoogleDriveProvider.ts` around lines 49 - 51,
Remove the empty JSDoc block above the constructor in the GoogleDriveProvider
class: locate the no-content /** */ block that precedes the constructor method
in GoogleDriveProvider and delete that blank doc comment so the constructor has
no empty JSDoc left behind; leave any meaningful comments or add a proper doc
comment only if you intend to document the constructor.
server/api/src/__tests__/services/activityLogService.test.ts (1)

42-51: Make console.error spy cleanup failure-safe.

If an assertion fails before Line 51, the spy can leak into subsequent tests. Prefer try/finally around assertions.

🔧 Suggested test hardening
-    const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
-
-    const result = await recordActivity(db as never, {
-      ownerId: "user-1",
-      kind: "lint_run",
-      actor: "user",
-    });
-    expect(result).toBeNull();
-    expect(errorSpy).toHaveBeenCalledWith("recordActivity failed (non-fatal)", expect.any(Error));
-    errorSpy.mockRestore();
+    const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+    try {
+      const result = await recordActivity(db as never, {
+        ownerId: "user-1",
+        kind: "lint_run",
+        actor: "user",
+      });
+      expect(result).toBeNull();
+      expect(errorSpy).toHaveBeenCalledWith("recordActivity failed (non-fatal)", expect.any(Error));
+    } finally {
+      errorSpy.mockRestore();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/__tests__/services/activityLogService.test.ts` around lines 42
- 51, The console.error spy (errorSpy) in the recordActivity test can leak if
assertions fail; wrap the creation of the spy, the call to recordActivity(...)
and the expects in a try/finally so that errorSpy.mockRestore() always runs,
keeping the test teardown failure-safe for console.error; locate the test that
calls recordActivity and sets errorSpy via vi.spyOn(console,
"error").mockImplementation(...) and move the expect/assert calls into a try
block and restore the spy in the finally block.
src/lib/wikiGenerator/wikiGeneratorPrompt.ts (1)

115-115: Make schema insertion newline-safe.

Line 115 concatenates {{schema}} and ## タイトル directly. If schema rendering ever omits a trailing newline, the header merges into schema text.

✅ Safer template tweak
-{{schema}}## タイトル
+{{schema}}
+## タイトル
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/wikiGenerator/wikiGeneratorPrompt.ts` at line 115, The template in
wikiGeneratorPrompt.ts concatenates {{schema}} and the header "## タイトル" without
a separator, which can merge the header into the schema if the rendered schema
lacks a trailing newline; update the template string that contains "{{schema}}##
タイトル" to ensure a newline (e.g., "{{schema}}\n\n## タイトル" or at least
"{{schema}}\n## タイトル") so the header always starts on its own line.
server/api/drizzle/0010_add_activity_log.sql (1)

11-12: Constrain kind/actor to known values at DB level.

Using plain text here allows silent drift/typos in activity taxonomy. Consider CHECK constraints (or enums) so invalid event categories are rejected early.

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

In `@server/api/drizzle/0010_add_activity_log.sql` around lines 11 - 12, Add a
DB-level constraint for the "kind" and "actor" columns to prevent silent typos:
replace the plain text columns in the 0010_add_activity_log.sql migration by
either (a) defining and using a PostgreSQL ENUM for activity kinds and actor
types and changing "kind" and "actor" from text to those ENUM types, or (b)
adding explicit CHECK constraints on "kind" and "actor" (e.g., CHECK(kind IN
(...)) and CHECK(actor IN (...))) listing the allowed values; update any
INSERTs/ORM mappings that write to these columns to use the chosen allowed
values.
server/api/src/lib/youtubeEmbedServerExtension.ts (1)

27-29: Consider URL-encoding the videoId.

The videoId is interpolated directly into the URL. While YouTube video IDs are typically alphanumeric with hyphens/underscores, if untrusted input somehow reaches this function, it could potentially be exploited.

🛡️ Defensive fix
 function buildYouTubeEmbedUrl(videoId: string): string {
-  return `https://www.youtube.com/embed/${videoId}`;
+  return `https://www.youtube.com/embed/${encodeURIComponent(videoId)}`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/lib/youtubeEmbedServerExtension.ts` around lines 27 - 29, The
buildYouTubeEmbedUrl function currently interpolates videoId directly into the
URL; change it to safely encode or validate the id before embedding by applying
a URL-encoding step (e.g., use encodeURIComponent on the videoId) or validate
against the expected YouTube ID pattern and then use the encoded/validated value
when constructing the embed URL in buildYouTubeEmbedUrl to prevent injection
from untrusted input.
admin/src/pages/ActivityLog.tsx (2)

84-84: Add TSDoc to the exported component.

Per coding guidelines, exported functions should have TSDoc/JSDoc. The component already has a brief comment on lines 80-83 but it's not a proper TSDoc block.

+/**
+ * 管理画面「活動ログ」ページ。フィルタリングと再読み込み機能を提供。
+ * Admin Activity Log page with filtering and reload capabilities.
+ */
 export default function ActivityLog() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/src/pages/ActivityLog.tsx` at line 84, Add a proper TSDoc block to the
exported React component ActivityLog: convert the existing brief comment into a
/** ... */ TSDoc immediately above export default function ActivityLog(),
including a one-line description of the component, an `@returns` tag specifying
JSX.Element (or React.ReactElement), and any pertinent notes (no params for this
component); ensure the TSDoc sits directly above the ActivityLog declaration so
linters and IDEs pick it up.

73-77: Missing wiki_generate case in formatDetail.

The wiki_generate kind is defined in KIND_LABELS but has no dedicated formatting in formatDetail. It falls back to JSON.stringify(detail) which may produce verbose output.

♻️ Suggested fix
   if (entry.kind === "wiki_schema_update") {
     const len = typeof detail.contentLength === "number" ? detail.contentLength : "?";
     return `content: ${len} chars`;
   }
+  if (entry.kind === "wiki_generate") {
+    const len = typeof detail.contentLength === "number" ? detail.contentLength : "?";
+    return `生成: ${len} chars`;
+  }
   return JSON.stringify(detail);
🤖 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 73 - 77, formatDetail currently
handles "wiki_schema_update" but not the "wiki_generate" kind referenced in
KIND_LABELS, so wiki_generate entries fall back to JSON.stringify; update
formatDetail to add a branch for entry.kind === "wiki_generate" that returns a
concise, human-friendly string (for example summarizing relevant fields such as
prompt, model, status, or number of pages/characters) rather than raw JSON;
locate the formatDetail function and the KIND_LABELS constant to ensure the new
case matches the label and include the specific detail properties you want
summarized (e.g., detail.prompt, detail.model, detail.pages.length or
detail.contentLength) in the returned string.
src/components/page/LintSuggestions.tsx (2)

38-46: Silent failure on fetch error may hide issues.

fetchPageFindings returns an empty array when the API returns a non-OK status. This masks potential errors (auth failures, server errors) from the user. Consider letting React Query handle the error state.

♻️ Alternative: throw on error
 async function fetchPageFindings(pageId: string): Promise<LintFindingResponse[]> {
   const baseUrl = getApiBaseUrl();
   const res = await fetch(`${baseUrl}/api/lint/findings/page/${encodeURIComponent(pageId)}`, {
     credentials: "include",
   });
-  if (!res.ok) return [];
+  if (!res.ok) {
+    if (res.status === 404) return []; // No findings is valid
+    throw new Error(`Failed to fetch lint findings: ${res.status}`);
+  }
   const data = (await res.json()) as { findings: LintFindingResponse[] };
   return data.findings;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/page/LintSuggestions.tsx` around lines 38 - 46, The
fetchPageFindings function currently swallows non-OK responses by returning an
empty array; change it to throw an error so React Query can surface error
states. In fetchPageFindings (and where getApiBaseUrl is used) check res.ok and
if false extract useful info (status, statusText and any JSON error message) and
throw a descriptive Error containing those details instead of returning []; keep
credentials: "include" and the same endpoint, but allow the thrown error to
propagate to React Query.

68-104: Consider adding the "stale" rule to icon and label mappings.

The PR adds a new stale lint rule, but ruleIcon and ruleLabel don't have explicit cases for it. It will fall through to the default (Info icon, raw rule name), which works but isn't as polished.

♻️ Suggested addition
 function ruleIcon(rule: string): ReactNode {
   switch (rule) {
     case "orphan":
       return <FileQuestion className="h-4 w-4" />;
     case "ghost_many":
       return <Ghost className="h-4 w-4" />;
     case "title_similar":
       return <FileText className="h-4 w-4" />;
     case "conflict":
       return <Zap className="h-4 w-4" />;
     case "broken_link":
       return <Unlink className="h-4 w-4" />;
+    case "stale":
+      return <AlertTriangle className="h-4 w-4" />;
     default:
       return <Info className="h-4 w-4" />;
   }
 }
 function ruleLabel(rule: string): string {
   switch (rule) {
     case "orphan":
       return "孤立ページ";
     case "ghost_many":
       return "Ghost Link 過多";
     case "title_similar":
       return "タイトル類似";
     case "conflict":
       return "矛盾";
     case "broken_link":
       return "リンク切れ";
+    case "stale":
+      return "古いページ";
     default:
       return rule;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/page/LintSuggestions.tsx` around lines 68 - 104, Add an
explicit "stale" mapping to both ruleIcon and ruleLabel: in ruleIcon(rule:
string) add a case "stale" that returns an appropriate stale/clock icon (e.g.,
Clock) with the same sizing class used elsewhere, and ensure Clock is imported
if not already; in ruleLabel(rule: string) add a case "stale" that returns a
concise Japanese label such as "古くなったページ" (or "陳腐化") instead of falling back to
the default. Keep the same switch structure and styling used by the other cases
(FileQuestion, Ghost, FileText, Zap, Unlink, Info).
server/api/src/routes/activity.ts (1)

108-120: Clamp limit before querying to avoid fetching unbounded rows.

Currently, limit is only clamped in the response (Line 132), but listActivityForOwner receives the raw value. If a caller passes limit=999999, the service will attempt to fetch that many rows from the DB.

♻️ Suggested change
   const limitRaw = Number(c.req.query("limit") ?? ACTIVITY_LIST_DEFAULT_LIMIT);
   const offsetRaw = Number(c.req.query("offset") ?? 0);
-  const limit = Number.isFinite(limitRaw) ? limitRaw : ACTIVITY_LIST_DEFAULT_LIMIT;
+  const limit = Number.isFinite(limitRaw)
+    ? Math.min(Math.max(limitRaw, 1), ACTIVITY_LIST_MAX_LIMIT)
+    : ACTIVITY_LIST_DEFAULT_LIMIT;
   const offset = Number.isFinite(offsetRaw) ? offsetRaw : 0;

Then simplify Line 132:

-    limit: Math.min(Math.max(limit, 1), ACTIVITY_LIST_MAX_LIMIT),
+    limit,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/routes/activity.ts` around lines 108 - 120, Clamp the parsed
limit (limitRaw) to a safe maximum before calling listActivityForOwner so the DB
query cannot request an excessively large number of rows: compute a sanitized
limit using Number.isFinite(limitRaw) ? Math.min(limitRaw,
ACTIVITY_LIST_DEFAULT_LIMIT) : ACTIVITY_LIST_DEFAULT_LIMIT (or similar) and pass
that sanitized value to listActivityForOwner; update any subsequent usage (the
later response clamping around line 132) to use the already-clamped limit or
remove the redundant clamp. Ensure you update references to limit/limitRaw
consistently so listActivityForOwner never receives the unbounded value.
server/api/src/routes/clip.ts (1)

182-206: Avoid redundant getUserTier and validateModelAccess calls.

Lines 187-188 re-fetch tier and modelInfo that were already obtained at Lines 148-149 (when hasProvider && hasModel). Since the usage-recording block is only entered when aiProvider && aiModel && body.model are truthy (same condition as the outer if-block), the earlier values can be reused.

♻️ Suggested change — hoist tier/modelInfo for reuse
+  let tier: Awaited<ReturnType<typeof getUserTier>> | undefined;
+  let modelInfo: Awaited<ReturnType<typeof validateModelAccessOrThrow>> | undefined;
+
   if (hasProvider && hasModel) {
     // ...
-    const tier = await getUserTier(userId, db);
-    const modelInfo = await validateModelAccessOrThrow(aiModel, tier, db);
+    tier = await getUserTier(userId, db);
+    modelInfo = await validateModelAccessOrThrow(aiModel, tier, db);
     // ...
   }

   // Inside the usage-recording block:
   if (result.aiUsage && aiProvider && aiModel && body.model) {
     try {
-      const tier = await getUserTier(userId, db);
-      const modelInfo = await validateModelAccess(body.model, tier, db);
+      // tier and modelInfo are already available from the validation block above
       const costUnits = calculateCost(
         { inputTokens, outputTokens },
-        modelInfo.inputCostUnits,
-        modelInfo.outputCostUnits,
+        modelInfo!.inputCostUnits,
+        modelInfo!.outputCostUnits,
       );
       // ...
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/routes/clip.ts` around lines 182 - 206, The usage-recording
block redundantly calls getUserTier and validateModelAccess again; instead,
hoist and reuse the previously fetched tier and modelInfo obtained when
hasProvider && hasModel is true so the if (result.aiUsage && aiProvider &&
aiModel && body.model) branch uses those existing variables; remove the
duplicate getUserTier(userId, db) and validateModelAccess(body.model, tier, db)
calls and reference the earlier tier and modelInfo when calling calculateCost
and recordUsage (ensure tier and modelInfo are in scope where result.aiUsage is
handled).
server/api/src/routes/wikiSchema.ts (1)

125-138: Consider moving the pageContents upsert inside the transaction.

The pageContents upsert (Lines 127-138) happens outside the transaction that creates/updates the pages row. If the page insert succeeds but the content upsert fails (e.g., due to a transient DB error), you'll have a schema page with no content — and the error will propagate to the caller while the partially-created page remains.

Moving this into the transaction ensures atomicity.

♻️ Suggested change
   const pageId = await db.transaction(async (tx) => {
     // ... existing page upsert logic ...

+    // Upsert page_contents within the same transaction for atomicity.
+    await tx
+      .insert(pageContents)
+      .values({
+        pageId: existing ? existing.id : newPage!.id,
+        ydocState: Buffer.alloc(0),
+        contentText: content,
+        updatedAt: now,
+      })
+      .onConflictDoUpdate({
+        target: pageContents.pageId,
+        set: { contentText: content, updatedAt: now },
+      });

     return existing ? existing.id : newPage!.id;
   });

-  // Upsert page_contents in a single round-trip.
-  // page_contents を 1 回の往復で upsert する。
-  await db
-    .insert(pageContents)
-    .values({
-      pageId,
-      ydocState: Buffer.alloc(0),
-      contentText: content,
-      updatedAt: now,
-    })
-    .onConflictDoUpdate({
-      target: pageContents.pageId,
-      set: { contentText: content, updatedAt: now },
-    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/routes/wikiSchema.ts` around lines 125 - 138, The
page_contents upsert using
pageContents.insert().values(...).onConflictDoUpdate(...) is currently executed
outside the transaction that creates/updates the pages row, risking partial
commits; move this entire upsert into the same transaction scope that handles
the pages mutation so both the pages and pageContents operations are executed
atomically. Locate the transaction block that writes to pages (the code handling
pageId, now, content and pages insertion/update) and include the pageContents
upsert there, ensuring you use the same pageId, content (contentText), ydocState
and updatedAt values and keep the onConflictDoUpdate for pageContents.pageId so
the transaction either commits both changes or rolls back both on error.
server/api/src/schema/lintFindings.ts (1)

49-53: Consider indexing the unresolved-findings hot path directly.

The new read paths filter by owner_id + resolved_at IS NULL and sort by created_at, but the current indexes only cover owner_id/rule. As resolved history grows, those queries will still scan past resolved rows. A partial index for unresolved rows, e.g. on (owner_id, created_at DESC) WHERE resolved_at IS NULL, would fit this PR's access pattern much better.

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

In `@server/api/src/schema/lintFindings.ts` around lines 49 - 53, Add a partial
index covering the unresolved-findings hot path: create an index (e.g.
idx_lint_findings_owner_unresolved) on (table.ownerId, table.createdAt DESC)
with a WHERE clause that selects only unresolved rows (table.resolvedAt IS NULL)
so queries filtering by owner + resolved_at IS NULL and ordering by created_at
can use the index instead of scanning resolved rows.
server/api/src/services/lintEngine/index.ts (1)

82-88: Add explicit return types to the exported lint helpers.

These signatures currently float with Drizzle inference. Giving them explicit Promise<...> return types keeps the service boundary stable and avoids silently changing callers when the query shape changes later.

As per coding guidelines, **/*.{ts,tsx}: Use TypeScript strict mode; any type is forbidden; type all values explicitly`.

Also applies to: 99-110, 122-129

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

In `@server/api/src/services/lintEngine/index.ts` around lines 82 - 88, The
exported helper getUnresolvedFindings currently relies on Drizzle inference;
update its signature to include an explicit return type such as
Promise<Array<InferModel<typeof lintFindings, "select">>> (import InferModel
from 'drizzle-orm') so the service boundary is stable, e.g. change function
declaration to export async function getUnresolvedFindings(ownerId: string, db:
Database): Promise<Array<InferModel<typeof lintFindings, "select">>> { ... };
apply the same explicit Promise<...> return-type pattern to the other exported
helpers referenced (the ones around lines 99-110 and 122-129) using
InferModel<typeof lintFindings, "select"> to reflect the query shape.
src/lib/wikiGenerator/wikiGeneratorProviders.ts (1)

16-19: Consider adding JSDoc for the private helper function.

While buildSchemaBlock is internal, adding @internal or a brief TSDoc would improve maintainability.

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

In `@src/lib/wikiGenerator/wikiGeneratorProviders.ts` around lines 16 - 19, Add a
short TSDoc/JSDoc block to the internal helper buildSchemaBlock describing its
purpose (builds the user schema markdown block from an optional string) and mark
it as internal (e.g., `@internal`) so maintainers know it’s private; place the doc
comment immediately above the buildSchemaBlock function and include parameter
and return annotations (e.g., `@param` userSchema and `@returns`) for clarity.
src/lib/ai/describeImage.ts (1)

124-138: Consider adding max_tokens for OpenAI vision call.

The Anthropic call (Line 150) specifies max_tokens: DEFAULT_MAX_TOKENS, but the OpenAI call omits it. While OpenAI has defaults, explicitly setting it ensures consistent behavior across providers and prevents unexpectedly long/expensive responses.

♻️ Suggested fix
       const response = await openai.chat.completions.create(
         {
           model: settings.model,
+          max_tokens: DEFAULT_MAX_TOKENS,
           messages: [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ai/describeImage.ts` around lines 124 - 138, The OpenAI vision call
in openai.chat.completions.create (inside describeImage) omits an explicit
max_tokens, causing inconsistent behavior versus the Anthropic call; add a
max_tokens property (use the same DEFAULT_MAX_TOKENS constant used for
Anthropic) to the request options alongside model, messages, and signal so
responses are bounded and costs are predictable.
src/lib/ocr/tesseractOcr.ts (1)

63-63: Default language fallback inconsistent with JSDoc.

The JSDoc at Line 23-24 states that languages falls back to detectOcrLanguages, but the implementation defaults to ["eng"] directly. Consider using detectOcrLanguages() for consistency.

♻️ Suggested fix
-  const langs = languages && languages.length > 0 ? languages : ["eng"];
+  const langs = languages && languages.length > 0 ? languages : detectOcrLanguages(undefined);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ocr/tesseractOcr.ts` at line 63, The fallback for OCR languages
currently hardcodes ["eng"] in the assignment to langs; update this to call
detectOcrLanguages() when the incoming languages parameter is empty or undefined
so the behavior matches the JSDoc. In the function that defines langs (look for
the langs variable assignment in tesseractOcr.ts and the function signature that
accepts languages), replace the hardcoded fallback with a call to
detectOcrLanguages() and ensure you handle its return (e.g., use its result if
it returns a non-empty array, otherwise fall back to ["eng"] as a final
safeguard).
server/api/src/services/indexBuilder.ts (1)

136-137: Escape titles before embedding them into [[...]] markdown links.

Line 136–137 writes raw titles into wiki-link syntax. Titles containing ]] or line breaks can corrupt the generated index markdown. A small sanitization helper here will make rendering robust.

💡 Suggested hardening
@@
 export function renderIndexMarkdown(categories: IndexCategory[], generatedAt: string): string {
+  const escapeWikiLinkTitle = (title: string): string =>
+    title.replace(/\]\]/g, "\\]\\]").replace(/\r?\n/g, " ");
@@
     for (const entry of category.entries) {
       const displayTitle = entry.title.trim().length === 0 ? "(無題 / untitled)" : entry.title;
-      lines.push(`- [[${displayTitle}]]`);
+      lines.push(`- [[${escapeWikiLinkTitle(displayTitle)}]]`);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/services/indexBuilder.ts` around lines 136 - 137, The code
writes raw entry.title into wiki-link notation via displayTitle and lines.push,
which can break markdown if titles contain "]]" or newlines; add a small
sanitization helper (e.g., escapeWikiLink or sanitizeTitle) that trims, removes
or replaces problematic sequences like "]]" and line breaks (or encodes them)
and returns a safe string, then use that helper when computing displayTitle and
when calling lines.push(`- [[${...}]]`) so the generated index always contains
escaped/safe titles.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7367fa55-fcbf-4e17-a413-822a32289dd3

📥 Commits

Reviewing files that changed from the base of the PR and between b14ef62 and 8c413a4.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • server/api/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (111)
  • .env.example
  • admin/src/App.tsx
  • admin/src/api/activity.ts
  • admin/src/api/lint.ts
  • admin/src/pages/ActivityLog.tsx
  • admin/src/pages/Layout.tsx
  • admin/src/pages/wiki-health/WikiHealthContent.tsx
  • admin/src/pages/wiki-health/index.tsx
  • package.json
  • server/api/drizzle/0007_add_sources_and_page_sources.sql
  • server/api/drizzle/0008_add_lint_findings.sql
  • server/api/drizzle/0009_add_page_is_schema.sql
  • server/api/drizzle/0010_add_activity_log.sql
  • server/api/drizzle/0011_add_page_special_kind.sql
  • server/api/drizzle/meta/_journal.json
  • server/api/package.json
  • server/api/src/__tests__/routes/clip.test.ts
  • server/api/src/__tests__/routes/ext.test.ts
  • server/api/src/__tests__/routes/ingest.test.ts
  • server/api/src/__tests__/services/activityLogService.test.ts
  • server/api/src/__tests__/services/indexBuilder.test.ts
  • server/api/src/__tests__/services/ingestPlanner.test.ts
  • server/api/src/app.ts
  • server/api/src/lib/aiAccessHelpers.ts
  • server/api/src/lib/articleExtractor.ts
  • server/api/src/lib/clipAndCreate.ts
  • server/api/src/lib/youtubeEmbedServerExtension.ts
  • server/api/src/lib/youtubeExtractor.test.ts
  • server/api/src/lib/youtubeExtractor.ts
  • server/api/src/routes/activity.ts
  • server/api/src/routes/clip.ts
  • server/api/src/routes/ext.ts
  • server/api/src/routes/ingest.ts
  • server/api/src/routes/lint.ts
  • server/api/src/routes/wikiSchema.ts
  • server/api/src/schema/activityLog.ts
  • server/api/src/schema/index.ts
  • server/api/src/schema/lintFindings.ts
  • server/api/src/schema/pageSources.ts
  • server/api/src/schema/pages.ts
  • server/api/src/schema/relations.ts
  • server/api/src/schema/sources.ts
  • server/api/src/services/activityLogService.ts
  • server/api/src/services/indexBuilder.ts
  • server/api/src/services/ingestPlanner.ts
  • server/api/src/services/lintEngine/index.ts
  • server/api/src/services/lintEngine/rules/brokenLink.ts
  • server/api/src/services/lintEngine/rules/conflict.test.ts
  • server/api/src/services/lintEngine/rules/conflict.ts
  • server/api/src/services/lintEngine/rules/ghostMany.ts
  • server/api/src/services/lintEngine/rules/orphan.ts
  • server/api/src/services/lintEngine/rules/stale.test.ts
  • server/api/src/services/lintEngine/rules/stale.ts
  • server/api/src/services/lintEngine/rules/titleSimilar.test.ts
  • server/api/src/services/lintEngine/rules/titleSimilar.ts
  • server/api/src/services/lintEngine/types.ts
  • server/api/src/services/youtubeService.test.ts
  • server/api/src/services/youtubeService.ts
  • src/App.tsx
  • src/components/ai-chat/AIChatMessage.tsx
  • src/components/ai-chat/AIChatMessages.tsx
  • src/components/ai-chat/AIChatPanelContent.tsx
  • src/components/ai-chat/EntityRow.tsx
  • src/components/ai-chat/PromoteToWikiDialog.tsx
  • src/components/editor/PageEditor/PageEditorContent.tsx
  • src/components/editor/PageEditor/usePendingChatPageGeneration.ts
  • src/components/editor/extensions/MarkdownPasteExtension.test.ts
  • src/components/editor/extensions/WikiLinkExtension.test.ts
  • src/components/editor/extensions/WikiLinkExtension.ts
  • src/components/editor/extensions/transformWikiLinksInContent.test.ts
  • src/components/editor/extensions/transformWikiLinksInContent.ts
  • src/components/layout/ImageCreateDialog.test.tsx
  • src/components/layout/ImageCreateDialog.tsx
  • src/components/page/LintSuggestions.tsx
  • src/hooks/useImageUpload.test.ts
  • src/hooks/useImageUpload.ts
  • src/hooks/usePromoteToWiki.ts
  • src/hooks/useWebClipper.test.ts
  • src/hooks/useWebClipper.ts
  • src/hooks/useWikiSchema.ts
  • src/i18n/index.ts
  • src/i18n/locales/en/aiChat.json
  • src/i18n/locales/en/wikiSchema.json
  • src/i18n/locales/ja/aiChat.json
  • src/i18n/locales/ja/wikiSchema.json
  • src/lib/ai/describeImage.test.ts
  • src/lib/ai/describeImage.ts
  • src/lib/aiChat/extractEntitiesPrompt.test.ts
  • src/lib/aiChat/extractEntitiesPrompt.ts
  • src/lib/api/apiClient.test.ts
  • src/lib/api/apiClient.ts
  • src/lib/contentUtils.ts
  • src/lib/markdownExport.ts
  • src/lib/ocr/tesseractOcr.test.ts
  • src/lib/ocr/tesseractOcr.ts
  • src/lib/storage/providers/GitHubProvider.ts
  • src/lib/storage/providers/GoogleDriveProvider.ts
  • src/lib/storage/providers/GyazoProvider.ts
  • src/lib/storage/providers/S3Provider.test.ts
  • src/lib/storage/providers/S3Provider.ts
  • src/lib/storage/types.ts
  • src/lib/wikiGenerator.test.ts
  • src/lib/wikiGenerator.ts
  • src/lib/wikiGenerator/wikiGeneratorFromChatPrompt.test.ts
  • src/lib/wikiGenerator/wikiGeneratorFromChatPrompt.ts
  • src/lib/wikiGenerator/wikiGeneratorPrompt.ts
  • src/lib/wikiGenerator/wikiGeneratorProviders.ts
  • src/pages/AIChatDetail.tsx
  • src/pages/IndexPage.tsx
  • src/pages/WikiSchemaPage.tsx
  • src/types/chatPageGeneration.ts

Comment on lines +23 to +29
const RULE_LABELS: Record<LintRule, string> = {
orphan: "孤立ページ / Orphan",
ghost_many: "Ghost Link 過多 / Ghost Excess",
title_similar: "タイトル類似 / Title Similar",
conflict: "矛盾 / Conflict",
broken_link: "リンク切れ / Broken Link",
};
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if stale rule exists in LintRule type definition
rg -n "stale" admin/src/api/lint.ts server/api/src/

Repository: otomatty/zedi

Length of output: 2689


🏁 Script executed:

# Check LintRule type definition in admin/src/api/lint.ts
cat admin/src/api/lint.ts

Repository: otomatty/zedi

Length of output: 2256


🏁 Script executed:

# Read WikiHealthContent.tsx to see exact RULE_LABELS content
sed -n '23,29p' admin/src/pages/wiki-health/WikiHealthContent.tsx

Repository: otomatty/zedi

Length of output: 291


Update LintRule type and add stale rule to RULE_LABELS.

The server-side implementation includes a stale lint rule, but the admin-side LintRule type definition in admin/src/api/lint.ts does not include it. This creates a type mismatch: if the API returns findings with rule: "stale", TypeScript will fail to type-check them. Update the LintRule type to include "stale" and add the corresponding label to RULE_LABELS.

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

In `@admin/src/pages/wiki-health/WikiHealthContent.tsx` around lines 23 - 29, The
LintRule union type in admin/src/api/lint.ts is missing "stale" which causes a
type mismatch when the server returns rule: "stale"; update the LintRule type to
include "stale" (e.g., add | "stale") and then add the corresponding label entry
to RULE_LABELS in WikiHealthContent.tsx (add stale: "陳腐化 / Stale" or your chosen
label) so the mapping covers the new rule; ensure any places that import or use
LintRule still compile after the addition.

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.

現コードを確認したところ、admin/src/api/lint.tsLintRule 型・admin/src/pages/wiki-health/WikiHealthContent.tsxRULE_LABELS ともに stale を含んでおり、Record<LintRule, string> の網羅性も保たれていました。

Verified in the current branch: LintRule already includes "stale", and RULE_LABELS defines stale: "古い情報 / Stale", so Record<LintRule, string> is exhaustive. No further change needed.

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, ご確認ありがとうございます。ファイル全体を見ると、現在のコードでは stale: "古い情報 / Stale" がすでに RULE_LABELS に含まれており、admin/src/api/lint.tsLintRule 型にも "stale" が定義されていることを確認しました。

差分の表示が RULE_LABELS の途中(broken_link の行)で切れていたため、stale エントリが存在しないと誤認してしまいました。指摘は的外れでした。失礼いたしました。


🧠 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: CLAUDE.md:0-0
Timestamp: 2026-03-29T06:01:24.318Z
Learning: Applies to **/*.{ts,tsx} : 公開 API や型の破壊的変更がないか確認する。

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.

Comment thread server/api/src/lib/articleExtractor.ts
Comment thread server/api/src/routes/ext.ts
Comment thread server/api/src/routes/ingest.ts Outdated
Comment thread server/api/src/services/indexBuilder.ts
Comment thread src/lib/markdownExport.ts
Comment thread src/lib/wikiGenerator/wikiGeneratorFromChatPrompt.ts
Comment thread src/pages/IndexPage.tsx Outdated
Comment on lines +58 to +74
const run = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await rebuildIndex();
setData(result);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
toast({ title: "Index の再構築に失敗しました", variant: "destructive" });
} finally {
setLoading(false);
}
}, [toast]);

useEffect(() => {
void run();
}, [run]);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 17, 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:

# First, let's read the file to see the actual code
cat -n src/pages/IndexPage.tsx | sed -n '50,80p'

Repository: otomatty/zedi

Length of output: 1310


🏁 Script executed:

# Check if bun and lint command are available
cd /tmp && which bun || echo "bun not found in PATH"

Repository: otomatty/zedi

Length of output: 139


🏁 Script executed:

# Look for ESLint configuration
find . -maxdepth 2 -name ".eslintrc*" -o -name "eslint.config.*" -o -name ".eslintignore" | head -20

Repository: otomatty/zedi

Length of output: 75


🏁 Script executed:

# Check package.json for lint script definition
cat package.json | grep -A 5 '"lint"'

Repository: otomatty/zedi

Length of output: 363


🏁 Script executed:

# Check the ESLint configuration
cat eslint.config.js | head -100

Repository: otomatty/zedi

Length of output: 2856


🏁 Script executed:

# Look for any react-hooks related rules
grep -i "react-hooks\|set-state" eslint.config.js

Repository: otomatty/zedi

Length of output: 141


🏁 Script executed:

# Check if there are any lint errors by examining the rules more
cat eslint.config.js

Repository: otomatty/zedi

Length of output: 4175


🏁 Script executed:

# Let me check the full ESLint config to see the actual react-hooks rules being applied
tail -50 eslint.config.js

Repository: otomatty/zedi

Length of output: 1355


🏁 Script executed:

# Let me search for any react-hooks rule configuration in the config
grep -n "react-hooks" eslint.config.js | head -20

Repository: otomatty/zedi

Length of output: 146


🏁 Script executed:

# Check if there's a specific rule override for exhaustive-deps
grep -i "exhaustive" eslint.config.js

Repository: otomatty/zedi

Length of output: 39


🏁 Script executed:

# Let me try to check if there's a way to see what react-hooks rules are actually enabled
# by checking the react-hooks plugin version and documentation
cat package.json | grep -A 2 -B 2 "react-hooks"

Repository: otomatty/zedi

Length of output: 257


🏁 Script executed:

# Let's try to understand what specific rule would be violated
# The review mentions "set-state-in-effect" but that's not a standard ESLint rule
# The standard react-hooks rules are: exhaustive-deps and rules-of-hooks
# Let me check the actual issue by looking at the pattern

# The code has:
# 1. useCallback with [toast] dependency
# 2. useEffect with [run] dependency where run depends on toast
# This means when toast changes, run is recreated, which triggers useEffect

# This violates exhaustive-deps because toast should be in useEffect deps
# but it's indirectly through run

# Let me verify by checking if there are any lint error comments in the file
grep -n "eslint-disable\|TODO\|FIXME\|lint" src/pages/IndexPage.tsx | head -20

Repository: otomatty/zedi

Length of output: 39


🏁 Script executed:

# Let's check the full context of the IndexPage file to see if there are any other clues
wc -l src/pages/IndexPage.tsx

Repository: otomatty/zedi

Length of output: 84


🏁 Script executed:

# Let me check if the file has been recently modified and if there are any comments about lint
git log --oneline src/pages/IndexPage.tsx | head -5

Repository: otomatty/zedi

Length of output: 114


🏁 Script executed:

# Let me verify what react-hooks rules are actually available in v7.0.1
# by checking what "recommended" includes
npm info eslint-plugin-react-hooks@7.0.1 2>/dev/null || echo "npm not available, checking alternative"

Repository: otomatty/zedi

Length of output: 967


🏁 Script executed:

# Since npm might not be available, let's check if there's documentation in node_modules
# or if we can find any documentation about the actual rules
find . -name "node_modules/eslint-plugin-react-hooks" -type d 2>/dev/null | head -1

Repository: otomatty/zedi

Length of output: 39


🏁 Script executed:

# Let me verify the actual issue by checking if the pattern in the code
# could trigger exhaustive-deps rule with the current setup
# The key issue would be: does toast change frequently?
# Let me check how useToast() is implemented

find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "useToast" | head -5

Repository: otomatty/zedi

Length of output: 258


🏁 Script executed:

# Let me search for useToast hook definition
grep -r "export.*useToast\|function useToast\|const useToast" --include="*.ts" --include="*.tsx" | head -10

Repository: otomatty/zedi

Length of output: 254


🏁 Script executed:

# Let me check the useToast implementation to understand its dependencies
cat -n packages/ui/src/hooks/use-toast.ts

Repository: otomatty/zedi

Length of output: 5381


🏁 Script executed:

# Now let me verify the actual lint error by checking if there's a way to see
# what the react-hooks/exhaustive-deps rule would say about the code
# The issue is that useEffect depends on [run], but run depends on [toast]
# This is an exhaustive-deps violation

# Let me check if there are any comments about this in the code or git history
git log -p --all -S "set-state-in-effect\|exhaustive-deps" -- src/pages/IndexPage.tsx | head -50

Repository: otomatty/zedi

Length of output: 39


🏁 Script executed:

# Let me also check if there are similar patterns in other files to see 
# how the project handles this
grep -r "useCallback.*\[toast\]" src --include="*.tsx" --include="*.ts" -A 5 | head -40

Repository: otomatty/zedi

Length of output: 39


🏁 Script executed:

# Let me check if there's actually a "set-state-in-effect" rule
# or if the issue is about exhaustive-deps
web_search "eslint react-hooks set-state-in-effect rule"

Repository: otomatty/zedi

Length of output: 107


🏁 Script executed:

# Let me verify the actual problem by checking what rules react-hooks provides
# The two main rules are: exhaustive-deps and rules-of-hooks
# Let me check if the code would actually fail lint

# The problem is:
# 1. useToast() returns a new object with a new toast function every render
# 2. useCallback depends on [toast]
# 3. So run is recreated every render because toast is not stable
# 4. useEffect depends on [run]
# 5. This creates an exhaustive-deps violation

# Let me check if there's an eslint-disable comment or any special handling
grep -B 5 -A 5 "useEffect" src/pages/IndexPage.tsx | grep -A 10 "useEffect"

Repository: otomatty/zedi

Length of output: 503


🏁 Script executed:

# Let me verify by checking if the object return from useToast is memoized
# or if each call creates a new object
cat -n packages/ui/src/hooks/use-toast.ts | sed -n '166,184p'

Repository: otomatty/zedi

Length of output: 625


🏁 Script executed:

# Let me check the actual structure more carefully
# The issue is whether `toast` from useToast() is stable across renders

# Looking at the code:
# - useToast returns a new object literal each time
# - But the toast function inside is the stable module-level function
# - However, ESLint sees it as part of the returned object

# Let me check if there are any eslint-disable comments in the actual file
cat -n src/pages/IndexPage.tsx | grep -i "eslint\|disable\|todo"

Repository: otomatty/zedi

Length of output: 138


🏁 Script executed:

# Let me verify if this is indeed an exhaustive-deps issue by checking
# the ESLint rule more carefully
# The return from useToast is: { ...state, toast, dismiss }
# Each render creates a NEW object, even if the properties are the same

# So when destructuring { toast } = useToast(), the object reference changes each render
# But the toast function reference itself should be stable since it's module-level

# Let me check if there's a memoization wrapper around useToast
grep -A 20 "function useToast" packages/ui/src/hooks/use-toast.ts | head -30

Repository: otomatty/zedi

Length of output: 521


🏁 Script executed:

# Let me search for any documentation or issues related to this pattern
find . -name "*.md" -type f | xargs grep -l "useToast\|IndexPage" | head -5

Repository: otomatty/zedi

Length of output: 39


🏁 Script executed:

# Let me check the actual git diff to see if there were any lint errors reported
git show HEAD:src/pages/IndexPage.tsx | grep -A 5 -B 5 "useEffect"

Repository: otomatty/zedi

Length of output: 651


🏁 Script executed:

# Let me check if there's a way to understand the actual dependency chain better
# The key insight: useToast returns a new object every render
# So destructuring { toast } creates a dependency on the object reference
# Even though the toast function itself is stable

# Let me verify by checking if there's any other pattern in the codebase
# for handling this situation
grep -r "useCallback.*\[" src --include="*.tsx" | grep "useToast\|toast" | head -10

Repository: otomatty/zedi

Length of output: 39


🏁 Script executed:

# Let me also check if eslint-disable-next-line is being used anywhere
grep -B 1 "useEffect" src/pages/IndexPage.tsx

Repository: otomatty/zedi

Length of output: 149


🏁 Script executed:

# Now let me verify the fundamental issue by analyzing the dependency pattern
# The real issue is whether the review's claim about a lint error is accurate

# Let me check if there's a tsconfig or eslintrc that would affect this
cat tsconfig.json | grep -A 5 -B 5 "strict"

Repository: otomatty/zedi

Length of output: 229


🏁 Script executed:

# The review mentions "react-hooks/set-state-in-effect" which is not a standard rule
# Let me search the codebase to see if this rule is mentioned anywhere
grep -r "set-state-in-effect" . --include="*.js" --include="*.ts" --include="*.tsx" --include="*.json"

Repository: otomatty/zedi

Length of output: 758


🏁 Script executed:

# Let me also verify what rules the react-hooks plugin actually provides
# by checking if there are any custom rules configured
grep -i "rules\|react-hooks" eslint.config.js | head -30

Repository: otomatty/zedi

Length of output: 266


🏁 Script executed:

# Great! The rule does exist. Let me check what version is actually installed
cat node_modules/.pnpm/eslint-plugin-react-hooks@7.1.0_eslint@9.39.4/package.json | grep -A 2 '"version"' | head -5

Repository: otomatty/zedi

Length of output: 166


🏁 Script executed:

# Let me check if the actual installed version is 7.1.0, not 7.0.1
ls -la node_modules/.pnpm/ | grep "react-hooks"

Repository: otomatty/zedi

Length of output: 155


🏁 Script executed:

# Now let me check the actual rule implementation to understand what it detects
cat node_modules/.pnpm/eslint-plugin-react-hooks@7.1.0_eslint@9.39.4/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js | grep -A 50 "name: 'set-state-in-effect'" | head -60

Repository: otomatty/zedi

Length of output: 2262


🏁 Script executed:

# Let me also verify the exact line numbers that would be reported
# by looking at how the rule works with the actual code pattern
grep -B 5 -A 10 "const run = useCallback" src/pages/IndexPage.tsx

Repository: otomatty/zedi

Length of output: 613


🏁 Script executed:

# Now that I know the rule exists, let me check the actual implementation
# The rule is new in v7.1.0 but package.json shows 7.0.1
# This means there was an update

# Let me check the actual installed version more carefully
cat node_modules/.pnpm/eslint-plugin-react-hooks@7.1.0_eslint@9.39.4/node_modules/eslint-plugin-react-hooks/package.json | grep '"version"'

Repository: otomatty/zedi

Length of output: 78


🏁 Script executed:

# Let me see what the set-state-in-effect rule specifically checks for
# by looking at how it's used in similar patterns in the codebase

# First, let me check the two files that already have eslint-disable for this rule
cat -n src/components/ai-chat/AIChatPanelContent.tsx | grep -B 10 -A 10 "set-state-in-effect"

Repository: otomatty/zedi

Length of output: 1143


🏁 Script executed:

# Let me also check the other file
cat -n src/hooks/useAuthenticatedImageUrl.ts | grep -B 10 -A 10 "set-state-in-effect"

Repository: otomatty/zedi

Length of output: 922


🏁 Script executed:

# Now let me verify the exact nature of the set-state-in-effect rule
# by checking what patterns trigger it

# The rule description says: "Validates against calling setState synchronously in an effect"
# The IndexPage code does this: useEffect calls run(), which calls setLoading(true) synchronously

# Let me verify if the current code would actually trigger this rule by checking
# whether there's a way to test this locally

# For now, let me check if there are any other instances in the codebase with similar patterns
grep -r "useEffect.*() => {" src --include="*.tsx" -A 3 | grep -B 3 "run()\|call()" | head -30

Repository: otomatty/zedi

Length of output: 168


🏁 Script executed:

# Let me verify the pattern more carefully by understanding what the rule detects
# The rule flags: setState called synchronously in an effect

# In IndexPage.tsx:
# useEffect(() => { void run(); }, [run])
# where run = useCallback(async () => { setLoading(true); ... }, [toast])

# This means: effect calls run(), which immediately (synchronously) calls setLoading(true)
# This IS a set-state-in-effect violation

# Let me confirm by checking if adding the mounted flag pattern would pass
# by looking at useAuthenticatedImageUrl again - does it have the flag pattern?

cat -n src/hooks/useAuthenticatedImageUrl.ts | sed -n '44,80p'

Repository: otomatty/zedi

Length of output: 1525


🏁 Script executed:

# Now I understand - the set-state-in-effect rule IS real (in v7.1.0)
# and the pattern in IndexPage DOES match what it flags

# Let me verify the exact violation:
# 1. useEffect({ void run() }, [run])
# 2. run calls setLoading(true) synchronously on first line
# This is exactly what set-state-in-effect detects

# The suggested refactor is correct - it moves the logic into the effect
# and adds a mounted check before state updates

# Let me verify that the suggested refactor is sound by checking if
# the dependencies would be correct

# Original:
# - useCallback with [toast]
# - useEffect with [run]
# Problem: toast in dependency → run recreated → useEffect triggers

# Suggested:
# - useEffect with [toast]
# - All state updates guarded by mounted flag
# This is the correct pattern

# The review appears to be technically CORRECT. Let me just confirm
# there are no other issues with the suggested refactor

cat -n src/pages/IndexPage.tsx | sed -n '58,80p'

Repository: otomatty/zedi

Length of output: 975


🏁 Script executed:

# Let me check the imports to ensure the refactor doesn't miss anything
head -20 src/pages/IndexPage.tsx

Repository: otomatty/zedi

Length of output: 760


Refactor mount fetch path to avoid lint-blocking react-hooks/set-state-in-effect.

The code calls run() from useEffect, and run() immediately sets state synchronously. This violates the react-hooks/set-state-in-effect rule (enabled in eslint-plugin-react-hooks v7.1.0) and will block bun run lint.

Suggested refactor
-  useEffect(() => {
-    void run();
-  }, [run]);
+  useEffect(() => {
+    let mounted = true;
+    (async () => {
+      try {
+        const result = await rebuildIndex();
+        if (!mounted) return;
+        setData(result);
+      } catch (e) {
+        if (!mounted) return;
+        setError(e instanceof Error ? e.message : String(e));
+        toast({ title: "Index の再構築に失敗しました", variant: "destructive" });
+      } finally {
+        if (mounted) setLoading(false);
+      }
+    })();
+    return () => {
+      mounted = false;
+    };
+  }, [toast]);
🧰 Tools
🪛 ESLint

[error] 73-73: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/src/pages/IndexPage.tsx:73:10
71 |
72 | useEffect(() => {

73 | void run();
| ^^^ Avoid calling setState() directly within an effect
74 | }, [run]);
75 |
76 | return (

(react-hooks/set-state-in-effect)

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

In `@src/pages/IndexPage.tsx` around lines 58 - 74, The effect calls run()
synchronously which triggers immediate state updates (setLoading/setError) and
violates react-hooks/set-state-in-effect; change the effect to invoke run
asynchronously (e.g. wrap run call in an async IIFE or use
Promise.resolve().then(() => run())) so the state updates occur after the effect
commit. Keep the run useCallback as-is (including its dependencies like toast)
and keep run in the useEffect dependency array; update the useEffect body to
call run via an async wrapper to avoid synchronous setState in the effect.

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.

対応不要と判断しました。

worktree でローカル実行した bun run lint および bunx eslint src/pages/IndexPage.tsx の結果はいずれも react-hooks/set-state-in-effect を含むエラーは 0 件でした。

本リポジトリの eslint-plugin-react-hooks は v7.0.1 で recommended 設定を継承しており、その設定にはこのルールが含まれます。実環境のリンタ(CI で走るもの)で違反が出ない以上、レビュー側の検出は version / 設定不一致による誤検出と判断し、コード変更は行いません。

Not reproducible locally — bun run lint and bunx eslint src/pages/IndexPage.tsx (using the project's eslint-plugin-react-hooks@7.0.1 and the recommended config) report 0 violations of react-hooks/set-state-in-effect for this file. CI uses the same config, so this looks like a false positive in the review tooling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

@otomatty、ご確認ありがとうございます。おっしゃる通りです。

The discrepancy was caused by a version mismatch in the review tooling. The react-hooks/set-state-in-effect rule is included in the recommended preset for eslint-plugin-react-hooks, however my sandbox had v7.1.0 installed while your project pins v7.0.1. There are known behavioral differences in v7.0.1 around how this rule detects violations, and it appears v7.0.1 does not flag the indirect setState call through the run useCallback as a violation — which your local bun run lint correctly confirms.

This was a false positive from the review tooling due to the version difference. No code change is needed. Apologies for the noise!


🌐 Web query:

eslint-plugin-react-hooks 7.0.1 recommended config set-state-in-effect rule included

💡 Result:

Yes. In eslint-plugin-react-hooks 7.0.1, the recommended preset includes the set-state-in-effect rule (rule name: react-hooks/set-state-in-effect). (react.dev)

Citations:


@otomatty、ご確認ありがとうございます。

The web search confirms that react-hooks/set-state-in-effect is included in the recommended preset of v7.0.1, so both versions have the rule. However, the key distinction is detection capability for indirect patterns: the rule may only flag setState called directly in an effect body in v7.0.1, whereas v7.1.0 (which was present in the review sandbox) appears to also trace through intermediate useCallback references. Since your local verification with v7.0.1 reports 0 violations, the sandbox's v7.1.0 detection was a false positive relative to what your actual CI runs.

Your decision to leave the code as-is is well-reasoned. Apologies for the noise!


✏️ Learnings added
Learnt from: otomatty
Repo: otomatty/zedi PR: 604
File: src/pages/IndexPage.tsx:58-74
Timestamp: 2026-04-17T07:12:27.068Z
Learning: In the otomatty/zedi repository, the project uses `eslint-plugin-react-hooks7.0.1` (not the latest v7.1.0). The CodeRabbit sandbox may have a newer version installed, which can cause false positive lint findings for rules like `react-hooks/set-state-in-effect`. When reviewing ESLint violations for this repo, verify whether the rule behavior changed between the project's pinned version and the sandbox version before flagging as an error.

Learnt from: otomatty
Repo: otomatty/zedi PR: 604
File: src/pages/IndexPage.tsx:58-74
Timestamp: 2026-04-17T07:12:27.068Z
Learning: In the otomatty/zedi repository, the project uses `eslint-plugin-react-hooks7.0.1`. The CodeRabbit review sandbox may have a newer version (e.g., v7.1.0) installed, which can cause false positive lint findings. Specifically, the `react-hooks/set-state-in-effect` rule in v7.1.0 traces through intermediate `useCallback` references to detect indirect setState calls inside effects, while v7.0.1 appears to only flag direct setState calls in effect bodies. Always verify ESLint violations against the project's actual pinned plugin version before flagging.

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: 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-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.

Comment on lines +31 to +44
useEffect(() => {
if (data && data.pageId !== syncedId) {
setSyncedId(data.pageId);
}
}, [data, syncedId]);

// Apply remote data when syncedId changes (avoids setState-in-render).
useEffect(() => {
if (!data) return;
setTitle(data.title || "Wiki Schema");
setContent(data.content || "");
setIsDirty(false);
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally sync once per pageId
}, [syncedId]);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 17, 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:

fd WikiSchemaPage.tsx

Repository: otomatty/zedi

Length of output: 85


🏁 Script executed:

# Check the file size and read the relevant sections
wc -l src/pages/WikiSchemaPage.tsx

Repository: otomatty/zedi

Length of output: 89


🏁 Script executed:

# Read the file to see the exact code
cat -n src/pages/WikiSchemaPage.tsx

Repository: otomatty/zedi

Length of output: 6230


🏁 Script executed:

# Try to run lint on this specific file
cd src/pages && bun run lint -- WikiSchemaPage.tsx 2>&1 || true

Repository: otomatty/zedi

Length of output: 98


🏁 Script executed:

# Check what lint commands are available
cat package.json | grep -A 5 '"lint"'

Repository: otomatty/zedi

Length of output: 363


🏁 Script executed:

fd -e eslintrc -e "eslintrc.js" -e "eslint.config.js" 2>/dev/null || echo "Searching for ESLint configs..."
find . -maxdepth 2 -name "*eslint*" -type f 2>/dev/null | head -20

Repository: otomatty/zedi

Length of output: 75


🏁 Script executed:

# Check if eslint is available and run it on the specific file
npx eslint src/pages/WikiSchemaPage.tsx 2>&1 || echo "ESLint check complete"

Repository: otomatty/zedi

Length of output: 2461


🏁 Script executed:

# Check for prettier issues as well
npx prettier --check src/pages/WikiSchemaPage.tsx 2>&1 || echo "Prettier check complete"

Repository: otomatty/zedi

Length of output: 290


🏁 Script executed:

# Look for ESLint config in package.json or root
cat .eslintrc.json 2>/dev/null || cat .eslintrc.js 2>/dev/null || cat eslint.config.js 2>/dev/null || echo "Checking package.json for eslintConfig..."
grep -A 30 '"eslintConfig"' package.json 2>/dev/null || echo "No ESLint config in package.json"

Repository: otomatty/zedi

Length of output: 4208


This effect-based draft sync blocks the lint gate.

Lines 33 and 40 violate react-hooks/set-state-in-effect: calling setSyncedId(), setTitle(), and setContent() synchronously within effects triggers cascading renders. The file will not pass bun run lint as written. Move the editable draft into a keyed child (for example keyed by data?.pageId) or another pattern that initializes local state on mount instead of copying remote data into state from an effect.

Per coding guidelines, bun run lint and bun run format:check checks must pass.

🧰 Tools
🪛 ESLint

[error] 33-33: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/src/pages/WikiSchemaPage.tsx:33:7
31 | useEffect(() => {
32 | if (data && data.pageId !== syncedId) {

33 | setSyncedId(data.pageId);
| ^^^^^^^^^^^ Avoid calling setState() directly within an effect
34 | }
35 | }, [data, syncedId]);
36 |

(react-hooks/set-state-in-effect)


[error] 40-40: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/src/pages/WikiSchemaPage.tsx:40:5
38 | useEffect(() => {
39 | if (!data) return;

40 | setTitle(data.title || "Wiki Schema");
| ^^^^^^^^ Avoid calling setState() directly within an effect
41 | setContent(data.content || "");
42 | setIsDirty(false);
43 | // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally sync once per pageId

(react-hooks/set-state-in-effect)

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

In `@src/pages/WikiSchemaPage.tsx` around lines 31 - 44, The current effects call
setSyncedId, setTitle and setContent synchronously and trigger the
react-hooks/set-state-in-effect lint error; instead remove the sync effects and
move the editable local state into a keyed child component (e.g., render a
WikiEditor or DraftEditor component and give it key={data?.pageId}) so React
remounts and initializes local state from props when pageId changes; eliminate
syncedId state and the setSyncedId/setTitle/setContent calls in the two
useEffect blocks, and ensure the child initializes its title/content via
useState from props (or a memoized initializer) so no parent effect sets state
directly.

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.

対応不要と判断しました(同上)。

worktree で bun run lint および bunx eslint src/pages/WikiSchemaPage.tsx を直接実行しても react-hooks/set-state-in-effect 系のエラーは 0 件でした。exhaustive-deps には既に eslint-disable-next-line を入れており、当該行についてプロジェクトのリンタとしては合格しています。

Not reproducible locally — running bunx eslint src/pages/WikiSchemaPage.tsx against eslint-plugin-react-hooks@7.0.1 (with the recommended config that includes set-state-in-effect) reports 0 errors here. Treating this as a tooling-side false positive.

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 ご確認いただきありがとうございます。サンドボックス環境の ESLint 設定とプロジェクトの実際の設定(eslint-plugin-react-hooks@7.0.1)の差異による誤検知でした。ご指摘の通り、bun run lint が通過しており問題ないと認識しました。今後の誤検知を防ぐため、この点を記録しておきます。


✏️ Learnings added
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 repository, the project uses `eslint-plugin-react-hooks7.0.1` with the `recommended` config. The `react-hooks/set-state-in-effect` rule does NOT fire errors for the two-effect pattern used in `src/pages/WikiSchemaPage.tsx` (where one effect sets `syncedId` and a second effect reads `syncedId` to sync remote data into local state). Running `bunx eslint src/pages/WikiSchemaPage.tsx` and `bun run lint` locally reports 0 errors for this pattern. The sandbox static analysis tool produced a false positive for this rule — do not flag this pattern as a lint error in this project.

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: CR
Repo: otomatty/zedi PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-29T06:01:24.318Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : `bun run lint` と `bun run format:check` が通る状態を維持する。

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-06T03:00:23.809Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Pass `bun run lint` and `bun run format:check` checks; maintain ESLint and Prettier compliance

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-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.

Comment thread src/pages/WikiSchemaPage.tsx
@otomatty otomatty self-assigned this Apr 17, 2026
@otomatty otomatty changed the base branch from main to develop April 17, 2026 06:42
devin-ai-integration[bot]

This comment was marked as resolved.

CodeRabbit のレビューコメントから、本 PR 由来の指摘 3 件を修正する。
他は既存コード由来の指摘または誤検知のため、別対応とする。

- admin LintRule 型に `stale` を追加し、`RULE_LABELS` に「古い情報 /
  Stale」を追加。サーバ側で既に追加済みの stale ルールを管理画面から
  表示・フィルタできるようにする。
- `rebuildIndexForOwner` の __index__ ページ insert を race-safe に
  する。部分ユニークインデックスで並行 rebuild が衝突した場合、
  エラーを伝搬させずに勝者行を再 SELECT する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

@otomatty
Copy link
Copy Markdown
Owner Author

レビューコメントを精査し、本 PR 由来の指摘のみを修正しました。対応区分を以下にまとめます。

修正済み(commit 37948e4

  • CodeRabbit @ admin/src/pages/wiki-health/WikiHealthContent.tsx:29 + LintRule 型: サーバ側で stale ルールを追加したのに admin の LintRule 型と RULE_LABELS を更新し忘れていました。admin/src/api/lint.ts"stale" を追加し、WikiHealthContent.tsx に「古い情報 / Stale」ラベルを追加しました。
  • CodeRabbit @ server/api/src/services/indexBuilder.ts:300 (race condition): 部分ユニークインデックス idx_pages_unique_special_kind_per_owner を追加した副作用で、2 つの並行 rebuild が SELECT を通過して INSERT 段で衝突するケースがあり得ました。INSERT を try/catch で囲み、衝突時は勝者行を再 SELECT して created=false で継続するようにしました。

対応不要(本 PR の変更対象外 / 別 Issue 推奨)

いずれも妥当な指摘なので、後続 PR / 別 Issue として切り出すのが適切だと思います。

誤検知(対応不要)

  • CodeRabbit @ src/pages/IndexPage.tsx:74 および src/pages/WikiSchemaPage.tsx:44,54(react-hooks/set-state-in-effect): 当該ルールは eslint-plugin-react-hooks v7.1.0 以降でのみ提供されます。本プロジェクトの package.json^7.0.1 を指定しており、実際に bun run lint を実行しても 0 errors(1277 warnings は全て既存)でした。「lint gate をブロックする」との指摘は現環境では発生しません。WikiSchemaPage.tsx は本 PR 未変更のため、どちらにせよ対応不要。

- wikiSchema PUT: page_contents の upsert を pages の更新と同一 tx に含め、
  部分失敗で本文だけ書き込まれる不整合を防ぐ。
- ingest: validateModelAccess を validateModelAccessOrThrow に置換し、
  既知の検証失敗を 4xx として返す。あわせて API キー解決を
  modelInfo.provider に統一し、クライアント供給値とのズレで
  別プロバイダーの鍵を引いてしまう不具合を回避。
- indexBuilder: __index__ 初回作成のレースを ON CONFLICT DO NOTHING
  + 再 SELECT で吸収。Postgres は生の一意制約違反で tx を失敗状態に
  するため、try/catch のみでは不十分なことを明示。

Made-with: Cursor
- brokenLink / orphan: 相関サブクエリの pages 参照に owner_id 条件を
  追加し、別ユーザーのページ削除/被リンク状態が検出結果に
  混入する経路を遮断。
- conflict: NUMBER_PATTERN を `\d[\d,.]+` から `\d[\d,.]*` に変更し、
  「3人」「7km」など 1 桁の数値主張を取りこぼさないようにする。
- articleExtractor: YouTube Shorts URL (`/shorts/<id>`) を動画パイプラインに
  含めるパターンを追加。

Made-with: Cursor
- useWikiSchema: ["wiki-schema"] の静的キャッシュキーを userId 込みに変更し、
  ログアウト/ログイン直後に他ユーザーの schema が一瞬見える可能性を排除。
- useImageUpload: provider.uploadImage 解決後の throwIfAborted を削除。
  完了済みアセットを孤児化させずに、キャンセルは渡した signal 経由で
  provider 内部に伝搬させる。
- ImageCreateDialog: 親側で open=false にされた遷移を useEffect で監視し、
  進行中の OCR / describe を必ず abort して内部状態をリセット。
- markdownExport: youtubeEmbed.videoId を `as string` ではなく
  `typeof === "string"` で実行時にガードし、想定外の値で .trim() が
  落ちないようにする。
- wikiGeneratorFromChatPrompt: <user_schema> ブロックに混入する
  </user_schema> をエスケープし、プロンプト構造インジェクションを防ぐ。
- WikiSchemaPage: 保存中 (isUpdating) の入力フィールドを disabled にして、
  途中編集が isDirty クリアで失われる事故を防止。

Made-with: Cursor
@otomatty
Copy link
Copy Markdown
Owner Author

レビュー対応サマリ / Review response summary

CodeRabbit / Devin / Bito 等からの全 28 件のレビューコメントを精査し、対応/対応不要を判断しました。

対応済み (15 件) — commits cbcd9a3, 23e50b9, 4795070

cbcd9a3 fix(api): wikiSchema トランザクション整合と ingest プロバイダー解決

  • wikiSchema.ts: pages 更新と page_contents upsert を同一 tx に統合(部分書き込み防止)
  • ingest.ts: validateModelAccessvalidateModelAccessOrThrow(4xx 化)
  • ingest.ts: API キー解決を modelInfo.provider に統一(クライアント供給値とのズレで別プロバイダー鍵を引く問題を回避)
  • indexBuilder.ts: __index__ 初回作成のレースを ON CONFLICT DO NOTHING + 再 SELECT で解消(PG の tx aborted state 対策)

23e50b9 fix(lint,extractor): cross-tenant 漏洩・1 桁数値検出・Shorts URL

  • brokenLink.ts / orphan.ts: 相関サブクエリの pages 参照に owner_id 条件を追加
  • conflict.ts: NUMBER_PATTERN\d[\d,.]+\d[\d,.]* に修正(1 桁数値の取りこぼし解消)
  • articleExtractor.ts: YouTube Shorts URL (/shorts/<id>) の抽出パターンを追加

4795070 fix(web): キャッシュ分離・abort 整理・型ガード・プロンプト堅牢化

  • useWikiSchema.ts: クエリキーを userId 込みに変更(ユーザー間キャッシュ漏洩防止)
  • useImageUpload.ts: 完了後の throwIfAborted() を削除(孤児アセット防止)
  • ImageCreateDialog.tsx: 親による open=false 遷移を useEffect で監視し abort/reset
  • markdownExport.ts: videoIdtypeof === "string" で実行時ガード
  • wikiGeneratorFromChatPrompt.ts: userSchema</user_schema> をエスケープ
  • WikiSchemaPage.tsx: 保存中 (isUpdating) のフォーム入力を disabled

対応不要 (7 件) — 各スレッドに根拠を返信済み

  • ext.ts provider/model pair 検証: modelInfo.provider を権威とする意図的な設計
  • youtubeService.ts AbortSignal: youtube-transcript パッケージ自体が未対応(レビュー本文も認知)
  • PromoteToWikiDialog AI extraction abort: 外側 if (!open) return null で unmount するため React 18+ では state 破損なし。トークン消費停止は別 PR の AbortSignal 配線対応として切り出し
  • PromoteToWikiDialog 複数選択時の AI 生成(2件): 並行 LLM コスト/レートポリシー未確定のため最初の 1 ページのみ生成する暫定設計(残りは後から個別生成)
  • PromoteToWikiDialog ダイアログ a11y: 全モーダル横断で別 PR にて統一対応
  • IndexPage.tsx / WikiSchemaPage.tsxreact-hooks/set-state-in-effect(3件): ローカル bun run lint / bunx eslint で 0 件再現せず(プロジェクトの eslint-plugin-react-hooks@7.0.1 + recommended を使用)。レビューツール側の誤検出と判断
  • WikiHealthContent.tsxstale 欠落: 現コードでは LintRule 型・RULE_LABELS ともに stale を含んでおり対応済み

検証 / Verification

  • bun run lint: 0 errors
  • bunx prettier --check (差分ファイル): all clean
  • 関連ユニットテスト: 全パス(stale.test.ts / conflict.test.ts / titleSimilar.test.ts / markdownExport.test.ts / wikiGeneratorFromChatPrompt.test.ts / useImageUpload.test.ts 等)

@coderabbitai 再レビューをお願いします / Please re-review.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

@otomatty 詳細な対応サマリをありがとうございます!15 件の修正内容(トランザクション整合、レース条件対応、クロステナント漏洩防止、abort 整理など)および対応不要 7 件の根拠を確認しました。対応内容は非常に丁寧で、セキュリティ・整合性上の重要な問題が適切に修正されています。

再レビューを開始します。


🧠 Learnings used
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: .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-06T03:00:23.809Z
Learning: PR review should evaluate security and performance impact, breaking changes in public APIs and types, error handling and logging appropriateness, and whether Japanese/English comments match project tone

Learnt from: CR
Repo: otomatty/zedi PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-06T03:00:23.809Z
Learning: When responding to PR review comments, follow the procedure in `.cursor/skills/handle-pr-review/SKILL.md`; validate comment against TSDoc/tests/type definitions and decide: fix, provide alternative solution, or explain why no action needed (with specification justification)
✅ Actions performed

Full review triggered.

devin-ai-integration[bot]

This comment was marked as resolved.

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.

Caution

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

⚠️ Outside diff range comments (2)
server/api/src/routes/wikiSchema.ts (1)

97-125: ⚠️ Potential issue | 🟠 Major

FOR UPDATE does not eliminate the first-create race.

When no schema row exists, concurrent requests can both pass the read, then one insert can hit unique-constraint failure and return 500. Line 102 locks existing rows only; it does not lock the missing-key gap.

Race-safe insert fallback pattern
-    } else {
-      const [newPage] = await tx
-        .insert(pages)
-        .values({
-          ownerId: userId,
-          title,
-          isSchema: true,
-          createdAt: now,
-          updatedAt: now,
-        })
-        .returning({ id: pages.id });
-
-      if (!newPage) {
-        throw new HTTPException(500, { message: "Failed to create schema page" });
-      }
-      resolvedPageId = newPage.id;
-    }
+    } else {
+      const [inserted] = await tx
+        .insert(pages)
+        .values({
+          ownerId: userId,
+          title,
+          isSchema: true,
+          createdAt: now,
+          updatedAt: now,
+        })
+        .onConflictDoNothing()
+        .returning({ id: pages.id });
+
+      if (inserted) {
+        resolvedPageId = inserted.id;
+      } else {
+        const [winner] = await tx
+          .select({ id: pages.id })
+          .from(pages)
+          .where(and(eq(pages.ownerId, userId), eq(pages.isSchema, true), eq(pages.isDeleted, false)))
+          .for("update")
+          .limit(1);
+        if (!winner) {
+          throw new HTTPException(500, { message: "Failed to resolve schema page after conflict" });
+        }
+        resolvedPageId = winner.id;
+        await tx.update(pages).set({ title, updatedAt: now }).where(eq(pages.id, winner.id));
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/routes/wikiSchema.ts` around lines 97 - 125, Concurrent
requests can still race when no schema row exists because FOR UPDATE only locks
existing rows and a simultaneous insert can hit a unique constraint and return
500; change the insert path inside the transaction (the block around
tx.select/tx.update/tx.insert that sets resolvedPageId) to be race-safe by
handling unique-constraint conflicts: either perform an upsert (insert ... on
conflict(...) do update returning pages.id) or wrap the tx.insert in a try/catch
and, on unique-constraint error, re-query the pages row (same where:
ownerId=userId, isSchema=true, isDeleted=false) and use that id instead of
throwing the HTTPException; keep using the same transaction/tx and ensure
resolvedPageId is set from the requery/upsert result.
server/api/src/schema/pages.ts (1)

42-59: ⚠️ Potential issue | 🟠 Major

Add the unique partial index to Drizzle schema to match the migration constraint.

The code comment correctly states that a partial unique index enforces one row per (owner, kind), but the Drizzle schema definition at line 58 only includes a regular index. The SQL migration creates the constraint (idx_pages_unique_special_kind_per_owner) at lines 12–14, while the ORM schema omits it. This creates schema drift and means the Drizzle definition does not reflect the actual database constraint.

Update the schema to include the unique partial index:

Schema alignment fix
-import { pgTable, uuid, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
+import { pgTable, uuid, text, timestamp, boolean, index, uniqueIndex } from "drizzle-orm/pg-core";
@@
     index("idx_pages_is_deleted")
       .on(table.ownerId)
       .where(sql`NOT ${table.isDeleted}`),
     index("idx_pages_owner_special_kind").on(table.ownerId, table.specialKind),
+    uniqueIndex("idx_pages_unique_special_kind_per_owner")
+      .on(table.ownerId, table.specialKind)
+      .where(sql`${table.specialKind} IS NOT NULL AND ${table.isDeleted} = false`),
   ],
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/schema/pages.ts` around lines 42 - 59, The schema is missing
the partial unique index that enforces one (owner, specialKind) row; update the
index list to add a unique partial index named
idx_pages_unique_special_kind_per_owner that targets table.ownerId and
table.specialKind and uses the same WHERE condition as the migration (NOT
table.isDeleted) so the Drizzle schema reflects the migration; locate the pages
table definition and replace or augment the existing
index("idx_pages_owner_special_kind").on(table.ownerId, table.specialKind) with
a unique/uniqueIndex entry matching idx_pages_unique_special_kind_per_owner and
the partial predicate.
🧹 Nitpick comments (5)
src/lib/wikiGenerator/wikiGeneratorFromChatPrompt.ts (1)

17-19: </user_schema> の亜種も潰しておくとさらに安全です。

現在は完全一致のみ置換なので、空白・大小文字のバリエーションも同時に無効化しておくのがおすすめです。

🔧 Hardening diff
 function escapeUserSchemaContent(input: string): string {
-  return input.replaceAll("</user_schema>", "<\\/user_schema>");
+  return input.replace(/<\/\s*user_schema\s*>/gi, "<\\/user_schema>");
 }

Also applies to: 38-38

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

In `@src/lib/wikiGenerator/wikiGeneratorFromChatPrompt.ts` around lines 17 - 19,
The current escapeUserSchemaContent function only replaces the exact string
"</user_schema>"; change it to use a global, case-insensitive regex that matches
optional whitespace inside the tag (e.g. match "</  user_schema  >", any
capitalization) so all variants get replaced, and perform a global replace to
catch multiple occurrences; apply the same fix to the other occurrence mentioned
(the replacement at the other call/site around line 38) so both use the same
regex-based replacement logic in escapeUserSchemaContent (or centralize
replacing there) and return the sanitized string.
server/api/src/app.ts (1)

123-123: 新規コメントを日英併記にそろえるとガイドライン準拠になります。

Line 123 のコメントも、このファイル内の他コメントと同様に日本語/英語の併記にそろえるのがよいです。

As per coding guidelines "Include both Japanese and English in comments and documentation".

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

In `@server/api/src/app.ts` at line 123, The single-line comment "// Activity log
+ __index__ rebuild (LLM Wiki P4)" should be converted to a bilingual
Japanese/English comment to match project guidelines; locate that comment in
app.ts (the "Activity log + __index__ rebuild (LLM Wiki P4)" text) and replace
it with a succinct Japanese translation plus the existing English text (or vice
versa), following the same format used by other comments in this file so it
remains consistent with surrounding bilingual entries.
server/api/src/__tests__/services/activityLogService.test.ts (1)

42-52: Prefer guaranteed mock restoration for test isolation.

If an assertion throws before Line 51, console.error may stay mocked. Using try/finally (or afterEach(vi.restoreAllMocks)) makes this robust.

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

In `@server/api/src/__tests__/services/activityLogService.test.ts` around lines 42
- 52, The test currently spies on console.error with errorSpy but may fail to
restore the mock if an assertion throws; update the test around recordActivity
to guarantee restoration by either wrapping the spy and assertions in a
try/finally that calls errorSpy.mockRestore() in finally, or register a global
cleanup like afterEach(() => vi.restoreAllMocks()) so console.error is always
restored; refer to the test's errorSpy, recordActivity, and console.error
symbols when making this change.
server/api/drizzle/0010_add_activity_log.sql (1)

11-12: Consider CHECK constraints for kind and actor.

Using plain text here allows invalid activity values from out-of-band writes. A DB-level CHECK would harden integrity for analytics and filtering.

Proposed SQL hardening
 CREATE TABLE "activity_log" (
     "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
     "owner_id" text NOT NULL,
     "kind" text NOT NULL,
     "actor" text NOT NULL,
@@
     "created_at" timestamp with time zone DEFAULT now() NOT NULL
 );
+ALTER TABLE "activity_log"
+  ADD CONSTRAINT "activity_log_kind_check"
+  CHECK ("kind" IN (
+    'clip_ingest','chat_promote','lint_run','wiki_generate','index_build','wiki_schema_update'
+  ));
+ALTER TABLE "activity_log"
+  ADD CONSTRAINT "activity_log_actor_check"
+  CHECK ("actor" IN ('user','ai','system'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/drizzle/0010_add_activity_log.sql` around lines 11 - 12, Add
DB-level CHECK constraints to the activity_log table to prevent invalid values
being inserted out-of-band: update the migration in 0010_add_activity_log.sql to
add a CHECK on the "kind" column (e.g. restrict to the allowed enum/list of
activity types) and a CHECK on the "actor" column (e.g. restrict format or
allowed actor types/values), referencing the "activity_log" table and the "kind"
and "actor" column names so the constraints are applied at creation time.
server/api/src/services/indexBuilder.ts (1)

328-339: Consider documenting the empty ydocState intent.

The empty buffer for ydocState is appropriate since this is a server-generated index page (not a collaborative Yjs document). A brief inline comment would help future maintainers understand this is intentional.

📝 Suggested documentation
     await tx
       .insert(pageContents)
       .values({
         pageId,
+        // Empty ydoc state: __index__ is server-generated, not collaboratively edited.
+        // 空の ydoc: __index__ はサーバー生成であり協調編集対象外。
         ydocState: Buffer.alloc(0),
         contentText: document.markdown,
         updatedAt: now,
       })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/services/indexBuilder.ts` around lines 328 - 339, Add a brief
inline comment next to the tx.insert(pageContents).values call explaining that
ydocState is intentionally set to an empty Buffer (Buffer.alloc(0)) because this
record represents a server-generated index page and is not a collaborative Yjs
document, so no Yjs state is stored; reference the insertion for pageContents /
ydocState to make the intent clear to future maintainers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@server/api/src/routes/wikiSchema.ts`:
- Around line 97-125: Concurrent requests can still race when no schema row
exists because FOR UPDATE only locks existing rows and a simultaneous insert can
hit a unique constraint and return 500; change the insert path inside the
transaction (the block around tx.select/tx.update/tx.insert that sets
resolvedPageId) to be race-safe by handling unique-constraint conflicts: either
perform an upsert (insert ... on conflict(...) do update returning pages.id) or
wrap the tx.insert in a try/catch and, on unique-constraint error, re-query the
pages row (same where: ownerId=userId, isSchema=true, isDeleted=false) and use
that id instead of throwing the HTTPException; keep using the same
transaction/tx and ensure resolvedPageId is set from the requery/upsert result.

In `@server/api/src/schema/pages.ts`:
- Around line 42-59: The schema is missing the partial unique index that
enforces one (owner, specialKind) row; update the index list to add a unique
partial index named idx_pages_unique_special_kind_per_owner that targets
table.ownerId and table.specialKind and uses the same WHERE condition as the
migration (NOT table.isDeleted) so the Drizzle schema reflects the migration;
locate the pages table definition and replace or augment the existing
index("idx_pages_owner_special_kind").on(table.ownerId, table.specialKind) with
a unique/uniqueIndex entry matching idx_pages_unique_special_kind_per_owner and
the partial predicate.

---

Nitpick comments:
In `@server/api/drizzle/0010_add_activity_log.sql`:
- Around line 11-12: Add DB-level CHECK constraints to the activity_log table to
prevent invalid values being inserted out-of-band: update the migration in
0010_add_activity_log.sql to add a CHECK on the "kind" column (e.g. restrict to
the allowed enum/list of activity types) and a CHECK on the "actor" column (e.g.
restrict format or allowed actor types/values), referencing the "activity_log"
table and the "kind" and "actor" column names so the constraints are applied at
creation time.

In `@server/api/src/__tests__/services/activityLogService.test.ts`:
- Around line 42-52: The test currently spies on console.error with errorSpy but
may fail to restore the mock if an assertion throws; update the test around
recordActivity to guarantee restoration by either wrapping the spy and
assertions in a try/finally that calls errorSpy.mockRestore() in finally, or
register a global cleanup like afterEach(() => vi.restoreAllMocks()) so
console.error is always restored; refer to the test's errorSpy, recordActivity,
and console.error symbols when making this change.

In `@server/api/src/app.ts`:
- Line 123: The single-line comment "// Activity log + __index__ rebuild (LLM
Wiki P4)" should be converted to a bilingual Japanese/English comment to match
project guidelines; locate that comment in app.ts (the "Activity log + __index__
rebuild (LLM Wiki P4)" text) and replace it with a succinct Japanese translation
plus the existing English text (or vice versa), following the same format used
by other comments in this file so it remains consistent with surrounding
bilingual entries.

In `@server/api/src/services/indexBuilder.ts`:
- Around line 328-339: Add a brief inline comment next to the
tx.insert(pageContents).values call explaining that ydocState is intentionally
set to an empty Buffer (Buffer.alloc(0)) because this record represents a
server-generated index page and is not a collaborative Yjs document, so no Yjs
state is stored; reference the insertion for pageContents / ydocState to make
the intent clear to future maintainers.

In `@src/lib/wikiGenerator/wikiGeneratorFromChatPrompt.ts`:
- Around line 17-19: The current escapeUserSchemaContent function only replaces
the exact string "</user_schema>"; change it to use a global, case-insensitive
regex that matches optional whitespace inside the tag (e.g. match "</ 
user_schema  >", any capitalization) so all variants get replaced, and perform a
global replace to catch multiple occurrences; apply the same fix to the other
occurrence mentioned (the replacement at the other call/site around line 38) so
both use the same regex-based replacement logic in escapeUserSchemaContent (or
centralize replacing there) and return the sanitized string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1169a1a7-a7ed-40cb-a19a-b4f4e6da3dec

📥 Commits

Reviewing files that changed from the base of the PR and between dcd90c1 and 4795070.

📒 Files selected for processing (37)
  • admin/src/App.tsx
  • admin/src/api/activity.ts
  • admin/src/api/lint.ts
  • admin/src/pages/ActivityLog.tsx
  • admin/src/pages/Layout.tsx
  • admin/src/pages/wiki-health/WikiHealthContent.tsx
  • server/api/drizzle/0010_add_activity_log.sql
  • server/api/drizzle/0011_add_page_special_kind.sql
  • server/api/drizzle/meta/_journal.json
  • server/api/src/__tests__/services/activityLogService.test.ts
  • server/api/src/__tests__/services/indexBuilder.test.ts
  • server/api/src/app.ts
  • server/api/src/lib/articleExtractor.ts
  • server/api/src/routes/activity.ts
  • server/api/src/routes/ingest.ts
  • server/api/src/routes/wikiSchema.ts
  • server/api/src/schema/activityLog.ts
  • server/api/src/schema/index.ts
  • server/api/src/schema/lintFindings.ts
  • server/api/src/schema/pages.ts
  • server/api/src/schema/relations.ts
  • server/api/src/services/activityLogService.ts
  • server/api/src/services/indexBuilder.ts
  • server/api/src/services/lintEngine/index.ts
  • server/api/src/services/lintEngine/rules/brokenLink.ts
  • server/api/src/services/lintEngine/rules/conflict.ts
  • server/api/src/services/lintEngine/rules/orphan.ts
  • server/api/src/services/lintEngine/rules/stale.test.ts
  • server/api/src/services/lintEngine/rules/stale.ts
  • src/App.tsx
  • src/components/layout/ImageCreateDialog.tsx
  • src/hooks/useImageUpload.ts
  • src/hooks/useWikiSchema.ts
  • src/lib/markdownExport.ts
  • src/lib/wikiGenerator/wikiGeneratorFromChatPrompt.ts
  • src/pages/IndexPage.tsx
  • src/pages/WikiSchemaPage.tsx

`/index` を開くたびに POST /api/activity/index/rebuild が走り、
activity_log に `index_build` エントリが蓄積される問題を修正する。

- 新規: GET /api/activity/index — `__index__` の現在状態を DB 書き込み
  なしで返す。`buildIndexForOwner` を再利用してカテゴリ概要を算出する。
- 変更: IndexPage は初回マウント時に GET を呼び、POST は明示的な
  再構築ボタン押下時のみ発行する。これによりページ閲覧で
  activity_log が汚れず、ユーザー意図と記録が一致する。
- POST 経路(recordActivity あり)は変更なし。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@otomatty
Copy link
Copy Markdown
Owner Author

Devin のレビュー 2 件への対応状況です。

修正済み

  • Devin @ src/pages/IndexPage.tsx:74(activity_log 汚染) — commit a987f79

    新しく GET /api/activity/index を追加しました。DB 書き込みを一切行わずに __index__ の現在状態(pageId / lastBuiltAt / カテゴリ概要)を返します。IndexPage はマウント時には GET のみを呼び、POST /api/activity/index/rebuild は明示的な「再構築」ボタン押下時だけ発行するように変更しました。これで画面閲覧だけで activity_logindex_build エントリが積み上がる挙動はなくなります。

対応済み(ただし他コミット由来)

  • Devin @ server/api/src/services/indexBuilder.ts:322(PostgreSQL aborted tx 問題)

    ご指摘の通り、try/catch は Postgres のトランザクション失敗モードでは機能しません。既に otomatty さんが commit cbcd9a3.onConflictDoNothing().returning() + 再 SELECT に置き換え済みで、同じ修正方針になっています。本 PR ブランチの HEAD で既に反映されているので追加対応は不要です。

WikiHealth ダッシュボードのサマリグリッドが `md:grid-cols-5` のままで、
stale 追加後は md ブレイクポイントで 5+1 の不揃いレイアウトになっていた。
`md:grid-cols-6` に更新し、全 6 ルールを 1 行に整列させる。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@otomatty otomatty merged commit 28a2845 into develop Apr 17, 2026
11 checks passed
@otomatty otomatty deleted the claude/condescending-johnson-a9cd98 branch April 17, 2026 08:36
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